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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/adapters/claude-code/hooks.d.ts +10 -4
- package/build/adapters/claude-code/hooks.js +22 -12
- package/build/adapters/claude-code/index.d.ts +24 -1
- package/build/adapters/claude-code/index.js +67 -11
- package/build/adapters/types.d.ts +57 -0
- package/build/adapters/types.js +29 -0
- package/build/cli.js +38 -13
- package/build/server.js +7 -0
- package/build/util/hook-config.d.ts +24 -1
- package/build/util/hook-config.js +39 -2
- package/build/util/plugin-cache-integrity.d.ts +37 -0
- package/build/util/plugin-cache-integrity.js +105 -0
- package/build/util/project-dir.d.ts +13 -0
- package/build/util/project-dir.js +11 -2
- package/cli.bundle.mjs +122 -122
- package/hooks/core/routing.mjs +114 -22
- package/hooks/gemini-cli/sessionstart.mjs +8 -6
- package/hooks/security.bundle.mjs +1 -0
- package/hooks/sessionstart.mjs +18 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -3
- package/scripts/plugin-cache-integrity.mjs +248 -0
- package/server.bundle.mjs +94 -94
- package/start.mjs +37 -0
- package/skills/UPSTREAM-CREDITS.md +0 -51
- package/skills/diagnose/SKILL.md +0 -122
- package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
- package/skills/grill-me/SKILL.md +0 -15
- package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
- package/skills/grill-with-docs/SKILL.md +0 -93
- package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
- package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
- package/skills/improve-codebase-architecture/SKILL.md +0 -76
- package/skills/tdd/SKILL.md +0 -114
- package/skills/tdd/deep-modules.md +0 -33
- package/skills/tdd/interface-design.md +0 -31
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/refactoring.md +0 -10
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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. */
|
package/build/adapters/types.js
CHANGED
|
@@ -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
|
|
374
|
-
if (
|
|
375
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
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
|
|
22
|
-
|
|
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>;
|