wip: snapshot das alterações não commitadas (2026-10-20)

Origem: D:/Documents/Github/PiAgentUI/PiAgentUi
Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 4
This commit is contained in:
Evandro committed 2026-10-20 00:48:28 -03:00
1 parent b3489e1ede
commit 98b5db029f
70 files changed
+9466 -9191

No files matched your search

@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import { describeExtensionCompatibility } from './piagentui-extension-compat.js'
describe('extension compatibility adapter', () => {
it('marks Pi packages with pi.extensions as supported', () => {
expect(
describeExtensionCompatibility({
name: 'pi-hud',
pi: { extensions: ['./extensions/hud.ts'] },
}),
).toEqual({
kind: 'pi',
status: 'supported',
reason: 'Package declares Pi extension entrypoints.',
capabilities: ['extensions'],
})
})
it('marks OpenCode-style manifests as partial without executing them', () => {
expect(
describeExtensionCompatibility({
name: 'opencode-toolkit',
opencode: { extensions: ['./plugin.ts'], commands: ['./commands.ts'] },
}),
).toEqual({
kind: 'opencode',
status: 'partial',
reason: 'OpenCode manifest detected; execution is disabled until sandbox permissions are implemented.',
capabilities: ['extensions', 'commands'],
})
})
it('marks packages without recognized manifests as unsupported', () => {
expect(describeExtensionCompatibility({ name: 'plain-package' })).toEqual({
kind: 'unknown',
status: 'unsupported',
reason: 'No Pi or OpenCode extension manifest was detected.',
capabilities: [],
})
})
})
+66
View File
@@ -0,0 +1,66 @@
export type ExtensionCompatibilityKind = 'pi' | 'opencode' | 'unknown'
export type ExtensionCompatibilityStatus = 'supported' | 'partial' | 'unsupported'
export type ExtensionCompatibilityCapability = 'extensions' | 'skills' | 'prompts' | 'themes' | 'commands'
export interface ExtensionCompatibilityDescriptor {
kind: ExtensionCompatibilityKind
status: ExtensionCompatibilityStatus
reason: string
capabilities: ExtensionCompatibilityCapability[]
}
interface ExtensionPackageManifest {
name?: unknown
pi?: unknown
opencode?: unknown
}
const PI_CAPABILITY_KEYS: ExtensionCompatibilityCapability[] = ['extensions', 'skills', 'prompts', 'themes']
const OPENCODE_CAPABILITY_KEYS: ExtensionCompatibilityCapability[] = ['extensions', 'commands']
export function describeExtensionCompatibility(manifest: ExtensionPackageManifest): ExtensionCompatibilityDescriptor {
const piCapabilities = collectCapabilities(manifest.pi, PI_CAPABILITY_KEYS)
if (piCapabilities.length > 0) {
return {
kind: 'pi',
status: 'supported',
reason: 'Package declares Pi extension entrypoints.',
capabilities: piCapabilities,
}
}
const opencodeCapabilities = collectCapabilities(manifest.opencode, OPENCODE_CAPABILITY_KEYS)
if (opencodeCapabilities.length > 0) {
return {
kind: 'opencode',
status: 'partial',
reason: 'OpenCode manifest detected; execution is disabled until sandbox permissions are implemented.',
capabilities: opencodeCapabilities,
}
}
return {
kind: 'unknown',
status: 'unsupported',
reason: 'No Pi or OpenCode extension manifest was detected.',
capabilities: [],
}
}
function collectCapabilities(
value: unknown,
capabilityKeys: ExtensionCompatibilityCapability[],
): ExtensionCompatibilityCapability[] {
if (!isRecord(value)) return []
return capabilityKeys.filter(key => hasManifestEntries(value[key]))
}
function hasManifestEntries(value: unknown): boolean {
if (typeof value === 'string') return value.trim().length > 0
if (Array.isArray(value)) return value.some(entry => typeof entry === 'string' && entry.trim().length > 0)
return false
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
@@ -95,6 +95,12 @@ describe('buildExtensionCatalog', () => {
status: 'active',
installedPath: userPackageRoot,
})
expect(userPackage?.compatibility).toEqual({
kind: 'pi',
status: 'supported',
reason: 'Package declares Pi extension entrypoints.',
capabilities: ['extensions', 'skills'],
})
expect(userPackage?.resources.extensions).toEqual([
expect.objectContaining({
name: 'demo.ts',
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { describeExtensionCompatibility, type ExtensionCompatibilityDescriptor } from './piagentui-extension-compat.js'
export type ExtensionPackageScope = 'user' | 'project'
export type ExtensionPackageStatus = 'active' | 'inactive' | 'missing' | 'error'
@@ -33,6 +34,7 @@ export interface ExtensionPackageInfo {
status: ExtensionPackageStatus
error?: string
resources: ExtensionPackageResources
compatibility: ExtensionCompatibilityDescriptor
}
export interface ExtensionCatalogSummary {
@@ -80,6 +82,7 @@ interface PackageJsonMetadata {
description?: string
keywords?: string[]
pi?: Partial<Record<ExtensionResourceType, string[]>> & { image?: string }
opencode?: unknown
}
export interface ParsedPiInstallCommand {
@@ -363,6 +366,7 @@ export function buildExtensionCatalog(options: BuildExtensionCatalogOptions): Ex
filtered: entry.filtered,
status: 'missing',
resources: { extensions: [], skills: [], prompts: [], themes: [] },
compatibility: describeExtensionCompatibility({}),
}
}
@@ -383,6 +387,7 @@ export function buildExtensionCatalog(options: BuildExtensionCatalogOptions): Ex
status,
error,
resources,
compatibility: describeExtensionCompatibility(metadata ?? {}),
}
})
+23
View File
@@ -8,6 +8,7 @@ import {
isRequestBodySizeAllowed,
getLocalNetworkUrls,
validateSttBaseUrl,
buildRuntimeCapabilities,
} from './piagentui-server-core.js'
describe('PiAgentUi Server Core Security Helpers', () => {
@@ -158,6 +159,28 @@ describe('PiAgentUi Server Core Security Helpers', () => {
})
})
describe('buildRuntimeCapabilities', () => {
it('should report session switching when runtime exposes switchSession', () => {
expect(buildRuntimeCapabilities({ switchSession: () => {} })).toEqual({
runtime: 'pi',
sessionSwitching: { available: true },
extensionLoading: { available: true, source: 'pi.extensions' },
mcp: { available: true, source: 'pi.mcp' },
slashCommands: { available: true, source: 'pi.commands' },
})
})
it('should report unavailable session switching without leaking context details', () => {
expect(buildRuntimeCapabilities({})).toEqual({
runtime: 'pi',
sessionSwitching: { available: false, reason: 'session_switching_unavailable' },
extensionLoading: { available: true, source: 'pi.extensions' },
mcp: { available: true, source: 'pi.mcp' },
slashCommands: { available: true, source: 'pi.commands' },
})
})
})
describe('isRequestBodySizeAllowed', () => {
it('should allow small request content sizes', () => {
expect(isRequestBodySizeAllowed('100')).toBe(true)
+30
View File
@@ -1,6 +1,23 @@
import * as path from 'node:path'
import * as os from 'node:os'
export interface RuntimeCapabilityStatus {
available: boolean
reason?: string
source?: string
}
export interface RuntimeCapabilities {
runtime: 'pi'
sessionSwitching: RuntimeCapabilityStatus
extensionLoading: RuntimeCapabilityStatus
mcp: RuntimeCapabilityStatus
slashCommands: RuntimeCapabilityStatus
}
interface RuntimeCapabilityContext {
switchSession?: unknown
}
/**
* Checks if a host string matches a loopback host (localhost or 127.0.0.1 or ::1)
*/
@@ -170,3 +187,16 @@ export function isRequestBodySizeAllowed(
if (isNaN(size)) return false
return size <= maxBytes
}
export function buildRuntimeCapabilities(ctx: RuntimeCapabilityContext | undefined): RuntimeCapabilities {
return {
runtime: 'pi',
sessionSwitching:
typeof ctx?.switchSession === 'function'
? { available: true }
: { available: false, reason: 'session_switching_unavailable' },
extensionLoading: { available: true, source: 'pi.extensions' },
mcp: { available: true, source: 'pi.mcp' },
slashCommands: { available: true, source: 'pi.commands' },
}
}
+284 -27
View File
@@ -12,10 +12,12 @@ import {
isRequestBodySizeAllowed,
getLocalNetworkUrls,
validateSttBaseUrl,
buildRuntimeCapabilities,
} from './piagentui-server-core.js'
import {
createPiAgentUiSettingsStore,
resolvePiAgentUiSettingsPaths,
resolvePiAgentUiAgentDir,
type PiAgentUiLocalSettings,
type StoredSttSettings,
type StoredTtsSettings,
@@ -732,6 +734,8 @@ export default function (pi: ExtensionAPI) {
apiKeyRequired: false,
transcriptionEndpoint: '/audio/transcriptions',
transcriptionModel: 'gpt-4o-mini-transcribe',
realtimeEndpoint: '/realtime/transcriptions',
realtimeModel: 'gpt-4o-mini-transcribe',
insertMode: 'append',
...getLocalSettings().stt,
}
@@ -746,6 +750,8 @@ export default function (pi: ExtensionAPI) {
baseUrl: effective.baseUrl || 'https://api.openai.com/v1',
transcriptionEndpoint: effective.transcriptionEndpoint || '/audio/transcriptions',
transcriptionModel: effective.transcriptionModel || 'gpt-4o-mini-transcribe',
realtimeEndpoint: effective.realtimeEndpoint || '/realtime/transcriptions',
realtimeModel: effective.realtimeModel || effective.transcriptionModel || 'gpt-4o-mini-transcribe',
insertMode: effective.insertMode === 'replace' ? 'replace' : 'append',
apiKeyRequired: effective.apiKeyRequired === true,
apiKeyConfigured: typeof effective.apiKey === 'string' && effective.apiKey.length > 0,
@@ -806,6 +812,18 @@ export default function (pi: ExtensionAPI) {
}
next.transcriptionModel = update.transcriptionModel.trim()
}
if (update.realtimeEndpoint !== undefined) {
if (typeof update.realtimeEndpoint !== 'string' || !update.realtimeEndpoint.startsWith('/')) {
return { ok: false, error: 'Invalid realtimeEndpoint' }
}
next.realtimeEndpoint = update.realtimeEndpoint
}
if (update.realtimeModel !== undefined) {
if (typeof update.realtimeModel !== 'string' || !update.realtimeModel.trim()) {
return { ok: false, error: 'Invalid realtimeModel' }
}
next.realtimeModel = update.realtimeModel.trim()
}
if (update.language !== undefined) {
if (update.language === null || update.language === '') delete next.language
else if (typeof update.language === 'string') next.language = update.language.trim()
@@ -954,6 +972,160 @@ export default function (pi: ExtensionAPI) {
return form
}
type SttRealtimeClientMessage =
| { type: 'audio.append'; audio: string }
| { type: 'audio.commit' }
| { type: 'session.close' }
function buildRealtimeProviderUrl(settings: StoredSttSettings): string {
const base = (settings.baseUrl || 'https://api.openai.com/v1').replace(/\/+$/, '')
const endpoint =
settings.realtimeEndpoint ||
(settings.providerKind === 'openai' ? '/realtime?intent=transcription' : '/realtime/transcriptions')
const wsBase = base.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:')
return `${wsBase}${endpoint}`
}
function buildRealtimeSessionConfigureMessage(settings: StoredSttSettings): Record<string, unknown> {
if (settings.providerKind === 'openai') {
const transcription: Record<string, unknown> = {
model: settings.realtimeModel || 'gpt-realtime-whisper',
}
if (settings.language) transcription.language = settings.language
return {
type: 'session.update',
session: {
type: 'transcription',
audio: {
input: {
format: { type: 'audio/pcm', rate: 24000 },
transcription,
},
},
},
}
}
return { type: 'session.configure', model: settings.realtimeModel || settings.transcriptionModel }
}
function translateRealtimeClientMessage(
message: SttRealtimeClientMessage,
settings: StoredSttSettings,
): Record<string, unknown> | null {
if (settings.providerKind !== 'openai') return message
if (message.type === 'audio.append') return { type: 'input_audio_buffer.append', audio: message.audio }
if (message.type === 'audio.commit') return { type: 'input_audio_buffer.commit' }
return null
}
function normalizeRealtimeProviderMessage(raw: string): { type: string; text?: string; error?: string } | null {
let event: any
try {
event = JSON.parse(raw)
} catch {
return null
}
const type = typeof event.type === 'string' ? event.type : ''
const isTranscriptionDelta =
type === 'conversation.item.input_audio_transcription.delta' ||
type === 'input_audio_buffer.transcription.delta' ||
type === 'input_audio_transcription.delta'
const isTranscriptionCompleted =
type === 'conversation.item.input_audio_transcription.completed' ||
type === 'conversation.item.input_audio_transcription.done' ||
type === 'input_audio_buffer.transcription.completed' ||
type === 'input_audio_buffer.transcription.done' ||
type === 'input_audio_transcription.completed' ||
type === 'input_audio_transcription.done'
const partialText =
event.text_delta || event.partial || event.transcript?.partial || (isTranscriptionDelta ? event.delta : undefined)
if (typeof partialText === 'string') return { type: 'transcript.partial', text: partialText }
const finalText =
event.text || event.final || event.transcript?.text || (isTranscriptionCompleted ? event.transcript : undefined)
if (typeof finalText === 'string') return { type: 'transcript.final', text: finalText }
const error = event.error?.message || event.error
if (typeof error === 'string') return { type: 'error', error }
return null
}
function handleSttRealtimeWebSocket(client: WebSocket, req: http.IncomingMessage) {
if (!checkAuth(req)) {
client.send(JSON.stringify({ type: 'error', error: 'Unauthorized' }))
client.close()
return
}
const settings = getEffectiveSttSettings()
if (!settings.enabled || settings.mode !== 'realtime') {
client.send(JSON.stringify({ type: 'error', error: 'Realtime STT is not enabled' }))
client.close()
return
}
if (settings.apiKeyRequired && !settings.apiKey) {
client.send(JSON.stringify({ type: 'error', error: 'STT API key is not configured' }))
client.close()
return
}
if (!validateSttBaseUrl(settings.baseUrl)) {
client.send(JSON.stringify({ type: 'error', error: 'Invalid STT baseUrl' }))
client.close()
return
}
const headers: Record<string, string> = {}
if (settings.apiKey) headers.Authorization = `Bearer ${settings.apiKey}`
const provider = new WebSocket(buildRealtimeProviderUrl(settings), { headers })
const closeBoth = () => {
if (provider.readyState === WebSocket.OPEN || provider.readyState === WebSocket.CONNECTING) provider.close()
if (client.readyState === WebSocket.OPEN) client.close()
}
provider.on('open', () => {
provider.send(JSON.stringify(buildRealtimeSessionConfigureMessage(settings)))
if (client.readyState === WebSocket.OPEN) client.send(JSON.stringify({ type: 'ready' }))
})
provider.on('message', data => {
const normalized = normalizeRealtimeProviderMessage(String(data))
if (normalized && client.readyState === WebSocket.OPEN) client.send(JSON.stringify(normalized))
})
provider.on('error', () => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: 'error', error: 'Realtime STT provider failed' }))
}
})
provider.on('close', () => {
if (client.readyState === WebSocket.OPEN) client.send(JSON.stringify({ type: 'closed' }))
})
client.on('message', data => {
if (provider.readyState !== WebSocket.OPEN) return
let message: SttRealtimeClientMessage
try {
message = JSON.parse(String(data))
} catch {
client.send(JSON.stringify({ type: 'error', error: 'Invalid realtime message' }))
return
}
if (message.type === 'session.close') {
closeBoth()
return
}
const providerMessage = translateRealtimeClientMessage(message, settings)
if (providerMessage) provider.send(JSON.stringify(providerMessage))
})
client.on('close', closeBoth)
client.on('error', closeBoth)
}
async function transcribeAudio(req: http.IncomingMessage) {
if (!isRequestBodySizeAllowed(req.headers['content-length'], 25 * 1024 * 1024)) {
return { status: 413, body: { error: 'Audio upload too large' } }
@@ -1149,6 +1321,9 @@ export default function (pi: ExtensionAPI) {
const authHeader = req.headers.authorization
if (validateBearerToken(authHeader, localToken)) return true
const queryToken = new URL(req.url || '/', 'http://127.0.0.1').searchParams.get('token')
if (queryToken && queryToken === localToken) return true
const host = req.headers.host
const origin = req.headers.origin as string | undefined
const lanAccessEnabled = getLocalSettings().network?.lanAccessEnabled === true
@@ -1400,10 +1575,107 @@ export default function (pi: ExtensionAPI) {
return
}
if (pathParts[1] === 'runtime' && pathParts[2] === 'capabilities' && method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(buildRuntimeCapabilities(latestCtx ?? undefined)))
return
}
if (pathParts[1] === 'providers' && pathParts[2] && pathParts[3] === 'api-key' && method === 'POST') {
try {
const registry = latestCtx?.modelRegistry as any
if (!registry?.authStorage?.set) {
res.writeHead(503, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Model registry is not ready' }))
return
}
const providerId = decodeURIComponent(pathParts[2])
const body = await readJsonBody(req)
const apiKey = typeof body?.apiKey === 'string' ? body.apiKey.trim() : ''
if (!apiKey) {
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'apiKey is required' }))
return
}
registry.authStorage.set(providerId, { type: 'api_key', key: apiKey })
registry.refresh?.()
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ ok: true }))
return
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Failed to save provider API key' }))
return
}
}
if (pathParts[1] === 'providers' && pathParts[2] && pathParts[3] === 'logout' && method === 'POST') {
try {
const registry = latestCtx?.modelRegistry as any
if (!registry?.authStorage?.logout) {
res.writeHead(503, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Model registry is not ready' }))
return
}
const providerId = decodeURIComponent(pathParts[2])
registry.authStorage.logout(providerId)
registry.refresh?.()
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ ok: true }))
return
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Failed to clear provider auth' }))
return
}
}
if (pathParts[1] === 'providers' && method === 'GET') {
const registry = latestCtx?.modelRegistry as any
const allModels: any[] = registry?.getAll?.() || []
const providerIds: string[] =
registry?.getProviderIds?.() || Array.from(new Set(allModels.map(model => model.provider).filter(Boolean)))
const providers = new Map<string, any>()
for (const providerId of providerIds) {
const providerModels: any[] =
registry?.getModelsForProvider?.(providerId) || allModels.filter(model => model.provider === providerId)
const authStatus = registry?.getProviderAuthStatus?.(providerId) ?? { configured: false }
const credential = registry?.authStorage?.get?.(providerId)
const usesOAuth =
credential?.type === 'oauth' ||
providerModels.some(model => registry?.isUsingOAuth?.(model)) ||
allModels.some(model => model.provider === providerId && registry?.isUsingOAuth?.(model))
providers.set(providerId, {
id: providerId,
name: registry?.getProviderDisplayName?.(providerId) || providerId,
models: providerModels.map(model => ({
id: model.id,
name: model.name || model.id,
provider: providerId,
family: model.family ?? '',
contextWindow: model.contextWindow ?? model.contextLimit,
maxOutputTokens: model.maxOutputTokens ?? model.maxTokens,
capabilities: {
input: { text: true, image: Array.isArray(model.input) ? model.input.includes('image') : undefined },
output: { text: true },
thinking: model.reasoning ?? model.supportsReasoning,
},
})),
auth: {
method: usesOAuth ? 'oauth' : 'apikey',
authorization: authStatus,
},
})
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ providers: Array.from(providers.values()) }))
return
}
if (pathParts[1] === 'models' && method === 'GET') {
const registry = latestCtx?.modelRegistry as any
const activeModel = latestCtx?.model as any
const registryModels: any[] =
latestCtx?.modelRegistry?.getAvailable() || latestCtx?.modelRegistry?.getAll() || []
const registryModels: any[] = registry?.getAvailable?.() || []
const mapModel = (m: any) => ({
id: m.id,
@@ -1423,7 +1695,9 @@ export default function (pi: ExtensionAPI) {
let models = registryModels.map(mapModel)
if (activeModel) {
const activeModelHasAuth = Boolean(activeModel && registry?.hasConfiguredAuth?.(activeModel))
if (activeModel && activeModelHasAuth) {
const registryActiveModel = registryModels.find(
(m: any) => m.id === activeModel.id && m.provider === activeModel.provider,
)
@@ -1443,26 +1717,6 @@ export default function (pi: ExtensionAPI) {
models = models.filter((m: any) => !(m.id === activeModel.id && m.provider === activeModel.provider))
models.unshift(activeMapped)
} else {
if (models.length === 0) {
models = [
{
id: 'gemini-2.5-flash',
name: 'Gemini 2.5 Flash',
provider: 'google',
family: '',
contextLimit: 128000,
outputLimit: 4096,
supportsReasoning: true,
supportsImages: true,
supportsPdf: false,
supportsAudio: false,
supportsVideo: false,
supportsToolcall: true,
variants: [],
},
]
}
}
res.writeHead(200, { 'Content-Type': 'application/json' })
@@ -2094,7 +2348,12 @@ export default function (pi: ExtensionAPI) {
}
// Upgrade WS safely
const pathname = new URL(request.url || '/', 'http://127.0.0.1').pathname
wss!.handleUpgrade(request, socket, head, (ws: WebSocket) => {
if (pathname === '/api/stt/realtime') {
handleSttRealtimeWebSocket(ws, request)
return
}
wss!.emit('connection', ws, request)
})
})
@@ -2122,8 +2381,7 @@ export default function (pi: ExtensionAPI) {
setWebLinkWidget(ctx)
// Write connection details to discovery file
const homeDir = process.env.USERPROFILE || process.env.HOME || ''
const piAgentDir = path.join(homeDir, '.pi', 'agent')
const piAgentDir = resolvePiAgentUiAgentDir()
const discoveryPath = path.join(piAgentDir, 'piagentui-port.json')
try {
if (!fs.existsSync(piAgentDir)) {
@@ -2149,8 +2407,7 @@ export default function (pi: ExtensionAPI) {
}
}
// Delete discovery file
const homeDir = process.env.USERPROFILE || process.env.HOME || ''
const discoveryPath = path.join(homeDir, '.pi', 'agent', 'piagentui-port.json')
const discoveryPath = path.join(resolvePiAgentUiAgentDir(), 'piagentui-port.json')
try {
if (fs.existsSync(discoveryPath)) {
fs.unlinkSync(discoveryPath)
+12 -3
View File
@@ -20,6 +20,8 @@ export type StoredSttSettings = {
apiKeyRequired?: boolean
transcriptionEndpoint?: string
transcriptionModel?: string
realtimeEndpoint?: string
realtimeModel?: string
language?: string
insertMode?: SttInsertMode
}
@@ -47,6 +49,7 @@ export type PiAgentUiLocalSettings = {
}
export interface PiAgentUiSettingsPaths {
agentDir: string
databasePath: string
legacyJsonPath: string
}
@@ -60,13 +63,19 @@ const SETTINGS_KEYS: Record<SettingsNamespace, string> = {
tts: 'settings',
}
export function resolvePiAgentUiAgentDir(homeDir = process.env.USERPROFILE || process.env.HOME || ''): string {
const envAgentDir = process.env.PI_CODING_AGENT_DIR?.trim()
return envAgentDir || path.join(homeDir, '.pi', 'agent')
}
export function resolvePiAgentUiSettingsPaths(
homeDir = process.env.USERPROFILE || process.env.HOME || '',
): PiAgentUiSettingsPaths {
const settingsDir = path.join(homeDir, '.pi', 'agent')
const agentDir = resolvePiAgentUiAgentDir(homeDir)
return {
databasePath: path.join(settingsDir, 'piagentui.db'),
legacyJsonPath: path.join(settingsDir, 'piagentui-settings.json'),
agentDir,
databasePath: path.join(agentDir, 'piagentui.db'),
legacyJsonPath: path.join(agentDir, 'piagentui-settings.json'),
}
}