@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,441 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import type { ContextModeSettings, IntegrationAudit, ProviderIntegration } from "../shared";
|
|
5
|
+
import type { LaunchDescriptor } from "./binary";
|
|
6
|
+
|
|
7
|
+
const AUDITED_PROVIDERS = [
|
|
8
|
+
"claude",
|
|
9
|
+
"codex",
|
|
10
|
+
"copilot",
|
|
11
|
+
"cursor",
|
|
12
|
+
"opencode",
|
|
13
|
+
"pi",
|
|
14
|
+
"omp",
|
|
15
|
+
"omp-plugin",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
type AuditedProvider = (typeof AUDITED_PROVIDERS)[number];
|
|
19
|
+
type ContextModePlatform =
|
|
20
|
+
| "claude-code"
|
|
21
|
+
| "codex"
|
|
22
|
+
| "copilot-cli"
|
|
23
|
+
| "cursor"
|
|
24
|
+
| "opencode"
|
|
25
|
+
| "pi"
|
|
26
|
+
| "omp";
|
|
27
|
+
|
|
28
|
+
type McpServer = {
|
|
29
|
+
type: "stdio";
|
|
30
|
+
command: string;
|
|
31
|
+
args: string[];
|
|
32
|
+
env: Record<string, string>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type CreateRequest = {
|
|
36
|
+
config: {
|
|
37
|
+
provider: string;
|
|
38
|
+
cwd?: string;
|
|
39
|
+
internal?: boolean;
|
|
40
|
+
mcpServers?: Readonly<Record<string, unknown>>;
|
|
41
|
+
};
|
|
42
|
+
env?: Readonly<Record<string, string>>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type SessionOpenRequest = {
|
|
46
|
+
provider: string;
|
|
47
|
+
cwd?: string;
|
|
48
|
+
env: Readonly<Record<string, string>>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface IntegrationDependencies {
|
|
52
|
+
home?: string;
|
|
53
|
+
pathExists?: (path: string) => Promise<boolean>;
|
|
54
|
+
fileContains?: (path: string, text: string) => Promise<boolean>;
|
|
55
|
+
now?: () => Date;
|
|
56
|
+
env?: NodeJS.ProcessEnv;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ContextModeStorageOptions {
|
|
60
|
+
home?: string;
|
|
61
|
+
cwd?: string;
|
|
62
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type ProviderContext = {
|
|
66
|
+
home: string;
|
|
67
|
+
cwd?: string;
|
|
68
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type NativeProbe = { kind: "path"; path: string } | { kind: "file"; path: string; marker: string };
|
|
72
|
+
|
|
73
|
+
type ProviderPolicy = {
|
|
74
|
+
platform: ContextModePlatform;
|
|
75
|
+
storageRoot: (context: ProviderContext) => string;
|
|
76
|
+
nativeProbes: (context: ProviderContext) => readonly NativeProbe[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function configuredRoot(
|
|
80
|
+
value: string | undefined,
|
|
81
|
+
fallback: string,
|
|
82
|
+
{ home, cwd }: Pick<ProviderContext, "home" | "cwd">,
|
|
83
|
+
): string {
|
|
84
|
+
const candidate = value?.trim();
|
|
85
|
+
if (!candidate) return fallback;
|
|
86
|
+
if (candidate === "~") return home;
|
|
87
|
+
if (candidate.startsWith("~/") || candidate.startsWith("~\\")) {
|
|
88
|
+
return join(home, candidate.slice(2));
|
|
89
|
+
}
|
|
90
|
+
return isAbsolute(candidate) ? candidate : resolve(cwd ?? home, candidate);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function claudeRoot(context: ProviderContext): string {
|
|
94
|
+
return configuredRoot(context.env.CLAUDE_CONFIG_DIR, join(context.home, ".claude"), context);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function codexRoot(context: ProviderContext): string {
|
|
98
|
+
return configuredRoot(context.env.CODEX_HOME, join(context.home, ".codex"), context);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function copilotRoot(context: ProviderContext): string {
|
|
102
|
+
return configuredRoot(context.env.COPILOT_HOME, join(context.home, ".copilot"), context);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function opencodeRoot(context: ProviderContext): string {
|
|
106
|
+
const configHome = configuredRoot(
|
|
107
|
+
context.env.XDG_CONFIG_HOME,
|
|
108
|
+
join(context.home, ".config"),
|
|
109
|
+
context,
|
|
110
|
+
);
|
|
111
|
+
return join(configHome, "opencode");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ompAgentRoot(context: ProviderContext): string {
|
|
115
|
+
return configuredRoot(
|
|
116
|
+
context.env.PI_CODING_AGENT_DIR,
|
|
117
|
+
join(context.home, ".omp", "agent"),
|
|
118
|
+
context,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function projectProbe(context: ProviderContext, ...segments: string[]): NativeProbe[] {
|
|
123
|
+
return context.cwd
|
|
124
|
+
? [{ kind: "file", path: join(context.cwd, ...segments), marker: "context-mode" }]
|
|
125
|
+
: [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const OMP_POLICY: ProviderPolicy = {
|
|
129
|
+
platform: "omp",
|
|
130
|
+
storageRoot: ({ home }) => join(home, ".omp", "context-mode"),
|
|
131
|
+
nativeProbes: (context) => [
|
|
132
|
+
...projectProbe(context, ".omp", "mcp.json"),
|
|
133
|
+
{ kind: "file", path: join(ompAgentRoot(context), "mcp.json"), marker: "context-mode" },
|
|
134
|
+
{
|
|
135
|
+
kind: "file",
|
|
136
|
+
path: join(context.home, ".omp", "plugins", "package.json"),
|
|
137
|
+
marker: '"context-mode"',
|
|
138
|
+
},
|
|
139
|
+
{ kind: "path", path: join(context.home, ".omp", "plugins", "node_modules", "context-mode") },
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const PROVIDER_POLICIES: Readonly<Record<Exclude<AuditedProvider, "omp-plugin">, ProviderPolicy>> =
|
|
144
|
+
{
|
|
145
|
+
claude: {
|
|
146
|
+
platform: "claude-code",
|
|
147
|
+
storageRoot: (context) => join(claudeRoot(context), "context-mode"),
|
|
148
|
+
nativeProbes: (context) => [
|
|
149
|
+
{
|
|
150
|
+
kind: "file",
|
|
151
|
+
path: join(claudeRoot(context), "settings.json"),
|
|
152
|
+
marker: "context-mode",
|
|
153
|
+
},
|
|
154
|
+
{ kind: "file", path: join(context.home, ".claude.json"), marker: "context-mode" },
|
|
155
|
+
{
|
|
156
|
+
kind: "file",
|
|
157
|
+
path: join(claudeRoot(context), "plugins", "installed_plugins.json"),
|
|
158
|
+
marker: "context-mode",
|
|
159
|
+
},
|
|
160
|
+
{ kind: "path", path: join(claudeRoot(context), "plugins", "cache", "context-mode") },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
codex: {
|
|
164
|
+
platform: "codex",
|
|
165
|
+
storageRoot: (context) => join(codexRoot(context), "context-mode"),
|
|
166
|
+
nativeProbes: (context) => {
|
|
167
|
+
const configPath = join(codexRoot(context), "config.toml");
|
|
168
|
+
return [
|
|
169
|
+
{ kind: "file", path: configPath, marker: '[plugins."context-mode@context-mode"]' },
|
|
170
|
+
{ kind: "file", path: configPath, marker: "[mcp_servers.context-mode]" },
|
|
171
|
+
];
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
copilot: {
|
|
175
|
+
platform: "copilot-cli",
|
|
176
|
+
storageRoot: (context) => join(copilotRoot(context), "context-mode"),
|
|
177
|
+
nativeProbes: (context) => [
|
|
178
|
+
{
|
|
179
|
+
kind: "file",
|
|
180
|
+
path: join(copilotRoot(context), "mcp-config.json"),
|
|
181
|
+
marker: "context-mode",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
cursor: {
|
|
186
|
+
platform: "cursor",
|
|
187
|
+
storageRoot: ({ home }) => join(home, ".cursor", "context-mode"),
|
|
188
|
+
nativeProbes: (context) => [
|
|
189
|
+
...projectProbe(context, ".cursor", "mcp.json"),
|
|
190
|
+
{
|
|
191
|
+
kind: "file",
|
|
192
|
+
path: join(context.home, ".cursor", "mcp.json"),
|
|
193
|
+
marker: "context-mode",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
kind: "path",
|
|
197
|
+
path: join(context.home, ".cursor", "plugins", "local", "context-mode"),
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
opencode: {
|
|
202
|
+
platform: "opencode",
|
|
203
|
+
storageRoot: (context) => join(opencodeRoot(context), "context-mode"),
|
|
204
|
+
nativeProbes: (context) => [
|
|
205
|
+
...(context.cwd
|
|
206
|
+
? [
|
|
207
|
+
"opencode.jsonc",
|
|
208
|
+
"opencode.json",
|
|
209
|
+
join(".opencode", "opencode.jsonc"),
|
|
210
|
+
join(".opencode", "opencode.json"),
|
|
211
|
+
].map(
|
|
212
|
+
(path): NativeProbe => ({
|
|
213
|
+
kind: "file",
|
|
214
|
+
path: join(context.cwd as string, path),
|
|
215
|
+
marker: "context-mode",
|
|
216
|
+
}),
|
|
217
|
+
)
|
|
218
|
+
: []),
|
|
219
|
+
{
|
|
220
|
+
kind: "file",
|
|
221
|
+
path: join(opencodeRoot(context), "opencode.jsonc"),
|
|
222
|
+
marker: "context-mode",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
kind: "file",
|
|
226
|
+
path: join(opencodeRoot(context), "opencode.json"),
|
|
227
|
+
marker: "context-mode",
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
pi: {
|
|
232
|
+
platform: "pi",
|
|
233
|
+
storageRoot: ({ home }) => join(home, ".pi", "context-mode"),
|
|
234
|
+
nativeProbes: ({ home }) => [
|
|
235
|
+
{ kind: "file", path: join(home, ".pi", "agent", "settings.json"), marker: "context-mode" },
|
|
236
|
+
{ kind: "path", path: join(home, ".pi", "extensions", "context-mode", "package.json") },
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
omp: OMP_POLICY,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
function providerPolicy(provider: string): ProviderPolicy | null {
|
|
243
|
+
if (provider === "omp-plugin" || provider.startsWith("omp-plugin-")) return OMP_POLICY;
|
|
244
|
+
return PROVIDER_POLICIES[provider as keyof typeof PROVIDER_POLICIES] ?? null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function defaultPathExists(path: string): Promise<boolean> {
|
|
248
|
+
try {
|
|
249
|
+
const metadata = await stat(path);
|
|
250
|
+
return metadata.isDirectory() || metadata.isFile();
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function defaultFileContains(path: string, text: string): Promise<boolean> {
|
|
257
|
+
try {
|
|
258
|
+
const metadata = await stat(path);
|
|
259
|
+
if (!metadata.isFile() || metadata.size > 1024 * 1024) return false;
|
|
260
|
+
return (await readFile(path, "utf8")).includes(text);
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function nativeIntegrationDetected(
|
|
267
|
+
policy: ProviderPolicy,
|
|
268
|
+
context: ProviderContext,
|
|
269
|
+
pathExists: (path: string) => Promise<boolean>,
|
|
270
|
+
fileContains: (path: string, text: string) => Promise<boolean>,
|
|
271
|
+
): Promise<boolean> {
|
|
272
|
+
for (const probe of policy.nativeProbes(context)) {
|
|
273
|
+
if (probe.kind === "path") {
|
|
274
|
+
if (await pathExists(probe.path)) return true;
|
|
275
|
+
} else if (await fileContains(probe.path, probe.marker)) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function serverEnvironment(
|
|
283
|
+
policy: ProviderPolicy,
|
|
284
|
+
storageRoot: string,
|
|
285
|
+
explicitEnvironment: Readonly<Record<string, string>> = {},
|
|
286
|
+
): Record<string, string> {
|
|
287
|
+
return {
|
|
288
|
+
CONTEXT_MODE_PLATFORM: explicitEnvironment.CONTEXT_MODE_PLATFORM ?? policy.platform,
|
|
289
|
+
CONTEXT_MODE_DIR: explicitEnvironment.CONTEXT_MODE_DIR ?? storageRoot,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function providerContext(
|
|
294
|
+
home: string,
|
|
295
|
+
cwd: string | undefined,
|
|
296
|
+
env: Readonly<Record<string, string | undefined>> | undefined,
|
|
297
|
+
): ProviderContext {
|
|
298
|
+
return { home, cwd, env: env ?? {} };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function contextModeStorageRootFor(
|
|
302
|
+
provider: string,
|
|
303
|
+
options: ContextModeStorageOptions = {},
|
|
304
|
+
): string | null {
|
|
305
|
+
const policy = providerPolicy(provider);
|
|
306
|
+
if (!policy) return null;
|
|
307
|
+
const home = options.home ?? homedir();
|
|
308
|
+
const context = providerContext(home, options.cwd, options.env);
|
|
309
|
+
const explicitRoot = options.env?.CONTEXT_MODE_DIR?.trim();
|
|
310
|
+
if (explicitRoot && isAbsolute(explicitRoot)) return resolve(explicitRoot);
|
|
311
|
+
const dataRoot = options.env?.CONTEXT_MODE_DATA_DIR?.trim();
|
|
312
|
+
if (dataRoot && isAbsolute(dataRoot)) return join(resolve(dataRoot), "context-mode");
|
|
313
|
+
return policy.storageRoot(context);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function createIntegrationAudit(
|
|
317
|
+
settings: ContextModeSettings,
|
|
318
|
+
runtime: { path: string; version: string | null },
|
|
319
|
+
dependencies: IntegrationDependencies = {},
|
|
320
|
+
): Promise<IntegrationAudit> {
|
|
321
|
+
const home = dependencies.home ?? homedir();
|
|
322
|
+
const pathExists = dependencies.pathExists ?? defaultPathExists;
|
|
323
|
+
const fileContains = dependencies.fileContains ?? defaultFileContains;
|
|
324
|
+
const hostEnvironment = dependencies.env ?? process.env;
|
|
325
|
+
const context = providerContext(home, undefined, hostEnvironment);
|
|
326
|
+
const nativeByPolicy = new Map<ProviderPolicy, Promise<boolean>>();
|
|
327
|
+
const detectNative = (policy: ProviderPolicy) => {
|
|
328
|
+
const existing = nativeByPolicy.get(policy);
|
|
329
|
+
if (existing) return existing;
|
|
330
|
+
const detected = nativeIntegrationDetected(policy, context, pathExists, fileContains);
|
|
331
|
+
nativeByPolicy.set(policy, detected);
|
|
332
|
+
return detected;
|
|
333
|
+
};
|
|
334
|
+
const providers: ProviderIntegration[] = [];
|
|
335
|
+
for (const provider of AUDITED_PROVIDERS) {
|
|
336
|
+
const policy = providerPolicy(provider);
|
|
337
|
+
if (!policy) continue;
|
|
338
|
+
const storageRoot = contextModeStorageRootFor(provider, { home, env: hostEnvironment });
|
|
339
|
+
if (!storageRoot) continue;
|
|
340
|
+
const reusesExistingStorage = await pathExists(storageRoot);
|
|
341
|
+
const native =
|
|
342
|
+
settings.autoInject && settings.preferNativeIntegrations && (await detectNative(policy));
|
|
343
|
+
const activation = !settings.autoInject ? "disabled" : native ? "native" : "mcp";
|
|
344
|
+
providers.push({
|
|
345
|
+
provider,
|
|
346
|
+
platform: policy.platform,
|
|
347
|
+
activation,
|
|
348
|
+
storageRoot,
|
|
349
|
+
reusesExistingStorage,
|
|
350
|
+
detail:
|
|
351
|
+
activation === "native"
|
|
352
|
+
? "An existing native Context Mode registration was found in an authoritative provider config location, so duplicate MCP injection is disabled. Already-running agents may need a provider refresh or restart to load it."
|
|
353
|
+
: activation === "mcp"
|
|
354
|
+
? "The bundled Context Mode MCP server is injected only when a new Paseo agent is created. Existing agents keep their saved MCP configuration and must be recreated or configured manually to gain its tools."
|
|
355
|
+
: "Automatic activation is disabled. Existing and new agents keep their current MCP configuration.",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
runtimePath: runtime.path,
|
|
360
|
+
runtimeVersion: runtime.version,
|
|
361
|
+
injectionEnabled: settings.autoInject,
|
|
362
|
+
providers,
|
|
363
|
+
checkedAt: (dependencies.now ?? (() => new Date()))().toISOString(),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function injectContextModeOnCreate<T extends CreateRequest>(
|
|
368
|
+
request: T,
|
|
369
|
+
settings: ContextModeSettings,
|
|
370
|
+
launch: LaunchDescriptor,
|
|
371
|
+
dependencies: IntegrationDependencies = {},
|
|
372
|
+
): Promise<T> {
|
|
373
|
+
if (
|
|
374
|
+
!settings.autoInject ||
|
|
375
|
+
request.config.internal ||
|
|
376
|
+
Object.hasOwn(request.config.mcpServers ?? {}, "context-mode")
|
|
377
|
+
) {
|
|
378
|
+
return request;
|
|
379
|
+
}
|
|
380
|
+
const policy = providerPolicy(request.config.provider);
|
|
381
|
+
if (!policy) return request;
|
|
382
|
+
const home = dependencies.home ?? homedir();
|
|
383
|
+
const hostEnvironment = dependencies.env ?? process.env;
|
|
384
|
+
const pathExists = dependencies.pathExists ?? defaultPathExists;
|
|
385
|
+
const fileContains = dependencies.fileContains ?? defaultFileContains;
|
|
386
|
+
const context = providerContext(home, request.config.cwd, {
|
|
387
|
+
...hostEnvironment,
|
|
388
|
+
...request.env,
|
|
389
|
+
});
|
|
390
|
+
if (
|
|
391
|
+
settings.preferNativeIntegrations &&
|
|
392
|
+
(await nativeIntegrationDetected(policy, context, pathExists, fileContains))
|
|
393
|
+
) {
|
|
394
|
+
return request;
|
|
395
|
+
}
|
|
396
|
+
const storageRoot = contextModeStorageRootFor(request.config.provider, {
|
|
397
|
+
home,
|
|
398
|
+
cwd: request.config.cwd,
|
|
399
|
+
env: { ...hostEnvironment, ...request.env },
|
|
400
|
+
});
|
|
401
|
+
if (!storageRoot) return request;
|
|
402
|
+
const env = serverEnvironment(policy, storageRoot, request.env);
|
|
403
|
+
const server: McpServer = {
|
|
404
|
+
type: "stdio",
|
|
405
|
+
command: launch.program,
|
|
406
|
+
args: launch.args,
|
|
407
|
+
env,
|
|
408
|
+
};
|
|
409
|
+
return {
|
|
410
|
+
...request,
|
|
411
|
+
config: {
|
|
412
|
+
...request.config,
|
|
413
|
+
mcpServers: { ...request.config.mcpServers, "context-mode": server },
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function injectContextModeEnvironment<T extends SessionOpenRequest>(
|
|
419
|
+
request: T,
|
|
420
|
+
settings: ContextModeSettings,
|
|
421
|
+
dependencies: IntegrationDependencies = {},
|
|
422
|
+
): Promise<T> {
|
|
423
|
+
if (!settings.autoInject) return request;
|
|
424
|
+
const policy = providerPolicy(request.provider);
|
|
425
|
+
if (!policy) return request;
|
|
426
|
+
const home = dependencies.home ?? homedir();
|
|
427
|
+
const hostEnvironment = dependencies.env ?? process.env;
|
|
428
|
+
const storageRoot = contextModeStorageRootFor(request.provider, {
|
|
429
|
+
home,
|
|
430
|
+
cwd: request.cwd,
|
|
431
|
+
env: { ...hostEnvironment, ...request.env },
|
|
432
|
+
});
|
|
433
|
+
if (!storageRoot) return request;
|
|
434
|
+
return {
|
|
435
|
+
...request,
|
|
436
|
+
env: {
|
|
437
|
+
...serverEnvironment(policy, storageRoot),
|
|
438
|
+
...request.env,
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import type { PluginSettings } from "@getpaseo/plugin/server";
|
|
4
|
+
import type {
|
|
5
|
+
ContextModePlatform,
|
|
6
|
+
ContextModeProvider,
|
|
7
|
+
FetchAndIndexInput,
|
|
8
|
+
IndexPathInput,
|
|
9
|
+
KnowledgeToolResult,
|
|
10
|
+
PurgeKnowledgeInput,
|
|
11
|
+
SearchKnowledgeInput,
|
|
12
|
+
} from "../shared/knowledge";
|
|
13
|
+
import type { ContextModeSettings, contextModeSettings } from "../shared/settings";
|
|
14
|
+
import { type BinaryResolution, type LaunchDescriptor, resolveContextModeBinary } from "./binary";
|
|
15
|
+
import { contextModeStorageRootFor } from "./integration";
|
|
16
|
+
import {
|
|
17
|
+
type ContextModeToolName,
|
|
18
|
+
callContextModeTool,
|
|
19
|
+
type McpProcessDependencies,
|
|
20
|
+
type McpToolArguments,
|
|
21
|
+
} from "./mcp-process";
|
|
22
|
+
|
|
23
|
+
type SettingsHandle = PluginSettings<typeof contextModeSettings.schema>;
|
|
24
|
+
|
|
25
|
+
type KnowledgeToolCaller = (
|
|
26
|
+
launch: LaunchDescriptor,
|
|
27
|
+
name: ContextModeToolName,
|
|
28
|
+
arguments_: McpToolArguments,
|
|
29
|
+
dependencies: McpProcessDependencies,
|
|
30
|
+
) => Promise<string>;
|
|
31
|
+
|
|
32
|
+
export interface KnowledgeHandlerDependencies {
|
|
33
|
+
now?: () => Date;
|
|
34
|
+
resolveBinary?: (settings: ContextModeSettings) => Promise<BinaryResolution>;
|
|
35
|
+
callTool?: KnowledgeToolCaller;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ContextModeKnowledgeHandlers {
|
|
39
|
+
search(input: SearchKnowledgeInput): Promise<KnowledgeToolResult>;
|
|
40
|
+
indexPath(input: IndexPathInput): Promise<KnowledgeToolResult>;
|
|
41
|
+
fetchAndIndex(input: FetchAndIndexInput): Promise<KnowledgeToolResult>;
|
|
42
|
+
purge(input: PurgeKnowledgeInput): Promise<KnowledgeToolResult>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PROVIDER_PLATFORMS: Readonly<Record<ContextModeProvider, ContextModePlatform>> = {
|
|
46
|
+
claude: "claude-code",
|
|
47
|
+
codex: "codex",
|
|
48
|
+
copilot: "copilot-cli",
|
|
49
|
+
cursor: "cursor",
|
|
50
|
+
opencode: "opencode",
|
|
51
|
+
pi: "pi",
|
|
52
|
+
omp: "omp",
|
|
53
|
+
"omp-plugin": "omp",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function contextModePlatformFor(provider: ContextModeProvider): ContextModePlatform {
|
|
57
|
+
return PROVIDER_PLATFORMS[provider];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function contextModeEnvironmentFor(
|
|
61
|
+
provider: ContextModeProvider,
|
|
62
|
+
home = homedir(),
|
|
63
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
64
|
+
): NodeJS.ProcessEnv {
|
|
65
|
+
const storageRoot = contextModeStorageRootFor(provider, { home, env });
|
|
66
|
+
if (!storageRoot) throw new Error(`Context Mode does not support provider ${provider}.`);
|
|
67
|
+
return {
|
|
68
|
+
CONTEXT_MODE_PLATFORM: contextModePlatformFor(provider),
|
|
69
|
+
CONTEXT_MODE_DIR: storageRoot,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function definedArguments(values: Record<string, unknown>): McpToolArguments {
|
|
74
|
+
return Object.fromEntries(
|
|
75
|
+
Object.entries(values).filter(([, value]) => value !== undefined),
|
|
76
|
+
) as McpToolArguments;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createContextModeKnowledgeHandlers(
|
|
80
|
+
settingsHandle: SettingsHandle,
|
|
81
|
+
dependencies: KnowledgeHandlerDependencies = {},
|
|
82
|
+
): ContextModeKnowledgeHandlers {
|
|
83
|
+
const now = dependencies.now ?? (() => new Date());
|
|
84
|
+
const resolveBinary = dependencies.resolveBinary ?? resolveContextModeBinary;
|
|
85
|
+
const callTool = dependencies.callTool ?? callContextModeTool;
|
|
86
|
+
|
|
87
|
+
async function launchDescriptor(): Promise<LaunchDescriptor> {
|
|
88
|
+
const settings = await settingsHandle.read();
|
|
89
|
+
if (settings.status !== "ready") {
|
|
90
|
+
throw new Error(`Context Mode settings are invalid: ${settings.error}`.slice(0, 4_096));
|
|
91
|
+
}
|
|
92
|
+
const binary = await resolveBinary(settings.values);
|
|
93
|
+
if (binary.state === "missing") throw new Error(binary.message.slice(0, 4_096));
|
|
94
|
+
return binary.launch;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function run(
|
|
98
|
+
provider: ContextModeProvider,
|
|
99
|
+
projectPath: string,
|
|
100
|
+
name: ContextModeToolName,
|
|
101
|
+
arguments_: McpToolArguments,
|
|
102
|
+
environmentOverrides: NodeJS.ProcessEnv = {},
|
|
103
|
+
): Promise<KnowledgeToolResult> {
|
|
104
|
+
const launch = await launchDescriptor();
|
|
105
|
+
const output = await callTool(launch, name, arguments_, {
|
|
106
|
+
env: { ...contextModeEnvironmentFor(provider), ...environmentOverrides },
|
|
107
|
+
cwd: projectPath,
|
|
108
|
+
});
|
|
109
|
+
return { provider, output, completedAt: now().toISOString() };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
search({ provider, projectPath, queries, limit, source, contentType, sort }) {
|
|
114
|
+
return run(
|
|
115
|
+
provider,
|
|
116
|
+
projectPath,
|
|
117
|
+
"ctx_search",
|
|
118
|
+
definedArguments({ queries, limit, source, contentType, sort }),
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
indexPath({
|
|
122
|
+
provider,
|
|
123
|
+
projectPath,
|
|
124
|
+
path,
|
|
125
|
+
source,
|
|
126
|
+
include,
|
|
127
|
+
exclude,
|
|
128
|
+
maxDepth,
|
|
129
|
+
maxFiles,
|
|
130
|
+
extensions,
|
|
131
|
+
respectGitignore,
|
|
132
|
+
followSymlinks,
|
|
133
|
+
}) {
|
|
134
|
+
if (!isAbsolute(path)) {
|
|
135
|
+
throw new Error("Knowledge indexing requires an absolute path on this host.");
|
|
136
|
+
}
|
|
137
|
+
return run(
|
|
138
|
+
provider,
|
|
139
|
+
projectPath,
|
|
140
|
+
"ctx_index",
|
|
141
|
+
definedArguments({
|
|
142
|
+
path,
|
|
143
|
+
source,
|
|
144
|
+
include,
|
|
145
|
+
exclude,
|
|
146
|
+
maxDepth,
|
|
147
|
+
maxFiles,
|
|
148
|
+
extensions,
|
|
149
|
+
respectGitignore,
|
|
150
|
+
followSymlinks,
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
fetchAndIndex({ provider, projectPath, url, source, force, ttl }) {
|
|
155
|
+
return run(
|
|
156
|
+
provider,
|
|
157
|
+
projectPath,
|
|
158
|
+
"ctx_fetch_and_index",
|
|
159
|
+
definedArguments({ url, source, force, ttl }),
|
|
160
|
+
{ CTX_FETCH_STRICT: "1" },
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
async purge(input) {
|
|
164
|
+
const raw = input as PurgeKnowledgeInput & { sessionId?: string };
|
|
165
|
+
if (raw.confirm !== true) {
|
|
166
|
+
throw new Error("Purge requires exact confirmation with confirm: true.");
|
|
167
|
+
}
|
|
168
|
+
if (raw.scope === "project") {
|
|
169
|
+
if (raw.sessionId !== undefined) {
|
|
170
|
+
throw new Error("Purge requires exactly one scope.");
|
|
171
|
+
}
|
|
172
|
+
return await run(raw.provider, raw.projectPath, "ctx_purge", {
|
|
173
|
+
confirm: true,
|
|
174
|
+
scope: "project",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (raw.scope !== "session" || !raw.sessionId) {
|
|
178
|
+
throw new Error("Session purge requires exactly one non-empty sessionId scope.");
|
|
179
|
+
}
|
|
180
|
+
return await run(raw.provider, raw.projectPath, "ctx_purge", {
|
|
181
|
+
confirm: true,
|
|
182
|
+
scope: "session",
|
|
183
|
+
sessionId: raw.sessionId,
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|