context-mode 1.0.106 → 1.0.108
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 +22 -18
- package/build/adapters/claude-code/index.js +26 -9
- package/build/adapters/copilot-base.d.ts +3 -3
- package/build/adapters/cursor/hooks.js +8 -0
- package/build/adapters/cursor/index.js +4 -1
- package/build/adapters/gemini-cli/hooks.d.ts +6 -1
- package/build/adapters/gemini-cli/hooks.js +7 -1
- package/build/adapters/gemini-cli/index.js +12 -0
- package/build/adapters/kiro/hooks.js +4 -0
- package/build/adapters/kiro/index.d.ts +9 -2
- package/build/adapters/kiro/index.js +49 -27
- package/build/adapters/opencode/index.js +11 -5
- package/build/adapters/qwen-code/index.js +18 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +0 -4
- package/build/adapters/vscode-copilot/hooks.js +6 -6
- package/build/cli.js +93 -12
- package/build/openclaw/mcp-tools.d.ts +54 -0
- package/build/openclaw/mcp-tools.js +198 -0
- package/build/openclaw-plugin.d.ts +9 -0
- package/build/openclaw-plugin.js +132 -16
- package/build/opencode-plugin.d.ts +29 -4
- package/build/opencode-plugin.js +154 -7
- package/build/pi-extension.js +123 -29
- package/build/server.d.ts +1 -0
- package/build/server.js +26 -1
- package/build/session/analytics.js +36 -13
- package/build/session/extract.d.ts +1 -1
- package/build/session/extract.js +46 -1
- package/cli.bundle.mjs +133 -132
- package/hooks/core/platform-detect.mjs +49 -0
- package/hooks/core/routing.mjs +13 -1
- package/hooks/cursor/afteragentresponse.mjs +74 -0
- package/hooks/ensure-deps.mjs +28 -12
- package/hooks/gemini-cli/beforeagent.mjs +99 -0
- package/hooks/kiro/agentspawn.mjs +97 -0
- package/hooks/kiro/userpromptsubmit.mjs +88 -0
- package/hooks/posttooluse.mjs +90 -80
- package/hooks/precompact.mjs +56 -46
- package/hooks/pretooluse.mjs +161 -167
- package/hooks/routing-block.mjs +2 -2
- package/hooks/run-hook.mjs +82 -0
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/sessionstart.mjs +187 -153
- package/hooks/userpromptsubmit.mjs +69 -58
- package/hooks/vscode-copilot/sessionstart.mjs +13 -14
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/heal-better-sqlite3.mjs +108 -0
- package/scripts/postinstall.mjs +27 -0
- package/server.bundle.mjs +79 -79
- package/skills/UPSTREAM-CREDITS.md +51 -0
- package/skills/context-mode-ops/SKILL.md +147 -0
- package/skills/diagnose/SKILL.md +122 -0
- package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/skills/grill-me/SKILL.md +15 -0
- package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
- package/skills/grill-with-docs/SKILL.md +93 -0
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/skills/improve-codebase-architecture/SKILL.md +76 -0
- package/skills/tdd/SKILL.md +114 -0
- package/skills/tdd/deep-modules.md +33 -0
- package/skills/tdd/interface-design.md +31 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/refactoring.md +10 -0
- package/skills/tdd/tests.md +61 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection from process env vars.
|
|
3
|
+
*
|
|
4
|
+
* Each supported platform sets a distinctive env var when invoking
|
|
5
|
+
* hook scripts; we use those to pick the correct tool-namer prefix
|
|
6
|
+
* and routing block. Falls back to "claude-code" when nothing matches
|
|
7
|
+
* so existing CC behavior is preserved.
|
|
8
|
+
*
|
|
9
|
+
* SINGLE SOURCE OF TRUTH: this table mirrors `PLATFORM_ENV_VARS` in
|
|
10
|
+
* `src/adapters/detect.ts:33-77`. Every entry has been verified against
|
|
11
|
+
* the platform's own runtime source code (full audit May 2026, see
|
|
12
|
+
* git blame). DO NOT add platform env vars here that aren't also in
|
|
13
|
+
* detect.ts — the two MUST stay in lock-step or detection will diverge
|
|
14
|
+
* between MCP-server-side and hook-script-side.
|
|
15
|
+
*
|
|
16
|
+
* Order matters — same as detect.ts. Forks listed BEFORE the fork's
|
|
17
|
+
* parent so collision detection works (e.g. cursor BEFORE vscode-copilot
|
|
18
|
+
* because Cursor inherits VSCODE_PID as a fork; antigravity BEFORE
|
|
19
|
+
* vscode-copilot for the same reason).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Mirror of `PLATFORM_ENV_VARS` in src/adapters/detect.ts:33-77.
|
|
23
|
+
// Keep in lock-step. If you change one, change the other.
|
|
24
|
+
const PLATFORM_ENV_VARS_MIRROR = [
|
|
25
|
+
["claude-code", ["CLAUDE_PROJECT_DIR", "CLAUDE_SESSION_ID"]],
|
|
26
|
+
["antigravity", ["ANTIGRAVITY_CLI_ALIAS"]],
|
|
27
|
+
["cursor", ["CURSOR_TRACE_ID", "CURSOR_CLI"]],
|
|
28
|
+
["kilo", ["KILO_PID"]],
|
|
29
|
+
["opencode", ["OPENCODE", "OPENCODE_PID"]],
|
|
30
|
+
["zed", ["ZED_SESSION_ID", "ZED_TERM"]],
|
|
31
|
+
["codex", ["CODEX_THREAD_ID", "CODEX_CI"]],
|
|
32
|
+
["gemini-cli", ["GEMINI_PROJECT_DIR", "GEMINI_CLI"]],
|
|
33
|
+
["vscode-copilot", ["VSCODE_PID", "VSCODE_CWD"]],
|
|
34
|
+
["jetbrains-copilot", ["IDEA_INITIAL_DIRECTORY"]],
|
|
35
|
+
["qwen-code", ["QWEN_PROJECT_DIR"]],
|
|
36
|
+
["pi", ["PI_PROJECT_DIR"]],
|
|
37
|
+
// openclaw — no auto-set process env vars; falls through to default
|
|
38
|
+
// kiro — no auto-set process env vars; falls through to default
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function detectPlatformFromEnv(env = process.env) {
|
|
42
|
+
for (const [platform, vars] of PLATFORM_ENV_VARS_MIRROR) {
|
|
43
|
+
if (vars.some((v) => env[v])) return platform;
|
|
44
|
+
}
|
|
45
|
+
return "claude-code";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Re-exported for tests so they can assert against the same canonical table.
|
|
49
|
+
export { PLATFORM_ENV_VARS_MIRROR };
|
package/hooks/core/routing.mjs
CHANGED
|
@@ -133,13 +133,25 @@ export async function initSecurity(buildDir) {
|
|
|
133
133
|
* - VS Code Copilot: run_in_terminal (command field), read_file, run_vs_code_task
|
|
134
134
|
*/
|
|
135
135
|
const TOOL_ALIASES = {
|
|
136
|
-
// Gemini CLI
|
|
136
|
+
// Gemini CLI / Qwen Code (share native tool names — Qwen is Gemini fork:
|
|
137
|
+
// refs/platforms/qwen-code/packages/core/src/tools/tool-names.ts)
|
|
137
138
|
"run_shell_command": "Bash",
|
|
138
139
|
"read_file": "Read",
|
|
139
140
|
"read_many_files": "Read",
|
|
140
141
|
"grep_search": "Grep",
|
|
141
142
|
"search_file_content": "Grep",
|
|
142
143
|
"web_fetch": "WebFetch",
|
|
144
|
+
// Qwen Code additional tool names (no routing branch yet but normalized
|
|
145
|
+
// so future routing logic works without per-platform fallback):
|
|
146
|
+
"write_file": "Write",
|
|
147
|
+
"edit": "Edit",
|
|
148
|
+
"glob": "Glob",
|
|
149
|
+
"todo_write": "TodoWrite",
|
|
150
|
+
"ask_user_question": "AskUserQuestion",
|
|
151
|
+
"list_directory": "LS",
|
|
152
|
+
"save_memory": "Memory",
|
|
153
|
+
"skill": "Skill",
|
|
154
|
+
"exit_plan_mode": "ExitPlanMode",
|
|
143
155
|
// OpenCode
|
|
144
156
|
"bash": "Bash",
|
|
145
157
|
"view": "Read",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../suppress-stderr.mjs";
|
|
3
|
+
import "../ensure-deps.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* Cursor afterAgentResponse hook — fires once Cursor finishes streaming
|
|
6
|
+
* the assistant turn back to the user (per Cursor v1 hook docs:
|
|
7
|
+
* https://cursor.com/docs/agent/hooks#afteragentresponse).
|
|
8
|
+
*
|
|
9
|
+
* Input: { "conversation_id": "...", "generation_id": "...", "text": "<final assistant message>" }
|
|
10
|
+
* Output: { "additional_context": "" } (Cursor rejects empty stdout, so emit a no-op payload)
|
|
11
|
+
*
|
|
12
|
+
* The hook records the agent_response event so session analytics + the
|
|
13
|
+
* resume directive can reconstruct the last assistant turn. Mirrors
|
|
14
|
+
* stop.mjs (turn lifecycle telemetry) — afterAgentResponse fires
|
|
15
|
+
* BEFORE stop, so we capture the produced text here while stop captures
|
|
16
|
+
* loop_count + final status.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
readStdin,
|
|
21
|
+
parseStdin,
|
|
22
|
+
getSessionId,
|
|
23
|
+
getSessionDBPath,
|
|
24
|
+
getInputProjectDir,
|
|
25
|
+
CURSOR_OPTS,
|
|
26
|
+
} from "../session-helpers.mjs";
|
|
27
|
+
import { dirname } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { createSessionLoaders } from "../session-loaders.mjs";
|
|
30
|
+
|
|
31
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
|
|
33
|
+
const OPTS = CURSOR_OPTS;
|
|
34
|
+
|
|
35
|
+
// Truncate large assistant responses before persisting — keeps the
|
|
36
|
+
// session_events table from ballooning when an agent emits long output.
|
|
37
|
+
const MAX_TEXT_BYTES = 8 * 1024;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const raw = await readStdin();
|
|
41
|
+
const input = parseStdin(raw);
|
|
42
|
+
const projectDir = getInputProjectDir(input, CURSOR_OPTS);
|
|
43
|
+
|
|
44
|
+
if (projectDir && !process.env.CURSOR_CWD) {
|
|
45
|
+
process.env.CURSOR_CWD = projectDir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const text = typeof input.text === "string" ? input.text : "";
|
|
49
|
+
const truncated = text.length > MAX_TEXT_BYTES
|
|
50
|
+
? text.slice(0, MAX_TEXT_BYTES)
|
|
51
|
+
: text;
|
|
52
|
+
|
|
53
|
+
const { SessionDB } = await loadSessionDB();
|
|
54
|
+
const dbPath = getSessionDBPath(OPTS);
|
|
55
|
+
const db = new SessionDB({ dbPath });
|
|
56
|
+
const sessionId = getSessionId(input, OPTS);
|
|
57
|
+
|
|
58
|
+
db.ensureSession(sessionId, projectDir);
|
|
59
|
+
db.insertEvent(sessionId, {
|
|
60
|
+
type: "agent_response",
|
|
61
|
+
generation_id: input.generation_id ?? null,
|
|
62
|
+
text_bytes: text.length,
|
|
63
|
+
truncated: text.length > MAX_TEXT_BYTES,
|
|
64
|
+
text: truncated,
|
|
65
|
+
}, "AfterAgentResponse");
|
|
66
|
+
|
|
67
|
+
db.close();
|
|
68
|
+
} catch {
|
|
69
|
+
// Cursor treats stderr as hook failure; swallow and continue.
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Cursor rejects empty stdout as "no valid response", so emit a no-op
|
|
73
|
+
// additional_context payload (same convention as sessionstart.mjs).
|
|
74
|
+
process.stdout.write(JSON.stringify({ additional_context: "" }) + "\n");
|
package/hooks/ensure-deps.mjs
CHANGED
|
@@ -22,11 +22,28 @@
|
|
|
22
22
|
import { existsSync, copyFileSync } from "node:fs";
|
|
23
23
|
import { execSync } from "node:child_process";
|
|
24
24
|
import { resolve, dirname } from "node:path";
|
|
25
|
-
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26
26
|
|
|
27
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
28
|
const root = resolve(__dirname, "..");
|
|
29
29
|
|
|
30
|
+
// Shared 3-layer heal helper (also used by scripts/postinstall.mjs).
|
|
31
|
+
// Lazy-loaded via dynamic import so older installs and synthetic test
|
|
32
|
+
// harnesses (e.g. tests/session-hooks-smoke) — which don't ship
|
|
33
|
+
// `scripts/heal-better-sqlite3.mjs` — degrade to a no-op instead of
|
|
34
|
+
// crashing the hook with ERR_MODULE_NOT_FOUND. Best-effort posture
|
|
35
|
+
// matches the rest of this module.
|
|
36
|
+
async function healBetterSqlite3Binding(pkgRoot) {
|
|
37
|
+
try {
|
|
38
|
+
const helperPath = resolve(__dirname, "..", "scripts", "heal-better-sqlite3.mjs");
|
|
39
|
+
if (!existsSync(helperPath)) return { healed: false, reason: "helper-missing" };
|
|
40
|
+
const mod = await import(pathToFileURL(helperPath).href);
|
|
41
|
+
return mod.healBetterSqlite3Binding(pkgRoot);
|
|
42
|
+
} catch {
|
|
43
|
+
return { healed: false, reason: "helper-error" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
const NATIVE_DEPS = ["better-sqlite3"];
|
|
31
48
|
const NATIVE_BINARIES = {
|
|
32
49
|
"better-sqlite3": ["build", "Release", "better_sqlite3.node"],
|
|
@@ -46,7 +63,7 @@ function hasModernSqlite() {
|
|
|
46
63
|
return major > 22 || (major === 22 && minor >= 5);
|
|
47
64
|
}
|
|
48
65
|
|
|
49
|
-
export function ensureDeps() {
|
|
66
|
+
export async function ensureDeps() {
|
|
50
67
|
// Bun ships bun:sqlite and never needs better-sqlite3
|
|
51
68
|
if (typeof globalThis.Bun !== "undefined") return;
|
|
52
69
|
for (const pkg of NATIVE_DEPS) {
|
|
@@ -62,14 +79,11 @@ export function ensureDeps() {
|
|
|
62
79
|
});
|
|
63
80
|
} catch { /* best effort — hook degrades gracefully without DB */ }
|
|
64
81
|
} else if (!existsSync(resolve(pkgDir, ...NATIVE_BINARIES[pkg]))) {
|
|
65
|
-
// Package installed but native binary missing (e.g., npm ignore-scripts=true
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
timeout: 120000,
|
|
71
|
-
});
|
|
72
|
-
} catch { /* best effort — hook degrades gracefully without DB */ }
|
|
82
|
+
// Package installed but native binary missing (e.g., npm ignore-scripts=true,
|
|
83
|
+
// or Windows where `npm rebuild` falls through to node-gyp without MSVC — #408).
|
|
84
|
+
// Delegate to the shared 3-layer heal (single source of truth, also used by
|
|
85
|
+
// scripts/postinstall.mjs).
|
|
86
|
+
try { await healBetterSqlite3Binding(root); } catch { /* helper already best-effort */ }
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
}
|
|
@@ -188,6 +202,8 @@ export function codesignBinary(binaryPath) {
|
|
|
188
202
|
}
|
|
189
203
|
}
|
|
190
204
|
|
|
191
|
-
// Auto-run on import (like suppress-stderr.mjs)
|
|
192
|
-
|
|
205
|
+
// Auto-run on import (like suppress-stderr.mjs).
|
|
206
|
+
// Top-level await ensures the heal completes before the importer's next
|
|
207
|
+
// statement runs (which is typically `new Database(...)`).
|
|
208
|
+
await ensureDeps();
|
|
193
209
|
ensureNativeCompat(root);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../suppress-stderr.mjs";
|
|
3
|
+
import "../ensure-deps.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* Gemini CLI BeforeAgent hook — UserPromptSubmit equivalent.
|
|
6
|
+
*
|
|
7
|
+
* Captures every user prompt so the LLM can resume from the exact point
|
|
8
|
+
* the user left off after compact / resume. Mirrors hooks/userpromptsubmit.mjs
|
|
9
|
+
* but reads `input.prompt` (Gemini wire shape, types.ts:547-549) and emits
|
|
10
|
+
* `hookSpecificOutput.hookEventName: "BeforeAgent"` (types.ts:554-559) so
|
|
11
|
+
* Gemini's hookRunner appends additionalContext to the prompt
|
|
12
|
+
* (hookRunner.ts:183-197).
|
|
13
|
+
*
|
|
14
|
+
* Must be fast (<10ms). Single SQLite write.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
readStdin,
|
|
19
|
+
parseStdin,
|
|
20
|
+
getSessionId,
|
|
21
|
+
getSessionDBPath,
|
|
22
|
+
getInputProjectDir,
|
|
23
|
+
GEMINI_OPTS,
|
|
24
|
+
} from "../session-helpers.mjs";
|
|
25
|
+
import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
|
|
26
|
+
import { dirname } from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
|
|
31
|
+
const OPTS = GEMINI_OPTS;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const raw = await readStdin();
|
|
35
|
+
const input = parseStdin(raw);
|
|
36
|
+
const projectDir = getInputProjectDir(input, OPTS);
|
|
37
|
+
|
|
38
|
+
const prompt = input.prompt ?? input.message ?? "";
|
|
39
|
+
const trimmed = (prompt || "").trim();
|
|
40
|
+
|
|
41
|
+
// Skip system-generated messages — only capture genuine user prompts.
|
|
42
|
+
const isSystemMessage = trimmed.startsWith("<task-notification>")
|
|
43
|
+
|| trimmed.startsWith("<system-reminder>")
|
|
44
|
+
|| trimmed.startsWith("<context_guidance>")
|
|
45
|
+
|| trimmed.startsWith("<tool-result>");
|
|
46
|
+
|
|
47
|
+
if (trimmed.length > 0 && !isSystemMessage) {
|
|
48
|
+
const { SessionDB } = await loadSessionDB();
|
|
49
|
+
const { extractUserEvents } = await loadExtract();
|
|
50
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
51
|
+
const dbPath = getSessionDBPath(OPTS);
|
|
52
|
+
const db = new SessionDB({ dbPath });
|
|
53
|
+
const sessionId = getSessionId(input, OPTS);
|
|
54
|
+
|
|
55
|
+
db.ensureSession(sessionId, projectDir);
|
|
56
|
+
|
|
57
|
+
// 1. Always save the raw prompt.
|
|
58
|
+
const promptEvent = {
|
|
59
|
+
type: "user_prompt",
|
|
60
|
+
category: "user-prompt",
|
|
61
|
+
data: prompt,
|
|
62
|
+
priority: 1,
|
|
63
|
+
};
|
|
64
|
+
const promptAttributions = attributeAndInsertEvents(
|
|
65
|
+
db, sessionId, [promptEvent], input, projectDir, "BeforeAgent", resolveProjectAttributions,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// 2. Extract decision/role/intent/data from user message.
|
|
69
|
+
const userEvents = extractUserEvents(trimmed);
|
|
70
|
+
const savedLastKnown = promptAttributions[0]?.projectDir || null;
|
|
71
|
+
const sessionStats = db.getSessionStats(sessionId);
|
|
72
|
+
const lastKnownProjectDir = typeof db.getLatestAttributedProjectDir === "function"
|
|
73
|
+
? db.getLatestAttributedProjectDir(sessionId)
|
|
74
|
+
: null;
|
|
75
|
+
const userAttributions = resolveProjectAttributions(userEvents, {
|
|
76
|
+
sessionOriginDir: sessionStats?.project_dir || projectDir,
|
|
77
|
+
inputProjectDir: projectDir,
|
|
78
|
+
workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
|
|
79
|
+
lastKnownProjectDir: savedLastKnown || lastKnownProjectDir,
|
|
80
|
+
});
|
|
81
|
+
for (let i = 0; i < userEvents.length; i++) {
|
|
82
|
+
db.insertEvent(sessionId, userEvents[i], "BeforeAgent", userAttributions[i]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
db.close();
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// BeforeAgent must never block the session — silent fallback.
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Emit empty additionalContext so Gemini's hookRunner treats the response as
|
|
92
|
+
// metadata (no prompt mutation). Matches the SessionStart hook's structured-
|
|
93
|
+
// output convention (sessionstart.mjs:130-135).
|
|
94
|
+
console.log(JSON.stringify({
|
|
95
|
+
hookSpecificOutput: {
|
|
96
|
+
hookEventName: "BeforeAgent",
|
|
97
|
+
additionalContext: "",
|
|
98
|
+
},
|
|
99
|
+
}));
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../suppress-stderr.mjs";
|
|
3
|
+
import "../ensure-deps.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* Kiro CLI agentSpawn hook for context-mode.
|
|
6
|
+
*
|
|
7
|
+
* agentSpawn is the Kiro semantic equivalent of Claude Code's SessionStart:
|
|
8
|
+
* fires once when an agent loads. We use it to inject the routing block and
|
|
9
|
+
* (on resume/compact) the session-resume directive.
|
|
10
|
+
*
|
|
11
|
+
* Source: https://kiro.dev/docs/cli/hooks
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createRoutingBlock } from "../routing-block.mjs";
|
|
15
|
+
import { createToolNamer } from "../core/tool-naming.mjs";
|
|
16
|
+
|
|
17
|
+
const toolNamer = createToolNamer("kiro");
|
|
18
|
+
const ROUTING_BLOCK = createRoutingBlock(toolNamer);
|
|
19
|
+
import {
|
|
20
|
+
writeSessionEventsFile,
|
|
21
|
+
buildSessionDirective,
|
|
22
|
+
getSessionEvents,
|
|
23
|
+
} from "../session-directive.mjs";
|
|
24
|
+
import {
|
|
25
|
+
readStdin,
|
|
26
|
+
parseStdin,
|
|
27
|
+
getSessionId,
|
|
28
|
+
getSessionDBPath,
|
|
29
|
+
getSessionEventsPath,
|
|
30
|
+
getCleanupFlagPath,
|
|
31
|
+
getInputProjectDir,
|
|
32
|
+
KIRO_OPTS,
|
|
33
|
+
} from "../session-helpers.mjs";
|
|
34
|
+
import { createSessionLoaders } from "../session-loaders.mjs";
|
|
35
|
+
import { unlinkSync } from "node:fs";
|
|
36
|
+
import { fileURLToPath } from "node:url";
|
|
37
|
+
|
|
38
|
+
const HOOK_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
39
|
+
const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
|
|
40
|
+
const OPTS = KIRO_OPTS;
|
|
41
|
+
|
|
42
|
+
let additionalContext = ROUTING_BLOCK;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readStdin();
|
|
46
|
+
const input = parseStdin(raw);
|
|
47
|
+
// Kiro stdin shape mirrors codex/CC: { source, cwd, ... }. Default startup
|
|
48
|
+
// when no source provided (single-shot agent spawn).
|
|
49
|
+
const source = input.source ?? "startup";
|
|
50
|
+
const projectDir = getInputProjectDir(input, OPTS);
|
|
51
|
+
|
|
52
|
+
if (source === "compact" || source === "resume") {
|
|
53
|
+
const { SessionDB } = await loadSessionDB();
|
|
54
|
+
const dbPath = getSessionDBPath(OPTS);
|
|
55
|
+
const db = new SessionDB({ dbPath });
|
|
56
|
+
|
|
57
|
+
if (source === "compact") {
|
|
58
|
+
const sessionId = getSessionId(input, OPTS);
|
|
59
|
+
const resume = db.getResume(sessionId);
|
|
60
|
+
if (resume && !resume.consumed) {
|
|
61
|
+
db.markResumeConsumed(sessionId);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
try { unlinkSync(getCleanupFlagPath(OPTS)); } catch { /* no flag */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sessionId = getSessionId(input, OPTS);
|
|
68
|
+
const events = sessionId ? getSessionEvents(db, sessionId) : [];
|
|
69
|
+
if (events.length > 0) {
|
|
70
|
+
const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS));
|
|
71
|
+
additionalContext += buildSessionDirective(source, eventMeta, toolNamer);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
db.close();
|
|
75
|
+
} else if (source === "startup") {
|
|
76
|
+
const { SessionDB } = await loadSessionDB();
|
|
77
|
+
const dbPath = getSessionDBPath(OPTS);
|
|
78
|
+
const db = new SessionDB({ dbPath });
|
|
79
|
+
try { unlinkSync(getSessionEventsPath(OPTS)); } catch { /* no stale file */ }
|
|
80
|
+
|
|
81
|
+
db.cleanupOldSessions(7);
|
|
82
|
+
db.db.exec(`DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)`);
|
|
83
|
+
|
|
84
|
+
const sessionId = getSessionId(input, OPTS);
|
|
85
|
+
db.ensureSession(sessionId, projectDir);
|
|
86
|
+
|
|
87
|
+
db.close();
|
|
88
|
+
}
|
|
89
|
+
// clear => routing block only
|
|
90
|
+
} catch {
|
|
91
|
+
// Swallow errors — hook must not fail
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Kiro CLI agentSpawn JSON output shape mirrors CC SessionStart.
|
|
95
|
+
process.stdout.write(JSON.stringify({
|
|
96
|
+
hookSpecificOutput: { hookEventName: "agentSpawn", additionalContext },
|
|
97
|
+
}) + "\n");
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../suppress-stderr.mjs";
|
|
3
|
+
import "../ensure-deps.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* Kiro CLI UserPromptSubmit hook — capture user prompts for continuity.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors hooks/codex/userpromptsubmit.mjs (same json-stdio paradigm).
|
|
8
|
+
* Kiro stdin: { hook_event_name: "userPromptSubmit", cwd, prompt }
|
|
9
|
+
*
|
|
10
|
+
* Source: https://kiro.dev/docs/cli/hooks
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
readStdin,
|
|
15
|
+
parseStdin,
|
|
16
|
+
getSessionId,
|
|
17
|
+
getSessionDBPath,
|
|
18
|
+
getInputProjectDir,
|
|
19
|
+
KIRO_OPTS,
|
|
20
|
+
} from "../session-helpers.mjs";
|
|
21
|
+
import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
|
|
22
|
+
import { dirname } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
|
|
27
|
+
const OPTS = KIRO_OPTS;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const raw = await readStdin();
|
|
31
|
+
const input = parseStdin(raw);
|
|
32
|
+
const projectDir = getInputProjectDir(input, OPTS);
|
|
33
|
+
|
|
34
|
+
const prompt = input.prompt ?? input.message ?? "";
|
|
35
|
+
const trimmed = (prompt || "").trim();
|
|
36
|
+
|
|
37
|
+
const isSystemMessage = trimmed.startsWith("<task-notification>")
|
|
38
|
+
|| trimmed.startsWith("<system-reminder>")
|
|
39
|
+
|| trimmed.startsWith("<context_guidance>")
|
|
40
|
+
|| trimmed.startsWith("<tool-result>");
|
|
41
|
+
|
|
42
|
+
if (trimmed.length > 0 && !isSystemMessage) {
|
|
43
|
+
const { SessionDB } = await loadSessionDB();
|
|
44
|
+
const { extractUserEvents } = await loadExtract();
|
|
45
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
46
|
+
const dbPath = getSessionDBPath(OPTS);
|
|
47
|
+
const db = new SessionDB({ dbPath });
|
|
48
|
+
const sessionId = getSessionId(input, OPTS);
|
|
49
|
+
|
|
50
|
+
db.ensureSession(sessionId, projectDir);
|
|
51
|
+
|
|
52
|
+
const promptEvent = {
|
|
53
|
+
type: "user_prompt",
|
|
54
|
+
category: "user-prompt",
|
|
55
|
+
data: prompt,
|
|
56
|
+
priority: 1,
|
|
57
|
+
};
|
|
58
|
+
const promptAttributions = attributeAndInsertEvents(
|
|
59
|
+
db, sessionId, [promptEvent], input, projectDir, "UserPromptSubmit", resolveProjectAttributions,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const userEvents = extractUserEvents(trimmed);
|
|
63
|
+
const savedLastKnown = promptAttributions[0]?.projectDir || null;
|
|
64
|
+
const sessionStats = db.getSessionStats(sessionId);
|
|
65
|
+
const lastKnownProjectDir = typeof db.getLatestAttributedProjectDir === "function"
|
|
66
|
+
? db.getLatestAttributedProjectDir(sessionId)
|
|
67
|
+
: null;
|
|
68
|
+
const userAttributions = resolveProjectAttributions(userEvents, {
|
|
69
|
+
sessionOriginDir: sessionStats?.project_dir || projectDir,
|
|
70
|
+
inputProjectDir: projectDir,
|
|
71
|
+
workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
|
|
72
|
+
lastKnownProjectDir: savedLastKnown || lastKnownProjectDir,
|
|
73
|
+
});
|
|
74
|
+
for (let i = 0; i < userEvents.length; i++) {
|
|
75
|
+
db.insertEvent(sessionId, userEvents[i], "UserPromptSubmit", userAttributions[i]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
db.close();
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Kiro hooks must not block the session.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Kiro CLI userPromptSubmit accepts JSON output for additionalContext (see docs).
|
|
85
|
+
// We don't inject context here — that's the agentSpawn hook's job.
|
|
86
|
+
process.stdout.write(JSON.stringify({
|
|
87
|
+
hookSpecificOutput: { hookEventName: "userPromptSubmit", additionalContext: "" },
|
|
88
|
+
}) + "\n");
|