snow-flow 10.0.185 → 10.0.186-dev.682

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.
Files changed (62) hide show
  1. package/bin/index.js.map +9 -9
  2. package/bin/worker.js.map +7 -7
  3. package/mcp/servicenow-unified.js +116 -116
  4. package/package.json +1 -1
  5. package/parsers-config.ts +2 -1
  6. package/src/bun/index.ts +10 -9
  7. package/src/cli/cmd/agent.ts +3 -3
  8. package/src/cli/cmd/auth.ts +46 -0
  9. package/src/cli/cmd/import.ts +2 -2
  10. package/src/cli/cmd/session.ts +9 -12
  11. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +2 -1
  12. package/src/cli/cmd/tui/component/prompt/index.tsx +19 -6
  13. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  14. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  15. package/src/cli/cmd/tui/routes/home.tsx +16 -2
  16. package/src/cli/cmd/tui/routes/session/index.tsx +122 -53
  17. package/src/cli/cmd/tui/routes/session/permission.tsx +9 -1
  18. package/src/cli/cmd/tui/routes/session/sidebar.tsx +9 -1
  19. package/src/cli/cmd/tui/thread.ts +4 -1
  20. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +1 -1
  21. package/src/cli/cmd/tui/util/clipboard.ts +3 -3
  22. package/src/cli/cmd/tui/worker.ts +6 -1
  23. package/src/config/config.ts +28 -0
  24. package/src/context/context-db.ts +437 -0
  25. package/src/format/formatter.ts +14 -5
  26. package/src/global/index.ts +3 -4
  27. package/src/mcp/index.ts +7 -2
  28. package/src/mcp/oauth-callback.ts +7 -15
  29. package/src/mcp/oauth-provider.ts +34 -3
  30. package/src/project/project.ts +8 -4
  31. package/src/provider/models.ts +1 -1
  32. package/src/provider/provider.ts +88 -9
  33. package/src/provider/transform.ts +7 -2
  34. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_capacity_plan.ts +20 -7
  35. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_retrospective.ts +6 -8
  36. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_sprint_manage.ts +46 -28
  37. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_team_manage.ts +53 -41
  38. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_velocity_report.ts +8 -1
  39. package/src/servicenow/servicenow-mcp-unified/tools/automation/snow_schedule_script_job.ts +388 -243
  40. package/src/session/compaction.ts +126 -23
  41. package/src/session/message-v2.ts +33 -10
  42. package/src/session/processor.ts +29 -17
  43. package/src/session/prompt.ts +34 -6
  44. package/src/share/share-next.ts +2 -2
  45. package/src/shell/shell.ts +2 -1
  46. package/src/tool/edit.ts +15 -1
  47. package/src/tool/registry.ts +9 -1
  48. package/src/tool/truncation.ts +17 -0
  49. package/src/tool/websearch.ts +1 -1
  50. package/src/tool/websearch.txt +2 -2
  51. package/src/tool/write.ts +3 -4
  52. package/src/util/filesystem.ts +36 -7
  53. package/src/util/keybind.ts +1 -1
  54. package/src/util/log.ts +8 -5
  55. package/src/util/token.ts +28 -0
  56. package/test/cli/plugin-auth-picker.test.ts +120 -0
  57. package/test/fixture/fixture.ts +3 -0
  58. package/test/mcp/oauth-auto-connect.test.ts +197 -0
  59. package/test/project/project.test.ts +47 -0
  60. package/test/provider/provider.test.ts +2 -0
  61. package/test/provider/transform.test.ts +32 -0
  62. package/test/tool/edit.test.ts +679 -0
@@ -56,7 +56,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
56
56
  setStore("active", order[nextIndex])
57
57
  evt.preventDefault()
58
58
  }
