@omercnet/paseo-context-mode 3.1.1-next.332.1 → 3.1.1-next.353.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/index.server.ts CHANGED
@@ -45,7 +45,7 @@ export default function contribute(server: PluginServerContext) {
45
45
  server.before("agent.create", async ({ request }, { paseo }) => {
46
46
  const current = await settings.read();
47
47
  if (current.status !== "ready") return request;
48
- const binary = await resolveContextModeBinary(current.values);
48
+ const binary = await resolveContextModeBinary(current.values, { paseo });
49
49
  if (binary.state !== "found") return request;
50
50
  const providers = await readProviderOverrides(paseo);
51
51
  return injectContextModeOnCreate(request, current.values, binary.launch, { providers });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-context-mode",
3
- "version": "3.1.1-next.332.1",
3
+ "version": "3.1.1-next.353.1",
4
4
  "type": "module",
5
5
  "description": "Provider-aware Context Mode health, savings, and MCP activation for Paseo.",
6
6
  "license": "MIT",
package/server/actions.ts CHANGED
@@ -6,8 +6,9 @@ import {
6
6
  type DoctorReport,
7
7
  type DoctorStatusRow,
8
8
  } from "../shared/actions";
9
- import type { ContextModeSettings, contextModeSettings } from "../shared/settings";
9
+ import type { contextModeSettings } from "../shared/settings";
10
10
  import { type BinaryResolution, type LaunchDescriptor, resolveContextModeBinary } from "./binary";
11
+ import type { ProviderConfigSource } from "./integration";
11
12
  import { contextModeEnvironmentFor, contextModePlatformFor } from "./knowledge";
12
13
  import {
13
14
  callContextModeTool,
@@ -28,7 +29,7 @@ type ActionToolCaller = (
28
29
 
29
30
  export interface ActionHandlerDependencies {
30
31
  osPlatform?: NodeJS.Platform;
31
- resolveBinary?: (settings: ContextModeSettings) => Promise<BinaryResolution>;
32
+ resolveBinary?: typeof resolveContextModeBinary;
32
33
  inspect?: (
33
34
  launch: LaunchDescriptor,
34
35
  dependencies?: McpProcessDependencies,
@@ -36,10 +37,12 @@ export interface ActionHandlerDependencies {
36
37
  callTool?: ActionToolCaller;
37
38
  }
38
39
 
40
+ type ActionContext = { paseo?: ProviderConfigSource };
41
+
39
42
  export interface ContextModeActionHandlers {
40
- doctor(input: ContextModeActionInput): Promise<DoctorReport>;
41
- install(input: ContextModeActionInput): Promise<ContextModeAction>;
42
- upgrade(input: ContextModeActionInput): Promise<ContextModeAction>;
43
+ doctor(input: ContextModeActionInput, context?: ActionContext): Promise<DoctorReport>;
44
+ install(input: ContextModeActionInput, context?: ActionContext): Promise<ContextModeAction>;
45
+ upgrade(input: ContextModeActionInput, context?: ActionContext): Promise<ContextModeAction>;
43
46
  }
44
47
 
45
48
  export function parseContextModeDoctorOutput(rawOutput: string): DoctorStatusRow[] {
@@ -71,19 +74,24 @@ export function createContextModeActionHandlers(
71
74
  const callTool = dependencies.callTool ?? callContextModeTool;
72
75
  const osPlatform = dependencies.osPlatform ?? process.platform;
73
76
 
74
- async function resolvedBinary(): Promise<Extract<BinaryResolution, { state: "found" }>> {
77
+ async function resolvedBinary(
78
+ paseo: ProviderConfigSource | undefined,
79
+ ): Promise<Extract<BinaryResolution, { state: "found" }>> {
75
80
  const settings = await settingsHandle.read();
76
81
  if (settings.status !== "ready") {
77
82
  throw new Error(`Context Mode settings are invalid: ${settings.error}`.slice(0, 4_096));
78
83
  }
79
- const binary = await resolveBinary(settings.values);
84
+ const binary = await resolveBinary(settings.values, { paseo });
80
85
  if (binary.state === "missing") throw new Error(binary.message.slice(0, 4_096));
81
86
  return binary;
82
87
  }
83
88
 
84
- async function currentVersion(input: ContextModeActionInput): Promise<string | null> {
89
+ async function currentVersion(
90
+ input: ContextModeActionInput,
91
+ paseo: ProviderConfigSource | undefined,
92
+ ): Promise<string | null> {
85
93
  try {
86
- const binary = await resolvedBinary();
94
+ const binary = await resolvedBinary(paseo);
87
95
  const inspection = await inspect(binary.launch, {
88
96
  env: contextModeEnvironmentFor(input.provider),
89
97
  });
@@ -94,8 +102,8 @@ export function createContextModeActionHandlers(
94
102
  }
95
103
 
96
104
  return {
97
- async doctor(input) {
98
- const binary = await resolvedBinary();
105
+ async doctor(input, context) {
106
+ const binary = await resolvedBinary(context?.paseo);
99
107
  const rawOutput = await callTool(
100
108
  binary.launch,
101
109
  "ctx_doctor",
@@ -110,7 +118,7 @@ export function createContextModeActionHandlers(
110
118
  rawOutput,
111
119
  };
112
120
  },
113
- async install(input) {
121
+ async install(input, context) {
114
122
  const platform = contextModePlatformFor(input.provider);
115
123
  return {
116
124
  program: osPlatform === "win32" ? "npm.cmd" : "npm",
@@ -121,15 +129,15 @@ export function createContextModeActionHandlers(
121
129
  "--no-audit",
122
130
  "--no-fund",
123
131
  ],
124
- currentVersion: await currentVersion(input),
132
+ currentVersion: await currentVersion(input, context?.paseo),
125
133
  targetVersion: CONTEXT_MODE_TARGET_VERSION,
126
134
  platform,
127
135
  requiresRestart: true,
128
136
  };
129
137
  },
130
- async upgrade(input) {
138
+ async upgrade(input, context) {
131
139
  const platform = contextModePlatformFor(input.provider);
132
- const binary = await resolvedBinary();
140
+ const binary = await resolvedBinary(context?.paseo);
133
141
  const inspection = await inspect(binary.launch, {
134
142
  env: contextModeEnvironmentFor(input.provider),
135
143
  });
package/server/binary.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { constants } from "node:fs";
2
- import { access, realpath, stat } from "node:fs/promises";
2
+ import { access, readFile, realpath, stat } from "node:fs/promises";
3
3
  import { createRequire } from "node:module";
4
4
  import { join, posix, win32 } from "node:path";
5
5
  import type { ContextModeSettings } from "../shared";
6
+ import type { ProviderConfigSource } from "./integration";
6
7
 
7
8
  export interface LaunchDescriptor {
8
9
  program: string;
9
10
  args: string[];
11
+ env?: Record<string, string>;
10
12
  }
11
13
 
12
14
  export type BinaryResolution =
@@ -22,9 +24,32 @@ export interface BinaryResolutionDependencies {
22
24
  env?: NodeJS.ProcessEnv;
23
25
  platform?: NodeJS.Platform;
24
26
  nodePath?: string;
27
+ electron?: boolean;
25
28
  canExecute?: (path: string) => Promise<boolean>;
26
29
  isFile?: (path: string) => Promise<boolean>;
27
- bundledCliPath?: () => string | null;
30
+ paseo?: ProviderConfigSource;
31
+ }
32
+
33
+ /**
34
+ * Paseo evaluates the server bundle from the daemon's cwd with an empty `import.meta`, so the
35
+ * bundled runtime is resolved from this plugin's directory in the daemon config instead.
36
+ */
37
+ async function findBundledCli(paseo: ProviderConfigSource | undefined): Promise<string | null> {
38
+ const { config } = (await paseo?.config.get().catch(() => undefined)) ?? {};
39
+ const plugins = config?.plugins;
40
+ if (typeof plugins !== "object" || plugins === null) return null;
41
+ for (const source of Object.values(plugins)) {
42
+ const directory = (source as { path?: unknown } | null)?.path;
43
+ if (typeof directory !== "string") continue;
44
+ try {
45
+ const manifest = JSON.parse(await readFile(join(directory, "paseo-plugin.json"), "utf8"));
46
+ if (manifest?.id !== "context-mode") continue;
47
+ return createRequire(join(directory, "package.json")).resolve("context-mode/cli");
48
+ } catch {
49
+ // Not this plugin, or its install has no bundled runtime.
50
+ }
51
+ }
52
+ return null;
28
53
  }
29
54
 
30
55
  async function defaultIsFile(path: string): Promise<boolean> {
@@ -141,25 +166,21 @@ export async function resolveContextModeBinary(
141
166
  }
142
167
  }
143
168
 
144
- const bundledCandidates: string[] = [];
145
- const injectedBundledPath = dependencies.bundledCliPath?.();
146
- if (injectedBundledPath) bundledCandidates.push(injectedBundledPath);
147
- if (!dependencies.bundledCliPath) {
148
- bundledCandidates.push(join(process.cwd(), "node_modules", "context-mode", "cli.bundle.mjs"));
149
- try {
150
- bundledCandidates.push(createRequire(import.meta.url).resolve("context-mode/cli"));
151
- } catch {
152
- // The plugin bundle may run outside the dependency tree; the cwd candidate remains valid.
153
- }
154
- }
155
- for (const bundledCli of new Set(bundledCandidates)) {
156
- if (!(await isFile(bundledCli))) continue;
169
+ const bundledCli = await findBundledCli(dependencies.paseo);
170
+ if (bundledCli) {
157
171
  const canonicalPath = await realpath(bundledCli).catch(() => bundledCli);
172
+ // Under the desktop app `nodePath` is Electron. Paseo strips this variable from agent
173
+ // environments, so an injected MCP server needs it to start as Node.
174
+ const electron = dependencies.electron ?? process.versions.electron !== undefined;
158
175
  return {
159
176
  state: "found",
160
177
  path: canonicalPath,
161
178
  source: "bundled",
162
- launch: { program: nodePath, args: [canonicalPath] },
179
+ launch: {
180
+ program: nodePath,
181
+ args: [canonicalPath],
182
+ ...(electron ? { env: { ELECTRON_RUN_AS_NODE: "1" } } : {}),
183
+ },
163
184
  };
164
185
  }
165
186
 
@@ -42,7 +42,7 @@ function contextModeProbeOptions(env: NodeJS.ProcessEnv = process.env): ProbeOpt
42
42
 
43
43
  export interface ContextModeHandlerDependencies {
44
44
  now?: () => Date;
45
- resolveBinary?: (settings: ContextModeSettings) => Promise<BinaryResolution>;
45
+ resolveBinary?: typeof resolveContextModeBinary;
46
46
  inspect?: (launch: LaunchDescriptor, options: ProbeOptions) => Promise<McpInspection>;
47
47
  callTool?: (
48
48
  launch: LaunchDescriptor,
@@ -57,14 +57,13 @@ export interface ContextModeHandlerDependencies {
57
57
  ) => Promise<IntegrationAudit>;
58
58
  }
59
59
 
60
+ type HandlerContext = { paseo?: ProviderConfigSource };
61
+
60
62
  export interface ContextModeHandlers {
61
- status(input: ContextModeRefreshInput): Promise<BinaryStatus>;
62
- doctor(input: ContextModeRefreshInput): Promise<CommandReport>;
63
- stats(input: ContextModeRefreshInput): Promise<CommandReport>;
64
- audit(
65
- input: ContextModeRefreshInput,
66
- context?: { paseo?: ProviderConfigSource },
67
- ): Promise<IntegrationAudit>;
63
+ status(input: ContextModeRefreshInput, context?: HandlerContext): Promise<BinaryStatus>;
64
+ doctor(input: ContextModeRefreshInput, context?: HandlerContext): Promise<CommandReport>;
65
+ stats(input: ContextModeRefreshInput, context?: HandlerContext): Promise<CommandReport>;
66
+ audit(input: ContextModeRefreshInput, context?: HandlerContext): Promise<IntegrationAudit>;
68
67
  }
69
68
 
70
69
  interface CacheEntry<T> {
@@ -93,7 +92,7 @@ export function createContextModeHandlers(
93
92
  const cache = new Map<string, CacheEntry<BinaryStatus | CommandReport>>();
94
93
  const probe = contextModeProbeOptions();
95
94
 
96
- async function settingsAndBinary(): Promise<
95
+ async function settingsAndBinary(paseo: ProviderConfigSource | undefined): Promise<
97
96
  | {
98
97
  state: "ready";
99
98
  settings: ContextModeSettings;
@@ -115,7 +114,7 @@ export function createContextModeHandlers(
115
114
  },
116
115
  };
117
116
  }
118
- const binary = await resolveBinary(settings.values);
117
+ const binary = await resolveBinary(settings.values, { paseo });
119
118
  if (binary.state === "missing") {
120
119
  return {
121
120
  state: "unavailable",
@@ -159,8 +158,11 @@ export function createContextModeHandlers(
159
158
  return result;
160
159
  }
161
160
 
162
- async function status({ fresh }: ContextModeRefreshInput): Promise<BinaryStatus> {
163
- const resolved = await settingsAndBinary();
161
+ async function status(
162
+ { fresh }: ContextModeRefreshInput,
163
+ context?: HandlerContext,
164
+ ): Promise<BinaryStatus> {
165
+ const resolved = await settingsAndBinary(context?.paseo);
164
166
  if (resolved.state === "unavailable") return resolved.report;
165
167
  const cacheKey = `status:${resolved.binary.source}:${resolved.binary.path}`;
166
168
  return cached(cacheKey, 10_000, fresh, async () => {
@@ -182,8 +184,13 @@ export function createContextModeHandlers(
182
184
  });
183
185
  }
184
186
 
185
- async function report(name: ToolName, ttlMs: number, fresh: boolean): Promise<CommandReport> {
186
- const resolved = await settingsAndBinary();
187
+ async function report(
188
+ name: ToolName,
189
+ ttlMs: number,
190
+ fresh: boolean,
191
+ context: HandlerContext | undefined,
192
+ ): Promise<CommandReport> {
193
+ const resolved = await settingsAndBinary(context?.paseo);
187
194
  if (resolved.state === "unavailable") return resolved.report;
188
195
  return cached(`${name}:${resolved.binary.path}`, ttlMs, fresh, async () => {
189
196
  const checkedAt = now().toISOString();
@@ -202,9 +209,9 @@ export function createContextModeHandlers(
202
209
 
203
210
  async function audit(
204
211
  _input: ContextModeRefreshInput,
205
- context?: { paseo?: ProviderConfigSource },
212
+ context?: HandlerContext,
206
213
  ): Promise<IntegrationAudit> {
207
- const resolved = await settingsAndBinary();
214
+ const resolved = await settingsAndBinary(context?.paseo);
208
215
  if (resolved.state === "unavailable") {
209
216
  return {
210
217
  runtimePath: "",
@@ -227,8 +234,8 @@ export function createContextModeHandlers(
227
234
 
228
235
  return {
229
236
  status,
230
- doctor: ({ fresh }) => report("ctx_doctor", 10_000, fresh),
231
- stats: ({ fresh }) => report("ctx_stats", 5_000, fresh),
237
+ doctor: ({ fresh }, context) => report("ctx_doctor", 10_000, fresh, context),
238
+ stats: ({ fresh }, context) => report("ctx_stats", 5_000, fresh, context),
232
239
  audit,
233
240
  };
234
241
  }
@@ -63,7 +63,7 @@ export type ProviderOverrides = Readonly<
63
63
 
64
64
  /** Structural slice of the hook/RPC `paseo` API; avoids a client-package import. */
65
65
  export interface ProviderConfigSource {
66
- config: { get(): Promise<{ config: { providers?: unknown } }> };
66
+ config: { get(): Promise<{ config: { providers?: unknown; plugins?: unknown } }> };
67
67
  }
68
68
 
69
69
  /**
@@ -483,7 +483,7 @@ export async function injectContextModeOnCreate<T extends CreateRequest>(
483
483
  type: "stdio",
484
484
  command: launch.program,
485
485
  args: launch.args,
486
- env: serverEnvironment(policy, storageRoot, request.env),
486
+ env: { ...serverEnvironment(policy, storageRoot, request.env), ...launch.env },
487
487
  };
488
488
  return {
489
489
  ...request,
@@ -10,8 +10,8 @@ import type {
10
10
  PurgeKnowledgeInput,
11
11
  SearchKnowledgeInput,
12
12
  } from "../shared/knowledge";
13
- import type { ContextModeSettings, contextModeSettings } from "../shared/settings";
14
- import { type BinaryResolution, type LaunchDescriptor, resolveContextModeBinary } from "./binary";
13
+ import type { contextModeSettings } from "../shared/settings";
14
+ import { type LaunchDescriptor, resolveContextModeBinary } from "./binary";
15
15
  import {
16
16
  contextModeStorageRootFor,
17
17
  type ProviderConfigSource,
@@ -37,7 +37,7 @@ type KnowledgeToolCaller = (
37
37
 
38
38
  export interface KnowledgeHandlerDependencies {
39
39
  now?: () => Date;
40
- resolveBinary?: (settings: ContextModeSettings) => Promise<BinaryResolution>;
40
+ resolveBinary?: typeof resolveContextModeBinary;
41
41
  callTool?: KnowledgeToolCaller;
42
42
  }
43
43
 
@@ -99,12 +99,14 @@ export function createContextModeKnowledgeHandlers(
99
99
  const resolveBinary = dependencies.resolveBinary ?? resolveContextModeBinary;
100
100
  const callTool = dependencies.callTool ?? callContextModeTool;
101
101
 
102
- async function launchDescriptor(): Promise<LaunchDescriptor> {
102
+ async function launchDescriptor(
103
+ paseo: ProviderConfigSource | undefined,
104
+ ): Promise<LaunchDescriptor> {
103
105
  const settings = await settingsHandle.read();
104
106
  if (settings.status !== "ready") {
105
107
  throw new Error(`Context Mode settings are invalid: ${settings.error}`.slice(0, 4_096));
106
108
  }
107
- const binary = await resolveBinary(settings.values);
109
+ const binary = await resolveBinary(settings.values, { paseo });
108
110
  if (binary.state === "missing") throw new Error(binary.message.slice(0, 4_096));
109
111
  return binary.launch;
110
112
  }
@@ -117,7 +119,7 @@ export function createContextModeKnowledgeHandlers(
117
119
  context: KnowledgeContext | undefined,
118
120
  environmentOverrides: NodeJS.ProcessEnv = {},
119
121
  ): Promise<KnowledgeToolResult> {
120
- const launch = await launchDescriptor();
122
+ const launch = await launchDescriptor(context?.paseo);
121
123
  const providers = await readProviderOverrides(context?.paseo);
122
124
  const output = await callTool(launch, name, arguments_, {
123
125
  env: {
@@ -125,7 +125,10 @@ async function exchange(
125
125
  stdio: ["pipe", "pipe", "pipe"],
126
126
  windowsHide: true,
127
127
  cwd: dependencies.cwd,
128
- env: dependencies.env ? { ...process.env, ...dependencies.env } : undefined,
128
+ env:
129
+ launch.env || dependencies.env
130
+ ? { ...process.env, ...launch.env, ...dependencies.env }
131
+ : undefined,
129
132
  });
130
133
  } catch (error) {
131
134
  throw new ProcessFailure(