context-mode 1.0.88 → 1.0.90
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/README.md +184 -60
- package/build/adapters/antigravity/index.d.ts +3 -5
- package/build/adapters/antigravity/index.js +7 -35
- package/build/adapters/base.d.ts +27 -0
- package/build/adapters/base.js +59 -0
- package/build/adapters/claude-code/index.d.ts +9 -25
- package/build/adapters/claude-code/index.js +27 -141
- package/build/adapters/claude-code-base.d.ts +49 -0
- package/build/adapters/claude-code-base.js +113 -0
- package/build/adapters/client-map.js +5 -0
- package/build/adapters/codex/hooks.d.ts +21 -14
- package/build/adapters/codex/hooks.js +22 -15
- package/build/adapters/codex/index.d.ts +6 -10
- package/build/adapters/codex/index.js +13 -43
- package/build/adapters/copilot-base.d.ts +78 -0
- package/build/adapters/copilot-base.js +281 -0
- package/build/adapters/cursor/index.d.ts +3 -5
- package/build/adapters/cursor/index.js +6 -34
- package/build/adapters/detect.d.ts +7 -0
- package/build/adapters/detect.js +57 -56
- package/build/adapters/gemini-cli/index.d.ts +3 -5
- package/build/adapters/gemini-cli/index.js +7 -35
- package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
- package/build/adapters/jetbrains-copilot/config.js +8 -0
- package/build/adapters/jetbrains-copilot/hooks.d.ts +51 -0
- package/build/adapters/jetbrains-copilot/hooks.js +82 -0
- package/build/adapters/jetbrains-copilot/index.d.ts +24 -0
- package/build/adapters/jetbrains-copilot/index.js +119 -0
- package/build/adapters/kiro/hooks.d.ts +14 -0
- package/build/adapters/kiro/hooks.js +23 -0
- package/build/adapters/kiro/index.d.ts +3 -5
- package/build/adapters/kiro/index.js +10 -38
- package/build/adapters/openclaw/index.d.ts +3 -4
- package/build/adapters/openclaw/index.js +6 -22
- package/build/adapters/opencode/index.d.ts +2 -3
- package/build/adapters/opencode/index.js +5 -16
- package/build/adapters/qwen-code/index.d.ts +39 -0
- package/build/adapters/qwen-code/index.js +199 -0
- package/build/adapters/types.d.ts +1 -1
- package/build/adapters/vscode-copilot/index.d.ts +16 -46
- package/build/adapters/vscode-copilot/index.js +29 -320
- package/build/adapters/zed/index.d.ts +3 -5
- package/build/adapters/zed/index.js +7 -35
- package/build/cli.js +113 -47
- package/build/lifecycle.d.ts +23 -0
- package/build/lifecycle.js +54 -13
- package/build/opencode-plugin.d.ts +19 -7
- package/build/opencode-plugin.js +19 -7
- package/build/pi-extension.js +24 -7
- package/build/runtime.js +24 -9
- package/build/security.d.ts +17 -1
- package/build/security.js +40 -6
- package/build/server.js +129 -21
- package/build/session/analytics.d.ts +8 -7
- package/build/session/analytics.js +95 -75
- package/build/session/db.d.ts +10 -1
- package/build/session/db.js +67 -8
- package/build/session/extract.js +10 -2
- package/build/session/project-attribution.d.ts +73 -0
- package/build/session/project-attribution.js +231 -0
- package/build/store.d.ts +7 -0
- package/build/store.js +117 -18
- package/build/truncate.d.ts +6 -0
- package/build/truncate.js +51 -29
- package/build/types.d.ts +8 -0
- package/cli.bundle.mjs +157 -136
- package/configs/antigravity/GEMINI.md +31 -36
- package/configs/claude-code/CLAUDE.md +31 -37
- package/configs/codex/AGENTS.md +35 -49
- package/configs/cursor/context-mode.mdc +24 -25
- package/configs/gemini-cli/GEMINI.md +30 -36
- package/configs/jetbrains-copilot/copilot-instructions.md +59 -0
- package/configs/jetbrains-copilot/hooks.json +16 -0
- package/configs/jetbrains-copilot/mcp.json +8 -0
- package/configs/kilo/AGENTS.md +30 -36
- package/configs/kiro/KIRO.md +30 -36
- package/configs/kiro/agent.json +1 -1
- package/configs/openclaw/AGENTS.md +30 -36
- package/configs/opencode/AGENTS.md +30 -36
- package/configs/pi/AGENTS.md +31 -36
- package/configs/qwen-code/QWEN.md +63 -0
- package/configs/vscode-copilot/copilot-instructions.md +30 -36
- package/configs/zed/AGENTS.md +31 -36
- package/hooks/codex/posttooluse.mjs +7 -7
- package/hooks/codex/pretooluse.mjs +3 -3
- package/hooks/codex/sessionstart.mjs +2 -1
- package/hooks/core/formatters.mjs +24 -0
- package/hooks/core/routing.mjs +40 -15
- package/hooks/core/tool-naming.mjs +2 -0
- package/hooks/cursor/posttooluse.mjs +7 -7
- package/hooks/cursor/pretooluse.mjs +3 -3
- package/hooks/cursor/sessionstart.mjs +2 -1
- package/hooks/cursor/stop.mjs +2 -2
- package/hooks/ensure-deps.mjs +22 -10
- package/hooks/gemini-cli/aftertool.mjs +8 -8
- package/hooks/gemini-cli/beforetool.mjs +3 -2
- package/hooks/gemini-cli/precompress.mjs +2 -2
- package/hooks/gemini-cli/sessionstart.mjs +12 -4
- package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
- package/hooks/jetbrains-copilot/precompact.mjs +54 -0
- package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
- package/hooks/jetbrains-copilot/sessionstart.mjs +119 -0
- package/hooks/kiro/posttooluse.mjs +6 -7
- package/hooks/kiro/pretooluse.mjs +3 -2
- package/hooks/posttooluse.mjs +8 -8
- package/hooks/precompact.mjs +3 -4
- package/hooks/pretooluse.mjs +43 -20
- package/hooks/routing-block.mjs +35 -33
- package/hooks/session-attribution.bundle.mjs +1 -0
- package/hooks/session-db.bundle.mjs +27 -8
- package/hooks/session-extract.bundle.mjs +2 -1
- package/hooks/session-helpers.mjs +44 -3
- package/hooks/session-loaders.mjs +37 -0
- package/hooks/session-snapshot.bundle.mjs +14 -14
- package/hooks/sessionstart.mjs +5 -5
- package/hooks/userpromptsubmit.mjs +26 -9
- package/hooks/vscode-copilot/posttooluse.mjs +8 -8
- package/hooks/vscode-copilot/precompact.mjs +2 -2
- package/hooks/vscode-copilot/pretooluse.mjs +3 -2
- package/hooks/vscode-copilot/sessionstart.mjs +2 -2
- package/insight/server.mjs +262 -32
- package/insight/src/lib/api.ts +2 -1
- package/insight/src/routes/index.tsx +16 -3
- package/insight/src/routes/search.tsx +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +11 -2
- package/server.bundle.mjs +117 -99
- package/skills/ctx-insight/SKILL.md +1 -1
package/build/lifecycle.d.ts
CHANGED
|
@@ -17,6 +17,29 @@ export interface LifecycleGuardOptions {
|
|
|
17
17
|
/** Injectable parent-alive check (for testing). Default: ppid-based check. */
|
|
18
18
|
isParentAlive?: () => boolean;
|
|
19
19
|
}
|
|
20
|
+
/** Injectable dependencies for {@link makeDefaultIsParentAlive}. */
|
|
21
|
+
export interface IsParentAliveDeps {
|
|
22
|
+
/** Read the current ppid. Default: `() => process.ppid`. */
|
|
23
|
+
getPpid?: () => number;
|
|
24
|
+
/** Read the grandparent ppid. Default: ps-based POSIX probe, NaN on Windows. */
|
|
25
|
+
readGrandparentPpid?: () => number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build a parent-liveness check that handles the npm-exec wrapper case (#311).
|
|
29
|
+
*
|
|
30
|
+
* A plain ppid comparison misses Claude Code sessions launched via
|
|
31
|
+
* `start.mjs → npm exec → context-mode server`: when Claude Code dies,
|
|
32
|
+
* `start.mjs` reparents to init but `npm exec` stays alive, so the server's
|
|
33
|
+
* direct ppid never changes. We additionally check whether the grandparent
|
|
34
|
+
* process has been reparented to init (PID 1). When the original grandparent
|
|
35
|
+
* was already 1 (daemonized startup) the check is skipped, and on Windows
|
|
36
|
+
* where there's no cheap `ps` equivalent we also skip — so this change is
|
|
37
|
+
* strictly additive to the previous behavior.
|
|
38
|
+
*
|
|
39
|
+
* Exported for unit-testing with injected readers. Production code uses
|
|
40
|
+
* {@link defaultIsParentAlive} (captured once at module load).
|
|
41
|
+
*/
|
|
42
|
+
export declare function makeDefaultIsParentAlive(deps?: IsParentAliveDeps): () => boolean;
|
|
20
43
|
/**
|
|
21
44
|
* Start the lifecycle guard. Returns a cleanup function.
|
|
22
45
|
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
package/build/lifecycle.js
CHANGED
|
@@ -9,23 +9,64 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Cross-platform: macOS, Linux, Windows.
|
|
11
11
|
*/
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
/** Read grandparent PID via `ps -o ppid= -p $PPID`. Returns NaN on failure or Windows. */
|
|
14
|
+
function readGrandparentPpidImpl() {
|
|
15
|
+
if (process.platform === "win32")
|
|
16
|
+
return NaN;
|
|
17
|
+
const ppid = process.ppid;
|
|
18
|
+
if (!ppid || ppid <= 1)
|
|
19
|
+
return NaN;
|
|
20
|
+
try {
|
|
21
|
+
const out = execFileSync("ps", ["-o", "ppid=", "-p", String(ppid)], {
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
timeout: 2000,
|
|
24
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
25
|
+
}).trim();
|
|
26
|
+
const n = parseInt(out, 10);
|
|
27
|
+
return Number.isFinite(n) ? n : NaN;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return NaN;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
12
33
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
34
|
+
* Build a parent-liveness check that handles the npm-exec wrapper case (#311).
|
|
35
|
+
*
|
|
36
|
+
* A plain ppid comparison misses Claude Code sessions launched via
|
|
37
|
+
* `start.mjs → npm exec → context-mode server`: when Claude Code dies,
|
|
38
|
+
* `start.mjs` reparents to init but `npm exec` stays alive, so the server's
|
|
39
|
+
* direct ppid never changes. We additionally check whether the grandparent
|
|
40
|
+
* process has been reparented to init (PID 1). When the original grandparent
|
|
41
|
+
* was already 1 (daemonized startup) the check is skipped, and on Windows
|
|
42
|
+
* where there's no cheap `ps` equivalent we also skip — so this change is
|
|
43
|
+
* strictly additive to the previous behavior.
|
|
17
44
|
*
|
|
18
|
-
*
|
|
45
|
+
* Exported for unit-testing with injected readers. Production code uses
|
|
46
|
+
* {@link defaultIsParentAlive} (captured once at module load).
|
|
19
47
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
export function makeDefaultIsParentAlive(deps = {}) {
|
|
49
|
+
const getPpid = deps.getPpid ?? (() => process.ppid);
|
|
50
|
+
const readGp = deps.readGrandparentPpid ?? readGrandparentPpidImpl;
|
|
51
|
+
const originalPpid = getPpid();
|
|
52
|
+
const originalGrandparentPpid = readGp();
|
|
53
|
+
return () => {
|
|
54
|
+
const ppid = getPpid();
|
|
55
|
+
if (ppid !== originalPpid)
|
|
56
|
+
return false;
|
|
57
|
+
if (ppid === 0 || ppid === 1)
|
|
58
|
+
return false;
|
|
59
|
+
// Grandparent orphan check (#311): npm-exec wrappers stay alive past the
|
|
60
|
+
// session owner. If our grandparent is now PID 1 but wasn't at startup,
|
|
61
|
+
// the wrapping chain is orphaned and we should shut down.
|
|
62
|
+
if (!Number.isNaN(originalGrandparentPpid) && originalGrandparentPpid > 1) {
|
|
63
|
+
if (readGp() === 1)
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
28
68
|
}
|
|
69
|
+
const defaultIsParentAlive = makeDefaultIsParentAlive();
|
|
29
70
|
/**
|
|
30
71
|
* Start the lifecycle guard. Returns a cleanup function.
|
|
31
72
|
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenCode TypeScript plugin entry point for context-mode.
|
|
2
|
+
* OpenCode / KiloCode TypeScript plugin entry point for context-mode.
|
|
3
3
|
*
|
|
4
4
|
* Provides three hooks:
|
|
5
5
|
* - tool.execute.before — Routing enforcement (deny/modify/passthrough)
|
|
6
6
|
* - tool.execute.after — Session event capture
|
|
7
7
|
* - experimental.session.compacting — Compaction snapshot generation
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* KiloCode loads this via: import("context-mode") → expects default export
|
|
10
|
+
* with shape { server: (input) => Promise<Hooks> } (PluginModule).
|
|
11
|
+
*
|
|
12
|
+
* OpenCode loads this via: import("context-mode/plugin") → also supports
|
|
13
|
+
* the named export ContextModePlugin for backward compat.
|
|
10
14
|
*
|
|
11
15
|
* Constraints:
|
|
12
16
|
* - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
|
|
@@ -14,9 +18,10 @@
|
|
|
14
18
|
* - No routing file auto-write (avoid dirtying project trees)
|
|
15
19
|
* - Session cleanup happens at plugin init (no SessionStart)
|
|
16
20
|
*/
|
|
17
|
-
/** OpenCode plugin
|
|
21
|
+
/** KiloCode/OpenCode plugin input — both platforms pass at least `directory`. */
|
|
18
22
|
interface PluginContext {
|
|
19
23
|
directory: string;
|
|
24
|
+
[key: string]: unknown;
|
|
20
25
|
}
|
|
21
26
|
/** OpenCode tool.execute.before — first parameter */
|
|
22
27
|
interface BeforeHookInput {
|
|
@@ -51,12 +56,19 @@ interface CompactingHookOutput {
|
|
|
51
56
|
prompt?: string;
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
54
|
-
*
|
|
59
|
+
* Plugin factory. Called once when KiloCode/OpenCode loads the plugin.
|
|
55
60
|
* Returns an object mapping hook event names to async handler functions.
|
|
56
|
-
|
|
57
|
-
export
|
|
61
|
+
*
|
|
62
|
+
* KiloCode expects: export default { server: (input) => Promise<Hooks> }
|
|
63
|
+
* OpenCode expects: export const ContextModePlugin = (ctx) => Promise<Hooks>
|
|
64
|
+
*/
|
|
65
|
+
declare function createContextModePlugin(ctx: PluginContext): Promise<{
|
|
58
66
|
"tool.execute.before": (input: BeforeHookInput, output: BeforeHookOutput) => Promise<void>;
|
|
59
67
|
"tool.execute.after": (input: AfterHookInput, output: AfterHookOutput) => Promise<void>;
|
|
60
68
|
"experimental.session.compacting": (input: CompactingHookInput, output: CompactingHookOutput) => Promise<string>;
|
|
61
69
|
}>;
|
|
62
|
-
|
|
70
|
+
declare const _default: {
|
|
71
|
+
server: typeof createContextModePlugin;
|
|
72
|
+
};
|
|
73
|
+
export default _default;
|
|
74
|
+
export { createContextModePlugin as ContextModePlugin };
|
package/build/opencode-plugin.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenCode TypeScript plugin entry point for context-mode.
|
|
2
|
+
* OpenCode / KiloCode TypeScript plugin entry point for context-mode.
|
|
3
3
|
*
|
|
4
4
|
* Provides three hooks:
|
|
5
5
|
* - tool.execute.before — Routing enforcement (deny/modify/passthrough)
|
|
6
6
|
* - tool.execute.after — Session event capture
|
|
7
7
|
* - experimental.session.compacting — Compaction snapshot generation
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* KiloCode loads this via: import("context-mode") → expects default export
|
|
10
|
+
* with shape { server: (input) => Promise<Hooks> } (PluginModule).
|
|
11
|
+
*
|
|
12
|
+
* OpenCode loads this via: import("context-mode/plugin") → also supports
|
|
13
|
+
* the named export ContextModePlugin for backward compat.
|
|
10
14
|
*
|
|
11
15
|
* Constraints:
|
|
12
16
|
* - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
|
|
@@ -27,10 +31,13 @@ function getPlatform() {
|
|
|
27
31
|
}
|
|
28
32
|
// ── Plugin Factory ────────────────────────────────────────
|
|
29
33
|
/**
|
|
30
|
-
*
|
|
34
|
+
* Plugin factory. Called once when KiloCode/OpenCode loads the plugin.
|
|
31
35
|
* Returns an object mapping hook event names to async handler functions.
|
|
32
|
-
|
|
33
|
-
export
|
|
36
|
+
*
|
|
37
|
+
* KiloCode expects: export default { server: (input) => Promise<Hooks> }
|
|
38
|
+
* OpenCode expects: export const ContextModePlugin = (ctx) => Promise<Hooks>
|
|
39
|
+
*/
|
|
40
|
+
async function createContextModePlugin(ctx) {
|
|
34
41
|
// Resolve build dir from compiled JS location
|
|
35
42
|
const adapter = new OpenCodeAdapter(getPlatform());
|
|
36
43
|
const buildDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -52,7 +59,7 @@ export const ContextModePlugin = async (ctx) => {
|
|
|
52
59
|
const toolInput = output.args ?? {};
|
|
53
60
|
let decision;
|
|
54
61
|
try {
|
|
55
|
-
decision = routing.routePreToolUse(toolName, toolInput, projectDir,
|
|
62
|
+
decision = routing.routePreToolUse(toolName, toolInput, projectDir, getPlatform());
|
|
56
63
|
}
|
|
57
64
|
catch {
|
|
58
65
|
return; // Routing failure → allow passthrough
|
|
@@ -109,4 +116,9 @@ export const ContextModePlugin = async (ctx) => {
|
|
|
109
116
|
}
|
|
110
117
|
},
|
|
111
118
|
};
|
|
112
|
-
}
|
|
119
|
+
}
|
|
120
|
+
// ── Exports ──────────────────────────────────────────────
|
|
121
|
+
// KiloCode PluginModule: default export with { server } shape
|
|
122
|
+
// OpenCode compat: named export for direct import("context-mode/plugin")
|
|
123
|
+
export default { server: createContextModePlugin };
|
|
124
|
+
export { createContextModePlugin as ContextModePlugin };
|
package/build/pi-extension.js
CHANGED
|
@@ -110,6 +110,20 @@ function buildStatsText(db, sessionId) {
|
|
|
110
110
|
return "context-mode stats unavailable (session DB error)";
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
function resolveCommandContext(argsOrCtx, ctx) {
|
|
114
|
+
if (ctx !== undefined)
|
|
115
|
+
return ctx;
|
|
116
|
+
if (argsOrCtx && typeof argsOrCtx === "object")
|
|
117
|
+
return argsOrCtx;
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
function handleCommandText(text, ctx) {
|
|
121
|
+
if (ctx?.hasUI) {
|
|
122
|
+
ctx.ui.notify(text, "info");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
return { text };
|
|
126
|
+
}
|
|
113
127
|
// ── Extension entry point ────────────────────────────────
|
|
114
128
|
/** Pi extension default export. Called once by Pi runtime with the extension API. */
|
|
115
129
|
export default function piExtension(pi) {
|
|
@@ -300,16 +314,18 @@ export default function piExtension(pi) {
|
|
|
300
314
|
// ── 8. Slash commands ──────────────────────────────────
|
|
301
315
|
pi.registerCommand("ctx-stats", {
|
|
302
316
|
description: "Show context-mode session statistics",
|
|
303
|
-
handler: () => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
317
|
+
handler: async (argsOrCtx, maybeCtx) => {
|
|
318
|
+
const ctx = resolveCommandContext(argsOrCtx, maybeCtx);
|
|
319
|
+
const text = !_db || !_sessionId
|
|
320
|
+
? "context-mode: no active session"
|
|
321
|
+
: buildStatsText(_db, _sessionId);
|
|
322
|
+
return handleCommandText(text, ctx);
|
|
308
323
|
},
|
|
309
324
|
});
|
|
310
325
|
pi.registerCommand("ctx-doctor", {
|
|
311
326
|
description: "Run context-mode diagnostics",
|
|
312
|
-
handler: () => {
|
|
327
|
+
handler: async (argsOrCtx, maybeCtx) => {
|
|
328
|
+
const ctx = resolveCommandContext(argsOrCtx, maybeCtx);
|
|
313
329
|
const dbPath = getDBPath();
|
|
314
330
|
const dbExists = existsSync(dbPath);
|
|
315
331
|
const lines = [
|
|
@@ -334,7 +350,8 @@ export default function piExtension(pi) {
|
|
|
334
350
|
lines.push("- DB query error");
|
|
335
351
|
}
|
|
336
352
|
}
|
|
337
|
-
|
|
353
|
+
const text = lines.join("\n");
|
|
354
|
+
return handleCommandText(text, ctx);
|
|
338
355
|
},
|
|
339
356
|
});
|
|
340
357
|
}
|
package/build/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
const isWindows = process.platform === "win32";
|
|
4
4
|
function commandExists(cmd) {
|
|
@@ -14,10 +14,8 @@ function commandExists(cmd) {
|
|
|
14
14
|
function bunExists() {
|
|
15
15
|
if (commandExists("bun"))
|
|
16
16
|
return true;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
20
|
-
if (home && existsSync(`${home}/.bun/bin/bun`))
|
|
17
|
+
for (const p of bunFallbackPaths()) {
|
|
18
|
+
if (existsSync(p))
|
|
21
19
|
return true;
|
|
22
20
|
}
|
|
23
21
|
return false;
|
|
@@ -25,8 +23,24 @@ function bunExists() {
|
|
|
25
23
|
function bunCommand() {
|
|
26
24
|
if (commandExists("bun"))
|
|
27
25
|
return "bun";
|
|
26
|
+
for (const p of bunFallbackPaths()) {
|
|
27
|
+
if (existsSync(p))
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
28
30
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
29
|
-
return `${home}/.bun/bin/bun`;
|
|
31
|
+
return isWindows ? `${home}\\.bun\\bin\\bun.exe` : `${home}/.bun/bin/bun`;
|
|
32
|
+
}
|
|
33
|
+
/** Fallback paths where Bun may be installed but not on PATH. */
|
|
34
|
+
function bunFallbackPaths() {
|
|
35
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
36
|
+
if (isWindows) {
|
|
37
|
+
const localAppData = process.env.LOCALAPPDATA ?? "";
|
|
38
|
+
return [
|
|
39
|
+
...(home ? [`${home}\\.bun\\bin\\bun.exe`] : []),
|
|
40
|
+
...(localAppData ? [`${localAppData}\\bun\\bin\\bun.exe`] : []),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
return home ? [`${home}/.bun/bin/bun`] : [];
|
|
30
44
|
}
|
|
31
45
|
/**
|
|
32
46
|
* On Windows, resolve the first non-WSL bash in PATH.
|
|
@@ -61,10 +75,11 @@ function resolveWindowsBash() {
|
|
|
61
75
|
return null;
|
|
62
76
|
}
|
|
63
77
|
}
|
|
64
|
-
function getVersion(cmd) {
|
|
78
|
+
function getVersion(cmd, args = ["--version"]) {
|
|
65
79
|
try {
|
|
66
|
-
return
|
|
80
|
+
return execFileSync(cmd, args, {
|
|
67
81
|
encoding: "utf-8",
|
|
82
|
+
shell: process.platform === "win32",
|
|
68
83
|
stdio: ["pipe", "pipe", "pipe"],
|
|
69
84
|
timeout: 5000,
|
|
70
85
|
})
|
|
@@ -132,7 +147,7 @@ export function getRuntimeSummary(runtimes) {
|
|
|
132
147
|
if (runtimes.ruby)
|
|
133
148
|
lines.push(` Ruby: ${runtimes.ruby} (${getVersion(runtimes.ruby)})`);
|
|
134
149
|
if (runtimes.go)
|
|
135
|
-
lines.push(` Go: ${runtimes.go} (${getVersion(runtimes.go)})`);
|
|
150
|
+
lines.push(` Go: ${runtimes.go} (${getVersion(runtimes.go, ["version"])})`);
|
|
136
151
|
if (runtimes.rust)
|
|
137
152
|
lines.push(` Rust: ${runtimes.rust} (${getVersion(runtimes.rust)})`);
|
|
138
153
|
if (runtimes.php)
|
package/build/security.d.ts
CHANGED
|
@@ -104,8 +104,24 @@ export declare function evaluateCommandDenyOnly(command: string, policies: Secur
|
|
|
104
104
|
*
|
|
105
105
|
* Normalizes backslashes to forward slashes before matching so that
|
|
106
106
|
* Windows paths work with Unix-style glob patterns.
|
|
107
|
+
*
|
|
108
|
+
* When `projectRoot` is supplied, the path is also matched in its
|
|
109
|
+
* fully-resolved absolute form **and** — when the file exists — in
|
|
110
|
+
* its canonical form (`fs.realpathSync`). This prevents two classes
|
|
111
|
+
* of bypass:
|
|
112
|
+
*
|
|
113
|
+
* 1. `..` traversal: a relative path like `../../.ssh/id_rsa` no
|
|
114
|
+
* longer evades absolute-path deny rules.
|
|
115
|
+
* 2. Symlink escape: a project-local path whose realpath points
|
|
116
|
+
* outside the project (e.g. `safe.log -> ~/.ssh/id_rsa`) no
|
|
117
|
+
* longer evades absolute-path deny rules.
|
|
118
|
+
*
|
|
119
|
+
* realpath is best-effort: if the file does not exist yet (ENOENT)
|
|
120
|
+
* or the syscall fails for any reason, the lexical resolved form is
|
|
121
|
+
* still checked. This keeps the function usable for paths that will
|
|
122
|
+
* be created during execution.
|
|
107
123
|
*/
|
|
108
|
-
export declare function evaluateFilePath(filePath: string, denyGlobs: string[][], caseInsensitive?: boolean): {
|
|
124
|
+
export declare function evaluateFilePath(filePath: string, denyGlobs: string[][], caseInsensitive?: boolean, projectRoot?: string): {
|
|
109
125
|
denied: boolean;
|
|
110
126
|
matchedPattern?: string;
|
|
111
127
|
};
|
package/build/security.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
// ==============================================================================
|
|
@@ -358,14 +358,48 @@ export function evaluateCommandDenyOnly(command, policies, caseInsensitive = pro
|
|
|
358
358
|
*
|
|
359
359
|
* Normalizes backslashes to forward slashes before matching so that
|
|
360
360
|
* Windows paths work with Unix-style glob patterns.
|
|
361
|
+
*
|
|
362
|
+
* When `projectRoot` is supplied, the path is also matched in its
|
|
363
|
+
* fully-resolved absolute form **and** — when the file exists — in
|
|
364
|
+
* its canonical form (`fs.realpathSync`). This prevents two classes
|
|
365
|
+
* of bypass:
|
|
366
|
+
*
|
|
367
|
+
* 1. `..` traversal: a relative path like `../../.ssh/id_rsa` no
|
|
368
|
+
* longer evades absolute-path deny rules.
|
|
369
|
+
* 2. Symlink escape: a project-local path whose realpath points
|
|
370
|
+
* outside the project (e.g. `safe.log -> ~/.ssh/id_rsa`) no
|
|
371
|
+
* longer evades absolute-path deny rules.
|
|
372
|
+
*
|
|
373
|
+
* realpath is best-effort: if the file does not exist yet (ENOENT)
|
|
374
|
+
* or the syscall fails for any reason, the lexical resolved form is
|
|
375
|
+
* still checked. This keeps the function usable for paths that will
|
|
376
|
+
* be created during execution.
|
|
361
377
|
*/
|
|
362
|
-
export function evaluateFilePath(filePath, denyGlobs, caseInsensitive = process.platform === "win32") {
|
|
363
|
-
|
|
364
|
-
|
|
378
|
+
export function evaluateFilePath(filePath, denyGlobs, caseInsensitive = process.platform === "win32", projectRoot) {
|
|
379
|
+
const toForward = (path) => path.replace(/\\/g, "/");
|
|
380
|
+
// Match against the raw input, the lexically-resolved absolute path,
|
|
381
|
+
// and the canonical (symlink-resolved) path when the file exists.
|
|
382
|
+
// Deduplicated so absolute inputs and paths that don't cross symlinks
|
|
383
|
+
// don't pay the matching cost multiple times.
|
|
384
|
+
const candidates = new Set();
|
|
385
|
+
candidates.add(toForward(filePath));
|
|
386
|
+
if (projectRoot) {
|
|
387
|
+
const lexical = resolve(projectRoot, filePath);
|
|
388
|
+
candidates.add(toForward(lexical));
|
|
389
|
+
try {
|
|
390
|
+
candidates.add(toForward(realpathSync(lexical)));
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// File does not exist yet, or realpath failed — rely on lexical form.
|
|
394
|
+
}
|
|
395
|
+
}
|
|
365
396
|
for (const globs of denyGlobs) {
|
|
366
397
|
for (const glob of globs) {
|
|
367
|
-
|
|
368
|
-
|
|
398
|
+
const regex = fileGlobToRegex(glob, caseInsensitive);
|
|
399
|
+
for (const candidate of candidates) {
|
|
400
|
+
if (regex.test(candidate)) {
|
|
401
|
+
return { denied: true, matchedPattern: glob };
|
|
402
|
+
}
|
|
369
403
|
}
|
|
370
404
|
}
|
|
371
405
|
}
|