59
- if (evt.name === "space") {
59
+ if (evt.name === "space" || evt.name === " ") {
60
60
  if (store.active === "thinking") setStore("thinking", !store.thinking)
61
61
  if (store.active === "toolDetails") setStore("toolDetails", !store.toolDetails)
62
62
  if (store.active === "assistantMetadata") setStore("assistantMetadata", !store.assistantMetadata)
@@ -7,6 +7,7 @@ import { mkdtempSync } from "fs"
7
7
  import { rmSync } from "fs"
8
8
  import { randomBytes } from "crypto"
9
9
  import path from "path"
10
+ import { Filesystem } from "../../../../util/filesystem"
10
11
 
11
12
  /**
12
13
  * Writes text to clipboard via OSC 52 escape sequence.
@@ -41,9 +42,8 @@ export namespace Clipboard {
41
42
  await $`osascript -e 'set imageData to the clipboard as "PNGf"' -e 'set fileRef to open for access POSIX file "${tmpfile}" with write permission' -e 'set eof fileRef to 0' -e 'write imageData to fileRef' -e 'close access fileRef'`
42
43
  .nothrow()
43
44
  .quiet()
44
- const file = Bun.file(tmpfile)
45
- const buffer = await file.arrayBuffer()
46
- return { data: Buffer.from(buffer).toString("base64"), mime: "image/png" }
45
+ const buffer = await Filesystem.readBytes(tmpfile)
46
+ return { data: buffer.toString("base64"), mime: "image/png" }
47
47
  } catch {
48
48
  } finally {
49
49
  try {
@@ -161,7 +161,12 @@ export const rpc = {
161
161
  async shutdown() {
162
162
  Log.Default.info("worker shutting down")
163
163
  if (eventStream.abort) eventStream.abort.abort()
164
- await Instance.disposeAll()
164
+ await Promise.race([
165
+ Instance.disposeAll(),
166
+ new Promise((resolve) => {
167
+ setTimeout(resolve, 5000)
168
+ }),
169
+ ])
165
170
  if (server) server.stop(true)
166
171
  },
167
172
  }
@@ -1138,6 +1138,14 @@ export namespace Config {
1138
1138
  .describe(
1139
1139
  "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
1140
1140
  ),
1141
+ chunkTimeout: z
1142
+ .number()
1143
+ .int()
1144
+ .positive()
1145
+ .optional()
1146
+ .describe(
1147
+ "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
1148
+ ),
1141
1149
  })
1142
1150
  .catchall(z.any())
1143
1151
  .optional(),
@@ -1319,6 +1327,26 @@ export namespace Config {
1319
1327
  prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"),
1320
1328
  })
1321
1329
  .optional(),
1330
+ contextdb: z
1331
+ .object({
1332
+ enabled: z
1333
+ .boolean()
1334
+ .optional()
1335
+ .describe("Enable the ContextDB layer for cross-session memory and retrieval (default: true)"),
1336
+ persist_summaries: z
1337
+ .boolean()
1338
+ .optional()
1339
+ .describe("Persist compaction summaries to SQLite for future retrieval (default: true)"),
1340
+ cross_session_retrieval: z
1341
+ .boolean()
1342
+ .optional()
1343
+ .describe("Retrieve relevant context from prior sessions when assembling prompts (default: true)"),
1344
+ max_db_size_mb: z
1345
+ .number()
1346
+ .optional()
1347
+ .describe("Maximum ContextDB size in megabytes before automatic cleanup (default: 500)"),
1348
+ })
1349
+ .optional(),
1322
1350
  experimental: z
1323
1351
  .object({
1324
1352
  hook: z
@@ -0,0 +1,437 @@
1
+ import { Database } from "bun:sqlite"
2
+ import path from "path"
3
+ import { Global } from "../global"
4
+ import { Config } from "../config/config"
5
+ import { Log } from "../util/log"
6
+ import { Token } from "../util/token"
7
+
8
+ export namespace ContextDB {
9
+ const log = Log.create({ service: "context-db" })
10
+
11
+ let instance: Database | undefined
12
+ let initPromise: Promise<Database | undefined> | undefined
13
+ const DB_PATH = path.join(Global.Path.data, "context.db")
14
+
15
+ export async function get(): Promise<Database | undefined> {
16
+ if (instance) return instance
17
+ if (initPromise) return initPromise
18
+
19
+ initPromise = (async () => {
20
+ const config = await Config.get()
21
+ if (config.contextdb?.enabled === false) return undefined
22
+
23
+ const db = new Database(DB_PATH)
24
+ db.exec("PRAGMA journal_mode = WAL")
25
+ db.exec("PRAGMA busy_timeout = 5000")
26
+ migrate(db)
27
+ instance = db
28
+ return db
29
+ })()
30
+
31
+ return initPromise
32
+ }
33
+
34
+ function migrate(db: Database) {
35
+ db.exec(`
36
+ CREATE TABLE IF NOT EXISTS compaction_summaries (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ session_id TEXT NOT NULL,
39
+ project_id TEXT NOT NULL,
40
+ directory TEXT NOT NULL,
41
+ summary_text TEXT NOT NULL,
42
+ files_mentioned TEXT DEFAULT '',
43
+ tools_used TEXT DEFAULT '',
44
+ created_at INTEGER NOT NULL
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS session_metadata (
48
+ session_id TEXT PRIMARY KEY,
49
+ project_id TEXT NOT NULL,
50
+ directory TEXT NOT NULL,
51
+ title TEXT,
52
+ created_at INTEGER NOT NULL,
53
+ updated_at INTEGER NOT NULL,
54
+ message_count INTEGER DEFAULT 0,
55
+ compaction_count INTEGER DEFAULT 0
56
+ );
57
+
58
+ CREATE TABLE IF NOT EXISTS tool_output_cache (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ session_id TEXT NOT NULL,
61
+ tool_name TEXT NOT NULL,
62
+ input_hash TEXT NOT NULL,
63
+ output_summary TEXT NOT NULL,
64
+ output_path TEXT,
65
+ created_at INTEGER NOT NULL
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS memory_entries (
69
+ id TEXT PRIMARY KEY,
70
+ project_id TEXT NOT NULL,
71
+ category TEXT NOT NULL,
72
+ content TEXT NOT NULL,
73
+ source_session_id TEXT,
74
+ created_at INTEGER NOT NULL,
75
+ accessed_at INTEGER NOT NULL,
76
+ access_count INTEGER DEFAULT 0
77
+ );
78
+
79
+ CREATE TABLE IF NOT EXISTS context_trajectory (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ session_id TEXT NOT NULL,
82
+ source_type TEXT NOT NULL,
83
+ source_id TEXT,
84
+ tokens_used INTEGER,
85
+ relevance_score REAL,
86
+ created_at INTEGER NOT NULL
87
+ );
88
+
89
+ CREATE INDEX IF NOT EXISTS idx_compaction_project ON compaction_summaries(project_id);
90
+ CREATE INDEX IF NOT EXISTS idx_compaction_session ON compaction_summaries(session_id);
91
+ CREATE INDEX IF NOT EXISTS idx_tool_cache_hash ON tool_output_cache(input_hash);
92
+ CREATE INDEX IF NOT EXISTS idx_memory_project ON memory_entries(project_id, category);
93
+ CREATE INDEX IF NOT EXISTS idx_trajectory_session ON context_trajectory(session_id);
94
+ `)
95
+
96
+ // FTS5 tables — external content backed by regular tables
97
+ db.exec(`
98
+ CREATE VIRTUAL TABLE IF NOT EXISTS compaction_fts USING fts5(
99
+ summary_text,
100
+ files_mentioned,
101
+ content=compaction_summaries,
102
+ content_rowid=id
103
+ );
104
+
105
+ CREATE VIRTUAL TABLE IF NOT EXISTS tool_output_fts USING fts5(
106
+ output_summary,
107
+ tool_name,
108
+ content=tool_output_cache,
109
+ content_rowid=id
110
+ );
111
+ `)
112
+
113
+ // Sync triggers for compaction_summaries → compaction_fts
114
+ db.exec(`
115
+ CREATE TRIGGER IF NOT EXISTS compaction_ai AFTER INSERT ON compaction_summaries BEGIN
116
+ INSERT INTO compaction_fts(rowid, summary_text, files_mentioned)
117
+ VALUES (new.id, new.summary_text, new.files_mentioned);
118
+ END;
119
+
120
+ CREATE TRIGGER IF NOT EXISTS compaction_ad AFTER DELETE ON compaction_summaries BEGIN
121
+ INSERT INTO compaction_fts(compaction_fts, rowid, summary_text, files_mentioned)
122
+ VALUES('delete', old.id, old.summary_text, COALESCE(old.files_mentioned, ''));
123
+ END;
124
+ `)
125
+
126
+ // Sync triggers for tool_output_cache → tool_output_fts
127
+ db.exec(`
128
+ CREATE TRIGGER IF NOT EXISTS tool_cache_ai AFTER INSERT ON tool_output_cache BEGIN
129
+ INSERT INTO tool_output_fts(rowid, output_summary, tool_name)
130
+ VALUES (new.id, new.output_summary, new.tool_name);
131
+ END;
132
+
133
+ CREATE TRIGGER IF NOT EXISTS tool_cache_ad AFTER DELETE ON tool_output_cache BEGIN
134
+ INSERT INTO tool_output_fts(tool_output_fts, rowid, output_summary, tool_name)
135
+ VALUES('delete', old.id, old.output_summary, COALESCE(old.tool_name, ''));
136
+ END;
137
+ `)
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // FTS5 query sanitization
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function sanitizeQuery(raw: string): string {
145
+ return raw
146
+ .replace(/[^\w\s]/g, " ")
147
+ .split(/\s+/)
148
+ .filter((w) => w.length > 2)
149
+ .slice(0, 12)
150
+ .join(" ")
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Compaction summaries
155
+ // ---------------------------------------------------------------------------
156
+
157
+ export interface SummaryInput {
158
+ sessionID: string
159
+ projectID: string
160
+ directory: string
161
+ summaryText: string
162
+ filesMentioned?: string
163
+ toolsUsed?: string
164
+ }
165
+
166
+ export function storeSummary(input: SummaryInput) {
167
+ const db = instance
168
+ if (!db) return
169
+ db.prepare(
170
+ `INSERT INTO compaction_summaries (session_id, project_id, directory, summary_text, files_mentioned, tools_used, created_at)
171
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
172
+ ).run(
173
+ input.sessionID,
174
+ input.projectID,
175
+ input.directory,
176
+ input.summaryText,
177
+ input.filesMentioned ?? "",
178
+ input.toolsUsed ?? "",
179
+ Date.now(),
180
+ )
181
+
182
+ db.prepare(
183
+ `INSERT INTO session_metadata (session_id, project_id, directory, created_at, updated_at, compaction_count)
184
+ VALUES (?, ?, ?, ?, ?, 1)
185
+ ON CONFLICT(session_id) DO UPDATE SET
186
+ updated_at = excluded.updated_at,
187
+ compaction_count = compaction_count + 1`,
188
+ ).run(input.sessionID, input.projectID, input.directory, Date.now(), Date.now())
189
+
190
+ log.info("stored compaction summary", { sessionID: input.sessionID })
191
+ }
192
+
193
+ export interface SearchResult {
194
+ sessionID: string
195
+ summaryText: string
196
+ filesMentioned: string
197
+ createdAt: number
198
+ rank: number
199
+ }
200
+
201
+ export function searchSummaries(input: {
202
+ query: string
203
+ projectID: string
204
+ excludeSessionID?: string
205
+ limit?: number
206
+ maxTokens?: number
207
+ }): SearchResult[] {
208
+ const db = instance
209
+ if (!db) return []
210
+
211
+ const q = sanitizeQuery(input.query)
212
+ if (!q) return []
213
+
214
+ const limit = input.limit ?? 3
215
+
216
+ type Row = { session_id: string; summary_text: string; files_mentioned: string; created_at: number; rank: number }
217
+
218
+ const rows: Row[] = input.excludeSessionID
219
+ ? (db
220
+ .prepare(
221
+ `SELECT cs.session_id, cs.summary_text, cs.files_mentioned, cs.created_at, rank
222
+ FROM compaction_fts
223
+ JOIN compaction_summaries cs ON compaction_fts.rowid = cs.id
224
+ WHERE compaction_fts MATCH ?1
225
+ AND cs.project_id = ?2
226
+ AND cs.session_id != ?3
227
+ ORDER BY rank
228
+ LIMIT ?4`,
229
+ )
230
+ .all(q, input.projectID, input.excludeSessionID, limit) as Row[])
231
+ : (db
232
+ .prepare(
233
+ `SELECT cs.session_id, cs.summary_text, cs.files_mentioned, cs.created_at, rank
234
+ FROM compaction_fts
235
+ JOIN compaction_summaries cs ON compaction_fts.rowid = cs.id
236
+ WHERE compaction_fts MATCH ?1
237
+ AND cs.project_id = ?2
238
+ ORDER BY rank
239
+ LIMIT ?3`,
240
+ )
241
+ .all(q, input.projectID, limit) as Row[])
242
+
243
+ const results: SearchResult[] = []
244
+ let totalTokens = 0
245
+
246
+ for (const r of rows) {
247
+ const tokens = Token.estimate(r.summary_text)
248
+ if (input.maxTokens && totalTokens + tokens > input.maxTokens) break
249
+ totalTokens += tokens
250
+ results.push({
251
+ sessionID: r.session_id,
252
+ summaryText: r.summary_text,
253
+ filesMentioned: r.files_mentioned,
254
+ createdAt: r.created_at,
255
+ rank: r.rank,
256
+ })
257
+ }
258
+ return results
259
+ }
260
+
261
+ // ---------------------------------------------------------------------------
262
+ // Cross-session context retrieval (for prompt assembly)
263
+ // ---------------------------------------------------------------------------
264
+
265
+ export async function retrieveContext(input: {
266
+ sessionID: string
267
+ projectID: string
268
+ userQuery: string
269
+ }): Promise<string[]> {
270
+ const config = await Config.get()
271
+ if (config.contextdb?.cross_session_retrieval === false) return []
272
+
273
+ const db = await get()
274
+ if (!db) return []
275
+
276
+ const results = searchSummaries({
277
+ query: input.userQuery,
278
+ projectID: input.projectID,
279
+ excludeSessionID: input.sessionID,
280
+ limit: 3,
281
+ maxTokens: 10_000,
282
+ })
283
+
284
+ if (results.length === 0) return []
285
+
286
+ logTrajectory({
287
+ sessionID: input.sessionID,
288
+ sourceType: "cross-session",
289
+ tokensUsed: results.reduce((sum, r) => sum + Token.estimate(r.summaryText), 0),
290
+ })
291
+
292
+ const block = results.map((r) => r.summaryText).join("\n---\n")
293
+ return [`<prior-session-context>\n${block}\n</prior-session-context>`]
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Tool output cache
298
+ // ---------------------------------------------------------------------------
299
+
300
+ export function storeToolOutput(input: {
301
+ sessionID: string
302
+ toolName: string
303
+ inputHash: string
304
+ outputSummary: string
305
+ outputPath?: string
306
+ }) {
307
+ const db = instance
308
+ if (!db) return
309
+ db.prepare(
310
+ `INSERT INTO tool_output_cache (session_id, tool_name, input_hash, output_summary, output_path, created_at)
311
+ VALUES (?, ?, ?, ?, ?, ?)`,
312
+ ).run(input.sessionID, input.toolName, input.inputHash, input.outputSummary, input.outputPath ?? null, Date.now())
313
+ }
314
+
315
+ export function findToolOutput(inputHash: string): { outputSummary: string; outputPath: string | null } | undefined {
316
+ const db = instance
317
+ if (!db) return undefined
318
+ const row = db
319
+ .prepare(
320
+ `SELECT output_summary, output_path FROM tool_output_cache
321
+ WHERE input_hash = ?
322
+ ORDER BY created_at DESC
323
+ LIMIT 1`,
324
+ )
325
+ .get(inputHash) as { output_summary: string; output_path: string | null } | null
326
+ if (!row) return undefined
327
+ return { outputSummary: row.output_summary, outputPath: row.output_path }
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Memory entries
332
+ // ---------------------------------------------------------------------------
333
+
334
+ export function storeMemory(input: {
335
+ id: string
336
+ projectID: string
337
+ category: string
338
+ content: string
339
+ sourceSessionID?: string
340
+ }) {
341
+ const db = instance
342
+ if (!db) return
343
+ const now = Date.now()
344
+ db.prepare(
345
+ `INSERT OR REPLACE INTO memory_entries (id, project_id, category, content, source_session_id, created_at, accessed_at, access_count)
346
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0)`,
347
+ ).run(input.id, input.projectID, input.category, input.content, input.sourceSessionID ?? null, now, now)
348
+ }
349
+
350
+ export function getMemories(
351
+ projectID: string,
352
+ category?: string,
353
+ ): Array<{ id: string; category: string; content: string; accessCount: number }> {
354
+ const db = instance
355
+ if (!db) return []
356
+
357
+ const rows = category
358
+ ? db
359
+ .prepare(
360
+ `SELECT id, category, content, access_count FROM memory_entries
361
+ WHERE project_id = ? AND category = ?
362
+ ORDER BY accessed_at DESC`,
363
+ )
364
+ .all(projectID, category)
365
+ : db
366
+ .prepare(
367
+ `SELECT id, category, content, access_count FROM memory_entries
368
+ WHERE project_id = ?
369
+ ORDER BY accessed_at DESC`,
370
+ )
371
+ .all(projectID)
372
+
373
+ return (rows as Array<{ id: string; category: string; content: string; access_count: number }>).map((r) => ({
374
+ id: r.id,
375
+ category: r.category,
376
+ content: r.content,
377
+ accessCount: r.access_count,
378
+ }))
379
+ }
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // Context trajectory (debugging / observability)
383
+ // ---------------------------------------------------------------------------
384
+
385
+ export function logTrajectory(input: {
386
+ sessionID: string
387
+ sourceType: string
388
+ sourceID?: string
389
+ tokensUsed?: number
390
+ relevanceScore?: number
391
+ }) {
392
+ const db = instance
393
+ if (!db) return
394
+ db.prepare(
395
+ `INSERT INTO context_trajectory (session_id, source_type, source_id, tokens_used, relevance_score, created_at)
396
+ VALUES (?, ?, ?, ?, ?, ?)`,
397
+ ).run(
398
+ input.sessionID,
399
+ input.sourceType,
400
+ input.sourceID ?? null,
401
+ input.tokensUsed ?? null,
402
+ input.relevanceScore ?? null,
403
+ Date.now(),
404
+ )
405
+ }
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // File reference extraction (helper for compaction integration)
409
+ // ---------------------------------------------------------------------------
410
+
411
+ export function extractFileReferences(text: string): string {
412
+ const matches = text.match(/(?:^|\s)((?:\/|\.\.?\/|src\/|packages\/)\S+\.\w{1,10})/gm)
413
+ if (!matches) return ""
414
+ const unique = [...new Set(matches.map((m) => m.trim()))]
415
+ return unique.join(" ")
416
+ }
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // Maintenance
420
+ // ---------------------------------------------------------------------------
421
+
422
+ export function cleanup(maxAgeDays = 90) {
423
+ const db = instance
424
+ if (!db) return
425
+ const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000
426
+ db.prepare("DELETE FROM compaction_summaries WHERE created_at < ?").run(cutoff)
427
+ db.prepare("DELETE FROM tool_output_cache WHERE created_at < ?").run(cutoff)
428
+ db.prepare("DELETE FROM context_trajectory WHERE created_at < ?").run(cutoff)
429
+ log.info("cleaned up old context data", { maxAgeDays })
430
+ }
431
+
432
+ export function close() {
433
+ instance?.close()
434
+ instance = undefined
435
+ initPromise = undefined
436
+ }
437
+ }
@@ -67,7 +67,10 @@ export const prettier: Info = {
67
67
  async enabled() {
68
68
  const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
69
69
  for (const item of items) {
70
- const json = await Bun.file(item).json()
70
+ const json = await Filesystem.readJson<{
71
+ dependencies?: Record<string, string>
72
+ devDependencies?: Record<string, string>
73
+ }>(item)
71
74
  if (json.dependencies?.prettier) return true
72
75
  if (json.devDependencies?.prettier) return true
73
76
  }
@@ -86,7 +89,10 @@ export const oxfmt: Info = {
86
89
  if (!Flag.OPENCODE_EXPERIMENTAL_OXFMT) return false
87
90
  const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
88
91
  for (const item of items) {
89
- const json = await Bun.file(item).json()
92
+ const json = await Filesystem.readJson<{
93
+ dependencies?: Record<string, string>
94
+ devDependencies?: Record<string, string>
95
+ }>(item)
90
96
  if (json.dependencies?.oxfmt) return true
91
97
  if (json.devDependencies?.oxfmt) return true
92
98
  }
@@ -179,7 +185,7 @@ export const ruff: Info = {
179
185
  const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
180
186
  if (found.length > 0) {
181
187
  if (config === "pyproject.toml") {
182
- const content = await Bun.file(found[0]).text()
188
+ const content = await Filesystem.readText(found[0])
183
189
  if (content.includes("[tool.ruff]")) return true
184
190
  } else {
185
191
  return true
@@ -190,7 +196,7 @@ export const ruff: Info = {
190
196
  for (const dep of deps) {
191
197
  const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
192
198
  if (found.length > 0) {
193
- const content = await Bun.file(found[0]).text()
199
+ const content = await Filesystem.readText(found[0])
194
200
  if (content.includes("ruff")) return true
195
201
  }
196
202
  }
@@ -348,7 +354,10 @@ export const pint: Info = {
348
354
  async enabled() {
349
355
  const items = await Filesystem.findUp("composer.json", Instance.directory, Instance.worktree)
350
356
  for (const item of items) {
351
- const json = await Bun.file(item).json()
357
+ const json = await Filesystem.readJson<{
358
+ require?: Record<string, string>
359
+ "require-dev"?: Record<string, string>
360
+ }>(item)
352
361
  if (json.require?.["laravel/pint"]) return true
353
362
  if (json["require-dev"]?.["laravel/pint"]) return true
354
363
  }
@@ -2,6 +2,7 @@ import fs from "fs/promises"
2
2
  import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
3
3
  import path from "path"
4
4
  import os from "os"
5
+ import { Filesystem } from "../util/filesystem"
5
6
 
6
7
  const app = "snow-code"
7
8
 
@@ -35,9 +36,7 @@ await Promise.all([
35
36
 
36
37
  const CACHE_VERSION = "19"
37
38
 
38
- const version = await Bun.file(path.join(Global.Path.cache, "version"))
39
- .text()
40
- .catch(() => "0")
39
+ const version = await Filesystem.readText(path.join(Global.Path.cache, "version")).catch(() => "0")
41
40
 
42
41
  if (version !== CACHE_VERSION) {
43
42
  try {
@@ -51,5 +50,5 @@ if (version !== CACHE_VERSION) {
51
50
  ),
52
51
  )
53
52
  } catch (e) {}
54
- await Bun.file(path.join(Global.Path.cache, "version")).write(CACHE_VERSION)
53
+ await Filesystem.write(path.join(Global.Path.cache, "version"), CACHE_VERSION)
55
54
  }
package/src/mcp/index.ts CHANGED
@@ -494,8 +494,13 @@ export namespace MCP {
494
494
  } catch (error) {
495
495
  lastError = error instanceof Error ? error : new Error(String(error))
496
496
 
497
- // Handle OAuth-specific errors
498
- if (error instanceof UnauthorizedError) {
497
+ // Handle OAuth-specific errors.
498
+ // The SDK throws UnauthorizedError when auth() returns 'REDIRECT',
499
+ // but may also throw plain Errors when auth() fails internally
500
+ // (e.g. during discovery, registration, or state generation).
501
+ // When an authProvider is attached, treat both cases as auth-related.
502
+ const isAuthError = error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth"))
503
+ if (isAuthError) {
499
504
  log.info("mcp server requires authentication", { key, transport: name })
500
505
 
501
506
  // Check if this is a "needs registration" error
@@ -1,3 +1,4 @@
1
+ import { createConnection } from "net"
1
2
  import { Log } from "../util/log"
2
3
  import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
3
4
 
@@ -170,21 +171,12 @@ export namespace McpOAuthCallback {
170
171
 
171
172
  export async function isPortInUse(): Promise<boolean> {
172
173
  return new Promise((resolve) => {
173
- Bun.connect({
174
- hostname: "127.0.0.1",
175
- port: OAUTH_CALLBACK_PORT,
176
- socket: {
177
- open(socket) {
178
- socket.end()
179
- resolve(true)
180
- },
181
- error() {
182
- resolve(false)
183
- },
184
- data() {},
185
- close() {},
186
- },
187
- }).catch(() => {
174
+ const socket = createConnection(OAUTH_CALLBACK_PORT, "127.0.0.1")
175
+ socket.on("connect", () => {
176
+ socket.destroy()
177
+ resolve(true)
178
+ })
179
+ socket.on("error", () => {
188
180
  resolve(false)
189
181
  })
190
182
  })