@omercnet/paseo-context-mode 0.0.0
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/LICENSE +21 -0
- package/NOTICE +7 -0
- package/README.md +74 -0
- package/client/actions-section.tsx +621 -0
- package/client/analytics-dashboard.tsx +721 -0
- package/client/context-mode-pill.tsx +106 -0
- package/client/context-mode-surface.tsx +414 -0
- package/client/knowledge-section.tsx +780 -0
- package/client/settings-screen.tsx +226 -0
- package/index.client.tsx +43 -0
- package/index.server.ts +54 -0
- package/package.json +72 -0
- package/paseo-plugin.json +5 -0
- package/server/actions.ts +146 -0
- package/server/analytics.ts +424 -0
- package/server/binary.ts +182 -0
- package/server/context-mode-db.ts +481 -0
- package/server/handlers.ts +200 -0
- package/server/integration.ts +441 -0
- package/server/knowledge.ts +187 -0
- package/server/mcp-process.ts +357 -0
- package/server/storage-discovery.ts +225 -0
- package/shared/actions.ts +57 -0
- package/shared/analytics.ts +134 -0
- package/shared/contracts.ts +95 -0
- package/shared/index.ts +5 -0
- package/shared/knowledge.ts +150 -0
- package/shared/settings.ts +51 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
|
+
import type { LaunchDescriptor } from "./binary";
|
|
3
|
+
|
|
4
|
+
export const PROCESS_OUTPUT_LIMIT = 192 * 1_024;
|
|
5
|
+
export const PROCESS_TIMEOUT_MS = 12_000;
|
|
6
|
+
export const PROCESS_TERMINATION_GRACE_MS = 500;
|
|
7
|
+
const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
8
|
+
|
|
9
|
+
export type ProcessFailureCode =
|
|
10
|
+
| "timeout"
|
|
11
|
+
| "output-limit"
|
|
12
|
+
| "launch-failed"
|
|
13
|
+
| "protocol-error"
|
|
14
|
+
| "unsupported"
|
|
15
|
+
| "command-failed";
|
|
16
|
+
|
|
17
|
+
export class ProcessFailure extends Error {
|
|
18
|
+
constructor(
|
|
19
|
+
readonly code: ProcessFailureCode,
|
|
20
|
+
message: string,
|
|
21
|
+
) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "ProcessFailure";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface McpInspection {
|
|
28
|
+
version: string | null;
|
|
29
|
+
tools: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface McpProcessDependencies {
|
|
33
|
+
spawnProcess?: typeof spawn;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
outputLimit?: number;
|
|
36
|
+
env?: NodeJS.ProcessEnv;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
terminationGraceMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type McpJsonValue =
|
|
42
|
+
| null
|
|
43
|
+
| boolean
|
|
44
|
+
| number
|
|
45
|
+
| string
|
|
46
|
+
| McpJsonValue[]
|
|
47
|
+
| { [key: string]: McpJsonValue };
|
|
48
|
+
|
|
49
|
+
export type McpToolArguments = Readonly<Record<string, McpJsonValue>>;
|
|
50
|
+
|
|
51
|
+
export type ContextModeToolName =
|
|
52
|
+
| "ctx_doctor"
|
|
53
|
+
| "ctx_stats"
|
|
54
|
+
| "ctx_upgrade"
|
|
55
|
+
| "ctx_search"
|
|
56
|
+
| "ctx_index"
|
|
57
|
+
| "ctx_fetch_and_index"
|
|
58
|
+
| "ctx_purge";
|
|
59
|
+
|
|
60
|
+
export const CONTEXT_MODE_TOOL_TIMEOUT_MS: Readonly<Record<ContextModeToolName, number>> = {
|
|
61
|
+
ctx_doctor: 15_000,
|
|
62
|
+
ctx_stats: 15_000,
|
|
63
|
+
ctx_search: 20_000,
|
|
64
|
+
ctx_index: 120_000,
|
|
65
|
+
ctx_fetch_and_index: 45_000,
|
|
66
|
+
ctx_purge: 120_000,
|
|
67
|
+
ctx_upgrade: 120_000,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type JsonRpcResponse = {
|
|
71
|
+
id?: number;
|
|
72
|
+
result?: unknown;
|
|
73
|
+
error?: { code?: number; message?: string };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function safeMessage(value: unknown): string {
|
|
77
|
+
if (value instanceof Error && value.message) return value.message;
|
|
78
|
+
return String(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractToolOutput(result: unknown): string {
|
|
82
|
+
if (!result || typeof result !== "object") {
|
|
83
|
+
throw new ProcessFailure("protocol-error", "Context Mode returned an invalid tool result.");
|
|
84
|
+
}
|
|
85
|
+
const value = result as {
|
|
86
|
+
content?: Array<{ type?: string; text?: unknown }>;
|
|
87
|
+
structuredContent?: unknown;
|
|
88
|
+
isError?: boolean;
|
|
89
|
+
};
|
|
90
|
+
const text = value.content
|
|
91
|
+
?.filter((item) => item?.type === "text" && typeof item.text === "string")
|
|
92
|
+
.map((item) => item.text as string)
|
|
93
|
+
.join("\n");
|
|
94
|
+
const output =
|
|
95
|
+
text && text.trim().length > 0
|
|
96
|
+
? text
|
|
97
|
+
: value.structuredContent === undefined
|
|
98
|
+
? ""
|
|
99
|
+
: JSON.stringify(value.structuredContent, null, 2);
|
|
100
|
+
if (value.isError) {
|
|
101
|
+
throw new ProcessFailure("command-failed", output || "Context Mode reported a tool error.");
|
|
102
|
+
}
|
|
103
|
+
return output;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function exchange(
|
|
107
|
+
launch: LaunchDescriptor,
|
|
108
|
+
operation:
|
|
109
|
+
| { kind: "inspect" }
|
|
110
|
+
| { kind: "tool"; name: ContextModeToolName; arguments: McpToolArguments },
|
|
111
|
+
dependencies: McpProcessDependencies,
|
|
112
|
+
): Promise<McpInspection | string> {
|
|
113
|
+
const spawnProcess = dependencies.spawnProcess ?? spawn;
|
|
114
|
+
const timeoutMs =
|
|
115
|
+
dependencies.timeoutMs ??
|
|
116
|
+
(operation.kind === "tool" ? CONTEXT_MODE_TOOL_TIMEOUT_MS[operation.name] : PROCESS_TIMEOUT_MS);
|
|
117
|
+
const terminationGraceMs = dependencies.terminationGraceMs ?? PROCESS_TERMINATION_GRACE_MS;
|
|
118
|
+
const outputLimit = dependencies.outputLimit ?? PROCESS_OUTPUT_LIMIT;
|
|
119
|
+
|
|
120
|
+
const { promise, resolve, reject } = Promise.withResolvers<McpInspection | string>();
|
|
121
|
+
let child: ChildProcessWithoutNullStreams;
|
|
122
|
+
try {
|
|
123
|
+
child = spawnProcess(launch.program, launch.args, {
|
|
124
|
+
shell: false,
|
|
125
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
126
|
+
windowsHide: true,
|
|
127
|
+
cwd: dependencies.cwd,
|
|
128
|
+
env: dependencies.env ? { ...process.env, ...dependencies.env } : undefined,
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new ProcessFailure(
|
|
132
|
+
"launch-failed",
|
|
133
|
+
`Could not launch Context Mode: ${safeMessage(error)}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let settled = false;
|
|
138
|
+
let stdoutBytes = 0;
|
|
139
|
+
let stderrBytes = 0;
|
|
140
|
+
let lineBuffer = "";
|
|
141
|
+
let stderr = "";
|
|
142
|
+
let version: string | null = null;
|
|
143
|
+
|
|
144
|
+
const finish = (callback: () => void) => {
|
|
145
|
+
if (settled) return;
|
|
146
|
+
settled = true;
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
child.stdout.removeAllListeners();
|
|
149
|
+
child.stderr.removeAllListeners();
|
|
150
|
+
child.removeAllListeners();
|
|
151
|
+
child.stdin.removeAllListeners("error");
|
|
152
|
+
child.stdin.on("error", () => {});
|
|
153
|
+
|
|
154
|
+
let completed = false;
|
|
155
|
+
const complete = () => {
|
|
156
|
+
if (completed) return;
|
|
157
|
+
completed = true;
|
|
158
|
+
clearTimeout(graceTimer);
|
|
159
|
+
child.stdin.destroy();
|
|
160
|
+
callback();
|
|
161
|
+
};
|
|
162
|
+
const graceTimer = setTimeout(() => {
|
|
163
|
+
child.kill("SIGKILL");
|
|
164
|
+
complete();
|
|
165
|
+
}, terminationGraceMs);
|
|
166
|
+
child.once("exit", complete);
|
|
167
|
+
|
|
168
|
+
if (child.exitCode != null || child.signalCode != null) {
|
|
169
|
+
complete();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
if (!child.stdin.destroyed) child.stdin.end();
|
|
174
|
+
child.kill("SIGTERM");
|
|
175
|
+
} catch {
|
|
176
|
+
child.kill("SIGKILL");
|
|
177
|
+
complete();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const fail = (failure: ProcessFailure) => finish(() => reject(failure));
|
|
181
|
+
const writeMessage = (message: unknown) => {
|
|
182
|
+
if (settled) return;
|
|
183
|
+
if (child.stdin.destroyed || !child.stdin.writable) {
|
|
184
|
+
fail(new ProcessFailure("protocol-error", "Context Mode closed its protocol input."));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
child.stdin.write(`${JSON.stringify(message)}\n`, (error) => {
|
|
189
|
+
if (error) {
|
|
190
|
+
fail(
|
|
191
|
+
new ProcessFailure(
|
|
192
|
+
"protocol-error",
|
|
193
|
+
`Context Mode protocol write failed: ${safeMessage(error)}`,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
fail(
|
|
200
|
+
new ProcessFailure(
|
|
201
|
+
"protocol-error",
|
|
202
|
+
`Context Mode protocol write failed: ${safeMessage(error)}`,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const timer = setTimeout(
|
|
208
|
+
() =>
|
|
209
|
+
fail(new ProcessFailure("timeout", `Context Mode did not respond within ${timeoutMs} ms.`)),
|
|
210
|
+
timeoutMs,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
child.stdin.on("error", (error) => {
|
|
214
|
+
fail(
|
|
215
|
+
new ProcessFailure(
|
|
216
|
+
"protocol-error",
|
|
217
|
+
`Context Mode protocol input failed: ${safeMessage(error)}`,
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
child.once("error", (error) => {
|
|
223
|
+
fail(
|
|
224
|
+
new ProcessFailure("launch-failed", `Could not launch Context Mode: ${safeMessage(error)}`),
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
child.once("exit", (code, signal) => {
|
|
228
|
+
if (settled) return;
|
|
229
|
+
const detail = stderr.trim() || `exit ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`;
|
|
230
|
+
fail(new ProcessFailure("command-failed", `Context Mode stopped before replying: ${detail}`));
|
|
231
|
+
});
|
|
232
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
233
|
+
stderrBytes += chunk.byteLength;
|
|
234
|
+
if (stdoutBytes + stderrBytes > outputLimit) {
|
|
235
|
+
fail(
|
|
236
|
+
new ProcessFailure(
|
|
237
|
+
"output-limit",
|
|
238
|
+
`Context Mode exceeded the ${outputLimit}-byte output limit.`,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
stderr += chunk.toString("utf8");
|
|
244
|
+
});
|
|
245
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
246
|
+
stdoutBytes += chunk.byteLength;
|
|
247
|
+
if (stdoutBytes + stderrBytes > outputLimit) {
|
|
248
|
+
fail(
|
|
249
|
+
new ProcessFailure(
|
|
250
|
+
"output-limit",
|
|
251
|
+
`Context Mode exceeded the ${outputLimit}-byte output limit.`,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
lineBuffer += chunk.toString("utf8");
|
|
257
|
+
const lines = lineBuffer.split(/\r?\n/);
|
|
258
|
+
lineBuffer = lines.pop() ?? "";
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
if (!line.trim()) continue;
|
|
261
|
+
let response: JsonRpcResponse;
|
|
262
|
+
try {
|
|
263
|
+
response = JSON.parse(line) as JsonRpcResponse;
|
|
264
|
+
} catch {
|
|
265
|
+
fail(
|
|
266
|
+
new ProcessFailure("protocol-error", "Context Mode emitted invalid MCP protocol output."),
|
|
267
|
+
);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (response.id === 1) {
|
|
271
|
+
if (response.error) {
|
|
272
|
+
fail(
|
|
273
|
+
new ProcessFailure(
|
|
274
|
+
"protocol-error",
|
|
275
|
+
response.error.message ?? "Context Mode initialization failed.",
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const result = response.result as { serverInfo?: { version?: unknown } } | undefined;
|
|
281
|
+
version =
|
|
282
|
+
typeof result?.serverInfo?.version === "string" ? result.serverInfo.version : null;
|
|
283
|
+
writeMessage({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
284
|
+
writeMessage(
|
|
285
|
+
operation.kind === "inspect"
|
|
286
|
+
? { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }
|
|
287
|
+
: {
|
|
288
|
+
jsonrpc: "2.0",
|
|
289
|
+
id: 2,
|
|
290
|
+
method: "tools/call",
|
|
291
|
+
params: { name: operation.name, arguments: operation.arguments },
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (response.id !== 2) continue;
|
|
297
|
+
if (response.error) {
|
|
298
|
+
const message = response.error.message ?? "Context Mode rejected the request.";
|
|
299
|
+
const unsupported =
|
|
300
|
+
response.error.code === -32601 || /unknown|not found|unsupported/i.test(message);
|
|
301
|
+
fail(new ProcessFailure(unsupported ? "unsupported" : "command-failed", message));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (operation.kind === "inspect") {
|
|
305
|
+
const result = response.result as { tools?: Array<{ name?: unknown }> } | undefined;
|
|
306
|
+
const tools = (result?.tools ?? [])
|
|
307
|
+
.map((tool) => tool.name)
|
|
308
|
+
.filter((name): name is string => typeof name === "string");
|
|
309
|
+
finish(() => resolve({ version, tools }));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const output = extractToolOutput(response.result);
|
|
314
|
+
finish(() => resolve(output));
|
|
315
|
+
} catch (error) {
|
|
316
|
+
fail(
|
|
317
|
+
error instanceof ProcessFailure
|
|
318
|
+
? error
|
|
319
|
+
: new ProcessFailure("protocol-error", safeMessage(error)),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
writeMessage({
|
|
327
|
+
jsonrpc: "2.0",
|
|
328
|
+
id: 1,
|
|
329
|
+
method: "initialize",
|
|
330
|
+
params: {
|
|
331
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
332
|
+
capabilities: {},
|
|
333
|
+
clientInfo: { name: "paseo-context-mode", version: "0.0.0" },
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
return await promise;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function inspectContextMode(
|
|
340
|
+
launch: LaunchDescriptor,
|
|
341
|
+
dependencies: McpProcessDependencies = {},
|
|
342
|
+
): Promise<McpInspection> {
|
|
343
|
+
return (await exchange(launch, { kind: "inspect" }, dependencies)) as McpInspection;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function callContextModeTool(
|
|
347
|
+
launch: LaunchDescriptor,
|
|
348
|
+
name: ContextModeToolName,
|
|
349
|
+
arguments_: McpToolArguments = {},
|
|
350
|
+
dependencies: McpProcessDependencies = {},
|
|
351
|
+
): Promise<string> {
|
|
352
|
+
return (await exchange(
|
|
353
|
+
launch,
|
|
354
|
+
{ kind: "tool", name, arguments: arguments_ },
|
|
355
|
+
dependencies,
|
|
356
|
+
)) as string;
|
|
357
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import { lstat, readdir, realpath } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import type { AnalyticsWarning } from "../shared/analytics";
|
|
6
|
+
|
|
7
|
+
const MAX_DATABASES_PER_DIRECTORY = 512;
|
|
8
|
+
const MAX_LOCAL_DISCOVERY_WARNINGS = 10;
|
|
9
|
+
|
|
10
|
+
export interface ProviderStorageRoot {
|
|
11
|
+
provider: string;
|
|
12
|
+
root: string;
|
|
13
|
+
sessionsDir: string;
|
|
14
|
+
contentDir: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StorageDiscoveryResult {
|
|
18
|
+
roots: ProviderStorageRoot[];
|
|
19
|
+
warnings: AnalyticsWarning[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StorageDiscoveryOptions {
|
|
23
|
+
home?: string;
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
platform?: NodeJS.Platform;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ProviderLocation {
|
|
29
|
+
provider: string;
|
|
30
|
+
root: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function configuredDirectory(
|
|
34
|
+
value: string | undefined,
|
|
35
|
+
home: string,
|
|
36
|
+
envVar: string,
|
|
37
|
+
warnings: AnalyticsWarning[],
|
|
38
|
+
): string | null {
|
|
39
|
+
const trimmed = value?.trim();
|
|
40
|
+
if (!trimmed) return null;
|
|
41
|
+
if (trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
|
|
42
|
+
return resolve(home, trimmed.slice(2));
|
|
43
|
+
}
|
|
44
|
+
if (isAbsolute(trimmed)) return resolve(trimmed);
|
|
45
|
+
warnings.push({
|
|
46
|
+
code: "invalid-storage-root",
|
|
47
|
+
message: `${envVar} was ignored because it is not an absolute path.`,
|
|
48
|
+
});
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function providerLocations(
|
|
53
|
+
home: string,
|
|
54
|
+
env: NodeJS.ProcessEnv,
|
|
55
|
+
warnings: AnalyticsWarning[],
|
|
56
|
+
platform: NodeJS.Platform,
|
|
57
|
+
): ProviderLocation[] {
|
|
58
|
+
const configuredRoot = configuredDirectory(
|
|
59
|
+
env.CONTEXT_MODE_DIR,
|
|
60
|
+
home,
|
|
61
|
+
"CONTEXT_MODE_DIR",
|
|
62
|
+
warnings,
|
|
63
|
+
);
|
|
64
|
+
const configuredDataRoot = configuredDirectory(
|
|
65
|
+
env.CONTEXT_MODE_DATA_DIR,
|
|
66
|
+
home,
|
|
67
|
+
"CONTEXT_MODE_DATA_DIR",
|
|
68
|
+
warnings,
|
|
69
|
+
);
|
|
70
|
+
const claudeRoot =
|
|
71
|
+
configuredDirectory(env.CLAUDE_CONFIG_DIR, home, "CLAUDE_CONFIG_DIR", warnings) ??
|
|
72
|
+
join(home, ".claude");
|
|
73
|
+
const codexRoot =
|
|
74
|
+
configuredDirectory(env.CODEX_HOME, home, "CODEX_HOME", warnings) ?? join(home, ".codex");
|
|
75
|
+
const kimiRoot =
|
|
76
|
+
configuredDirectory(env.KIMI_CODE_HOME, home, "KIMI_CODE_HOME", warnings) ??
|
|
77
|
+
join(home, ".kimi-code");
|
|
78
|
+
const xdgRoot =
|
|
79
|
+
platform === "win32"
|
|
80
|
+
? (configuredDirectory(env.APPDATA, home, "APPDATA", warnings) ??
|
|
81
|
+
join(home, "AppData", "Roaming"))
|
|
82
|
+
: (configuredDirectory(env.XDG_CONFIG_HOME, home, "XDG_CONFIG_HOME", warnings) ??
|
|
83
|
+
join(home, ".config"));
|
|
84
|
+
|
|
85
|
+
return [
|
|
86
|
+
...(configuredRoot ? [{ provider: "custom", root: configuredRoot }] : []),
|
|
87
|
+
...(configuredDataRoot
|
|
88
|
+
? [{ provider: "custom-data", root: join(configuredDataRoot, "context-mode") }]
|
|
89
|
+
: []),
|
|
90
|
+
{ provider: "claude-code", root: join(claudeRoot, "context-mode") },
|
|
91
|
+
{ provider: "gemini-cli", root: join(home, ".gemini", "context-mode") },
|
|
92
|
+
{ provider: "openclaw", root: join(home, ".openclaw", "context-mode") },
|
|
93
|
+
{ provider: "codex", root: join(codexRoot, "context-mode") },
|
|
94
|
+
{ provider: "cursor", root: join(home, ".cursor", "context-mode") },
|
|
95
|
+
{ provider: "vscode-copilot", root: join(home, ".vscode", "context-mode") },
|
|
96
|
+
{ provider: "copilot-cli", root: join(home, ".copilot", "context-mode") },
|
|
97
|
+
{ provider: "kiro", root: join(home, ".kiro", "context-mode") },
|
|
98
|
+
{ provider: "pi", root: join(home, ".pi", "context-mode") },
|
|
99
|
+
{ provider: "omp", root: join(home, ".omp", "context-mode") },
|
|
100
|
+
{ provider: "qwen-code", root: join(home, ".qwen", "context-mode") },
|
|
101
|
+
{ provider: "kimi", root: join(kimiRoot, "context-mode") },
|
|
102
|
+
{ provider: "kilo", root: join(xdgRoot, "kilo", "context-mode") },
|
|
103
|
+
{ provider: "opencode", root: join(xdgRoot, "opencode", "context-mode") },
|
|
104
|
+
{ provider: "zed", root: join(home, ".config", "zed", "context-mode") },
|
|
105
|
+
{
|
|
106
|
+
provider: "jetbrains-copilot",
|
|
107
|
+
root: join(home, ".config", "JetBrains", "context-mode"),
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function storageWarning(provider: string, message: string, database?: string): AnalyticsWarning {
|
|
113
|
+
return {
|
|
114
|
+
code: "storage-unavailable",
|
|
115
|
+
provider,
|
|
116
|
+
...(database ? { database } : {}),
|
|
117
|
+
message,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function discoverContextModeStorageRoots(
|
|
122
|
+
options: StorageDiscoveryOptions = {},
|
|
123
|
+
): Promise<StorageDiscoveryResult> {
|
|
124
|
+
const home = resolve(options.home ?? homedir());
|
|
125
|
+
const env = options.env ?? process.env;
|
|
126
|
+
const warnings: AnalyticsWarning[] = [];
|
|
127
|
+
const roots: ProviderStorageRoot[] = [];
|
|
128
|
+
const seen = new Set<string>();
|
|
129
|
+
|
|
130
|
+
for (const location of providerLocations(
|
|
131
|
+
home,
|
|
132
|
+
env,
|
|
133
|
+
warnings,
|
|
134
|
+
options.platform ?? process.platform,
|
|
135
|
+
)) {
|
|
136
|
+
try {
|
|
137
|
+
const info = await lstat(location.root);
|
|
138
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
139
|
+
warnings.push(
|
|
140
|
+
storageWarning(
|
|
141
|
+
location.provider,
|
|
142
|
+
"The Context Mode storage root is not a regular directory.",
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const canonical = await realpath(location.root);
|
|
148
|
+
if (seen.has(canonical)) continue;
|
|
149
|
+
seen.add(canonical);
|
|
150
|
+
roots.push({
|
|
151
|
+
provider: location.provider,
|
|
152
|
+
root: canonical,
|
|
153
|
+
sessionsDir: join(canonical, "sessions"),
|
|
154
|
+
contentDir: join(canonical, "content"),
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
158
|
+
warnings.push(
|
|
159
|
+
storageWarning(location.provider, "The Context Mode storage root could not be inspected."),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { roots, warnings };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function listRegularContextModeDatabases(
|
|
168
|
+
provider: string,
|
|
169
|
+
directory: string,
|
|
170
|
+
): Promise<{ files: string[]; warnings: AnalyticsWarning[] }> {
|
|
171
|
+
let entries: Dirent[];
|
|
172
|
+
try {
|
|
173
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { files: [], warnings: [] };
|
|
176
|
+
return {
|
|
177
|
+
files: [],
|
|
178
|
+
warnings: [
|
|
179
|
+
storageWarning(
|
|
180
|
+
provider,
|
|
181
|
+
`The ${basename(directory)} database directory could not be read.`,
|
|
182
|
+
),
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const candidates = entries
|
|
188
|
+
.filter((entry) => entry.name.endsWith(".db"))
|
|
189
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
190
|
+
const files: string[] = [];
|
|
191
|
+
const warnings: AnalyticsWarning[] = [];
|
|
192
|
+
|
|
193
|
+
for (const entry of candidates.slice(0, MAX_DATABASES_PER_DIRECTORY)) {
|
|
194
|
+
const path = join(directory, entry.name);
|
|
195
|
+
try {
|
|
196
|
+
const info = await lstat(path);
|
|
197
|
+
if (entry.isFile() && info.isFile()) {
|
|
198
|
+
files.push(path);
|
|
199
|
+
} else if (warnings.length < MAX_LOCAL_DISCOVERY_WARNINGS) {
|
|
200
|
+
warnings.push({
|
|
201
|
+
code: "not-regular-database",
|
|
202
|
+
provider,
|
|
203
|
+
database: entry.name,
|
|
204
|
+
message: "Skipped a .db entry that is not a regular file.",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
if (warnings.length < MAX_LOCAL_DISCOVERY_WARNINGS) {
|
|
209
|
+
warnings.push(
|
|
210
|
+
storageWarning(provider, "A database disappeared during discovery.", entry.name),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (candidates.length > MAX_DATABASES_PER_DIRECTORY) {
|
|
217
|
+
warnings.push({
|
|
218
|
+
code: "result-truncated",
|
|
219
|
+
provider,
|
|
220
|
+
message: `Only the first ${MAX_DATABASES_PER_DIRECTORY} database files in ${basename(directory)} were inspected.`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { files, warnings };
|
|
225
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ContextModePlatformSchema, ContextModeProviderSchema } from "./knowledge";
|
|
4
|
+
|
|
5
|
+
export const CONTEXT_MODE_TARGET_VERSION = "1.0.169" as const;
|
|
6
|
+
|
|
7
|
+
const providerInput = z.object({ provider: ContextModeProviderSchema }).strict();
|
|
8
|
+
|
|
9
|
+
export const DoctorStatusRowSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
status: z.enum(["ok", "warning", "error"]),
|
|
12
|
+
check: z.string().trim().min(1).max(256),
|
|
13
|
+
detail: z.string().max(4_096),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
|
|
17
|
+
export const DoctorReportSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
platform: ContextModePlatformSchema,
|
|
20
|
+
rows: z.array(DoctorStatusRowSchema).max(64),
|
|
21
|
+
rawOutput: z.string().max(192 * 1_024),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
|
|
25
|
+
export const ContextModeActionSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
program: z.string().trim().min(1).max(4_096),
|
|
28
|
+
args: z.array(z.string().max(4_096)).max(32),
|
|
29
|
+
currentVersion: z.string().trim().min(1).max(128).nullable(),
|
|
30
|
+
targetVersion: z.union([z.literal(CONTEXT_MODE_TARGET_VERSION), z.literal("latest")]),
|
|
31
|
+
platform: ContextModePlatformSchema,
|
|
32
|
+
requiresRestart: z.boolean(),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
35
|
+
|
|
36
|
+
export const getContextModeDoctorReport = defineRpc({
|
|
37
|
+
name: "context-mode.actions.doctor",
|
|
38
|
+
input: providerInput,
|
|
39
|
+
output: DoctorReportSchema,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const getContextModeInstallAction = defineRpc({
|
|
43
|
+
name: "context-mode.actions.install",
|
|
44
|
+
input: providerInput,
|
|
45
|
+
output: ContextModeActionSchema,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const getContextModeUpgradeAction = defineRpc({
|
|
49
|
+
name: "context-mode.actions.upgrade",
|
|
50
|
+
input: providerInput,
|
|
51
|
+
output: ContextModeActionSchema,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export type DoctorStatusRow = z.infer<typeof DoctorStatusRowSchema>;
|
|
55
|
+
export type DoctorReport = z.infer<typeof DoctorReportSchema>;
|
|
56
|
+
export type ContextModeAction = z.infer<typeof ContextModeActionSchema>;
|
|
57
|
+
export type ContextModeActionInput = z.infer<typeof providerInput>;
|