context-mode 1.0.125 → 1.0.127

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.
Files changed (45) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/build/adapters/claude-code/hooks.d.ts +10 -4
  6. package/build/adapters/claude-code/hooks.js +22 -12
  7. package/build/adapters/claude-code/index.d.ts +24 -1
  8. package/build/adapters/claude-code/index.js +67 -11
  9. package/build/adapters/types.d.ts +57 -0
  10. package/build/adapters/types.js +29 -0
  11. package/build/cli.js +38 -13
  12. package/build/server.js +7 -0
  13. package/build/util/hook-config.d.ts +24 -1
  14. package/build/util/hook-config.js +39 -2
  15. package/build/util/plugin-cache-integrity.d.ts +37 -0
  16. package/build/util/plugin-cache-integrity.js +105 -0
  17. package/build/util/project-dir.d.ts +13 -0
  18. package/build/util/project-dir.js +11 -2
  19. package/cli.bundle.mjs +122 -122
  20. package/hooks/core/routing.mjs +114 -22
  21. package/hooks/gemini-cli/sessionstart.mjs +8 -6
  22. package/hooks/security.bundle.mjs +1 -0
  23. package/hooks/sessionstart.mjs +18 -0
  24. package/openclaw.plugin.json +1 -1
  25. package/package.json +4 -3
  26. package/scripts/plugin-cache-integrity.mjs +248 -0
  27. package/server.bundle.mjs +94 -94
  28. package/start.mjs +37 -0
  29. package/skills/UPSTREAM-CREDITS.md +0 -51
  30. package/skills/diagnose/SKILL.md +0 -122
  31. package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
  32. package/skills/grill-me/SKILL.md +0 -15
  33. package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
  34. package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
  35. package/skills/grill-with-docs/SKILL.md +0 -93
  36. package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
  37. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
  38. package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
  39. package/skills/improve-codebase-architecture/SKILL.md +0 -76
  40. package/skills/tdd/SKILL.md +0 -114
  41. package/skills/tdd/deep-modules.md +0 -33
  42. package/skills/tdd/interface-design.md +0 -31
  43. package/skills/tdd/mocking.md +0 -59
  44. package/skills/tdd/refactoring.md +0 -10
  45. package/skills/tdd/tests.md +0 -61
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.125"
9
+ "version": "1.0.127"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
15
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
- "version": "1.0.125",
16
+ "version": "1.0.127",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.125",
3
+ "version": "1.0.127",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -3,7 +3,7 @@
3
3
  "name": "Context Mode",
4
4
  "kind": "tool",
