aiwcli 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run.js +1 -1
- package/dist/commands/clear.js +28 -131
- package/dist/commands/init/index.js +3 -3
- package/dist/lib/gitignore-manager.d.ts +32 -0
- package/dist/lib/gitignore-manager.js +141 -2
- package/dist/templates/CLAUDE.md +8 -8
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
- package/dist/templates/_shared/.claude/settings.json +7 -7
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
- package/dist/templates/_shared/hooks-ts/session_end.ts +104 -0
- package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
- package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
- package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
- package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
- package/dist/templates/_shared/lib-ts/base/logger.ts +31 -15
- package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +139 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
- package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +61 -37
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
- package/dist/templates/_shared/lib-ts/types.ts +68 -55
- package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
- package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +21 -21
- package/dist/templates/_shared/scripts/status_line.ts +733 -0
- package/dist/templates/cc-native/.claude/settings.json +175 -185
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -9
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/__init__.py +0 -16
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +0 -177
- package/dist/templates/_shared/hooks/context_monitor.py +0 -270
- package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
- package/dist/templates/_shared/hooks/pre_compact.py +0 -104
- package/dist/templates/_shared/hooks/session_end.py +0 -173
- package/dist/templates/_shared/hooks/session_start.py +0 -206
- package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
- package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
- package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
- package/dist/templates/_shared/lib/__init__.py +0 -1
- package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__init__.py +0 -65
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
- package/dist/templates/_shared/lib/base/constants.py +0 -358
- package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
- package/dist/templates/_shared/lib/base/inference.py +0 -307
- package/dist/templates/_shared/lib/base/logger.py +0 -305
- package/dist/templates/_shared/lib/base/stop_words.py +0 -221
- package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
- package/dist/templates/_shared/lib/base/utils.py +0 -263
- package/dist/templates/_shared/lib/context/__init__.py +0 -102
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
- package/dist/templates/_shared/lib/context/context_selector.py +0 -508
- package/dist/templates/_shared/lib/context/context_store.py +0 -653
- package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
- package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
- package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
- package/dist/templates/_shared/lib/templates/README.md +0 -206
- package/dist/templates/_shared/lib/templates/__init__.py +0 -36
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/formatters.py +0 -146
- package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +0 -357
- package/dist/templates/_shared/scripts/status_line.py +0 -716
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/MIGRATION.md +0 -86
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
- package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
- package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
- package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* See SPEC.md §4
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import * as crypto from "node:crypto";
|
|
8
9
|
import * as fs from "node:fs";
|
|
10
|
+
import * as _os from "node:os";
|
|
9
11
|
import * as path from "node:path";
|
|
10
|
-
import * as os from "node:os";
|
|
11
|
-
import * as crypto from "node:crypto";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Write file atomically with retry logic.
|
|
@@ -21,7 +21,7 @@ export function atomicWrite(
|
|
|
21
21
|
content: string,
|
|
22
22
|
maxAttempts = 2,
|
|
23
23
|
backoffMs: number[] = [500, 1000],
|
|
24
|
-
): [boolean,
|
|
24
|
+
): [boolean, null | string] {
|
|
25
25
|
// Ensure parent directory exists
|
|
26
26
|
const dir = path.dirname(filePath);
|
|
27
27
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -53,7 +53,7 @@ export function atomicWrite(
|
|
|
53
53
|
fs.renameSync(tmpPath, filePath);
|
|
54
54
|
|
|
55
55
|
return [true, null];
|
|
56
|
-
} catch (
|
|
56
|
+
} catch (error: any) {
|
|
57
57
|
// Clean up temp file
|
|
58
58
|
try {
|
|
59
59
|
fs.unlinkSync(tmpPath);
|
|
@@ -62,11 +62,11 @@ export function atomicWrite(
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
if (attempt < maxAttempts - 1) {
|
|
65
|
-
const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs
|
|
65
|
+
const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs.at(-1) ?? 500;
|
|
66
66
|
sleepSync(waitMs);
|
|
67
67
|
} else {
|
|
68
|
-
const errType =
|
|
69
|
-
const errMsg = String(
|
|
68
|
+
const errType = error?.constructor?.name ?? "Error";
|
|
69
|
+
const errMsg = String(error).split("\n")[0]?.slice(0, 200) ?? "";
|
|
70
70
|
return [false, `${errType}: ${errMsg}`];
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -85,7 +85,7 @@ export function atomicAppend(
|
|
|
85
85
|
content: string,
|
|
86
86
|
maxAttempts = 2,
|
|
87
87
|
backoffMs: number[] = [500, 1000],
|
|
88
|
-
): [boolean,
|
|
88
|
+
): [boolean, null | string] {
|
|
89
89
|
// Ensure parent directory exists
|
|
90
90
|
const dir = path.dirname(filePath);
|
|
91
91
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -112,13 +112,13 @@ export function atomicAppend(
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
return [true, null];
|
|
115
|
-
} catch (
|
|
115
|
+
} catch (error: any) {
|
|
116
116
|
if (attempt < maxAttempts - 1) {
|
|
117
|
-
const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs
|
|
117
|
+
const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs.at(-1) ?? 500;
|
|
118
118
|
sleepSync(waitMs);
|
|
119
119
|
} else {
|
|
120
|
-
const errType =
|
|
121
|
-
const errMsg = String(
|
|
120
|
+
const errType = error?.constructor?.name ?? "Error";
|
|
121
|
+
const errMsg = String(error).split("\n")[0]?.slice(0, 200) ?? "";
|
|
122
122
|
return [false, `${errType}: ${errMsg}`];
|
|
123
123
|
}
|
|
124
124
|
}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* See SPEC.md §2
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as path from "node:path";
|
|
7
6
|
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
|
|
8
9
|
import { logWarn } from "./logger.js";
|
|
9
10
|
|
|
10
11
|
// Directory names (relative to project root)
|
|
@@ -28,9 +29,9 @@ export const RETRY_BACKOFF_MS = [500, 1000];
|
|
|
28
29
|
|
|
29
30
|
// Windows reserved filenames
|
|
30
31
|
const WINDOWS_RESERVED = new Set([
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
32
|
+
"AUX", "COM1", "COM2", "COM3",
|
|
33
|
+
"COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "LPT1", "LPT2",
|
|
34
|
+
"LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", "PRN",
|
|
34
35
|
]);
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -41,9 +42,9 @@ export function sanitizeContextId(contextId: string): string {
|
|
|
41
42
|
if (!contextId) return "context";
|
|
42
43
|
|
|
43
44
|
let result = contextId.toLowerCase();
|
|
44
|
-
result = result.
|
|
45
|
-
result = result.
|
|
46
|
-
result = result.
|
|
45
|
+
result = result.replaceAll(/[^a-z0-9_-]/g, "-");
|
|
46
|
+
result = result.replaceAll(/[-_]+/g, "-");
|
|
47
|
+
result = result.replaceAll(/^[-_]+|[-_]+$/g, "");
|
|
47
48
|
|
|
48
49
|
if (result.length > MAX_CONTEXT_ID_LENGTH) {
|
|
49
50
|
result = result.slice(0, MAX_CONTEXT_ID_LENGTH).replace(/[-_]+$/, "");
|
|
@@ -101,13 +102,18 @@ export function getProjectRoot(payloadCwd?: string): string {
|
|
|
101
102
|
} else {
|
|
102
103
|
return envDir;
|
|
103
104
|
}
|
|
104
|
-
}
|
|
105
|
+
}
|
|
106
|
+
|
|
105
107
|
if (payloadCwd) return payloadCwd;
|
|
106
108
|
return process.cwd();
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
// §2.4 — Path functions
|
|
110
112
|
|
|
113
|
+
export function getAiwcliDir(projectRoot?: string): string {
|
|
114
|
+
return path.join(projectRoot ?? getProjectRoot(), ".aiwcli");
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
export function getOutputDir(projectRoot?: string): string {
|
|
112
118
|
return path.join(projectRoot ?? getProjectRoot(), OUTPUT_DIR);
|
|
113
119
|
}
|
|
@@ -230,7 +236,8 @@ export function getHandoffFolderPath(
|
|
|
230
236
|
while (fs.existsSync(folder)) {
|
|
231
237
|
folder = path.join(handoffsDir, `${ts}-${counter}`);
|
|
232
238
|
counter++;
|
|
233
|
-
}
|
|
239
|
+
}
|
|
240
|
+
|
|
234
241
|
return folder;
|
|
235
242
|
}
|
|
236
243
|
|
|
@@ -268,8 +275,8 @@ export function sanitizeFilename(
|
|
|
268
275
|
maxLen = 32,
|
|
269
276
|
allowLeadingDot = false,
|
|
270
277
|
): string {
|
|
271
|
-
let result = s.
|
|
272
|
-
result = result.
|
|
278
|
+
let result = s.replaceAll(/[^A-Za-z0-9._-]+/g, "_");
|
|
279
|
+
result = result.replaceAll(/^[._-]+|[._-]+$/g, "").slice(0, maxLen) || "unknown";
|
|
273
280
|
|
|
274
281
|
if (!allowLeadingDot) {
|
|
275
282
|
result = result.replace(/^\.+/, "");
|
|
@@ -285,10 +292,10 @@ export function sanitizeFilename(
|
|
|
285
292
|
|
|
286
293
|
export function sanitizeTitle(s: string, maxLen = 50): string {
|
|
287
294
|
let result = s.toLowerCase().trim();
|
|
288
|
-
result = result.
|
|
289
|
-
result = result.
|
|
290
|
-
result = result.
|
|
291
|
-
result = result.
|
|
295
|
+
result = result.replaceAll(' ', "-");
|
|
296
|
+
result = result.replaceAll(/[^a-z0-9._-]+/g, "_");
|
|
297
|
+
result = result.replaceAll(/[-_]+/g, "-");
|
|
298
|
+
result = result.replaceAll(/^[._-]+|[._-]+$/g, "").slice(0, maxLen) || "unknown";
|
|
292
299
|
|
|
293
300
|
const baseName = (result.split(".")[0] ?? result).toUpperCase();
|
|
294
301
|
if (WINDOWS_RESERVED.has(baseName)) {
|
|
@@ -5,24 +5,26 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
import { getProjectRoot } from "./constants.js";
|
|
10
|
+
import { getContextPath as _getContextPath, hookLog, logDebug, setSessionId } from "./logger.js";
|
|
10
11
|
import { getContextBySessionId } from "../context/context-store.js";
|
|
11
12
|
import type { HookInput, HookOutput } from "../types.js";
|
|
12
13
|
|
|
13
14
|
// Re-export logger functions for convenience (matches Python hook_utils re-exports)
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
|
|
16
17
|
// Context window baseline: tokens not visible in hook data §5.9
|
|
17
18
|
export const CONTEXT_BASELINE_TOKENS = 22_600;
|
|
18
19
|
export const DEFAULT_CONTEXT_WINDOW_SIZE = 200_000;
|
|
19
20
|
|
|
20
21
|
// Event metadata stash — populated by loadHookInput(), read by runHook()
|
|
21
|
-
let _lastHookEvent:
|
|
22
|
-
let _lastToolName:
|
|
22
|
+
let _lastHookEvent: null | string = null;
|
|
23
|
+
let _lastToolName: null | string = null;
|
|
24
|
+
let _cachedHookName: null | string = null;
|
|
23
25
|
|
|
24
26
|
// Pre-fetched input stash
|
|
25
|
-
let _prefetchedInput: Record<string, any>
|
|
27
|
+
let _prefetchedInput: null | Record<string, any> = null;
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Load and parse JSON from stdin (or return prefetched input if set).
|
|
@@ -37,12 +39,13 @@ export function loadHookInput(): HookInput | null {
|
|
|
37
39
|
_lastHookEvent = result.hook_event_name ?? null;
|
|
38
40
|
_lastToolName = result.tool_name ?? null;
|
|
39
41
|
}
|
|
42
|
+
|
|
40
43
|
return result as HookInput;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
try {
|
|
44
47
|
// Read entire stdin using fd 0 (cross-platform, works on Windows)
|
|
45
|
-
const inputData = fs.readFileSync(0, "
|
|
48
|
+
const inputData = fs.readFileSync(0, "utf8").trim();
|
|
46
49
|
if (!inputData) return null;
|
|
47
50
|
|
|
48
51
|
const result = JSON.parse(inputData);
|
|
@@ -50,6 +53,7 @@ export function loadHookInput(): HookInput | null {
|
|
|
50
53
|
_lastHookEvent = result.hook_event_name ?? null;
|
|
51
54
|
_lastToolName = result.tool_name ?? null;
|
|
52
55
|
}
|
|
56
|
+
|
|
53
57
|
return result as HookInput;
|
|
54
58
|
} catch {
|
|
55
59
|
return null;
|
|
@@ -76,7 +80,7 @@ export function validateHookEvent(
|
|
|
76
80
|
*/
|
|
77
81
|
export function getToolInput(
|
|
78
82
|
payload: HookInput,
|
|
79
|
-
): Record<string, any>
|
|
83
|
+
): null | Record<string, any> {
|
|
80
84
|
const toolInput = payload.tool_input;
|
|
81
85
|
return toolInput && typeof toolInput === "object" ? toolInput : null;
|
|
82
86
|
}
|
|
@@ -92,53 +96,76 @@ export function checkSkipPersistence(
|
|
|
92
96
|
const toolInput = getToolInput(payload);
|
|
93
97
|
if (!toolInput) return false;
|
|
94
98
|
|
|
95
|
-
const metadata = toolInput
|
|
99
|
+
const {metadata} = toolInput;
|
|
96
100
|
if (metadata && typeof metadata === "object" && metadata.skip_persistence) {
|
|
97
101
|
logDebug(hookName, "Skipping persistence (skip_persistence flag set)");
|
|
98
102
|
return true;
|
|
99
103
|
}
|
|
104
|
+
|
|
100
105
|
return false;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
/**
|
|
104
109
|
* Emit hookSpecificOutput with additionalContext to stdout.
|
|
110
|
+
* hookEventName is required by Claude Code's Zod validator (discriminated union).
|
|
111
|
+
* Auto-detected from stdin payload (set by loadHookInput/runHook).
|
|
105
112
|
* See SPEC.md §5.5
|
|
106
113
|
*/
|
|
107
114
|
export function emitContext(additionalContext: string): void {
|
|
115
|
+
const eventName = _lastHookEvent ?? undefined;
|
|
108
116
|
const out: HookOutput = {
|
|
109
117
|
hookSpecificOutput: {
|
|
118
|
+
...(eventName ? { hookEventName: eventName } : {}),
|
|
110
119
|
additionalContext,
|
|
111
120
|
},
|
|
112
121
|
};
|
|
113
|
-
|
|
122
|
+
const json = JSON.stringify(out);
|
|
123
|
+
_logEmit("context", additionalContext.length, { additionalContext });
|
|
124
|
+
process.stdout.write(json + "\n");
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
/**
|
|
117
128
|
* Emit hookSpecificOutput that denies the tool call with context and reason.
|
|
129
|
+
* hookEventName is required by Claude Code's Zod validator (discriminated union).
|
|
130
|
+
* Auto-detected from stdin payload (set by loadHookInput/runHook).
|
|
118
131
|
* See SPEC.md §5.6
|
|
119
132
|
*/
|
|
120
133
|
export function emitContextAndBlock(
|
|
121
134
|
additionalContext: string,
|
|
122
135
|
reason: string,
|
|
123
136
|
): void {
|
|
137
|
+
const eventName = _lastHookEvent ?? undefined;
|
|
124
138
|
const out: HookOutput = {
|
|
125
139
|
hookSpecificOutput: {
|
|
140
|
+
...(eventName ? { hookEventName: eventName } : {}),
|
|
126
141
|
additionalContext,
|
|
127
142
|
permissionDecision: "deny",
|
|
128
143
|
permissionDecisionReason: reason,
|
|
129
144
|
},
|
|
130
145
|
};
|
|
131
|
-
|
|
146
|
+
const json = JSON.stringify(out);
|
|
147
|
+
_logEmit("block", additionalContext.length, { additionalContext, blockReason: reason });
|
|
148
|
+
process.stdout.write(json + "\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Log hook output (context or block) to hook-log.jsonl for visibility. */
|
|
152
|
+
function _logEmit(type: "block" | "context", chars: number, payload: Record<string, any>): void {
|
|
153
|
+
const hook = _cachedHookName ?? "unknown";
|
|
154
|
+
const msg = type === "block"
|
|
155
|
+
? `HOOK_OUTPUT [${type}] ${chars} chars, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
|
|
156
|
+
: `HOOK_OUTPUT [${type}] ${chars} chars`;
|
|
157
|
+
hookLog("info", hook, msg, { data: payload });
|
|
132
158
|
}
|
|
133
159
|
|
|
134
160
|
/**
|
|
135
161
|
* Auto-detect template origin from the hook script path.
|
|
136
162
|
*/
|
|
137
163
|
function detectTemplate(scriptPath = ""): string {
|
|
138
|
-
const p = (scriptPath || (process.argv[1] ?? "")).
|
|
164
|
+
const p = (scriptPath || (process.argv[1] ?? "")).replaceAll('\\', "/");
|
|
139
165
|
if (p.includes("/_shared/hooks/") || p.startsWith("_shared/hooks/")) {
|
|
140
166
|
return "shared";
|
|
141
167
|
}
|
|
168
|
+
|
|
142
169
|
const match = p.match(/_([a-z][a-z0-9-]*)\/hooks\//);
|
|
143
170
|
if (match?.[1]) return match[1]; // e.g., "cc-native"
|
|
144
171
|
return "unknown";
|
|
@@ -151,7 +178,7 @@ function detectTemplate(scriptPath = ""): string {
|
|
|
151
178
|
*/
|
|
152
179
|
export function parseContextWindow(
|
|
153
180
|
hookInput: HookInput,
|
|
154
|
-
): [
|
|
181
|
+
): [null | number, null | number] {
|
|
155
182
|
const contextWindow = hookInput.context_window;
|
|
156
183
|
if (!contextWindow) return [null, null];
|
|
157
184
|
|
|
@@ -177,7 +204,7 @@ export function parseContextWindow(
|
|
|
177
204
|
*/
|
|
178
205
|
export function getContextPercentRemaining(
|
|
179
206
|
hookInput: HookInput,
|
|
180
|
-
): [
|
|
207
|
+
): [null | number, null | number, null | number] {
|
|
181
208
|
const [tokensUsed, maxTokens] = parseContextWindow(hookInput);
|
|
182
209
|
|
|
183
210
|
if (tokensUsed !== null && maxTokens !== null && maxTokens > 0) {
|
|
@@ -206,6 +233,45 @@ export function getContextPercentRemaining(
|
|
|
206
233
|
return [null, null, null];
|
|
207
234
|
}
|
|
208
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Read stdin early and extract session_id + event metadata.
|
|
238
|
+
* Stashes parsed input for loadHookInput() to consume later.
|
|
239
|
+
*/
|
|
240
|
+
function _earlyReadInput(prefetchedInput?: Record<string, any>): void {
|
|
241
|
+
if (prefetchedInput !== undefined) {
|
|
242
|
+
_prefetchedInput = prefetchedInput;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// If we already have prefetched input, extract metadata from it
|
|
246
|
+
if (_prefetchedInput && typeof _prefetchedInput === "object") {
|
|
247
|
+
_lastHookEvent = _prefetchedInput.hook_event_name ?? null;
|
|
248
|
+
_lastToolName = _prefetchedInput.tool_name ?? null;
|
|
249
|
+
if (_prefetchedInput.session_id) {
|
|
250
|
+
setSessionId(_prefetchedInput.session_id);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Read stdin now so HOOK_START can include sid
|
|
257
|
+
try {
|
|
258
|
+
const inputData = fs.readFileSync(0, "utf8").trim();
|
|
259
|
+
if (inputData) {
|
|
260
|
+
const parsed = JSON.parse(inputData);
|
|
261
|
+
if (parsed && typeof parsed === "object") {
|
|
262
|
+
_prefetchedInput = parsed;
|
|
263
|
+
_lastHookEvent = parsed.hook_event_name ?? null;
|
|
264
|
+
_lastToolName = parsed.tool_name ?? null;
|
|
265
|
+
if (parsed.session_id) {
|
|
266
|
+
setSessionId(parsed.session_id);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Non-fatal — loadHookInput will return null
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
209
275
|
/**
|
|
210
276
|
* Standard hook entry point with lifecycle logging.
|
|
211
277
|
* See SPEC.md §5.7
|
|
@@ -215,23 +281,21 @@ export function runHook(
|
|
|
215
281
|
hookName = "unknown",
|
|
216
282
|
prefetchedInput?: Record<string, any>,
|
|
217
283
|
): never {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
284
|
+
_earlyReadInput(prefetchedInput);
|
|
285
|
+
_cachedHookName = hookName;
|
|
221
286
|
|
|
222
287
|
const startTime = performance.now();
|
|
223
288
|
const template = detectTemplate();
|
|
224
289
|
const event = _lastHookEvent ?? "unknown";
|
|
225
290
|
const tool = _lastToolName;
|
|
226
291
|
|
|
227
|
-
// HOOK_START
|
|
228
292
|
const startData: Record<string, any> = {
|
|
229
293
|
lifecycle: "start",
|
|
230
294
|
template,
|
|
231
295
|
event,
|
|
232
296
|
};
|
|
233
297
|
if (tool) startData.tool = tool;
|
|
234
|
-
|
|
298
|
+
hookLog("info", hookName, "HOOK_START", { data: startData });
|
|
235
299
|
|
|
236
300
|
let exitCode = 0;
|
|
237
301
|
let status = "success";
|
|
@@ -240,17 +304,17 @@ export function runHook(
|
|
|
240
304
|
try {
|
|
241
305
|
const result = mainFunc();
|
|
242
306
|
exitCode = typeof result === "number" ? result : 0;
|
|
243
|
-
status = exitCode
|
|
244
|
-
} catch (
|
|
245
|
-
if (
|
|
246
|
-
const code = parseInt(
|
|
247
|
-
exitCode = isNaN(code) ? (
|
|
307
|
+
status = exitCode === 0 ? "success" : "blocked";
|
|
308
|
+
} catch (error: any) {
|
|
309
|
+
if (error instanceof Error && error.message.startsWith("SystemExit:")) {
|
|
310
|
+
const code = parseInt(error.message.slice(11), 10);
|
|
311
|
+
exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
|
|
248
312
|
status = exitCode !== 0 ? "blocked" : "success";
|
|
249
313
|
} else {
|
|
250
314
|
exitCode = 0; // Non-blocking
|
|
251
315
|
status = "error";
|
|
252
|
-
const stack =
|
|
253
|
-
errorInfo = [
|
|
316
|
+
const stack = error instanceof Error ? error.stack ?? "" : "";
|
|
317
|
+
errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
|
|
254
318
|
}
|
|
255
319
|
}
|
|
256
320
|
|
|
@@ -268,47 +332,45 @@ export function runHookAsync(
|
|
|
268
332
|
hookName = "unknown",
|
|
269
333
|
prefetchedInput?: Record<string, any>,
|
|
270
334
|
): void {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
335
|
+
_earlyReadInput(prefetchedInput);
|
|
336
|
+
_cachedHookName = hookName;
|
|
274
337
|
|
|
275
338
|
const startTime = performance.now();
|
|
276
339
|
const template = detectTemplate();
|
|
277
340
|
const event = _lastHookEvent ?? "unknown";
|
|
278
341
|
const tool = _lastToolName;
|
|
279
342
|
|
|
280
|
-
// HOOK_START
|
|
281
343
|
const startData: Record<string, any> = {
|
|
282
344
|
lifecycle: "start",
|
|
283
345
|
template,
|
|
284
346
|
event,
|
|
285
347
|
};
|
|
286
348
|
if (tool) startData.tool = tool;
|
|
287
|
-
|
|
349
|
+
hookLog("info", hookName, "HOOK_START", { data: startData });
|
|
288
350
|
|
|
289
351
|
mainFunc()
|
|
290
352
|
.then((result) => {
|
|
291
353
|
const exitCode = typeof result === "number" ? result : 0;
|
|
292
|
-
_emitHookEnd(hookName, startTime, exitCode, exitCode
|
|
293
|
-
|
|
354
|
+
_emitHookEnd(hookName, startTime, exitCode, exitCode === 0 ? "success" : "blocked", null, startData, event, tool, template);
|
|
355
|
+
_drainAndExit(exitCode);
|
|
294
356
|
})
|
|
295
|
-
.catch((
|
|
357
|
+
.catch((error: any) => {
|
|
296
358
|
let exitCode = 0;
|
|
297
359
|
let status = "error";
|
|
298
360
|
let errorInfo: [Error, string] | null = null;
|
|
299
361
|
|
|
300
|
-
if (
|
|
301
|
-
const code = parseInt(
|
|
302
|
-
exitCode = isNaN(code) ? (
|
|
362
|
+
if (error instanceof Error && error.message.startsWith("SystemExit:")) {
|
|
363
|
+
const code = parseInt(error.message.slice(11), 10);
|
|
364
|
+
exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
|
|
303
365
|
status = exitCode !== 0 ? "blocked" : "success";
|
|
304
366
|
} else {
|
|
305
367
|
exitCode = 0; // Non-blocking (fail open)
|
|
306
|
-
const stack =
|
|
307
|
-
errorInfo = [
|
|
368
|
+
const stack = error instanceof Error ? error.stack ?? "" : "";
|
|
369
|
+
errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
|
|
308
370
|
}
|
|
309
371
|
|
|
310
372
|
_emitHookEnd(hookName, startTime, exitCode, status, errorInfo, startData, event, tool, template);
|
|
311
|
-
|
|
373
|
+
_drainAndExit(exitCode);
|
|
312
374
|
});
|
|
313
375
|
}
|
|
314
376
|
|
|
@@ -321,17 +383,13 @@ function _emitHookEnd(
|
|
|
321
383
|
errorInfo: [Error, string] | null,
|
|
322
384
|
startData: Record<string, any>,
|
|
323
385
|
event: string,
|
|
324
|
-
tool:
|
|
386
|
+
tool: null | string,
|
|
325
387
|
template: string,
|
|
326
388
|
): void {
|
|
327
|
-
// Retroactive HOOK_START to per-context log
|
|
389
|
+
// Retroactive HOOK_START to per-context log (context_path resolved after main runs)
|
|
328
390
|
const resolvedAfter = _getContextPath();
|
|
329
391
|
if (resolvedAfter && fs.existsSync(resolvedAfter)) {
|
|
330
|
-
hookLog("info", hookName, "HOOK_START", {
|
|
331
|
-
data: startData,
|
|
332
|
-
context_path: resolvedAfter,
|
|
333
|
-
stderr: false,
|
|
334
|
-
});
|
|
392
|
+
hookLog("info", hookName, "HOOK_START", { data: startData });
|
|
335
393
|
}
|
|
336
394
|
|
|
337
395
|
const durationMs = Math.round((performance.now() - startTime) * 10) / 10;
|
|
@@ -350,11 +408,32 @@ function _emitHookEnd(
|
|
|
350
408
|
if (errorInfo) {
|
|
351
409
|
const [err, tb] = errorInfo;
|
|
352
410
|
endData.error_type = err.constructor.name;
|
|
353
|
-
|
|
354
|
-
|
|
411
|
+
hookLog("error", hookName, `[${endEvent}] ${err.constructor.name}: ${String(err).replaceAll(/[\n\r]/g, " ").slice(0, 200)}`, { traceback_str: tb });
|
|
412
|
+
hookLog("error", hookName, `HOOK_END: ${err}`, { data: endData, traceback_str: tb });
|
|
355
413
|
} else if (status === "blocked") {
|
|
356
|
-
|
|
414
|
+
hookLog("warn", hookName, "HOOK_END", { data: endData });
|
|
357
415
|
} else {
|
|
358
|
-
|
|
416
|
+
hookLog("info", hookName, "HOOK_END", { data: endData });
|
|
359
417
|
}
|
|
360
418
|
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Drain stdout before exiting to ensure pipe consumers receive all data.
|
|
422
|
+
* On Windows, stdout to a pipe is fully buffered — process.exit() can
|
|
423
|
+
* discard unflushed data. This waits for the write buffer to drain.
|
|
424
|
+
*/
|
|
425
|
+
function _drainAndExit(code: number): void {
|
|
426
|
+
// If stdout is already finished or not writable, exit immediately
|
|
427
|
+
if (!process.stdout.writable || process.stdout.writableFinished) {
|
|
428
|
+
process.exit(code);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Attempt to end stdout and wait for drain
|
|
432
|
+
const timeout = setTimeout(() => process.exit(code), 1000); // safety fallback
|
|
433
|
+
process.stdout.end(() => {
|
|
434
|
+
clearTimeout(timeout);
|
|
435
|
+
process.exit(code);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export {hookLog, logBlocking, logDebug, logDiagnostic, logError, logHookError, logInfo, logWarn, setContextPath, setSessionId} from "./logger.js";
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
+
|
|
8
9
|
import { logDebug, logWarn } from "./logger.js";
|
|
9
10
|
import { STOP_WORDS } from "./stop-words.js";
|
|
10
11
|
import { cleanTextForSlug } from "./utils.js";
|
|
@@ -44,19 +45,25 @@ export function inference(
|
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
47
|
const isWin = process.platform === "win32";
|
|
47
|
-
|
|
48
|
+
// On Windows with shell:true, Node.js sets windowsVerbatimArguments —
|
|
49
|
+
// args are joined with spaces, NOT individually quoted. We must manually
|
|
50
|
+
// wrap multi-word/special-char args in "..." for cmd.exe parsing.
|
|
51
|
+
// Inside double quotes: "" = literal ", and |&<> are safe.
|
|
52
|
+
const empty = isWin ? '""' : "";
|
|
53
|
+
let promptArg = fullPrompt;
|
|
54
|
+
if (isWin) {
|
|
55
|
+
promptArg = '"' + fullPrompt.replaceAll(/\r?\n/g, " ").replaceAll('"', '""') + '"';
|
|
56
|
+
}
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
// (no string interpolation — avoids command injection)
|
|
51
|
-
stdout = execFileSync(
|
|
58
|
+
const stdout = execFileSync(
|
|
52
59
|
"claude",
|
|
53
|
-
["--model", model, "--print", "--setting-sources",
|
|
60
|
+
["--model", model, "--print", "--setting-sources", empty, "-p", promptArg],
|
|
54
61
|
{
|
|
55
62
|
timeout: timeoutSec * 1000,
|
|
56
63
|
env,
|
|
57
64
|
encoding: "utf-8",
|
|
58
65
|
stdio: ["pipe", "pipe", "pipe"],
|
|
59
|
-
shell: isWin, // Windows needs shell for
|
|
66
|
+
shell: isWin, // Windows needs shell for .cmd resolution
|
|
60
67
|
},
|
|
61
68
|
);
|
|
62
69
|
|
|
@@ -66,10 +73,10 @@ export function inference(
|
|
|
66
73
|
output: stdout.trim(),
|
|
67
74
|
latency_ms: latencyMs,
|
|
68
75
|
};
|
|
69
|
-
} catch (
|
|
76
|
+
} catch (error: any) {
|
|
70
77
|
const latencyMs = Date.now() - startTime;
|
|
71
78
|
|
|
72
|
-
if (
|
|
79
|
+
if (error.code === "ETIMEDOUT" || error.killed) {
|
|
73
80
|
return {
|
|
74
81
|
success: false,
|
|
75
82
|
output: "",
|
|
@@ -78,7 +85,7 @@ export function inference(
|
|
|
78
85
|
};
|
|
79
86
|
}
|
|
80
87
|
|
|
81
|
-
if (
|
|
88
|
+
if (error.code === "ENOENT") {
|
|
82
89
|
return {
|
|
83
90
|
success: false,
|
|
84
91
|
output: "",
|
|
@@ -88,11 +95,11 @@ export function inference(
|
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
// Non-zero exit code
|
|
91
|
-
if (
|
|
98
|
+
if (error.status !== undefined && error.status !== 0) {
|
|
92
99
|
return {
|
|
93
100
|
success: false,
|
|
94
|
-
output: (
|
|
95
|
-
error: (
|
|
101
|
+
output: (error.stdout ?? "").toString().trim(),
|
|
102
|
+
error: (error.stderr ?? "").toString().trim() || `Exit code: ${error.status}`,
|
|
96
103
|
latency_ms: latencyMs,
|
|
97
104
|
};
|
|
98
105
|
}
|
|
@@ -100,7 +107,7 @@ export function inference(
|
|
|
100
107
|
return {
|
|
101
108
|
success: false,
|
|
102
109
|
output: "",
|
|
103
|
-
error: String(
|
|
110
|
+
error: String(error),
|
|
104
111
|
latency_ms: latencyMs,
|
|
105
112
|
};
|
|
106
113
|
}
|
|
@@ -126,13 +133,13 @@ Output ONLY the keywords separated by spaces, nothing else.`;
|
|
|
126
133
|
export function generateSemanticSummary(
|
|
127
134
|
prompt: string,
|
|
128
135
|
timeout = 15,
|
|
129
|
-
):
|
|
136
|
+
): null | string {
|
|
130
137
|
const result = inference(CONTEXT_ID_SYSTEM_PROMPT, prompt, "standard", timeout);
|
|
131
138
|
|
|
132
139
|
if (!result.success || !result.output) return null;
|
|
133
140
|
|
|
134
141
|
let summary = result.output.trim();
|
|
135
|
-
summary = summary.
|
|
142
|
+
summary = summary.replaceAll(/^["']+|["']+$/g, "");
|
|
136
143
|
summary = summary.replace(/[.!?]+$/, "");
|
|
137
144
|
|
|
138
145
|
// Filter stop words
|
|
@@ -187,7 +194,7 @@ Respond with ONLY a JSON object: {"slug": "your 8-12 word phrase here"}`;
|
|
|
187
194
|
export function generateContextIdSlug(
|
|
188
195
|
prompt: string,
|
|
189
196
|
timeout = 3,
|
|
190
|
-
):
|
|
197
|
+
): null | string {
|
|
191
198
|
const truncated = prompt.slice(0, 500);
|
|
192
199
|
|
|
193
200
|
const result = inference(CONTEXT_ID_SLUG_PROMPT, truncated, "fast", timeout);
|
|
@@ -200,7 +207,7 @@ export function generateContextIdSlug(
|
|
|
200
207
|
const raw = result.output.trim();
|
|
201
208
|
|
|
202
209
|
// Parse JSON response, fall back to raw text
|
|
203
|
-
let slug:
|
|
210
|
+
let slug: null | string = null;
|
|
204
211
|
try {
|
|
205
212
|
const parsed = JSON.parse(raw);
|
|
206
213
|
if (parsed && typeof parsed === "object" && "slug" in parsed) {
|
|
@@ -213,11 +220,11 @@ export function generateContextIdSlug(
|
|
|
213
220
|
if (!slug) slug = raw;
|
|
214
221
|
|
|
215
222
|
// Clean up
|
|
216
|
-
slug = slug.
|
|
223
|
+
slug = slug.replaceAll(/^["'`]+|["'`]+$/g, "");
|
|
217
224
|
slug = slug.replace(/[.!?]+$/, "");
|
|
218
|
-
slug = slug.
|
|
219
|
-
slug = slug.
|
|
220
|
-
slug = slug.
|
|
225
|
+
slug = slug.replaceAll('-', " ");
|
|
226
|
+
slug = slug.replaceAll(/[^a-zA-Z0-9 ]/g, "");
|
|
227
|
+
slug = slug.replaceAll(/\s+/g, " ").trim();
|
|
221
228
|
|
|
222
229
|
const words = slug.split(" ");
|
|
223
230
|
|