@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.
@@ -0,0 +1,424 @@
1
+ import { basename } from "node:path";
2
+ import type {
3
+ AnalyticsByteAccounting,
4
+ AnalyticsDashboard,
5
+ AnalyticsWarning,
6
+ IndexedSourceAnalytics,
7
+ ProviderAnalytics,
8
+ } from "../shared/analytics";
9
+ import {
10
+ CONTEXT_MODE_ANALYTICS_SCHEMA_VERSION,
11
+ SUPPORTED_CONTEXT_MODE_DATABASE_VERSION,
12
+ } from "../shared/analytics";
13
+ import {
14
+ ContextModeDatabaseError,
15
+ type ContextModeDatabaseSnapshot,
16
+ readContextModeDatabase,
17
+ type SessionDatabaseSnapshot,
18
+ } from "./context-mode-db";
19
+ import {
20
+ discoverContextModeStorageRoots,
21
+ listRegularContextModeDatabases,
22
+ type StorageDiscoveryOptions,
23
+ type StorageDiscoveryResult,
24
+ } from "./storage-discovery";
25
+
26
+ const MAX_WARNINGS = 20;
27
+ const MAX_SOURCES = 2_000;
28
+ const MAX_PROVIDERS = 32;
29
+ const MAX_CATEGORIES = 128;
30
+ const MAX_DAYS = 400;
31
+
32
+ interface MutableByteAccounting {
33
+ eventDataBytes: number;
34
+ avoidedBytes: number;
35
+ returnedBytes: number;
36
+ snapshotBytes: number;
37
+ indexedBytes: number;
38
+ }
39
+
40
+ interface MutableAggregate {
41
+ databases: number;
42
+ events: number;
43
+ toolCalls: number;
44
+ indexedSources: number;
45
+ indexedChunks: number;
46
+ sessions: Set<string>;
47
+ projects: Set<string>;
48
+ bytes: MutableByteAccounting;
49
+ }
50
+
51
+ interface MutableProviderAggregate extends MutableAggregate {
52
+ provider: string;
53
+ }
54
+
55
+ interface MutableSummary {
56
+ count: number;
57
+ savedBytes: number;
58
+ }
59
+
60
+ interface MutableSource {
61
+ provider: string;
62
+ source: string;
63
+ chunks: number;
64
+ codeChunks: number;
65
+ indexedBytes: number;
66
+ indexedAt: string | null;
67
+ state: "ready" | "empty" | "unavailable";
68
+ detail?: string;
69
+ }
70
+
71
+ export interface ContextModeAnalyticsDependencies extends StorageDiscoveryOptions {
72
+ now?: () => Date;
73
+ discoverStorage?: (options?: StorageDiscoveryOptions) => Promise<StorageDiscoveryResult>;
74
+ listDatabases?: typeof listRegularContextModeDatabases;
75
+ readDatabase?: (path: string) => ContextModeDatabaseSnapshot;
76
+ }
77
+
78
+ class WarningCollector {
79
+ readonly items: AnalyticsWarning[] = [];
80
+ #omitted = 0;
81
+
82
+ add(warning: AnalyticsWarning): void {
83
+ const normalized: AnalyticsWarning = {
84
+ ...warning,
85
+ ...(warning.provider ? { provider: warning.provider.slice(0, 128) } : {}),
86
+ ...(warning.database ? { database: warning.database.slice(0, 512) } : {}),
87
+ message: warning.message.slice(0, 512),
88
+ };
89
+ if (this.items.length < MAX_WARNINGS - 1) this.items.push(normalized);
90
+ else this.#omitted++;
91
+ }
92
+
93
+ finish(): AnalyticsWarning[] {
94
+ if (this.#omitted > 0) {
95
+ this.items.push({
96
+ code: "warning-limit",
97
+ message: `${this.#omitted} additional analytics warnings were omitted.`,
98
+ });
99
+ }
100
+ return this.items;
101
+ }
102
+
103
+ get hasWarnings(): boolean {
104
+ return this.items.length > 0 || this.#omitted > 0;
105
+ }
106
+ }
107
+
108
+ function createAggregate(): MutableAggregate {
109
+ return {
110
+ databases: 0,
111
+ events: 0,
112
+ toolCalls: 0,
113
+ indexedSources: 0,
114
+ indexedChunks: 0,
115
+ sessions: new Set<string>(),
116
+ projects: new Set<string>(),
117
+ bytes: {
118
+ eventDataBytes: 0,
119
+ avoidedBytes: 0,
120
+ returnedBytes: 0,
121
+ snapshotBytes: 0,
122
+ indexedBytes: 0,
123
+ },
124
+ };
125
+ }
126
+
127
+ function boundedAdd(left: number, right: number): number {
128
+ return Math.min(Number.MAX_SAFE_INTEGER, left + right);
129
+ }
130
+
131
+ function addBytes(target: MutableByteAccounting, source: Partial<MutableByteAccounting>): void {
132
+ target.eventDataBytes = boundedAdd(target.eventDataBytes, source.eventDataBytes ?? 0);
133
+ target.avoidedBytes = boundedAdd(target.avoidedBytes, source.avoidedBytes ?? 0);
134
+ target.returnedBytes = boundedAdd(target.returnedBytes, source.returnedBytes ?? 0);
135
+ target.snapshotBytes = boundedAdd(target.snapshotBytes, source.snapshotBytes ?? 0);
136
+ target.indexedBytes = boundedAdd(target.indexedBytes, source.indexedBytes ?? 0);
137
+ }
138
+
139
+ function savedBytes(bytes: MutableByteAccounting): number {
140
+ return [bytes.eventDataBytes, bytes.avoidedBytes, bytes.snapshotBytes, bytes.indexedBytes].reduce(
141
+ boundedAdd,
142
+ 0,
143
+ );
144
+ }
145
+
146
+ function byteAccounting(bytes: MutableByteAccounting): AnalyticsByteAccounting {
147
+ return { ...bytes, savedBytes: savedBytes(bytes) };
148
+ }
149
+
150
+ function tokenFields(bytes: MutableByteAccounting): {
151
+ inputTokens: number;
152
+ outputTokens: number;
153
+ savedTokens: number;
154
+ savingsPercent: number;
155
+ } {
156
+ const saved = savedBytes(bytes);
157
+ const possibleInput = boundedAdd(saved, bytes.returnedBytes);
158
+ return {
159
+ inputTokens: Math.floor(possibleInput / 4),
160
+ outputTokens: Math.floor(bytes.returnedBytes / 4),
161
+ savedTokens: Math.floor(saved / 4),
162
+ savingsPercent: possibleInput > 0 ? Math.round((saved / possibleInput) * 1_000) / 10 : 0,
163
+ };
164
+ }
165
+
166
+ function incrementSummary(
167
+ summaries: Map<string, MutableSummary>,
168
+ key: string,
169
+ count: number,
170
+ bytes: number,
171
+ ): void {
172
+ const current = summaries.get(key) ?? { count: 0, savedBytes: 0 };
173
+ current.count = boundedAdd(current.count, count);
174
+ current.savedBytes = boundedAdd(current.savedBytes, bytes);
175
+ summaries.set(key, current);
176
+ }
177
+
178
+ function mergeSessionSnapshot(
179
+ target: MutableAggregate,
180
+ snapshot: SessionDatabaseSnapshot,
181
+ sessionNamespace: string,
182
+ categoryTotals?: Map<string, MutableSummary>,
183
+ dayTotals?: Map<string, MutableSummary>,
184
+ ): void {
185
+ target.events = boundedAdd(target.events, snapshot.events);
186
+ target.toolCalls = boundedAdd(target.toolCalls, snapshot.toolCalls);
187
+ addBytes(target.bytes, snapshot);
188
+ for (const session of snapshot.sessions) target.sessions.add(`${sessionNamespace}\0${session}`);
189
+ for (const project of snapshot.projects) target.projects.add(project);
190
+ if (categoryTotals) {
191
+ for (const category of snapshot.byCategory) {
192
+ incrementSummary(categoryTotals, category.category, category.count, category.savedBytes);
193
+ }
194
+ }
195
+ if (dayTotals) {
196
+ for (const day of snapshot.byDay) {
197
+ incrementSummary(dayTotals, day.date, day.calls, day.savedBytes);
198
+ }
199
+ }
200
+ }
201
+
202
+ function mergeSource(target: Map<string, MutableSource>, source: MutableSource): void {
203
+ const key = `${source.provider}\0${source.source}`;
204
+ const current = target.get(key);
205
+ if (!current) {
206
+ target.set(key, source);
207
+ return;
208
+ }
209
+ current.chunks = boundedAdd(current.chunks, source.chunks);
210
+ current.codeChunks = boundedAdd(current.codeChunks, source.codeChunks);
211
+ current.indexedBytes = boundedAdd(current.indexedBytes, source.indexedBytes);
212
+ if (source.indexedAt && (!current.indexedAt || source.indexedAt > current.indexedAt)) {
213
+ current.indexedAt = source.indexedAt;
214
+ }
215
+ if (source.state === "ready") current.state = "ready";
216
+ else if (source.state === "unavailable" && current.state === "empty")
217
+ current.state = "unavailable";
218
+ if (!current.detail && source.detail) current.detail = source.detail;
219
+ }
220
+
221
+ function providerOutput(aggregate: MutableProviderAggregate): ProviderAnalytics {
222
+ return {
223
+ provider: aggregate.provider,
224
+ databases: aggregate.databases,
225
+ projects: aggregate.projects.size,
226
+ sessions: aggregate.sessions.size,
227
+ events: aggregate.events,
228
+ toolCalls: aggregate.toolCalls,
229
+ indexedSources: aggregate.indexedSources,
230
+ indexedChunks: aggregate.indexedChunks,
231
+ ...tokenFields(aggregate.bytes),
232
+ byteAccounting: byteAccounting(aggregate.bytes),
233
+ };
234
+ }
235
+
236
+ function databaseWarning(provider: string, path: string, error: unknown): AnalyticsWarning {
237
+ if (error instanceof ContextModeDatabaseError) {
238
+ return {
239
+ code: error.code,
240
+ provider,
241
+ database: error.database,
242
+ message: error.message,
243
+ };
244
+ }
245
+ return {
246
+ code: "database-query-failed",
247
+ provider,
248
+ database: basename(path),
249
+ message: "The database could not be inspected.",
250
+ };
251
+ }
252
+
253
+ export async function collectContextModeAnalytics(
254
+ dependencies: ContextModeAnalyticsDependencies = {},
255
+ ): Promise<AnalyticsDashboard> {
256
+ const discoverStorage = dependencies.discoverStorage ?? discoverContextModeStorageRoots;
257
+ const listDatabases = dependencies.listDatabases ?? listRegularContextModeDatabases;
258
+ const readDatabase = dependencies.readDatabase ?? readContextModeDatabase;
259
+ const warningCollector = new WarningCollector();
260
+ const discovery = await discoverStorage({ home: dependencies.home, env: dependencies.env });
261
+ for (const warning of discovery.warnings) warningCollector.add(warning);
262
+
263
+ const total = createAggregate();
264
+ const providers: MutableProviderAggregate[] = [];
265
+ const categories = new Map<string, MutableSummary>();
266
+ const days = new Map<string, MutableSummary>();
267
+ const sourceMap = new Map<string, MutableSource>();
268
+ let attemptedDatabases = 0;
269
+ let successfulDatabases = 0;
270
+
271
+ for (const root of discovery.roots) {
272
+ const provider: MutableProviderAggregate = { provider: root.provider, ...createAggregate() };
273
+ providers.push(provider);
274
+
275
+ for (const [directoryKind, directory] of [
276
+ ["session", root.sessionsDir],
277
+ ["content", root.contentDir],
278
+ ] as const) {
279
+ const listing = await listDatabases(root.provider, directory);
280
+ for (const warning of listing.warnings) warningCollector.add(warning);
281
+
282
+ for (const path of listing.files) {
283
+ attemptedDatabases++;
284
+ try {
285
+ const snapshot = readDatabase(path);
286
+ successfulDatabases++;
287
+ total.databases = boundedAdd(total.databases, 1);
288
+ provider.databases = boundedAdd(provider.databases, 1);
289
+
290
+ if (snapshot.kind === "session") {
291
+ mergeSessionSnapshot(total, snapshot, root.provider, categories, days);
292
+ mergeSessionSnapshot(provider, snapshot, root.provider);
293
+ continue;
294
+ }
295
+
296
+ total.indexedSources = boundedAdd(total.indexedSources, snapshot.sources.length);
297
+ total.indexedChunks = boundedAdd(total.indexedChunks, snapshot.chunks);
298
+ provider.indexedSources = boundedAdd(provider.indexedSources, snapshot.sources.length);
299
+ provider.indexedChunks = boundedAdd(provider.indexedChunks, snapshot.chunks);
300
+ addBytes(total.bytes, { indexedBytes: snapshot.indexedBytes });
301
+ addBytes(provider.bytes, { indexedBytes: snapshot.indexedBytes });
302
+ for (const day of snapshot.byDay) {
303
+ incrementSummary(days, day.date, day.calls, day.savedBytes);
304
+ }
305
+ for (const source of snapshot.sources) {
306
+ mergeSource(sourceMap, {
307
+ provider: root.provider,
308
+ source: source.source,
309
+ chunks: source.chunks,
310
+ codeChunks: source.codeChunks,
311
+ indexedBytes: source.indexedBytes,
312
+ indexedAt: source.indexedAt,
313
+ state: source.chunks > 0 ? "ready" : "empty",
314
+ ...(source.detail ? { detail: source.detail } : {}),
315
+ });
316
+ }
317
+ for (const message of snapshot.inconsistencies) {
318
+ warningCollector.add({
319
+ code: "inconsistent-index",
320
+ provider: root.provider,
321
+ database: basename(path),
322
+ message,
323
+ });
324
+ }
325
+ } catch (error) {
326
+ const warning = databaseWarning(root.provider, path, error);
327
+ warningCollector.add(warning);
328
+ if (directoryKind === "content") {
329
+ mergeSource(sourceMap, {
330
+ provider: root.provider,
331
+ source: `[database] ${basename(path)}`,
332
+ chunks: 0,
333
+ codeChunks: 0,
334
+ indexedBytes: 0,
335
+ indexedAt: null,
336
+ state: "unavailable",
337
+ detail: warning.message,
338
+ });
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+
345
+ const allSources = [...sourceMap.values()].sort(
346
+ (left, right) =>
347
+ left.provider.localeCompare(right.provider) || left.source.localeCompare(right.source),
348
+ );
349
+ if (allSources.length > MAX_SOURCES) {
350
+ warningCollector.add({
351
+ code: "result-truncated",
352
+ message: `Only the first ${MAX_SOURCES} indexed sources are included in the dashboard.`,
353
+ });
354
+ }
355
+ const sources: IndexedSourceAnalytics[] = allSources.slice(0, MAX_SOURCES);
356
+ const allProviders = providers.map(providerOutput);
357
+ const allCategories = [...categories]
358
+ .map(([category, summary]) => ({
359
+ category,
360
+ count: summary.count,
361
+ savedTokens: Math.floor(summary.savedBytes / 4),
362
+ }))
363
+ .sort((left, right) => right.count - left.count || left.category.localeCompare(right.category));
364
+ const allDays = [...days]
365
+ .map(([date, summary]) => ({
366
+ date,
367
+ calls: summary.count,
368
+ savedTokens: Math.floor(summary.savedBytes / 4),
369
+ }))
370
+ .sort((left, right) => left.date.localeCompare(right.date));
371
+ for (const [count, maximum, label] of [
372
+ [allProviders.length, MAX_PROVIDERS, "provider"],
373
+ [allCategories.length, MAX_CATEGORIES, "category"],
374
+ [allDays.length, MAX_DAYS, "daily"],
375
+ ] as const) {
376
+ if (count > maximum) {
377
+ warningCollector.add({
378
+ code: "result-truncated",
379
+ message: `Only the first ${maximum} ${label} analytics rows are included.`,
380
+ });
381
+ }
382
+ }
383
+ const byProvider = allProviders.slice(0, MAX_PROVIDERS);
384
+ const byCategory = allCategories.slice(0, MAX_CATEGORIES);
385
+ const byDay = allDays.slice(-MAX_DAYS);
386
+ const hadWarnings = warningCollector.hasWarnings;
387
+ const completeness =
388
+ attemptedDatabases > 0 && successfulDatabases === 0
389
+ ? "unsupported"
390
+ : hadWarnings
391
+ ? "partial"
392
+ : "complete";
393
+ const warnings = warningCollector.finish();
394
+
395
+ return {
396
+ schemaVersion: CONTEXT_MODE_ANALYTICS_SCHEMA_VERSION,
397
+ contextModeVersion: SUPPORTED_CONTEXT_MODE_DATABASE_VERSION,
398
+ completeness,
399
+ warnings,
400
+ totals: {
401
+ providers: providers.length,
402
+ databases: total.databases,
403
+ projects: total.projects.size,
404
+ sessions: total.sessions.size,
405
+ events: total.events,
406
+ toolCalls: total.toolCalls,
407
+ indexedSources: total.indexedSources,
408
+ indexedChunks: total.indexedChunks,
409
+ ...tokenFields(total.bytes),
410
+ byteAccounting: byteAccounting(total.bytes),
411
+ },
412
+ byProvider,
413
+ byCategory,
414
+ byDay,
415
+ sources,
416
+ generatedAt: (dependencies.now ?? (() => new Date()))().toISOString(),
417
+ };
418
+ }
419
+
420
+ export function createContextModeAnalyticsHandler(
421
+ dependencies: ContextModeAnalyticsDependencies = {},
422
+ ): (_input: Record<string, never>) => Promise<AnalyticsDashboard> {
423
+ return async () => collectContextModeAnalytics(dependencies);
424
+ }
@@ -0,0 +1,182 @@
1
+ import { constants } from "node:fs";
2
+ import { access, realpath, stat } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import { join, posix, win32 } from "node:path";
5
+ import type { ContextModeSettings } from "../shared";
6
+
7
+ export interface LaunchDescriptor {
8
+ program: string;
9
+ args: string[];
10
+ }
11
+
12
+ export type BinaryResolution =
13
+ | {
14
+ state: "found";
15
+ path: string;
16
+ source: "settings" | "path" | "bundled";
17
+ launch: LaunchDescriptor;
18
+ }
19
+ | { state: "missing"; code: "not-installed" | "unsupported"; message: string };
20
+
21
+ export interface BinaryResolutionDependencies {
22
+ env?: NodeJS.ProcessEnv;
23
+ platform?: NodeJS.Platform;
24
+ nodePath?: string;
25
+ canExecute?: (path: string) => Promise<boolean>;
26
+ isFile?: (path: string) => Promise<boolean>;
27
+ bundledCliPath?: () => string | null;
28
+ }
29
+
30
+ async function defaultIsFile(path: string): Promise<boolean> {
31
+ try {
32
+ return (await stat(path)).isFile();
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ async function defaultCanExecute(path: string, platform: NodeJS.Platform): Promise<boolean> {
39
+ if (!(await defaultIsFile(path))) return false;
40
+ try {
41
+ await access(path, platform === "win32" ? constants.F_OK : constants.X_OK);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ function pathCandidates(
49
+ name: string,
50
+ directory: string,
51
+ platform: NodeJS.Platform,
52
+ env: NodeJS.ProcessEnv,
53
+ ) {
54
+ const pathApi = platform === "win32" ? win32 : posix;
55
+ if (platform !== "win32") return [pathApi.join(directory, name)];
56
+ const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
57
+ return [
58
+ pathApi.join(directory, name),
59
+ ...extensions.map((extension) => pathApi.join(directory, `${name}${extension}`)),
60
+ ];
61
+ }
62
+
63
+ async function windowsLaunchDescriptor(
64
+ candidate: string,
65
+ nodePath: string,
66
+ isFile: (path: string) => Promise<boolean>,
67
+ ): Promise<LaunchDescriptor | null> {
68
+ const extension = win32.extname(candidate).toLowerCase();
69
+ if (extension === ".exe" || extension === ".com") {
70
+ return { program: candidate, args: [] };
71
+ }
72
+ if ([".js", ".cjs", ".mjs"].includes(extension)) {
73
+ return { program: nodePath, args: [candidate] };
74
+ }
75
+
76
+ const directory = win32.dirname(candidate);
77
+ const moduleCandidates = [
78
+ win32.join(directory, "node_modules", "context-mode", "cli.bundle.mjs"),
79
+ win32.join(directory, "node_modules", "context-mode", "build", "cli.js"),
80
+ win32.join(directory, "..", "context-mode", "cli.bundle.mjs"),
81
+ win32.join(directory, "..", "context-mode", "build", "cli.js"),
82
+ ];
83
+ for (const modulePath of moduleCandidates) {
84
+ if (await isFile(modulePath)) return { program: nodePath, args: [modulePath] };
85
+ }
86
+ return null;
87
+ }
88
+
89
+ export async function resolveContextModeBinary(
90
+ settings: ContextModeSettings,
91
+ dependencies: BinaryResolutionDependencies = {},
92
+ ): Promise<BinaryResolution> {
93
+ const env = dependencies.env ?? process.env;
94
+ const platform = dependencies.platform ?? process.platform;
95
+ const pathApi = platform === "win32" ? win32 : posix;
96
+ const nodePath = dependencies.nodePath ?? process.execPath;
97
+ const canExecute =
98
+ dependencies.canExecute ?? ((path: string) => defaultCanExecute(path, platform));
99
+ const isFile = dependencies.isFile ?? defaultIsFile;
100
+ const configuredPath = settings.binaryPath.trim();
101
+ let unsupportedShim: string | null = null;
102
+
103
+ async function resolveCandidate(
104
+ candidate: string,
105
+ source: "settings" | "path",
106
+ ): Promise<Extract<BinaryResolution, { state: "found" }> | null> {
107
+ if (!(await canExecute(candidate))) return null;
108
+ const canonicalPath = await realpath(candidate).catch(() => candidate);
109
+ if (platform !== "win32") {
110
+ return {
111
+ state: "found",
112
+ path: canonicalPath,
113
+ source,
114
+ launch: { program: canonicalPath, args: [] },
115
+ };
116
+ }
117
+ const launch = await windowsLaunchDescriptor(canonicalPath, nodePath, isFile);
118
+ if (!launch) {
119
+ unsupportedShim = canonicalPath;
120
+ return null;
121
+ }
122
+ return { state: "found", path: canonicalPath, source, launch };
123
+ }
124
+
125
+ if (settings.binaryMode === "path" && configuredPath.length > 0) {
126
+ if (!pathApi.isAbsolute(configuredPath)) {
127
+ return {
128
+ state: "missing",
129
+ code: "not-installed",
130
+ message: "The configured Context Mode binary path is not absolute.",
131
+ };
132
+ }
133
+ const configured = await resolveCandidate(configuredPath, "settings");
134
+ if (configured) return configured;
135
+ }
136
+
137
+ for (const directory of (env.PATH ?? "").split(pathApi.delimiter).filter(Boolean)) {
138
+ for (const candidate of pathCandidates("context-mode", directory, platform, env)) {
139
+ const resolved = await resolveCandidate(candidate, "path");
140
+ if (resolved) return resolved;
141
+ }
142
+ }
143
+
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;
157
+ const canonicalPath = await realpath(bundledCli).catch(() => bundledCli);
158
+ return {
159
+ state: "found",
160
+ path: canonicalPath,
161
+ source: "bundled",
162
+ launch: { program: nodePath, args: [canonicalPath] },
163
+ };
164
+ }
165
+
166
+ if (unsupportedShim) {
167
+ return {
168
+ code: "unsupported",
169
+ state: "missing",
170
+ message: `Context Mode was found at ${unsupportedShim}, but this Windows shim could not be safely resolved to its CLI module. Reinstall Context Mode with npm or configure the absolute cli.bundle.mjs path.`,
171
+ };
172
+ }
173
+ const configuredDetail =
174
+ settings.binaryMode === "path" && configuredPath.length > 0
175
+ ? ` The configured path (${configuredPath}) was unavailable, and no fallback was found on PATH.`
176
+ : "";
177
+ return {
178
+ code: "not-installed",
179
+ state: "missing",
180
+ message: `Context Mode is not installed or is not executable on this Paseo host.${configuredDetail}`,
181
+ };
182
+ }