5
5
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
6
- "version": "1.0.125",
6
+ "version": "1.0.127",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.125",
3
+ "version": "1.0.127",
4
4
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -80,11 +80,17 @@ export declare function isContextModeHook(entry: {
80
80
  export declare function buildHookCommand(hookType: HookType, pluginRoot?: string): string;
81
81
  /**
82
82
  * Extract the hook script file path from a command string.
83
- * Returns the path if the command uses the `node "/path/to/hook.mjs"` format
84
- * or the new `"/path/to/node" "/path/to/hook.mjs"` format (#369, #372),
85
- * or null if it uses the CLI dispatcher format (which is path-independent).
86
83
  *
87
- * Handles both quoted and unquoted paths, and both forward/back slashes.
84
+ * Algo-D2 twin same shape as `src/util/hook-config.ts::extractHookScriptPath`.
85
+ * Delegates to `parseNodeCommand` for canonical buildNodeCommand-shape;
86
+ * keeps narrow legacy fallbacks for pre-D3 settings.json entries
87
+ * (`node "X.mjs"` and `node X.mjs` with no internal whitespace).
88
+ *
89
+ * Pre-D2 this matched `node\s+"?([^"]+\.mjs)"?` — the unquoted fallback
90
+ * silently grabbed the tail after the last whitespace, producing the
91
+ * #548 doubled-path FAIL on Windows paths with spaces. The new shape
92
+ * refuses ambiguous input; doctor (Algo-D1) falls through to direct
93
+ * `existsSync` instead of trusting the regex.
88
94
  */
89
95
  export declare function extractHookScriptPath(command: string): string | null;
90
96
  /**
@@ -1,4 +1,4 @@
1
- import { buildNodeCommand } from "../types.js";
1
+ import { buildNodeCommand, parseNodeCommand } from "../types.js";
2
2
  /**
3
3
  * adapters/claude-code/hooks — Claude Code hook definitions and matchers.
4
4
  *
@@ -142,20 +142,30 @@ export function buildHookCommand(hookType, pluginRoot) {
142
142
  }
143
143
  /**
144
144
  * Extract the hook script file path from a command string.
145
- * Returns the path if the command uses the `node "/path/to/hook.mjs"` format
146
- * or the new `"/path/to/node" "/path/to/hook.mjs"` format (#369, #372),
147
- * or null if it uses the CLI dispatcher format (which is path-independent).
148
145
  *
149
- * Handles both quoted and unquoted paths, and both forward/back slashes.
146
+ * Algo-D2 twin same shape as `src/util/hook-config.ts::extractHookScriptPath`.
147
+ * Delegates to `parseNodeCommand` for canonical buildNodeCommand-shape;
148
+ * keeps narrow legacy fallbacks for pre-D3 settings.json entries
149
+ * (`node "X.mjs"` and `node X.mjs` with no internal whitespace).
150
+ *
151
+ * Pre-D2 this matched `node\s+"?([^"]+\.mjs)"?` — the unquoted fallback
152
+ * silently grabbed the tail after the last whitespace, producing the
153
+ * #548 doubled-path FAIL on Windows paths with spaces. The new shape
154
+ * refuses ambiguous input; doctor (Algo-D1) falls through to direct
155
+ * `existsSync` instead of trusting the regex.
150
156
  */
151
157
  export function extractHookScriptPath(command) {
152
- // New format: "nodePath" "scriptPath.mjs" (from buildNodeCommand)
153
- const newFmt = command.match(/"[^"]+"\s+"([^"]+\.mjs)"/);
154
- if (newFmt)
155
- return newFmt[1];
156
- // Legacy format: node "/path/to/hooks/scriptname.mjs" or node /path/to/hooks/scriptname.mjs
157
- const match = command.match(/node\s+"?([^"]+\.mjs)"?/);
158
- return match?.[1] ?? null;
158
+ const parsed = parseNodeCommand(command);
159
+ if (parsed) {
160
+ return parsed.scriptPath.endsWith(".mjs") ? parsed.scriptPath : null;
161
+ }
162
+ const legacyQuoted = command.match(/^\s*node\s+"([^"]+\.mjs)"\s*$/);
163
+ if (legacyQuoted)
164
+ return legacyQuoted[1];
165
+ const legacyBare = command.match(/^\s*node\s+(\S+\.mjs)\s*$/);
166
+ if (legacyBare)
167
+ return legacyBare[1];
168
+ return null;
159
169
  }
160
170
  /**
161
171
  * Check if a hook entry is a context-mode hook (any hook type).
@@ -12,7 +12,7 @@
12
12
  * - Plugin registry: <configDir>/plugins/installed_plugins.json
13
13
  */
14
14
  import { ClaudeCodeBaseAdapter, type ClaudeCodeWireInput } from "../claude-code-base.js";
15
- import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, HookRegistration } from "../types.js";
15
+ import { type HookAdapter, type HookParadigm, type PlatformCapabilities, type DiagnosticResult, type HookRegistration, type HealthCheck } from "../types.js";
16
16
  export declare class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter implements HookAdapter {
17
17
  constructor();
18
18
  readonly name = "Claude Code";
@@ -44,6 +44,29 @@ export declare class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter implements
44
44
  readSettings(): Record<string, unknown> | null;
45
45
  writeSettings(settings: Record<string, unknown>): void;
46
46
  validateHooks(pluginRoot: string): DiagnosticResult[];
47
+ /**
48
+ * Adapter-defined health checks (Algo-D1 + Algo-D5).
49
+ *
50
+ * For each entry in HOOK_SCRIPTS (the canonical hookType → scriptName
51
+ * map), emit a HealthCheck that joins `pluginRoot + "hooks" +
52
+ * scriptName` and probes via `existsSync`. Crucially, this NEVER
53
+ * parses a hook command — pluginRoot and scriptName are both in our
54
+ * hand, so the regex round-trip that produced the #548 doubled-path
55
+ * FAIL is bypassed entirely.
56
+ *
57
+ * The hook check derives from HOOK_SCRIPTS (single source of truth in
58
+ * src/adapters/claude-code/hooks.ts), so adding a new hook event in
59
+ * that map auto-extends doctor coverage — no parallel hardcoded list
60
+ * to maintain.
61
+ *
62
+ * Algo-D5: appends a single "Plugin cache integrity" check that
63
+ * delegates to the same helper start.mjs uses at boot
64
+ * (scripts/plugin-cache-integrity.mjs::assertPluginCacheIntegrity).
65
+ * Same code, two callsites — boot fail-fast and doctor diagnostic
66
+ * agree byte-for-byte. Users hitting #550 get the actionable signal
67
+ * without restarting the MCP server.
68
+ */
69
+ getHealthChecks(pluginRoot: string): readonly HealthCheck[];
47
70
  /** Read plugin hooks from hooks/hooks.json or .claude-plugin/hooks/hooks.json */
48
71
  private readPluginHooks;
49
72
  /** Check if a hook type is configured in either settings.json or plugin hooks */
@@ -16,6 +16,8 @@ import { resolve, join } from "node:path";
16
16
  import { homedir } from "node:os";
17
17
  import { ClaudeCodeBaseAdapter } from "../claude-code-base.js";
18
18
  import { resolveClaudeConfigDir } from "../../util/claude-config.js";
19
+ import { checkPluginCacheIntegritySync } from "../../util/plugin-cache-integrity.js";
20
+ import { buildNodeCommand, } from "../types.js";
19
21
  import { HOOK_TYPES, HOOK_SCRIPTS, REQUIRED_HOOKS, PRE_TOOL_USE_MATCHERS, PRE_TOOL_USE_MATCHER_PATTERN, isContextModeHook, isAnyContextModeHook, extractHookScriptPath, buildHookCommand, } from "./hooks.js";
20
22
  // ─────────────────────────────────────────────────────────
21
23
  // Adapter implementation
@@ -67,13 +69,20 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
67
69
  return join(this.getConfigDir(), "settings.json");
68
70
  }
