@omercnet/paseo-context-mode 3.1.3 → 3.1.4-next.434.1
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/OVERVIEW.md +3 -3
- package/README.md +13 -15
- package/client/analytics-dashboard.tsx +745 -613
- package/client/controls.tsx +295 -0
- package/client/diagnostics-section.tsx +178 -0
- package/client/{context-mode-footer.tsx → footer.tsx} +22 -20
- package/client/knowledge-section.tsx +725 -697
- package/client/{context-mode-pill.tsx → pill.tsx} +5 -5
- package/client/scoped-knowledge-section.tsx +14 -20
- package/client/screen-scope.ts +12 -6
- package/client/settings-screen.tsx +94 -97
- package/client/surface.tsx +377 -0
- package/index.client.tsx +3 -3
- package/index.server.ts +18 -27
- package/npm-shrinkwrap.json +8 -1747
- package/package.json +7 -7
- package/server/actions.ts +91 -102
- package/server/analytics.ts +257 -308
- package/server/binary.ts +119 -94
- package/server/database.ts +463 -0
- package/server/handlers.ts +40 -95
- package/server/integration.ts +230 -361
- package/server/knowledge.ts +65 -137
- package/server/mcp-process.ts +135 -86
- package/server/storage-discovery.ts +46 -199
- package/shared/actions.ts +2 -27
- package/shared/analytics.ts +60 -33
- package/shared/contracts.ts +16 -47
- package/shared/knowledge.ts +62 -65
- package/shared/providers.ts +62 -0
- package/shared/settings.ts +33 -18
- package/client/actions-section.tsx +0 -621
- package/client/context-mode-surface.tsx +0 -444
- package/server/context-mode-db.ts +0 -481
- package/shared/index.ts +0 -5
package/server/knowledge.ts
CHANGED
|
@@ -2,49 +2,42 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
3
|
import type { PluginSettings } from "@getpaseo/plugin/server";
|
|
4
4
|
import type {
|
|
5
|
-
ContextModePlatform,
|
|
6
|
-
ContextModeProvider,
|
|
7
5
|
FetchAndIndexInput,
|
|
8
6
|
IndexPathInput,
|
|
9
7
|
KnowledgeToolResult,
|
|
10
8
|
PurgeKnowledgeInput,
|
|
9
|
+
ResumeSessionInput,
|
|
11
10
|
SearchKnowledgeInput,
|
|
11
|
+
SearchKnowledgeResult,
|
|
12
|
+
SearchPage,
|
|
12
13
|
} from "../shared/knowledge";
|
|
14
|
+
import { IN_PROCESS_PROVIDERS, PANEL_PLATFORM } from "../shared/providers";
|
|
13
15
|
import type { contextModeSettings } from "../shared/settings";
|
|
14
16
|
import { type LaunchDescriptor, resolveContextModeBinary } from "./binary";
|
|
15
|
-
import {
|
|
16
|
-
contextModeStorageRootFor,
|
|
17
|
-
type ProviderConfigSource,
|
|
18
|
-
type ProviderOverrides,
|
|
19
|
-
readProviderOverrides,
|
|
20
|
-
resolveProvider,
|
|
21
|
-
} from "./integration";
|
|
17
|
+
import { type ProviderConfigSource, readProviderOverrides, resolveProvider } from "./integration";
|
|
22
18
|
import {
|
|
23
19
|
type ContextModeToolName,
|
|
24
20
|
callContextModeTool,
|
|
25
|
-
type
|
|
21
|
+
type McpJsonValue,
|
|
26
22
|
type McpToolArguments,
|
|
27
23
|
} from "./mcp-process";
|
|
24
|
+
import { resolveContextModeDataDir } from "./storage-discovery";
|
|
28
25
|
|
|
29
26
|
type SettingsHandle = PluginSettings<typeof contextModeSettings.schema>;
|
|
30
27
|
|
|
31
|
-
type KnowledgeToolCaller = (
|
|
32
|
-
launch: LaunchDescriptor,
|
|
33
|
-
name: ContextModeToolName,
|
|
34
|
-
arguments_: McpToolArguments,
|
|
35
|
-
dependencies: McpProcessDependencies,
|
|
36
|
-
) => Promise<string>;
|
|
37
|
-
|
|
38
28
|
export interface KnowledgeHandlerDependencies {
|
|
39
29
|
now?: () => Date;
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
home?: string;
|
|
40
32
|
resolveBinary?: typeof resolveContextModeBinary;
|
|
41
|
-
callTool?:
|
|
33
|
+
callTool?: typeof callContextModeTool;
|
|
42
34
|
}
|
|
43
35
|
|
|
44
36
|
type KnowledgeContext = { paseo?: ProviderConfigSource };
|
|
45
37
|
|
|
46
38
|
export interface ContextModeKnowledgeHandlers {
|
|
47
|
-
search(input: SearchKnowledgeInput, context?: KnowledgeContext): Promise<
|
|
39
|
+
search(input: SearchKnowledgeInput, context?: KnowledgeContext): Promise<SearchKnowledgeResult>;
|
|
40
|
+
resume(input: ResumeSessionInput, context?: KnowledgeContext): Promise<KnowledgeToolResult>;
|
|
48
41
|
indexPath(input: IndexPathInput, context?: KnowledgeContext): Promise<KnowledgeToolResult>;
|
|
49
42
|
fetchAndIndex(
|
|
50
43
|
input: FetchAndIndexInput,
|
|
@@ -53,42 +46,26 @@ export interface ContextModeKnowledgeHandlers {
|
|
|
53
46
|
purge(input: PurgeKnowledgeInput, context?: KnowledgeContext): Promise<KnowledgeToolResult>;
|
|
54
47
|
}
|
|
55
48
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
codex: "codex",
|
|
59
|
-
copilot: "copilot-cli",
|
|
60
|
-
cursor: "cursor",
|
|
61
|
-
opencode: "opencode",
|
|
62
|
-
pi: "pi",
|
|
63
|
-
omp: "omp",
|
|
64
|
-
"omp-plugin": "omp",
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export function contextModePlatformFor(provider: ContextModeProvider): ContextModePlatform {
|
|
68
|
-
return PROVIDER_PLATFORMS[provider];
|
|
69
|
-
}
|
|
49
|
+
/** Search replies end with `More: ctx_search({..., offset:N})`, or `more: ... after:MS` for timeline. */
|
|
50
|
+
const NEXT_PAGE_LINE = /^[Mm]ore: ctx_search\(\{.*\b(offset|after):(\d+)\}\)\s*$/gm;
|
|
70
51
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
home = homedir(),
|
|
78
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
79
|
-
providers: ProviderOverrides = {},
|
|
80
|
-
): NodeJS.ProcessEnv {
|
|
81
|
-
const { base, policy, env: providerEnv } = resolveProvider(provider, providers);
|
|
82
|
-
const storageRoot = contextModeStorageRootFor(base, { home, env: { ...env, ...providerEnv } });
|
|
83
|
-
if (!policy || !storageRoot)
|
|
84
|
-
throw new Error(`Context Mode does not support provider ${provider}.`);
|
|
85
|
-
return { CONTEXT_MODE_PLATFORM: policy.platform, CONTEXT_MODE_DIR: storageRoot };
|
|
52
|
+
/** The cursor of the last footer; the UI searches one query at a time, so there is one. */
|
|
53
|
+
function nextSearchPage(output: string): SearchPage | null {
|
|
54
|
+
const last = [...output.matchAll(NEXT_PAGE_LINE)].at(-1);
|
|
55
|
+
if (!last) return null;
|
|
56
|
+
const cursor = Number(last[2]);
|
|
57
|
+
return last[1] === "after" ? { after: cursor } : { offset: cursor };
|
|
86
58
|
}
|
|
87
59
|
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
60
|
+
function purgeArguments(input: PurgeKnowledgeInput): McpToolArguments {
|
|
61
|
+
switch (input.scope) {
|
|
62
|
+
case "project":
|
|
63
|
+
return { project: true };
|
|
64
|
+
case "session":
|
|
65
|
+
return { session: input.sessionId };
|
|
66
|
+
case "source":
|
|
67
|
+
return { source: input.source };
|
|
68
|
+
}
|
|
92
69
|
}
|
|
93
70
|
|
|
94
71
|
export function createContextModeKnowledgeHandlers(
|
|
@@ -96,6 +73,8 @@ export function createContextModeKnowledgeHandlers(
|
|
|
96
73
|
dependencies: KnowledgeHandlerDependencies = {},
|
|
97
74
|
): ContextModeKnowledgeHandlers {
|
|
98
75
|
const now = dependencies.now ?? (() => new Date());
|
|
76
|
+
const env = dependencies.env ?? process.env;
|
|
77
|
+
const home = dependencies.home ?? homedir();
|
|
99
78
|
const resolveBinary = dependencies.resolveBinary ?? resolveContextModeBinary;
|
|
100
79
|
const callTool = dependencies.callTool ?? callContextModeTool;
|
|
101
80
|
|
|
@@ -117,104 +96,53 @@ export function createContextModeKnowledgeHandlers(
|
|
|
117
96
|
name: ContextModeToolName,
|
|
118
97
|
arguments_: McpToolArguments,
|
|
119
98
|
context: KnowledgeContext | undefined,
|
|
120
|
-
environmentOverrides: NodeJS.ProcessEnv = {},
|
|
121
99
|
): Promise<KnowledgeToolResult> {
|
|
100
|
+
// A configured provider inherits its parent's agent id; its own env may move the data dir.
|
|
101
|
+
const {
|
|
102
|
+
base,
|
|
103
|
+
platform,
|
|
104
|
+
env: providerEnv,
|
|
105
|
+
} = resolveProvider(provider, await readProviderOverrides(context?.paseo));
|
|
106
|
+
const inProcess = IN_PROCESS_PROVIDERS.some((entry) => entry.provider === base);
|
|
107
|
+
if (platform === null && !inProcess) {
|
|
108
|
+
throw new Error(`Context Mode does not support provider ${provider}.`);
|
|
109
|
+
}
|
|
122
110
|
const launch = await launchDescriptor(context?.paseo);
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
111
|
+
const output = await callTool(
|
|
112
|
+
launch,
|
|
113
|
+
name,
|
|
114
|
+
arguments_,
|
|
115
|
+
{},
|
|
116
|
+
{
|
|
117
|
+
platform: platform ?? PANEL_PLATFORM,
|
|
118
|
+
cwd: projectPath,
|
|
119
|
+
dataDir: resolveContextModeDataDir({ ...env, ...providerEnv }, home),
|
|
128
120
|
},
|
|
129
|
-
|
|
130
|
-
});
|
|
121
|
+
);
|
|
131
122
|
return { provider, output, completedAt: now().toISOString() };
|
|
132
123
|
}
|
|
133
124
|
|
|
134
125
|
return {
|
|
135
|
-
search({ provider, projectPath, queries,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
definedArguments({ queries, limit, source, contentType, sort }),
|
|
141
|
-
context,
|
|
142
|
-
);
|
|
126
|
+
async search({ provider, projectPath, queries, scope, source, sort, page }, context) {
|
|
127
|
+
const arguments_: Record<string, McpJsonValue> = { queries, scope, sort, ...page };
|
|
128
|
+
if (source !== undefined) arguments_.source = source;
|
|
129
|
+
const result = await run(provider, projectPath, "ctx_search", arguments_, context);
|
|
130
|
+
return { ...result, next: nextSearchPage(result.output) };
|
|
143
131
|
},
|
|
144
|
-
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
path,
|
|
149
|
-
source,
|
|
150
|
-
include,
|
|
151
|
-
exclude,
|
|
152
|
-
maxDepth,
|
|
153
|
-
maxFiles,
|
|
154
|
-
extensions,
|
|
155
|
-
respectGitignore,
|
|
156
|
-
followSymlinks,
|
|
157
|
-
},
|
|
158
|
-
context,
|
|
159
|
-
) {
|
|
132
|
+
resume({ provider, projectPath, sessionId }, context) {
|
|
133
|
+
return run(provider, projectPath, "ctx_resume", { session: sessionId }, context);
|
|
134
|
+
},
|
|
135
|
+
indexPath({ provider, projectPath, path }, context) {
|
|
160
136
|
if (!isAbsolute(path)) {
|
|
161
137
|
throw new Error("Knowledge indexing requires an absolute path on this host.");
|
|
162
138
|
}
|
|
163
|
-
return run(
|
|
164
|
-
provider,
|
|
165
|
-
projectPath,
|
|
166
|
-
"ctx_index",
|
|
167
|
-
definedArguments({
|
|
168
|
-
path,
|
|
169
|
-
source,
|
|
170
|
-
include,
|
|
171
|
-
exclude,
|
|
172
|
-
maxDepth,
|
|
173
|
-
maxFiles,
|
|
174
|
-
extensions,
|
|
175
|
-
respectGitignore,
|
|
176
|
-
followSymlinks,
|
|
177
|
-
}),
|
|
178
|
-
context,
|
|
179
|
-
);
|
|
139
|
+
return run(provider, projectPath, "ctx_index", { path }, context);
|
|
180
140
|
},
|
|
181
|
-
fetchAndIndex({ provider, projectPath, url
|
|
182
|
-
return run(
|
|
183
|
-
provider,
|
|
184
|
-
projectPath,
|
|
185
|
-
"ctx_fetch_and_index",
|
|
186
|
-
definedArguments({ url, source, force, ttl }),
|
|
187
|
-
context,
|
|
188
|
-
{ CTX_FETCH_STRICT: "1" },
|
|
189
|
-
);
|
|
141
|
+
fetchAndIndex({ provider, projectPath, url }, context) {
|
|
142
|
+
return run(provider, projectPath, "ctx_index", { url }, context);
|
|
190
143
|
},
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (raw.confirm !== true) {
|
|
194
|
-
throw new Error("Purge requires exact confirmation with confirm: true.");
|
|
195
|
-
}
|
|
196
|
-
if (raw.scope === "project") {
|
|
197
|
-
if (raw.sessionId !== undefined) {
|
|
198
|
-
throw new Error("Purge requires exactly one scope.");
|
|
199
|
-
}
|
|
200
|
-
return await run(
|
|
201
|
-
raw.provider,
|
|
202
|
-
raw.projectPath,
|
|
203
|
-
"ctx_purge",
|
|
204
|
-
{ confirm: true, scope: "project" },
|
|
205
|
-
context,
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
if (raw.scope !== "session" || !raw.sessionId) {
|
|
209
|
-
throw new Error("Session purge requires exactly one non-empty sessionId scope.");
|
|
210
|
-
}
|
|
211
|
-
return await run(
|
|
212
|
-
raw.provider,
|
|
213
|
-
raw.projectPath,
|
|
214
|
-
"ctx_purge",
|
|
215
|
-
{ confirm: true, scope: "session", sessionId: raw.sessionId },
|
|
216
|
-
context,
|
|
217
|
-
);
|
|
144
|
+
purge(input, context) {
|
|
145
|
+
return run(input.provider, input.projectPath, "ctx_purge", purgeArguments(input), context);
|
|
218
146
|
},
|
|
219
147
|
};
|
|
220
148
|
}
|
package/server/mcp-process.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
|
-
import
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { type ContextModePlatform, PANEL_PLATFORM } from "../shared/providers";
|
|
4
|
+
import { type LaunchDescriptor, readContextModeVersion } from "./binary";
|
|
3
5
|
|
|
4
6
|
export const PROCESS_OUTPUT_LIMIT = 192 * 1_024;
|
|
5
7
|
export const PROCESS_TIMEOUT_MS = 12_000;
|
|
@@ -33,9 +35,15 @@ export interface McpProcessDependencies {
|
|
|
33
35
|
spawnProcess?: typeof spawn;
|
|
34
36
|
timeoutMs?: number;
|
|
35
37
|
outputLimit?: number;
|
|
36
|
-
env?: NodeJS.ProcessEnv;
|
|
37
|
-
cwd?: string;
|
|
38
38
|
terminationGraceMs?: number;
|
|
39
|
+
readVersion?: (launch: LaunchDescriptor) => Promise<string | null>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `--agent` label, project cwd (Context Mode keys its store by cwd) and optional data dir. */
|
|
43
|
+
export interface ContextModeProcessOptions {
|
|
44
|
+
platform: ContextModePlatform;
|
|
45
|
+
cwd?: string;
|
|
46
|
+
dataDir?: string;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
export type McpJsonValue =
|
|
@@ -48,87 +56,107 @@ export type McpJsonValue =
|
|
|
48
56
|
|
|
49
57
|
export type McpToolArguments = Readonly<Record<string, McpJsonValue>>;
|
|
50
58
|
|
|
51
|
-
export
|
|
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,
|
|
59
|
+
export const CONTEXT_MODE_TOOL_TIMEOUT_MS = {
|
|
64
60
|
ctx_index: 120_000,
|
|
65
|
-
|
|
61
|
+
ctx_search: 20_000,
|
|
62
|
+
ctx_resume: 20_000,
|
|
63
|
+
ctx_doctor: 30_000,
|
|
66
64
|
ctx_purge: 120_000,
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
/** The tools the plugin calls; a runtime missing any of them (Context Mode 1.x) is unsupported. */
|
|
68
|
+
export type ContextModeToolName = keyof typeof CONTEXT_MODE_TOOL_TIMEOUT_MS;
|
|
69
|
+
|
|
70
|
+
/** Every plugin-initiated call shares this journal session, so it never shows up as agent work. */
|
|
71
|
+
export const PLUGIN_SESSION_ID = "paseo-context-mode";
|
|
72
|
+
|
|
73
|
+
const CORE_TOOLS = Object.keys(CONTEXT_MODE_TOOL_TIMEOUT_MS);
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const DEFAULT_OPTIONS: ContextModeProcessOptions = { platform: PANEL_PLATFORM };
|
|
76
|
+
|
|
77
|
+
/** Command line that runs the stdio MCP server for one agent. */
|
|
78
|
+
export function serverArgs(
|
|
79
|
+
launch: LaunchDescriptor,
|
|
80
|
+
platform: ContextModePlatform,
|
|
81
|
+
): { command: string; args: string[] } {
|
|
82
|
+
return { command: launch.program, args: [...launch.args, "server", "--agent", platform] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const JsonRpcResponseSchema = z.looseObject({
|
|
86
|
+
id: z.number().optional(),
|
|
87
|
+
result: z.unknown().optional(),
|
|
88
|
+
error: z.looseObject({ code: z.number().optional(), message: z.string().optional() }).optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
type JsonRpcResponse = z.infer<typeof JsonRpcResponseSchema>;
|
|
92
|
+
|
|
93
|
+
interface McpRequest {
|
|
94
|
+
method: "tools/list" | "tools/call";
|
|
95
|
+
params: Record<string, McpJsonValue>;
|
|
96
|
+
}
|
|
75
97
|
|
|
76
98
|
function safeMessage(value: unknown): string {
|
|
77
99
|
if (value instanceof Error && value.message) return value.message;
|
|
78
100
|
return String(value);
|
|
79
101
|
}
|
|
80
102
|
|
|
103
|
+
const ToolResultSchema = z.looseObject({
|
|
104
|
+
content: z.array(z.looseObject({ type: z.string().optional(), text: z.unknown() })).optional(),
|
|
105
|
+
structuredContent: z.unknown().optional(),
|
|
106
|
+
isError: z.boolean().optional(),
|
|
107
|
+
});
|
|
108
|
+
|
|
81
109
|
function extractToolOutput(result: unknown): string {
|
|
82
|
-
|
|
110
|
+
const parsed = ToolResultSchema.safeParse(result);
|
|
111
|
+
if (!parsed.success) {
|
|
83
112
|
throw new ProcessFailure("protocol-error", "Context Mode returned an invalid tool result.");
|
|
84
113
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
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)
|
|
114
|
+
const { content = [], structuredContent, isError } = parsed.data;
|
|
115
|
+
const text = content
|
|
116
|
+
.flatMap((item) => (item.type === "text" && typeof item.text === "string" ? [item.text] : []))
|
|
93
117
|
.join("\n");
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
: JSON.stringify(value.structuredContent, null, 2);
|
|
100
|
-
if (value.isError) {
|
|
118
|
+
let output = text;
|
|
119
|
+
if (text.trim().length === 0) {
|
|
120
|
+
output = structuredContent === undefined ? "" : JSON.stringify(structuredContent, null, 2);
|
|
121
|
+
}
|
|
122
|
+
if (isError) {
|
|
101
123
|
throw new ProcessFailure("command-failed", output || "Context Mode reported a tool error.");
|
|
102
124
|
}
|
|
103
125
|
return output;
|
|
104
126
|
}
|
|
105
127
|
|
|
106
|
-
|
|
128
|
+
/** One MCP session: initialize, send `request`, and parse its result. */
|
|
129
|
+
async function exchange<T>(
|
|
107
130
|
launch: LaunchDescriptor,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
request: McpRequest,
|
|
132
|
+
parseResult: (result: unknown) => T,
|
|
133
|
+
defaultTimeoutMs: number,
|
|
111
134
|
dependencies: McpProcessDependencies,
|
|
112
|
-
|
|
135
|
+
options: ContextModeProcessOptions,
|
|
136
|
+
): Promise<T> {
|
|
113
137
|
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);
|
|
138
|
+
const timeoutMs = dependencies.timeoutMs ?? defaultTimeoutMs;
|
|
117
139
|
const terminationGraceMs = dependencies.terminationGraceMs ?? PROCESS_TERMINATION_GRACE_MS;
|
|
118
140
|
const outputLimit = dependencies.outputLimit ?? PROCESS_OUTPUT_LIMIT;
|
|
119
141
|
|
|
120
|
-
const { promise, resolve, reject } = Promise.withResolvers<
|
|
142
|
+
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
143
|
+
const { command, args } = serverArgs(launch, options.platform);
|
|
144
|
+
const env = {
|
|
145
|
+
...launch.env,
|
|
146
|
+
...(options.dataDir ? { CONTEXT_MODE_DATA_DIR: options.dataDir } : {}),
|
|
147
|
+
// Claude Code resolves its project from this variable before cwd.
|
|
148
|
+
...(options.platform === "claude-code" && options.cwd
|
|
149
|
+
? { CLAUDE_PROJECT_DIR: options.cwd }
|
|
150
|
+
: {}),
|
|
151
|
+
};
|
|
121
152
|
let child: ChildProcessWithoutNullStreams;
|
|
122
153
|
try {
|
|
123
|
-
child = spawnProcess(
|
|
154
|
+
child = spawnProcess(command, args, {
|
|
124
155
|
shell: false,
|
|
125
156
|
stdio: ["pipe", "pipe", "pipe"],
|
|
126
157
|
windowsHide: true,
|
|
127
|
-
cwd:
|
|
128
|
-
env:
|
|
129
|
-
launch.env || dependencies.env
|
|
130
|
-
? { ...process.env, ...launch.env, ...dependencies.env }
|
|
131
|
-
: undefined,
|
|
158
|
+
cwd: options.cwd,
|
|
159
|
+
env: Object.keys(env).length > 0 ? { ...process.env, ...env } : undefined,
|
|
132
160
|
});
|
|
133
161
|
} catch (error) {
|
|
134
162
|
throw new ProcessFailure(
|
|
@@ -142,7 +170,6 @@ async function exchange(
|
|
|
142
170
|
let stderrBytes = 0;
|
|
143
171
|
let lineBuffer = "";
|
|
144
172
|
let stderr = "";
|
|
145
|
-
let version: string | null = null;
|
|
146
173
|
|
|
147
174
|
const finish = (callback: () => void) => {
|
|
148
175
|
if (settled) return;
|
|
@@ -263,7 +290,7 @@ async function exchange(
|
|
|
263
290
|
if (!line.trim()) continue;
|
|
264
291
|
let response: JsonRpcResponse;
|
|
265
292
|
try {
|
|
266
|
-
response = JSON.parse(line)
|
|
293
|
+
response = JsonRpcResponseSchema.parse(JSON.parse(line));
|
|
267
294
|
} catch {
|
|
268
295
|
fail(
|
|
269
296
|
new ProcessFailure("protocol-error", "Context Mode emitted invalid MCP protocol output."),
|
|
@@ -280,20 +307,8 @@ async function exchange(
|
|
|
280
307
|
);
|
|
281
308
|
return;
|
|
282
309
|
}
|
|
283
|
-
const result = response.result as { serverInfo?: { version?: unknown } } | undefined;
|
|
284
|
-
version =
|
|
285
|
-
typeof result?.serverInfo?.version === "string" ? result.serverInfo.version : null;
|
|
286
310
|
writeMessage({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
287
|
-
writeMessage(
|
|
288
|
-
operation.kind === "inspect"
|
|
289
|
-
? { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }
|
|
290
|
-
: {
|
|
291
|
-
jsonrpc: "2.0",
|
|
292
|
-
id: 2,
|
|
293
|
-
method: "tools/call",
|
|
294
|
-
params: { name: operation.name, arguments: operation.arguments },
|
|
295
|
-
},
|
|
296
|
-
);
|
|
311
|
+
writeMessage({ jsonrpc: "2.0", id: 2, ...request });
|
|
297
312
|
continue;
|
|
298
313
|
}
|
|
299
314
|
if (response.id !== 2) continue;
|
|
@@ -304,17 +319,9 @@ async function exchange(
|
|
|
304
319
|
fail(new ProcessFailure(unsupported ? "unsupported" : "command-failed", message));
|
|
305
320
|
return;
|
|
306
321
|
}
|
|
307
|
-
if (operation.kind === "inspect") {
|
|
308
|
-
const result = response.result as { tools?: Array<{ name?: unknown }> } | undefined;
|
|
309
|
-
const tools = (result?.tools ?? [])
|
|
310
|
-
.map((tool) => tool.name)
|
|
311
|
-
.filter((name): name is string => typeof name === "string");
|
|
312
|
-
finish(() => resolve({ version, tools }));
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
322
|
try {
|
|
316
|
-
const
|
|
317
|
-
finish(() => resolve(
|
|
323
|
+
const value = parseResult(response.result);
|
|
324
|
+
finish(() => resolve(value));
|
|
318
325
|
} catch (error) {
|
|
319
326
|
fail(
|
|
320
327
|
error instanceof ProcessFailure
|
|
@@ -339,22 +346,64 @@ async function exchange(
|
|
|
339
346
|
return await promise;
|
|
340
347
|
}
|
|
341
348
|
|
|
349
|
+
const ToolListSchema = z.looseObject({
|
|
350
|
+
tools: z.array(z.looseObject({ name: z.string() })).default([]),
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
function extractToolNames(result: unknown): string[] {
|
|
354
|
+
const parsed = ToolListSchema.safeParse(result ?? {});
|
|
355
|
+
if (!parsed.success) {
|
|
356
|
+
throw new ProcessFailure("protocol-error", "Context Mode returned an invalid tool list.");
|
|
357
|
+
}
|
|
358
|
+
return parsed.data.tools.map(({ name }) => name);
|
|
359
|
+
}
|
|
360
|
+
|
|
342
361
|
export async function inspectContextMode(
|
|
343
362
|
launch: LaunchDescriptor,
|
|
344
363
|
dependencies: McpProcessDependencies = {},
|
|
364
|
+
options: ContextModeProcessOptions = DEFAULT_OPTIONS,
|
|
345
365
|
): Promise<McpInspection> {
|
|
346
|
-
|
|
366
|
+
const [tools, version] = await Promise.all([
|
|
367
|
+
exchange(
|
|
368
|
+
launch,
|
|
369
|
+
{ method: "tools/list", params: {} },
|
|
370
|
+
extractToolNames,
|
|
371
|
+
PROCESS_TIMEOUT_MS,
|
|
372
|
+
dependencies,
|
|
373
|
+
options,
|
|
374
|
+
),
|
|
375
|
+
(dependencies.readVersion ?? readContextModeVersion)(launch),
|
|
376
|
+
]);
|
|
377
|
+
const missing = CORE_TOOLS.filter((tool) => !tools.includes(tool));
|
|
378
|
+
if (missing.length > 0) {
|
|
379
|
+
const doctorOnly = tools.length === 1 && tools[0] === "ctx_doctor";
|
|
380
|
+
const reason = doctorOnly
|
|
381
|
+
? "Context Mode needs Node.js 24.15 or later with node:sqlite; run ctx_doctor for details."
|
|
382
|
+
: "Context Mode 2.x is required; clear the configured binary path to use the bundled runtime.";
|
|
383
|
+
throw new ProcessFailure(
|
|
384
|
+
"unsupported",
|
|
385
|
+
`Context Mode ${version ?? "(unknown version)"} lacks ${missing.join(", ")}. ${reason}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return { version, tools };
|
|
347
389
|
}
|
|
348
390
|
|
|
349
391
|
export async function callContextModeTool(
|
|
350
392
|
launch: LaunchDescriptor,
|
|
351
393
|
name: ContextModeToolName,
|
|
352
|
-
arguments_: McpToolArguments
|
|
353
|
-
dependencies: McpProcessDependencies
|
|
394
|
+
arguments_: McpToolArguments,
|
|
395
|
+
dependencies: McpProcessDependencies,
|
|
396
|
+
options: ContextModeProcessOptions,
|
|
354
397
|
): Promise<string> {
|
|
355
|
-
return
|
|
398
|
+
return exchange(
|
|
356
399
|
launch,
|
|
357
|
-
{
|
|
400
|
+
{
|
|
401
|
+
method: "tools/call",
|
|
402
|
+
params: { name, arguments: arguments_, _meta: { sessionId: PLUGIN_SESSION_ID } },
|
|
403
|
+
},
|
|
404
|
+
extractToolOutput,
|
|
405
|
+
CONTEXT_MODE_TOOL_TIMEOUT_MS[name],
|
|
358
406
|
dependencies,
|
|
359
|
-
|
|
407
|
+
options,
|
|
408
|
+
);
|
|
360
409
|
}
|