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.
- package/bin/index.js.map +9 -9
- package/bin/worker.js.map +7 -7
- package/mcp/servicenow-unified.js +116 -116
- package/package.json +1 -1
- package/parsers-config.ts +2 -1
- package/src/bun/index.ts +10 -9
- package/src/cli/cmd/agent.ts +3 -3
- package/src/cli/cmd/auth.ts +46 -0
- package/src/cli/cmd/import.ts +2 -2
- package/src/cli/cmd/session.ts +9 -12
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +2 -1
- package/src/cli/cmd/tui/component/prompt/index.tsx +19 -6
- package/src/cli/cmd/tui/component/spinner.tsx +24 -0
- package/src/cli/cmd/tui/context/exit.tsx +1 -1
- package/src/cli/cmd/tui/routes/home.tsx +16 -2
- package/src/cli/cmd/tui/routes/session/index.tsx +122 -53
- package/src/cli/cmd/tui/routes/session/permission.tsx +9 -1
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +9 -1
- package/src/cli/cmd/tui/thread.ts +4 -1
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +1 -1
- package/src/cli/cmd/tui/util/clipboard.ts +3 -3
- package/src/cli/cmd/tui/worker.ts +6 -1
- package/src/config/config.ts +28 -0
- package/src/context/context-db.ts +437 -0
- package/src/format/formatter.ts +14 -5
- package/src/global/index.ts +3 -4
- package/src/mcp/index.ts +7 -2
- package/src/mcp/oauth-callback.ts +7 -15
- package/src/mcp/oauth-provider.ts +34 -3
- package/src/project/project.ts +8 -4
- package/src/provider/models.ts +1 -1
- package/src/provider/provider.ts +88 -9
- package/src/provider/transform.ts +7 -2
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_capacity_plan.ts +20 -7
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_retrospective.ts +6 -8
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_sprint_manage.ts +46 -28
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_team_manage.ts +53 -41
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_velocity_report.ts +8 -1
- package/src/servicenow/servicenow-mcp-unified/tools/automation/snow_schedule_script_job.ts +388 -243
- package/src/session/compaction.ts +126 -23
- package/src/session/message-v2.ts +33 -10
- package/src/session/processor.ts +29 -17
- package/src/session/prompt.ts +34 -6
- package/src/share/share-next.ts +2 -2
- package/src/shell/shell.ts +2 -1
- package/src/tool/edit.ts +15 -1
- package/src/tool/registry.ts +9 -1
- package/src/tool/truncation.ts +17 -0
- package/src/tool/websearch.ts +1 -1
- package/src/tool/websearch.txt +2 -2
- package/src/tool/write.ts +3 -4
- package/src/util/filesystem.ts +36 -7
- package/src/util/keybind.ts +1 -1
- package/src/util/log.ts +8 -5
- package/src/util/token.ts +28 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/fixture/fixture.ts +3 -0
- package/test/mcp/oauth-auto-connect.test.ts +197 -0
- package/test/project/project.test.ts +47 -0
- package/test/provider/provider.test.ts +2 -0
- package/test/provider/transform.test.ts +32 -0
- 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
|
|
45
|
-
|
|
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
|
|
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
|
}
|
package/src/config/config.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/format/formatter.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/global/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
})
|