69
71
  generateHookConfig(pluginRoot) {
70
- // Paths must be double-quoted so that hosts (or our own diagnostic
71
- // regex) parse them correctly when pluginRoot contains spaces — common
72
- // on Windows where the user folder is e.g. "C:\Users\First Last\...".
73
- // Without quotes, extractHookScriptPath's `\S+\.mjs` fallback grabs
74
- // only the tail after the last space, producing a doubled-path FAIL
75
- // in `ctx doctor`. Matches the quoting style used in hooks/hooks.json.
76
- const preToolUseCommand = `node "${pluginRoot}/hooks/pretooluse.mjs"`;
72
+ // Algo-D3: every command flows through `buildNodeCommand` (defined in
73
+ // src/adapters/types.ts), which:
74
+ // - quotes both nodePath and scriptPath (#548 Windows pluginRoots
75
+ // with spaces no longer fall through extractHookScriptPath's
76
+ // ambiguous-tail fallback),
77
+ // - swaps backslashes for forward slashes (#372 MSYS path mangling),
78
+ // - uses `process.execPath` instead of bare `node` (#369 PATH
79
+ // resolution on Git Bash).
80
+ // Pre-D3 we hand-rolled `node "${pluginRoot}/hooks/X.mjs"` for all
81
+ // five events; bare `node` made claude-code the lone outlier and
82
+ // dropping the execPath swap re-opened the Windows class. Algo-D3.5
83
+ // (CI invariant in tests/adapters/claude-code.test.ts) locks this in
84
+ // for adapter #16.
85
+ const preToolUseCommand = buildNodeCommand(`${pluginRoot}/hooks/pretooluse.mjs`);
77
86
  const preToolUseMatchers = [...PRE_TOOL_USE_MATCHERS];
78
87
  return {
79
88
  PreToolUse: preToolUseMatchers.map((matcher) => ({
@@ -86,7 +95,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
86
95
  hooks: [
87
96
  {
88
97
  type: "command",
89
- command: `node "${pluginRoot}/hooks/posttooluse.mjs"`,
98
+ command: buildNodeCommand(`${pluginRoot}/hooks/posttooluse.mjs`),
90
99
  },
91
100
  ],
92
101
  },
@@ -97,7 +106,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
97
106
  hooks: [
98
107
  {
99
108
  type: "command",
100
- command: `node "${pluginRoot}/hooks/precompact.mjs"`,
109
+ command: buildNodeCommand(`${pluginRoot}/hooks/precompact.mjs`),
101
110
  },
102
111
  ],
103
112
  },
@@ -108,7 +117,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
108
117
  hooks: [
109
118
  {
110
119
  type: "command",
111
- command: `node "${pluginRoot}/hooks/userpromptsubmit.mjs"`,
120
+ command: buildNodeCommand(`${pluginRoot}/hooks/userpromptsubmit.mjs`),
112
121
  },
113
122
  ],
114
123
  },
@@ -119,7 +128,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
119
128
  hooks: [
120
129
  {
121
130
  type: "command",
122
- command: `node "${pluginRoot}/hooks/sessionstart.mjs"`,
131
+ command: buildNodeCommand(`${pluginRoot}/hooks/sessionstart.mjs`),
123
132
  },
124
133
  ],
125
134
  },
@@ -177,6 +186,53 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
177
186
  });
