@bibobonking/opensol 0.0.0-stage → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build.yml +25 -0
- package/README.md +74 -2
- package/dist/assets/index-BmN7pBji.css +1 -0
- package/dist/assets/index-DgNDjkOz.js +301 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +55 -0
- package/dist/preview-host.html +33 -0
- package/index.html +54 -0
- package/package.json +43 -4
- package/postcss.config.js +6 -0
- package/public/favicon.svg +15 -0
- package/public/preview-host.html +33 -0
- package/server/README.md +75 -0
- package/server/daemon.mjs +1082 -0
- package/server/lib/checkpoints.mjs +124 -0
- package/server/lib/checkpoints.test.mjs +110 -0
- package/server/lib/deploy.mjs +436 -0
- package/server/lib/http.mjs +237 -0
- package/server/lib/plugins.mjs +321 -0
- package/server/lib/plugins.test.mjs +140 -0
- package/server/lib/providers.mjs +594 -0
- package/server/lib/store.mjs +491 -0
- package/server/lib/tools.mjs +990 -0
- package/server/opensol.mjs +202 -0
- package/server/package.json +42 -0
- package/shared/plugin-builder.test.mjs +43 -0
- package/shared/plugin-package.d.mts +34 -0
- package/shared/plugin-package.mjs +197 -0
- package/shared/plugin-package.test.mjs +94 -0
- package/src/App.tsx +152 -0
- package/src/agent/adapters/anthropic.ts +233 -0
- package/src/agent/adapters/gemini.ts +225 -0
- package/src/agent/adapters/openai-responses.ts +215 -0
- package/src/agent/adapters/openai.ts +200 -0
- package/src/agent/adapters/shared.ts +96 -0
- package/src/agent/approval.ts +20 -0
- package/src/agent/compact.ts +234 -0
- package/src/agent/index.ts +36 -0
- package/src/agent/model.ts +20 -0
- package/src/agent/prompt.ts +107 -0
- package/src/agent/queue.test.mjs +111 -0
- package/src/agent/queue.ts +84 -0
- package/src/agent/subagent.ts +109 -0
- package/src/agent/tools.ts +541 -0
- package/src/components/ChatView.tsx +403 -0
- package/src/components/CommandPalette.tsx +416 -0
- package/src/components/Composer.tsx +678 -0
- package/src/components/ContextBar.tsx +58 -0
- package/src/components/DeployModal.tsx +512 -0
- package/src/components/ErrorBoundary.tsx +75 -0
- package/src/components/HomeDashboard.tsx +189 -0
- package/src/components/Logo.tsx +41 -0
- package/src/components/Markdown.tsx +232 -0
- package/src/components/MessageItem.tsx +655 -0
- package/src/components/ModelPicker.tsx +487 -0
- package/src/components/PixleSol.tsx +243 -0
- package/src/components/PluginBuilder.tsx +285 -0
- package/src/components/PluginManager.tsx +341 -0
- package/src/components/PreviewPane.tsx +166 -0
- package/src/components/SettingsModal.tsx +2052 -0
- package/src/components/Sidebar.tsx +504 -0
- package/src/components/Splash.tsx +104 -0
- package/src/components/ToolCallCard.tsx +177 -0
- package/src/components/TopBar.tsx +182 -0
- package/src/components/TrajectoryView.tsx +178 -0
- package/src/components/WorkspacePanel.tsx +710 -0
- package/src/components/ui.tsx +311 -0
- package/src/core/kernel.ts +45 -0
- package/src/core/plugins/tools/fs.ts +219 -0
- package/src/core/plugins/tools/git.ts +83 -0
- package/src/core/plugins/tools/grep.ts +98 -0
- package/src/core/plugins/tools/shell.ts +88 -0
- package/src/core/plugins/tools/subagent.ts +36 -0
- package/src/core/plugins/tools/web.ts +97 -0
- package/src/core/services/agent-loop.ts +417 -0
- package/src/core/services/approval.ts +66 -0
- package/src/core/services/compaction.ts +45 -0
- package/src/core/services/llm.ts +32 -0
- package/src/core/services/sessions.ts +47 -0
- package/src/core/services/subagents.ts +15 -0
- package/src/core/services/system-prompt.ts +177 -0
- package/src/core/services/tools.ts +135 -0
- package/src/core/services/ui.ts +109 -0
- package/src/core/types.ts +139 -0
- package/src/data/providers.ts +125 -0
- package/src/data/skills.ts +68 -0
- package/src/index.css +776 -0
- package/src/lib/boot.ts +94 -0
- package/src/lib/context.ts +75 -0
- package/src/lib/conversation.test.mjs +64 -0
- package/src/lib/conversation.ts +59 -0
- package/src/lib/daemon.ts +475 -0
- package/src/lib/hooks.ts +33 -0
- package/src/lib/permissions.ts +60 -0
- package/src/lib/pixlesol.test.mjs +51 -0
- package/src/lib/pixlesol.ts +55 -0
- package/src/lib/plugin-folder.test.mjs +145 -0
- package/src/lib/plugin-folder.ts +96 -0
- package/src/lib/reasoning.ts +87 -0
- package/src/lib/sse.ts +75 -0
- package/src/lib/themes.test.mjs +72 -0
- package/src/lib/themes.ts +52 -0
- package/src/lib/usage.test.mjs +72 -0
- package/src/lib/usage.ts +155 -0
- package/src/lib/utils.ts +73 -0
- package/src/main.tsx +16 -0
- package/src/plugins/api.test.mjs +77 -0
- package/src/plugins/api.ts +113 -0
- package/src/plugins/bridge.d.mts +1 -0
- package/src/plugins/bridge.mjs +26 -0
- package/src/plugins/bridge.test.mjs +17 -0
- package/src/plugins/index.ts +52 -0
- package/src/plugins/preview.test.mjs +18 -0
- package/src/plugins/preview.ts +34 -0
- package/src/plugins/runtime.ts +194 -0
- package/src/plugins/types.ts +40 -0
- package/src/store/chats.ts +278 -0
- package/src/store/models.ts +120 -0
- package/src/store/settings.ts +445 -0
- package/src/store/ui.ts +132 -0
- package/src/store/workspace.ts +299 -0
- package/src/types.ts +337 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +83 -0
- package/tsconfig.json +22 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'
|
|
2
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
// OpenSol keeps its data in ~/.opensol. An existing ~/.maxcode (from before the
|
|
8
|
+
// rename) keeps being used so saved keys, chats and plugins are not orphaned.
|
|
9
|
+
function resolveDataDir() {
|
|
10
|
+
const override = process.env.OPENSOL_DATA || process.env.MAXCODE_DATA
|
|
11
|
+
if (override) return path.resolve(override)
|
|
12
|
+
const next = path.join(os.homedir(), '.opensol')
|
|
13
|
+
const legacy = path.join(os.homedir(), '.maxcode')
|
|
14
|
+
return !existsSync(next) && existsSync(legacy) ? legacy : next
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const DATA_DIR = resolveDataDir()
|
|
18
|
+
|
|
19
|
+
const STATE_FILE = path.join(DATA_DIR, 'state.json')
|
|
20
|
+
const SECRETS_FILE = path.join(DATA_DIR, 'secrets.enc')
|
|
21
|
+
const MASTER_FILE = path.join(DATA_DIR, 'master.key')
|
|
22
|
+
const MODELS_CACHE = path.join(DATA_DIR, 'models-cache.json')
|
|
23
|
+
const AAD = Buffer.from('maxcode-secrets-v1', 'utf8')
|
|
24
|
+
|
|
25
|
+
export const STATE_VERSION = 2
|
|
26
|
+
|
|
27
|
+
const DEFAULT_STATE = () => ({
|
|
28
|
+
version: STATE_VERSION,
|
|
29
|
+
createdAt: new Date().toISOString(),
|
|
30
|
+
settings: {},
|
|
31
|
+
chats: { sessions: [], activeId: null },
|
|
32
|
+
workspaces: [],
|
|
33
|
+
activeWorkspace: null,
|
|
34
|
+
legacyMigratedAt: null,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const EMPTY_SECRETS = () => ({
|
|
38
|
+
providers: {},
|
|
39
|
+
search: {},
|
|
40
|
+
deploy: {
|
|
41
|
+
cloudflare: {},
|
|
42
|
+
netlify: {},
|
|
43
|
+
vercel: {},
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const queues = new Map()
|
|
48
|
+
let stateCache = null
|
|
49
|
+
let secretsCache = null
|
|
50
|
+
let masterKeyCache = null
|
|
51
|
+
|
|
52
|
+
export async function ensureDataDir() {
|
|
53
|
+
await mkdir(DATA_DIR, { recursive: true, mode: 0o700 })
|
|
54
|
+
await chmod(DATA_DIR, 0o700).catch(() => undefined)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function enqueue(key, task) {
|
|
58
|
+
const previous = queues.get(key) ?? Promise.resolve()
|
|
59
|
+
const next = previous.then(task, task)
|
|
60
|
+
// The tail never rejects (callers handle `next`), so a failed write cannot
|
|
61
|
+
// surface as an unhandled rejection, and the entry is actually cleaned up.
|
|
62
|
+
const tail = next.then(
|
|
63
|
+
() => undefined,
|
|
64
|
+
() => undefined,
|
|
65
|
+
)
|
|
66
|
+
queues.set(key, tail)
|
|
67
|
+
void tail.then(() => {
|
|
68
|
+
if (queues.get(key) === tail) queues.delete(key)
|
|
69
|
+
})
|
|
70
|
+
return next
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readJson(file, fallback) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(await readFile(file, 'utf8'))
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return fallback
|
|
78
|
+
if (error instanceof SyntaxError) {
|
|
79
|
+
const backup = `${file}.broken-${Date.now()}`
|
|
80
|
+
await rename(file, backup).catch(() => undefined)
|
|
81
|
+
throw new Error(`OpenSol data is damaged. The file was moved to ${backup}.`)
|
|
82
|
+
}
|
|
83
|
+
throw error
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function writeAtomic(file, value, mode = 0o600) {
|
|
88
|
+
const temp = `${file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`
|
|
89
|
+
await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { mode })
|
|
90
|
+
await chmod(temp, mode).catch(() => undefined)
|
|
91
|
+
await rename(temp, file)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mergeLegacySettings(current, legacy) {
|
|
95
|
+
if (!legacy || typeof legacy !== 'object') return current
|
|
96
|
+
const merged = { ...legacy, ...current }
|
|
97
|
+
merged.providers = { ...(legacy.providers ?? {}), ...(current.providers ?? {}) }
|
|
98
|
+
merged.tools = { ...(legacy.tools ?? {}), ...(current.tools ?? {}) }
|
|
99
|
+
if (!Array.isArray(current.skills) || current.skills.length === 0) {
|
|
100
|
+
if (Array.isArray(legacy.skills)) merged.skills = legacy.skills
|
|
101
|
+
}
|
|
102
|
+
return merged
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function loadState() {
|
|
106
|
+
if (stateCache) return stateCache
|
|
107
|
+
await ensureDataDir()
|
|
108
|
+
const parsed = await readJson(STATE_FILE, null)
|
|
109
|
+
const base = DEFAULT_STATE()
|
|
110
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
111
|
+
stateCache = base
|
|
112
|
+
return stateCache
|
|
113
|
+
}
|
|
114
|
+
stateCache = {
|
|
115
|
+
...base,
|
|
116
|
+
...parsed,
|
|
117
|
+
version: STATE_VERSION,
|
|
118
|
+
settings:
|
|
119
|
+
parsed.settings && typeof parsed.settings === 'object' ? parsed.settings : {},
|
|
120
|
+
chats:
|
|
121
|
+
parsed.chats && Array.isArray(parsed.chats.sessions)
|
|
122
|
+
? {
|
|
123
|
+
sessions: parsed.chats.sessions,
|
|
124
|
+
activeId: typeof parsed.chats.activeId === 'string' ? parsed.chats.activeId : null,
|
|
125
|
+
}
|
|
126
|
+
: base.chats,
|
|
127
|
+
workspaces: Array.isArray(parsed.workspaces)
|
|
128
|
+
? parsed.workspaces
|
|
129
|
+
.filter((entry) => entry && typeof entry.path === 'string')
|
|
130
|
+
.slice(0, 20)
|
|
131
|
+
: [],
|
|
132
|
+
activeWorkspace: typeof parsed.activeWorkspace === 'string' ? parsed.activeWorkspace : null,
|
|
133
|
+
}
|
|
134
|
+
return stateCache
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function saveState(next) {
|
|
138
|
+
stateCache = next
|
|
139
|
+
await enqueue('state', () => writeAtomic(STATE_FILE, next))
|
|
140
|
+
return next
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function patchState(patch) {
|
|
144
|
+
const state = await loadState()
|
|
145
|
+
const next = { ...state, ...patch }
|
|
146
|
+
return saveState(next)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function cachedState() {
|
|
150
|
+
return stateCache
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function masterKey() {
|
|
154
|
+
if (masterKeyCache) return masterKeyCache
|
|
155
|
+
await ensureDataDir()
|
|
156
|
+
try {
|
|
157
|
+
const raw = (await readFile(MASTER_FILE, 'utf8')).trim()
|
|
158
|
+
const key = Buffer.from(raw, 'base64')
|
|
159
|
+
if (key.length !== 32) throw new Error('The local master key is invalid.')
|
|
160
|
+
masterKeyCache = key
|
|
161
|
+
return key
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (!error || error.code !== 'ENOENT') throw error
|
|
164
|
+
const key = randomBytes(32)
|
|
165
|
+
await writeFile(MASTER_FILE, `${key.toString('base64')}\n`, { mode: 0o600 })
|
|
166
|
+
await chmod(MASTER_FILE, 0o600).catch(() => undefined)
|
|
167
|
+
masterKeyCache = key
|
|
168
|
+
return key
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function loadSecrets() {
|
|
173
|
+
if (secretsCache) return secretsCache
|
|
174
|
+
await ensureDataDir()
|
|
175
|
+
const envelope = await readJson(SECRETS_FILE, null)
|
|
176
|
+
if (!envelope) {
|
|
177
|
+
secretsCache = EMPTY_SECRETS()
|
|
178
|
+
return secretsCache
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const key = await masterKey()
|
|
182
|
+
const iv = Buffer.from(envelope.iv, 'base64')
|
|
183
|
+
const tag = Buffer.from(envelope.tag, 'base64')
|
|
184
|
+
const data = Buffer.from(envelope.data, 'base64')
|
|
185
|
+
if (iv.length !== 12 || tag.length !== 16) throw new Error('Invalid encrypted file.')
|
|
186
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv)
|
|
187
|
+
decipher.setAAD(AAD)
|
|
188
|
+
decipher.setAuthTag(tag)
|
|
189
|
+
const plain = Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8')
|
|
190
|
+
const parsed = JSON.parse(plain)
|
|
191
|
+
secretsCache = {
|
|
192
|
+
...EMPTY_SECRETS(),
|
|
193
|
+
...parsed,
|
|
194
|
+
providers: { ...(parsed.providers ?? {}) },
|
|
195
|
+
search: { ...(parsed.search ?? {}) },
|
|
196
|
+
deploy: {
|
|
197
|
+
cloudflare: { ...(parsed.deploy?.cloudflare ?? {}) },
|
|
198
|
+
netlify: { ...(parsed.deploy?.netlify ?? {}) },
|
|
199
|
+
vercel: { ...(parsed.deploy?.vercel ?? {}) },
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
return secretsCache
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`OpenSol could not decrypt its saved keys. Restore ${MASTER_FILE} or reset secrets from Settings → About. (${error instanceof Error ? error.message : 'unknown error'})`,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function loadSecretsSafe() {
|
|
211
|
+
try {
|
|
212
|
+
return { secrets: await loadSecrets(), error: '' }
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return { secrets: EMPTY_SECRETS(), error: error instanceof Error ? error.message : 'Could not read saved keys.' }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function saveSecrets(next) {
|
|
219
|
+
await ensureDataDir()
|
|
220
|
+
const key = await masterKey()
|
|
221
|
+
const iv = randomBytes(12)
|
|
222
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv)
|
|
223
|
+
cipher.setAAD(AAD)
|
|
224
|
+
const data = Buffer.concat([cipher.update(JSON.stringify(next), 'utf8'), cipher.final()])
|
|
225
|
+
const tag = cipher.getAuthTag()
|
|
226
|
+
const envelope = {
|
|
227
|
+
version: 1,
|
|
228
|
+
algorithm: 'aes-256-gcm',
|
|
229
|
+
iv: iv.toString('base64'),
|
|
230
|
+
tag: tag.toString('base64'),
|
|
231
|
+
data: data.toString('base64'),
|
|
232
|
+
}
|
|
233
|
+
secretsCache = next
|
|
234
|
+
await enqueue('secrets', () => writeAtomic(SECRETS_FILE, envelope))
|
|
235
|
+
return next
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function patchSecrets(patch) {
|
|
239
|
+
const secrets = await loadSecrets()
|
|
240
|
+
const next = {
|
|
241
|
+
...secrets,
|
|
242
|
+
...patch,
|
|
243
|
+
providers: { ...secrets.providers, ...(patch.providers ?? {}) },
|
|
244
|
+
search: { ...secrets.search, ...(patch.search ?? {}) },
|
|
245
|
+
deploy: {
|
|
246
|
+
cloudflare: { ...secrets.deploy.cloudflare, ...(patch.deploy?.cloudflare ?? {}) },
|
|
247
|
+
netlify: { ...secrets.deploy.netlify, ...(patch.deploy?.netlify ?? {}) },
|
|
248
|
+
vercel: { ...secrets.deploy.vercel, ...(patch.deploy?.vercel ?? {}) },
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
return saveSecrets(next)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function hintFor(value) {
|
|
255
|
+
const text = typeof value === 'string' ? value.trim() : ''
|
|
256
|
+
if (!text) return ''
|
|
257
|
+
if (text.length <= 8) return '••••'
|
|
258
|
+
return `••••${text.slice(-4)}`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function secretSummary(secrets) {
|
|
262
|
+
const providers = {}
|
|
263
|
+
for (const [id, value] of Object.entries(secrets.providers ?? {})) {
|
|
264
|
+
const apiKey = typeof value?.apiKey === 'string' ? value.apiKey : ''
|
|
265
|
+
providers[id] = { configured: Boolean(apiKey), hint: hintFor(apiKey) }
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
providers,
|
|
269
|
+
search: {
|
|
270
|
+
configured: Boolean(secrets.search?.apiKey),
|
|
271
|
+
hint: hintFor(secrets.search?.apiKey),
|
|
272
|
+
},
|
|
273
|
+
deploy: {
|
|
274
|
+
cloudflare: {
|
|
275
|
+
configured: Boolean(secrets.deploy?.cloudflare?.apiToken && secrets.deploy?.cloudflare?.accountId),
|
|
276
|
+
hint: hintFor(secrets.deploy?.cloudflare?.apiToken),
|
|
277
|
+
fields: {
|
|
278
|
+
accountId: secrets.deploy?.cloudflare?.accountId ?? '',
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
netlify: {
|
|
282
|
+
configured: Boolean(secrets.deploy?.netlify?.authToken),
|
|
283
|
+
hint: hintFor(secrets.deploy?.netlify?.authToken),
|
|
284
|
+
fields: {
|
|
285
|
+
siteId: secrets.deploy?.netlify?.siteId ?? '',
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
vercel: {
|
|
289
|
+
configured: Boolean(secrets.deploy?.vercel?.token),
|
|
290
|
+
hint: hintFor(secrets.deploy?.vercel?.token),
|
|
291
|
+
fields: {
|
|
292
|
+
projectId: secrets.deploy?.vercel?.projectId ?? '',
|
|
293
|
+
teamId: secrets.deploy?.vercel?.teamId ?? '',
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
encryption: 'AES-256-GCM',
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function providerKey(providerId) {
|
|
302
|
+
const { secrets } = await loadSecretsSafe()
|
|
303
|
+
return secrets.providers?.[providerId]?.apiKey ?? ''
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function setProviderSecret(providerId, apiKey) {
|
|
307
|
+
const secrets = await loadSecrets()
|
|
308
|
+
const providers = { ...secrets.providers }
|
|
309
|
+
const clean = typeof apiKey === 'string' ? apiKey.trim() : ''
|
|
310
|
+
if (clean) providers[providerId] = { ...(providers[providerId] ?? {}), apiKey: clean }
|
|
311
|
+
else delete providers[providerId]
|
|
312
|
+
await saveSecrets({ ...secrets, providers })
|
|
313
|
+
return { configured: Boolean(clean), hint: hintFor(clean) }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function setSearchSecret(apiKey) {
|
|
317
|
+
const secrets = await loadSecrets()
|
|
318
|
+
const clean = typeof apiKey === 'string' ? apiKey.trim() : ''
|
|
319
|
+
await saveSecrets({ ...secrets, search: clean ? { apiKey: clean } : {} })
|
|
320
|
+
return { configured: Boolean(clean), hint: hintFor(clean) }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function setDeploySecret(provider, patch) {
|
|
324
|
+
if (!['cloudflare', 'netlify', 'vercel'].includes(provider)) {
|
|
325
|
+
throw new Error('Unknown deploy provider.')
|
|
326
|
+
}
|
|
327
|
+
const secrets = await loadSecrets()
|
|
328
|
+
const current = { ...(secrets.deploy?.[provider] ?? {}) }
|
|
329
|
+
const next = { ...current }
|
|
330
|
+
for (const [key, value] of Object.entries(patch ?? {})) {
|
|
331
|
+
if (typeof value !== 'string') continue
|
|
332
|
+
const clean = value.trim()
|
|
333
|
+
if (clean) next[key] = clean
|
|
334
|
+
else delete next[key]
|
|
335
|
+
}
|
|
336
|
+
const deploy = { ...secrets.deploy, [provider]: next }
|
|
337
|
+
await saveSecrets({ ...secrets, deploy })
|
|
338
|
+
return secretSummary({ ...secrets, deploy }).deploy[provider]
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function addWorkspace(rawPath) {
|
|
342
|
+
const state = await loadState()
|
|
343
|
+
const resolved = path.resolve(rawPath)
|
|
344
|
+
const name = path.basename(resolved) || resolved
|
|
345
|
+
const entry = { path: resolved, name, lastOpened: Date.now() }
|
|
346
|
+
const workspaces = [entry, ...state.workspaces.filter((item) => item.path !== resolved)].slice(0, 12)
|
|
347
|
+
await saveState({ ...state, workspaces, activeWorkspace: resolved })
|
|
348
|
+
return entry
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function forgetWorkspace() {
|
|
352
|
+
const state = await loadState()
|
|
353
|
+
await saveState({ ...state, activeWorkspace: null })
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function saveSettings(settings) {
|
|
357
|
+
const state = await loadState()
|
|
358
|
+
await saveState({ ...state, settings })
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function saveChats(chats) {
|
|
362
|
+
const state = await loadState()
|
|
363
|
+
await saveState({
|
|
364
|
+
...state,
|
|
365
|
+
chats: {
|
|
366
|
+
sessions: Array.isArray(chats.sessions) ? chats.sessions : [],
|
|
367
|
+
activeId: typeof chats.activeId === 'string' ? chats.activeId : null,
|
|
368
|
+
},
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export async function migrateLegacy(payload) {
|
|
373
|
+
const state = await loadState()
|
|
374
|
+
const legacySettings =
|
|
375
|
+
payload?.settings && typeof payload.settings === 'object'
|
|
376
|
+
? payload.settings.state && typeof payload.settings.state === 'object'
|
|
377
|
+
? payload.settings.state
|
|
378
|
+
: payload.settings
|
|
379
|
+
: {}
|
|
380
|
+
const legacyChats =
|
|
381
|
+
payload?.chats && typeof payload.chats === 'object'
|
|
382
|
+
? Array.isArray(payload.chats.state?.sessions)
|
|
383
|
+
? payload.chats.state
|
|
384
|
+
: payload.chats
|
|
385
|
+
: {}
|
|
386
|
+
|
|
387
|
+
let changed = false
|
|
388
|
+
const nextSettings = mergeLegacySettings(state.settings ?? {}, legacySettings)
|
|
389
|
+
|
|
390
|
+
const { secrets } = await loadSecretsSafe()
|
|
391
|
+
const nextSecrets = {
|
|
392
|
+
...secrets,
|
|
393
|
+
providers: { ...(secrets.providers ?? {}) },
|
|
394
|
+
search: { ...(secrets.search ?? {}) },
|
|
395
|
+
deploy: {
|
|
396
|
+
cloudflare: { ...(secrets.deploy?.cloudflare ?? {}) },
|
|
397
|
+
netlify: { ...(secrets.deploy?.netlify ?? {}) },
|
|
398
|
+
vercel: { ...(secrets.deploy?.vercel ?? {}) },
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const [provider, value] of Object.entries(legacySettings.providers ?? {})) {
|
|
403
|
+
if (!value || typeof value !== 'object') continue
|
|
404
|
+
const baseUrl = typeof value.baseUrl === 'string' ? value.baseUrl.trim() : ''
|
|
405
|
+
if (baseUrl) {
|
|
406
|
+
nextSettings.providers = {
|
|
407
|
+
...(nextSettings.providers ?? {}),
|
|
408
|
+
[provider]: { ...(nextSettings.providers?.[provider] ?? {}), baseUrl },
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const apiKey = typeof value.apiKey === 'string' ? value.apiKey.trim() : ''
|
|
412
|
+
if (apiKey && !nextSecrets.providers[provider]?.apiKey) {
|
|
413
|
+
nextSecrets.providers[provider] = { apiKey }
|
|
414
|
+
changed = true
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const searchKey =
|
|
418
|
+
typeof legacySettings.searchApiKey === 'string' ? legacySettings.searchApiKey.trim() : ''
|
|
419
|
+
if (searchKey && !nextSecrets.search.apiKey) {
|
|
420
|
+
nextSecrets.search = { apiKey: searchKey }
|
|
421
|
+
changed = true
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
let addedChats = 0
|
|
425
|
+
const legacySessions = Array.isArray(legacyChats.sessions) ? legacyChats.sessions : []
|
|
426
|
+
let chats = state.chats
|
|
427
|
+
if (state.chats.sessions.length === 0 && legacySessions.length > 0) {
|
|
428
|
+
const activeId =
|
|
429
|
+
typeof legacyChats.activeId === 'string' &&
|
|
430
|
+
legacySessions.some((session) => session?.id === legacyChats.activeId)
|
|
431
|
+
? legacyChats.activeId
|
|
432
|
+
: legacySessions[0]?.id ?? null
|
|
433
|
+
chats = { sessions: legacySessions, activeId }
|
|
434
|
+
addedChats = legacySessions.length
|
|
435
|
+
changed = true
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const settingsChanged =
|
|
439
|
+
JSON.stringify(nextSettings) !== JSON.stringify(state.settings ?? {})
|
|
440
|
+
if (changed) await saveSecrets(nextSecrets)
|
|
441
|
+
const next = {
|
|
442
|
+
...state,
|
|
443
|
+
settings: nextSettings,
|
|
444
|
+
chats,
|
|
445
|
+
legacyMigratedAt: state.legacyMigratedAt ?? new Date().toISOString(),
|
|
446
|
+
}
|
|
447
|
+
if (settingsChanged || addedChats > 0 || !state.legacyMigratedAt) await saveState(next)
|
|
448
|
+
return {
|
|
449
|
+
migratedSettings: settingsChanged,
|
|
450
|
+
migratedChats: addedChats,
|
|
451
|
+
migratedSecrets: changed,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function resetAll({ keepMaster = true } = {}) {
|
|
456
|
+
stateCache = null
|
|
457
|
+
secretsCache = null
|
|
458
|
+
await rm(STATE_FILE, { force: true })
|
|
459
|
+
await rm(SECRETS_FILE, { force: true })
|
|
460
|
+
if (!keepMaster) {
|
|
461
|
+
masterKeyCache = null
|
|
462
|
+
await rm(MASTER_FILE, { force: true })
|
|
463
|
+
}
|
|
464
|
+
await rm(MODELS_CACHE, { force: true })
|
|
465
|
+
const fresh = DEFAULT_STATE()
|
|
466
|
+
await saveState(fresh)
|
|
467
|
+
return fresh
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export async function resetSecrets() {
|
|
471
|
+
secretsCache = null
|
|
472
|
+
masterKeyCache = null
|
|
473
|
+
await rm(SECRETS_FILE, { force: true })
|
|
474
|
+
await rm(MASTER_FILE, { force: true })
|
|
475
|
+
const empty = EMPTY_SECRETS()
|
|
476
|
+
await saveSecrets(empty)
|
|
477
|
+
return empty
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export async function loadModelsCache() {
|
|
481
|
+
return readJson(MODELS_CACHE, null)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export async function saveModelsCache(value) {
|
|
485
|
+
await ensureDataDir()
|
|
486
|
+
await enqueue('models-cache', () => writeAtomic(MODELS_CACHE, value, 0o600))
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function dataPaths() {
|
|
490
|
+
return { dataDir: DATA_DIR, stateFile: STATE_FILE, secretsFile: SECRETS_FILE, masterFile: MASTER_FILE }
|
|
491
|
+
}
|