178
187
  return results;
179
188
  }
189
+ /**
190
+ * Adapter-defined health checks (Algo-D1 + Algo-D5).
191
+ *
192
+ * For each entry in HOOK_SCRIPTS (the canonical hookType → scriptName
193
+ * map), emit a HealthCheck that joins `pluginRoot + "hooks" +
194
+ * scriptName` and probes via `existsSync`. Crucially, this NEVER
195
+ * parses a hook command — pluginRoot and scriptName are both in our
196
+ * hand, so the regex round-trip that produced the #548 doubled-path
197
+ * FAIL is bypassed entirely.
198
+ *
199
+ * The hook check derives from HOOK_SCRIPTS (single source of truth in
200
+ * src/adapters/claude-code/hooks.ts), so adding a new hook event in
201
+ * that map auto-extends doctor coverage — no parallel hardcoded list
202
+ * to maintain.
203
+ *
204
+ * Algo-D5: appends a single "Plugin cache integrity" check that
205
+ * delegates to the same helper start.mjs uses at boot
206
+ * (scripts/plugin-cache-integrity.mjs::assertPluginCacheIntegrity).
207
+ * Same code, two callsites — boot fail-fast and doctor diagnostic
208
+ * agree byte-for-byte. Users hitting #550 get the actionable signal
209
+ * without restarting the MCP server.
210
+ */
211
+ getHealthChecks(pluginRoot) {
212
+ const hookChecks = Object.entries(HOOK_SCRIPTS).map(([hookType, scriptName]) => {
213
+ const absolutePath = join(pluginRoot, "hooks", scriptName);
214
+ return {
215
+ name: `Hook script: ${hookType} (${scriptName})`,
216
+ check: () => {
217
+ // Direct existsSync — no hook-command parsing, no regex.
218
+ // pluginRoot is the value the doctor was invoked with;
219
+ // scriptName comes from the canonical HOOK_SCRIPTS map.
220
+ if (existsSync(absolutePath)) {
221
+ return { status: "OK", detail: absolutePath };
222
+ }
223
+ return {
224
+ status: "FAIL",
225
+ detail: `not found at ${absolutePath}`,
226
+ };
227
+ },
228
+ };
229
+ });
230
+ const integrityCheck = {
231
+ name: "Plugin cache integrity",
232
+ check: () => checkPluginCacheIntegritySync(pluginRoot),
233
+ };
234
+ return [...hookChecks, integrityCheck];
235
+ }
180
236
  /** Read plugin hooks from hooks/hooks.json or .claude-plugin/hooks/hooks.json */
181
237
  readPluginHooks(pluginRoot) {
182
238
  const candidates = [
@@ -206,6 +206,21 @@ export interface HookAdapter {
206
206
  writeSettings(settings: Record<string, unknown>): void;
207
207
  /** Validate that hooks are properly configured for this platform. */
208
208
  validateHooks(pluginRoot: string): DiagnosticResult[];
209
+ /**
210
+ * Adapter-defined per-platform health checks (Algo-D1).
211
+ *
212
+ * OPTIONAL. Adapters that don't override return nothing — they don't
213
+ * have this class of check today. claude-code overrides with hook-script
214
+ * existence checks that join `pluginRoot + scriptName` directly via
215
+ * `existsSync`, so doctor never round-trips through a regex on a hook
216
+ * command (the #548 root cause).
217
+ *
218
+ * Adapter #16 with hook scripts inherits the contract by overriding;
219
+ * adapter #17 without hook scripts simply doesn't override. The doctor
220
+ * iterates `adapter.getHealthChecks?.(pluginRoot) ?? []` and renders
221
+ * each — no per-adapter wiring in the doctor body.
222
+ */
223
+ getHealthChecks?(pluginRoot: string): readonly HealthCheck[];
209
224
  /** Check if the plugin is registered/enabled on this platform. */
210
225
  checkPluginRegistration(): DiagnosticResult;
211
226
  /** Get the installed version from this platform's registry/marketplace. */
@@ -230,6 +245,26 @@ export interface DiagnosticResult {
230
245
  /** Suggested fix command (if applicable). */
231
246
  fix?: string;
232
247
  }
248
+ /**
249
+ * Adapter-defined health check (Algo-D1).
250
+ *
251
+ * Lighter-weight than `DiagnosticResult`: adapters declare a name and a
252
+ * synchronous `check()` thunk. The doctor renders the result. The
253
+ * thunk-style intentionally avoids forcing adapters into async — the
254
+ * existsSync probe used by claude-code is sync and the doctor invokes it
255
+ * directly without an `await`. Adapters needing async work return a
256
+ * pre-resolved status (the check ran at thunk-creation time) or extend
257
+ * `validateHooks()` instead.
258
+ */
259
+ export interface HealthCheck {
260
+ /** Human-readable check title (e.g. "Hook script exists: pretooluse.mjs"). */
261
+ readonly name: string;
262
+ /** Synchronous check thunk. Returns OK or FAIL with optional detail. */
263
+ check(): {
264
+ status: "OK" | "FAIL";
265
+ detail?: string;
266
+ };
267
+ }
233
268
  /**
234
269
  * Build a cross-platform `node <script>` command string.
235
270
  *
@@ -243,6 +278,28 @@ export interface DiagnosticResult {
243
278
  * Safe on macOS/Linux — quoting and forward slashes are no-ops there.
244
279
  */
245
280
  export declare function buildNodeCommand(scriptPath: string): string;
281
+ /**
282
+ * Strict inverse of `buildNodeCommand`.
283
+ *
284
+ * Returns `{ nodePath, scriptPath }` ONLY when `cmd` could have been
285
+ * produced by `buildNodeCommand` — i.e. exactly two double-quoted args
286
+ * separated by whitespace. Anything else (bare `node …`, single quotes,
287
+ * unquoted ambiguous input, CLI dispatcher entries) returns `null`.
288
+ *
289
+ * Why strict: the legacy `\S+\.mjs` fallback in
290
+ * `src/util/hook-config.ts:24` and the two-step regex in
291
+ * `src/adapters/claude-code/hooks.ts:178` silently grabbed the path tail
292
+ * after the last whitespace whenever the host wire-format dropped quotes,
293
+ * producing the #548 doubled-path FAIL when `pluginRoot` contained
294
+ * spaces (e.g. `C:\Users\High Ground Services\…`). A canonical inverse
295
+ * lets every emit (`buildNodeCommand`) round-trip through every parse
296
+ * (`parseNodeCommand`) without inventing fallbacks. Adapter #16 inherits
297
+ * the contract by importing one module.
298
+ */
299
+ export declare function parseNodeCommand(cmd: string): {
300
+ nodePath: string;
301
+ scriptPath: string;
302
+ } | null;
246
303
  /** Supported platform identifiers. */
247
304
  export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "kilo" | "openclaw" | "codex" | "vscode-copilot" | "jetbrains-copilot" | "cursor" | "antigravity" | "kiro" | "pi" | "omp" | "zed" | "qwen-code" | "unknown";
248
305
  /** Detection signal used to identify which platform is running. */
@@ -33,3 +33,32 @@ export function buildNodeCommand(scriptPath) {
33
33
  const safePath = scriptPath.replace(/\\/g, "/");
34
34
  return `"${nodePath}" "${safePath}"`;
35
35
  }
36
+ /**
37
+ * Strict inverse of `buildNodeCommand`.
38
+ *
39
+ * Returns `{ nodePath, scriptPath }` ONLY when `cmd` could have been
40
+ * produced by `buildNodeCommand` — i.e. exactly two double-quoted args
41
+ * separated by whitespace. Anything else (bare `node …`, single quotes,
42
+ * unquoted ambiguous input, CLI dispatcher entries) returns `null`.
43
+ *
44
+ * Why strict: the legacy `\S+\.mjs` fallback in
45
+ * `src/util/hook-config.ts:24` and the two-step regex in
46
+ * `src/adapters/claude-code/hooks.ts:178` silently grabbed the path tail
47
+ * after the last whitespace whenever the host wire-format dropped quotes,
48
+ * producing the #548 doubled-path FAIL when `pluginRoot` contained
49
+ * spaces (e.g. `C:\Users\High Ground Services\…`). A canonical inverse
50
+ * lets every emit (`buildNodeCommand`) round-trip through every parse
51
+ * (`parseNodeCommand`) without inventing fallbacks. Adapter #16 inherits
52
+ * the contract by importing one module.
53
+ */
54
+ export function parseNodeCommand(cmd) {
55
+ if (typeof cmd !== "string" || cmd.length === 0)
56
+ return null;
57
+ // Match `"<nodePath>" "<scriptPath>"` with arbitrary whitespace
58
+ // separator. Both segments must be non-empty and contain no embedded
59
+ // double quotes — buildNodeCommand never emits embedded quotes.
60
+ const m = cmd.match(/^"([^"]+)"\s+"([^"]+)"\s*$/);
61
+ if (!m)
62
+ return null;
63
+ return { nodePath: m[1], scriptPath: m[2] };
64
+ }
package/build/cli.js CHANGED
@@ -368,22 +368,47 @@ async function doctor() {
368
368
  (result.fix ? color.dim(`\n Run: ${result.fix}`) : ""));
369
369
  }
370
370
  }
371
- // Hook scripts exist
371
+ // Hook scripts exist — Algo-D1 protocol path takes precedence.
372
+ // Adapters that override `getHealthChecks` (claude-code today) get a
373
+ // direct `existsSync(join(pluginRoot, "hooks", scriptName))` per
374
+ // HOOK_SCRIPTS entry — no regex round-trip on a hook command, so the
375
+ // #548 doubled-path FAIL class can't surface. Adapters that don't
376
+ // override fall through to the legacy `getHookScriptPaths` flow which
377
+ // generates the hook config and parses each command via
378
+ // `extractHookScriptPath`. Post-D3 every adapter emits buildNodeCommand-
379
+ // shape, so the legacy flow is also safe — but the direct existsSync
380
+ // path is strictly preferable when the adapter offers it.
372
381
  p.log.step("Checking hook scripts...");
373
- const hookScriptPaths = getHookScriptPaths(adapter, pluginRoot);
374
- if (hookScriptPaths.length === 0) {
375
- p.log.success(color.green("Hook scripts: PASS") + color.dim(" no direct .mjs script paths to verify"));
382
+ const adapterHealthChecks = adapter.getHealthChecks?.(pluginRoot) ?? [];
383
+ if (adapterHealthChecks.length > 0) {
384
+ for (const hc of adapterHealthChecks) {
385
+ const result = hc.check();
386
+ if (result.status === "OK") {
387
+ p.log.success(color.green(`${hc.name}: PASS`) +
388
+ (result.detail ? color.dim(` — ${result.detail}`) : ""));
389
+ }
390
+ else {
391
+ p.log.error(color.red(`${hc.name}: FAIL`) +
392
+ (result.detail ? color.dim(` — ${result.detail}`) : ""));
393
+ }
394
+ }
376
395
  }
377
396
  else {
378
- for (const scriptPath of hookScriptPaths) {
379
- const absolutePath = resolve(pluginRoot, scriptPath);
380
- try {
381
- accessSync(absolutePath, constants.R_OK);
382
- p.log.success(color.green("Hook script exists: PASS") + color.dim(` — ${absolutePath}`));
383
- }
384
- catch {
385
- p.log.error(color.red("Hook script exists: FAIL") +
386
- color.dim(` — not found at ${absolutePath}`));
397
+ const hookScriptPaths = getHookScriptPaths(adapter, pluginRoot);
398
+ if (hookScriptPaths.length === 0) {
399
+ p.log.success(color.green("Hook scripts: PASS") + color.dim(" — no direct .mjs script paths to verify"));
400
+ }
401
+ else {
402
+ for (const scriptPath of hookScriptPaths) {
403
+ const absolutePath = resolve(pluginRoot, scriptPath);
404
+ try {
405
+ accessSync(absolutePath, constants.R_OK);
406
+ p.log.success(color.green("Hook script exists: PASS") + color.dim(` — ${absolutePath}`));
407
+ }
408
+ catch {
409
+ p.log.error(color.red("Hook script exists: FAIL") +
410
+ color.dim(` — not found at ${absolutePath}`));
411
+ }
387
412
  }
388
413
  }
389
414
  }
package/build/server.js CHANGED
@@ -198,6 +198,12 @@ function getProjectDir() {
198
198
  // path on detected platform so non-Claude hosts skip the heuristic and
199
199
  // fall through to PWD/cwd cleanly.
200
200
  //
201
+ // The Claude heuristic must also be fresh. Hosts such as Pi can be
202
+ // misdetected as Claude Code solely because ~/.claude exists; without a
203
+ // freshness guard an old Claude transcript can globally hijack ctx shell cwd
204
+ // after reboot. Active Claude sessions update their transcript as the user
205
+ // interacts, so stale transcripts should fall through to PWD/cwd.
206
+ //
201
207
  // Issue #545 (v1.0.124): pass strictPlatform for ALL adapters so the
202
208
  // env-var cascade is built ALGORITHMICALLY from the platform's own
203
209
  // workspace vars + universal escape hatch — foreign workspace vars (e.g.
@@ -220,6 +226,7 @@ function getProjectDir() {
220
226
  cwd: process.cwd(),
221
227
  pwd: process.env.PWD,
222
228
  transcriptsRoot,
229
+ transcriptMaxAgeMs: 5 * 60 * 1000,
223
230
  strictPlatform,
224
231
  });
225
232
  }
@@ -1,4 +1,27 @@
1
- import type { HookAdapter } from "../adapters/types.js";
1
+ import { type HookAdapter } from "../adapters/types.js";
2
2
  export declare function getCommandsFromHookEntry(entry: unknown): string[];
3
+ /**
4
+ * Extract the hook script path from a hook command string.
5
+ *
6
+ * Post Algo-D2 this is a thin wrapper around `parseNodeCommand` with a
7
+ * single legacy fallback retained for stale-entry cleanup
8
+ * (`configureAllHooks` walks pre-v1.0.124 settings.json shapes that
9
+ * predate `buildNodeCommand`). The legacy branches are deliberately
10
+ * narrow:
11
+ *
12
+ * 1) Canonical: `"<nodePath>" "<scriptPath>.mjs"` — `parseNodeCommand`
13
+ * handles this; round-trips with `buildNodeCommand`.
14
+ * 2) Legacy quoted: `node "<scriptPath>.mjs"` — emitted by claude-code
15
+ * pre-D3. The script segment is fully quoted, no whitespace
16
+ * ambiguity.
17
+ * 3) Legacy unquoted: `node <scriptPath>.mjs` — only when the entire
18
+ * command is whitespace-safe (exactly two whitespace-separated
19
+ * tokens). The #548 wire shape — `node C:/Users/High Ground …` —
20
+ * contains internal whitespace so this branch refuses it. Returns
21
+ * `null` instead of grabbing the tail after the last whitespace.
22
+ *
23
+ * Anything else returns `null`, letting the doctor (Algo-D1) fall
24
+ * through to direct `existsSync` instead of trusting the regex.
25
+ */
3
26
  export declare function extractHookScriptPath(command: string): string | null;
4
27
  export declare function getHookScriptPaths(adapter: HookAdapter, pluginRoot: string): string[];
@@ -1,3 +1,4 @@
1
+ import { parseNodeCommand } from "../adapters/types.js";
1
2
  export function getCommandsFromHookEntry(entry) {
2
3
  const commands = [];
3
4
  if (entry && typeof entry === "object") {
@@ -17,9 +18,45 @@ export function getCommandsFromHookEntry(entry) {
17
18
  }
18
19
  return commands;
19
20
  }
21
+ /**
22
+ * Extract the hook script path from a hook command string.
23
+ *
24
+ * Post Algo-D2 this is a thin wrapper around `parseNodeCommand` with a
25
+ * single legacy fallback retained for stale-entry cleanup
26
+ * (`configureAllHooks` walks pre-v1.0.124 settings.json shapes that
27
+ * predate `buildNodeCommand`). The legacy branches are deliberately
28
+ * narrow:
29
+ *
30
+ * 1) Canonical: `"<nodePath>" "<scriptPath>.mjs"` — `parseNodeCommand`
31
+ * handles this; round-trips with `buildNodeCommand`.
32
+ * 2) Legacy quoted: `node "<scriptPath>.mjs"` — emitted by claude-code
33
+ * pre-D3. The script segment is fully quoted, no whitespace
34
+ * ambiguity.
35
+ * 3) Legacy unquoted: `node <scriptPath>.mjs` — only when the entire
36
+ * command is whitespace-safe (exactly two whitespace-separated
37
+ * tokens). The #548 wire shape — `node C:/Users/High Ground …` —
38
+ * contains internal whitespace so this branch refuses it. Returns
39
+ * `null` instead of grabbing the tail after the last whitespace.
40
+ *
41
+ * Anything else returns `null`, letting the doctor (Algo-D1) fall
42
+ * through to direct `existsSync` instead of trusting the regex.
43
+ */
20
44
  export function extractHookScriptPath(command) {
21
- const match = command.match(/(?:"([^"]+\.mjs)"|'([^']+\.mjs)'|(\S+\.mjs))/);
22
- return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
45
+ const parsed = parseNodeCommand(command);
46
+ if (parsed) {
47
+ return parsed.scriptPath.endsWith(".mjs") ? parsed.scriptPath : null;
48
+ }
49
+ // Legacy quoted: `node "/path/with spaces/x.mjs"` (pre-D3 claude-code emit).
50
+ const legacyQuoted = command.match(/^\s*node\s+"([^"]+\.mjs)"\s*$/);
51
+ if (legacyQuoted)
52
+ return legacyQuoted[1];
53
+ // Legacy unquoted: `node /path/x.mjs` — refuses internal whitespace
54
+ // by anchoring both tokens. The #548 ambiguous shape has 3+ tokens
55
+ // (spaces in the path) and falls through to `null`.
56
+ const legacyBare = command.match(/^\s*node\s+(\S+\.mjs)\s*$/);
57
+ if (legacyBare)
58
+ return legacyBare[1];
59
+ return null;
23
60
  }
24
61
  export function getHookScriptPaths(adapter, pluginRoot) {
25
62
  const paths = new Set();
@@ -0,0 +1,37 @@
1
+ /**
2
+ * TypeScript surface for the start.mjs plugin-cache integrity helper.
3
+ *
4
+ * The actual logic lives in `scripts/plugin-cache-integrity.mjs` (raw
5
+ * `.mjs` so start.mjs can import it without a TS toolchain at boot —
6
+ * #550 fail-fast happens BEFORE any bundle is loaded). This module is
7
+ * the bridge that lets TS consumers (claude-code adapter's
8
+ * getHealthChecks for Algo-D5, the cli doctor surface) call the same
9
+ * function without duplicating the implementation.
10
+ *
11
+ * Single source of truth: scripts/plugin-cache-integrity.mjs. Boot
12
+ * fail-fast (Algo-D4) and doctor diagnostic (Algo-D5) agree
13
+ * byte-for-byte because they call the same exported function.
14
+ *
15
+ * Top-level dynamic import is used (not a static `import` from `.mjs`)
16
+ * because the project is ESM and `import` of a sibling `.mjs` from a
17
+ * `.ts` file relies on the bundler / loader resolving `.mjs`
18
+ * extensions, which esbuild can do but tsc-only typecheck cannot. The
19
+ * dynamic import is resolved by the runtime (Node ESM) regardless of
20
+ * how the consumer was bundled. Errors are caught and surfaced as a
21
+ * FAIL detail — the helper is required to ship in the npm tarball
22
+ * (package.json files[]); a missing helper means the install is
23
+ * fundamentally broken.
24
+ */
25
+ /**
26
+ * Run the integrity check synchronously. If the helper module is
27
+ * still loading (not yet cached) returns a FAIL with detail
28
+ * "integrity helper not yet loaded" — caller should retry once the
29
+ * doctor command's IO is complete. In practice the doctor is invoked
30
+ * many MS after module load so this fallback is defensive only.
31
+ */
32
+ export declare function checkPluginCacheIntegritySync(pluginRoot: string): {
33
+ status: "OK" | "FAIL";
34
+ detail: string;
35
+ };
36
+ /** Force-await the helper load. Tests use this to deflake the eager fire-and-forget. */
37
+ export declare function ensurePluginCacheIntegrityLoaded(): Promise<void>;