context-mode 1.0.157 → 1.0.159
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/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/adapters/codex/index.d.ts +4 -1
- package/build/adapters/codex/index.js +237 -45
- package/build/session/db.d.ts +35 -0
- package/build/session/db.js +71 -0
- package/build/session/error-classifier.d.ts +87 -0
- package/build/session/error-classifier.js +303 -0
- package/cli.bundle.mjs +222 -202
- package/hooks/session-db.bundle.mjs +23 -6
- package/hooks/session-loaders.mjs +310 -6
- package/hooks/sessionstart.mjs +52 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +145 -125
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* error-classifier — PRD-context-as-a-service §5 ABI parity
|
|
3
|
+
*
|
|
4
|
+
* Pure classifiers that derive per-event metadata from PostToolUse hook
|
|
5
|
+
* stdin, mirroring the seed shape produced by context-mode-platform's
|
|
6
|
+
* `seed.ts` so the bridge can ship full seed-shape parity.
|
|
7
|
+
*
|
|
8
|
+
* The dashboard reads three derived columns that are populated by the
|
|
9
|
+
* platform seeder but, per the OSS handoff (ANOMALY #3), are NEVER READ
|
|
10
|
+
* by the engine when ingesting live events:
|
|
11
|
+
*
|
|
12
|
+
* • error_category — one of 10 fixed buckets
|
|
13
|
+
* • error_tool — tool name that produced the error
|
|
14
|
+
* • command_type — git / test / build / lint / install / format / run / other
|
|
15
|
+
* • command_tool — first token of the command (npm, git, pytest, …)
|
|
16
|
+
* • duration_bucket — fast / medium / slow / timeout
|
|
17
|
+
*
|
|
18
|
+
* We populate them anyway, with sane defaults derived from message text
|
|
19
|
+
* and tool name, so the dashboard renders symmetrically with seed.
|
|
20
|
+
*
|
|
21
|
+
* Hard constraints:
|
|
22
|
+
* 1. Pure functions — no I/O, no globals, no module state.
|
|
23
|
+
* 2. `error_category` MUST match seed's `pickErrorClassification`
|
|
24
|
+
* output exactly. The literal union below is the ABI.
|
|
25
|
+
* 3. No external dependencies; TypeScript stdlib only.
|
|
26
|
+
* 4. Robust to null / undefined / empty / malformed input.
|
|
27
|
+
*/
|
|
28
|
+
export type ErrorCategory = "file_not_found" | "command_not_found" | "edit_match_failed" | "test_failed" | "syntax_error" | "runtime_error" | "permission_denied" | "git_conflict" | "timeout" | "unknown";
|
|
29
|
+
export type CommandType = "test" | "build" | "lint" | "git" | "install" | "format" | "run" | "other";
|
|
30
|
+
export type DurationBucket = "fast" | "medium" | "slow" | "timeout";
|
|
31
|
+
export interface ErrorClassification {
|
|
32
|
+
error_category: ErrorCategory;
|
|
33
|
+
error_tool: string;
|
|
34
|
+
}
|
|
35
|
+
export interface CommandClassification {
|
|
36
|
+
command_type: CommandType;
|
|
37
|
+
command_tool: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Classify an error message + tool name into one of seed's 10 categories.
|
|
41
|
+
*
|
|
42
|
+
* Patterns (ordered most-specific → least-specific so the first match
|
|
43
|
+
* wins). Rationale for each pattern is inline.
|
|
44
|
+
*
|
|
45
|
+
* • file_not_found — Node's ENOENT, Python's FileNotFoundError, and
|
|
46
|
+
* Claude's "Cannot find module" all signal a
|
|
47
|
+
* missing file/path.
|
|
48
|
+
* • command_not_found — POSIX shells emit "command not found" (exit 127)
|
|
49
|
+
* and "bash: <bin>: not found"; npx prints similar.
|
|
50
|
+
* • edit_match_failed — Claude's Edit tool prints "old_string not found"
|
|
51
|
+
* or "matches multiple locations" when the patch
|
|
52
|
+
* context is stale.
|
|
53
|
+
* • test_failed — vitest/jest/pytest format `FAIL ` prefix; npm
|
|
54
|
+
* prints "Test failed".
|
|
55
|
+
* • syntax_error — SyntaxError (JS/TS), tsc TS-codes, or Python's
|
|
56
|
+
* SyntaxError header.
|
|
57
|
+
* • runtime_error — TypeError/ReferenceError/RangeError (uncaught).
|
|
58
|
+
* • permission_denied — EACCES / "permission denied" from fs or sudo.
|
|
59
|
+
* • git_conflict — git's "CONFLICT" header on rebase/merge, plus
|
|
60
|
+
* "Merge conflict in".
|
|
61
|
+
* • timeout — explicit "timeout"/"timed out"/"ETIMEDOUT".
|
|
62
|
+
* • unknown — fallthrough; never throws.
|
|
63
|
+
*/
|
|
64
|
+
export declare function classifyError(message: unknown, toolName: unknown): ErrorClassification;
|
|
65
|
+
/**
|
|
66
|
+
* Classify a Bash command into a workflow bucket. The bucket is derived
|
|
67
|
+
* from the (tool, sub-verb) pair: `npm test` → test, `npm run build` →
|
|
68
|
+
* build, `git commit` → git, `pip install` → install, etc.
|
|
69
|
+
*
|
|
70
|
+
* Heuristic ordering — most-specific verb checks first; falls through to
|
|
71
|
+
* `run` for generic execution and `other` for anything we can't classify.
|
|
72
|
+
*/
|
|
73
|
+
export declare function classifyCommand(command: unknown): CommandClassification;
|
|
74
|
+
/**
|
|
75
|
+
* Bucketise a latency_ms reading into the four fixed buckets the
|
|
76
|
+
* dashboard renders. Bucket edges (in ms):
|
|
77
|
+
*
|
|
78
|
+
* fast : [0, 1_000)
|
|
79
|
+
* medium : [1_000, 10_000)
|
|
80
|
+
* slow : [10_000, 60_000)
|
|
81
|
+
* timeout : [60_000, ∞)
|
|
82
|
+
*
|
|
83
|
+
* Defensive: negative and non-numeric inputs collapse to `fast` (the
|
|
84
|
+
* neutral bucket) rather than throwing, because hooks have been seen
|
|
85
|
+
* to emit `null` for very short calls.
|
|
86
|
+
*/
|
|
87
|
+
export declare function bucketizeDuration(latencyMs: unknown): DurationBucket;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* error-classifier — PRD-context-as-a-service §5 ABI parity
|
|
3
|
+
*
|
|
4
|
+
* Pure classifiers that derive per-event metadata from PostToolUse hook
|
|
5
|
+
* stdin, mirroring the seed shape produced by context-mode-platform's
|
|
6
|
+
* `seed.ts` so the bridge can ship full seed-shape parity.
|
|
7
|
+
*
|
|
8
|
+
* The dashboard reads three derived columns that are populated by the
|
|
9
|
+
* platform seeder but, per the OSS handoff (ANOMALY #3), are NEVER READ
|
|
10
|
+
* by the engine when ingesting live events:
|
|
11
|
+
*
|
|
12
|
+
* • error_category — one of 10 fixed buckets
|
|
13
|
+
* • error_tool — tool name that produced the error
|
|
14
|
+
* • command_type — git / test / build / lint / install / format / run / other
|
|
15
|
+
* • command_tool — first token of the command (npm, git, pytest, …)
|
|
16
|
+
* • duration_bucket — fast / medium / slow / timeout
|
|
17
|
+
*
|
|
18
|
+
* We populate them anyway, with sane defaults derived from message text
|
|
19
|
+
* and tool name, so the dashboard renders symmetrically with seed.
|
|
20
|
+
*
|
|
21
|
+
* Hard constraints:
|
|
22
|
+
* 1. Pure functions — no I/O, no globals, no module state.
|
|
23
|
+
* 2. `error_category` MUST match seed's `pickErrorClassification`
|
|
24
|
+
* output exactly. The literal union below is the ABI.
|
|
25
|
+
* 3. No external dependencies; TypeScript stdlib only.
|
|
26
|
+
* 4. Robust to null / undefined / empty / malformed input.
|
|
27
|
+
*/
|
|
28
|
+
// ── Internal helpers ────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Normalise message for case-insensitive substring matching. Coerces
|
|
31
|
+
* non-string input (null, undefined, numbers from errno objects) to
|
|
32
|
+
* empty string so downstream `.includes()` is always safe.
|
|
33
|
+
*/
|
|
34
|
+
function normalise(s) {
|
|
35
|
+
if (typeof s !== "string")
|
|
36
|
+
return "";
|
|
37
|
+
return s.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Best-effort `error_tool` derivation. Prefers the PostToolUse-supplied
|
|
41
|
+
* `toolName`; falls back to scanning the message for a canonical tool
|
|
42
|
+
* name (Read/Edit/Write/Bash/Grep/Glob) so isolated error strings still
|
|
43
|
+
* resolve to a useful value rather than the literal "unknown".
|
|
44
|
+
*/
|
|
45
|
+
function deriveErrorTool(toolName, message) {
|
|
46
|
+
if (typeof toolName === "string" && toolName.trim().length > 0) {
|
|
47
|
+
return toolName.trim();
|
|
48
|
+
}
|
|
49
|
+
// Canonical Claude Code tool names that may appear in error strings.
|
|
50
|
+
const known = ["Edit", "Read", "Write", "Bash", "Grep", "Glob", "MultiEdit"];
|
|
51
|
+
for (const t of known) {
|
|
52
|
+
if (message.toLowerCase().includes(t.toLowerCase()))
|
|
53
|
+
return t;
|
|
54
|
+
}
|
|
55
|
+
return "unknown";
|
|
56
|
+
}
|
|
57
|
+
// ── Error classifier ────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Classify an error message + tool name into one of seed's 10 categories.
|
|
60
|
+
*
|
|
61
|
+
* Patterns (ordered most-specific → least-specific so the first match
|
|
62
|
+
* wins). Rationale for each pattern is inline.
|
|
63
|
+
*
|
|
64
|
+
* • file_not_found — Node's ENOENT, Python's FileNotFoundError, and
|
|
65
|
+
* Claude's "Cannot find module" all signal a
|
|
66
|
+
* missing file/path.
|
|
67
|
+
* • command_not_found — POSIX shells emit "command not found" (exit 127)
|
|
68
|
+
* and "bash: <bin>: not found"; npx prints similar.
|
|
69
|
+
* • edit_match_failed — Claude's Edit tool prints "old_string not found"
|
|
70
|
+
* or "matches multiple locations" when the patch
|
|
71
|
+
* context is stale.
|
|
72
|
+
* • test_failed — vitest/jest/pytest format `FAIL ` prefix; npm
|
|
73
|
+
* prints "Test failed".
|
|
74
|
+
* • syntax_error — SyntaxError (JS/TS), tsc TS-codes, or Python's
|
|
75
|
+
* SyntaxError header.
|
|
76
|
+
* • runtime_error — TypeError/ReferenceError/RangeError (uncaught).
|
|
77
|
+
* • permission_denied — EACCES / "permission denied" from fs or sudo.
|
|
78
|
+
* • git_conflict — git's "CONFLICT" header on rebase/merge, plus
|
|
79
|
+
* "Merge conflict in".
|
|
80
|
+
* • timeout — explicit "timeout"/"timed out"/"ETIMEDOUT".
|
|
81
|
+
* • unknown — fallthrough; never throws.
|
|
82
|
+
*/
|
|
83
|
+
export function classifyError(message, toolName) {
|
|
84
|
+
const msg = normalise(message);
|
|
85
|
+
const tool = typeof toolName === "string" ? toolName : "";
|
|
86
|
+
const error_tool = deriveErrorTool(toolName, typeof message === "string" ? message : "");
|
|
87
|
+
// Empty / malformed input → unknown bucket but still return a valid tool.
|
|
88
|
+
if (msg.length === 0) {
|
|
89
|
+
return { error_category: "unknown", error_tool };
|
|
90
|
+
}
|
|
91
|
+
// ── file_not_found ────────────────────────────────────────────────
|
|
92
|
+
// Node's ENOENT carries "no such file or directory"; Claude's Read
|
|
93
|
+
// tool surfaces the raw errno line. "cannot find module" is the
|
|
94
|
+
// require-resolution variant.
|
|
95
|
+
if (msg.includes("enoent") ||
|
|
96
|
+
msg.includes("no such file") ||
|
|
97
|
+
msg.includes("cannot find module") ||
|
|
98
|
+
msg.includes("filenotfounderror")) {
|
|
99
|
+
return { error_category: "file_not_found", error_tool };
|
|
100
|
+
}
|
|
101
|
+
// ── command_not_found ────────────────────────────────────────────
|
|
102
|
+
// POSIX shells: "<shell>: <bin>: command not found" → exit 127.
|
|
103
|
+
// Some hooks surface the bare exit code without the message.
|
|
104
|
+
if (msg.includes("command not found") ||
|
|
105
|
+
msg.includes(": not found") ||
|
|
106
|
+
/\bexit(?:\s+code)?\s*[:=]?\s*127\b/.test(msg)) {
|
|
107
|
+
return { error_category: "command_not_found", error_tool };
|
|
108
|
+
}
|
|
109
|
+
// ── edit_match_failed ────────────────────────────────────────────
|
|
110
|
+
// Claude Code's Edit tool prints these exact phrases when the
|
|
111
|
+
// `old_string` parameter no longer matches the file contents.
|
|
112
|
+
// Gate on the Edit tool to avoid false positives when a Bash script
|
|
113
|
+
// happens to contain the literal text "old_string".
|
|
114
|
+
if ((tool === "Edit" || tool === "MultiEdit") &&
|
|
115
|
+
(msg.includes("old_string") ||
|
|
116
|
+
msg.includes("could not find string") ||
|
|
117
|
+
msg.includes("string to replace not found") ||
|
|
118
|
+
msg.includes("matches multiple"))) {
|
|
119
|
+
return { error_category: "edit_match_failed", error_tool };
|
|
120
|
+
}
|
|
121
|
+
// Edit-style failure can also leak without tool name — match the
|
|
122
|
+
// distinctive Claude phrasing alone.
|
|
123
|
+
if (msg.includes("old_string not found") || msg.includes("string to replace not found")) {
|
|
124
|
+
return { error_category: "edit_match_failed", error_tool };
|
|
125
|
+
}
|
|
126
|
+
// ── git_conflict ─────────────────────────────────────────────────
|
|
127
|
+
// Check BEFORE test_failed because "CONFLICT" can co-occur with the
|
|
128
|
+
// word "fail" in some merge outputs.
|
|
129
|
+
if (msg.includes("conflict") &&
|
|
130
|
+
(msg.includes("merge") || msg.includes("rebase") || msg.includes("git"))) {
|
|
131
|
+
return { error_category: "git_conflict", error_tool };
|
|
132
|
+
}
|
|
133
|
+
if (msg.startsWith("conflict") || msg.includes("merge conflict")) {
|
|
134
|
+
return { error_category: "git_conflict", error_tool };
|
|
135
|
+
}
|
|
136
|
+
// ── timeout ──────────────────────────────────────────────────────
|
|
137
|
+
// Check BEFORE test_failed; "test timed out" should bucket as timeout.
|
|
138
|
+
if (msg.includes("etimedout") ||
|
|
139
|
+
msg.includes("timed out") ||
|
|
140
|
+
msg.includes("timeout") ||
|
|
141
|
+
msg.includes("deadline exceeded")) {
|
|
142
|
+
return { error_category: "timeout", error_tool };
|
|
143
|
+
}
|
|
144
|
+
// ── permission_denied ────────────────────────────────────────────
|
|
145
|
+
if (msg.includes("eacces") ||
|
|
146
|
+
msg.includes("permission denied") ||
|
|
147
|
+
msg.includes("operation not permitted") ||
|
|
148
|
+
msg.includes("eperm")) {
|
|
149
|
+
return { error_category: "permission_denied", error_tool };
|
|
150
|
+
}
|
|
151
|
+
// ── syntax_error ─────────────────────────────────────────────────
|
|
152
|
+
// tsc errors look like "error TS2322"; JS SyntaxError header is exact.
|
|
153
|
+
if (msg.includes("syntaxerror") ||
|
|
154
|
+
/\berror\s+ts\d{3,5}\b/.test(msg) ||
|
|
155
|
+
msg.includes("unexpected token") ||
|
|
156
|
+
msg.includes("unexpected end of") ||
|
|
157
|
+
msg.includes("parse error")) {
|
|
158
|
+
return { error_category: "syntax_error", error_tool };
|
|
159
|
+
}
|
|
160
|
+
// ── test_failed ──────────────────────────────────────────────────
|
|
161
|
+
// vitest/jest print `FAIL `; npm test prints "Test failed".
|
|
162
|
+
// pytest prints "FAILED" capital. Bias to Bash tool but accept
|
|
163
|
+
// unscoped messages because hooks sometimes drop tool name.
|
|
164
|
+
if (msg.includes("test failed") ||
|
|
165
|
+
msg.includes("tests failed") ||
|
|
166
|
+
/\bfail(?:ed)?\b.*\btest\b/.test(msg) ||
|
|
167
|
+
/\b\d+\s+tests?\s+failed\b/.test(msg) ||
|
|
168
|
+
msg.includes("assertion") ||
|
|
169
|
+
/^fail\s/.test(msg) ||
|
|
170
|
+
/\nfail\s/.test(msg)) {
|
|
171
|
+
return { error_category: "test_failed", error_tool };
|
|
172
|
+
}
|
|
173
|
+
// ── runtime_error ────────────────────────────────────────────────
|
|
174
|
+
// Catch-all for uncaught JS exceptions and the common Python ones.
|
|
175
|
+
// Placed near the end so more specific buckets (syntax_error) win
|
|
176
|
+
// when both could match.
|
|
177
|
+
if (msg.includes("typeerror") ||
|
|
178
|
+
msg.includes("referenceerror") ||
|
|
179
|
+
msg.includes("rangeerror") ||
|
|
180
|
+
msg.includes("uncaught exception") ||
|
|
181
|
+
msg.includes("traceback (most recent call last)") ||
|
|
182
|
+
msg.includes("nullpointerexception")) {
|
|
183
|
+
return { error_category: "runtime_error", error_tool };
|
|
184
|
+
}
|
|
185
|
+
// Fallthrough — categorisation is best-effort; the dashboard renders
|
|
186
|
+
// "unknown" as a neutral bucket rather than dropping the event.
|
|
187
|
+
return { error_category: "unknown", error_tool };
|
|
188
|
+
}
|
|
189
|
+
// ── Command classifier ──────────────────────────────────────────────────
|
|
190
|
+
/**
|
|
191
|
+
* Tokenise a shell command, stripping leading env-var prefixes
|
|
192
|
+
* (`FOO=bar npm run build`) and `sudo`/`time` wrappers so the first
|
|
193
|
+
* meaningful token is returned.
|
|
194
|
+
*/
|
|
195
|
+
function firstToken(command) {
|
|
196
|
+
const parts = command.trim().split(/\s+/);
|
|
197
|
+
let i = 0;
|
|
198
|
+
// Skip env-var assignments: KEY=value
|
|
199
|
+
while (i < parts.length && /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(parts[i]))
|
|
200
|
+
i++;
|
|
201
|
+
// Skip common wrappers that aren't the "real" tool.
|
|
202
|
+
while (i < parts.length && (parts[i] === "sudo" || parts[i] === "time" || parts[i] === "nice"))
|
|
203
|
+
i++;
|
|
204
|
+
return parts[i] ?? "";
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Classify a Bash command into a workflow bucket. The bucket is derived
|
|
208
|
+
* from the (tool, sub-verb) pair: `npm test` → test, `npm run build` →
|
|
209
|
+
* build, `git commit` → git, `pip install` → install, etc.
|
|
210
|
+
*
|
|
211
|
+
* Heuristic ordering — most-specific verb checks first; falls through to
|
|
212
|
+
* `run` for generic execution and `other` for anything we can't classify.
|
|
213
|
+
*/
|
|
214
|
+
export function classifyCommand(command) {
|
|
215
|
+
if (typeof command !== "string" || command.trim().length === 0) {
|
|
216
|
+
return { command_type: "other", command_tool: "" };
|
|
217
|
+
}
|
|
218
|
+
const raw = command.trim();
|
|
219
|
+
const tool = firstToken(raw).toLowerCase();
|
|
220
|
+
const lower = raw.toLowerCase();
|
|
221
|
+
// ── git ──────────────────────────────────────────────────────────
|
|
222
|
+
if (tool === "git") {
|
|
223
|
+
return { command_type: "git", command_tool: "git" };
|
|
224
|
+
}
|
|
225
|
+
// ── install ──────────────────────────────────────────────────────
|
|
226
|
+
// npm/pnpm/yarn/pip install variants come before generic test/build
|
|
227
|
+
// because `npm install --save-dev jest` would otherwise match test.
|
|
228
|
+
if (/\b(install|add|ci)\b/.test(lower) &&
|
|
229
|
+
(tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun" || tool === "pip" || tool === "pip3")) {
|
|
230
|
+
return { command_type: "install", command_tool: tool };
|
|
231
|
+
}
|
|
232
|
+
if (tool === "brew" && /\binstall\b/.test(lower)) {
|
|
233
|
+
return { command_type: "install", command_tool: "brew" };
|
|
234
|
+
}
|
|
235
|
+
// ── test ─────────────────────────────────────────────────────────
|
|
236
|
+
// Direct test runners, plus `npm test` / `pnpm test` / `yarn test`.
|
|
237
|
+
if (tool === "vitest" || tool === "jest" || tool === "pytest" ||
|
|
238
|
+
tool === "mocha" || tool === "playwright" || tool === "cypress") {
|
|
239
|
+
return { command_type: "test", command_tool: tool };
|
|
240
|
+
}
|
|
241
|
+
if ((tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun") &&
|
|
242
|
+
/\btest\b/.test(lower)) {
|
|
243
|
+
return { command_type: "test", command_tool: tool };
|
|
244
|
+
}
|
|
245
|
+
// ── lint ─────────────────────────────────────────────────────────
|
|
246
|
+
if (tool === "eslint" || tool === "tslint" || tool === "ruff" || tool === "flake8" || tool === "pylint") {
|
|
247
|
+
return { command_type: "lint", command_tool: tool };
|
|
248
|
+
}
|
|
249
|
+
if (/\blint\b/.test(lower) && (tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun")) {
|
|
250
|
+
return { command_type: "lint", command_tool: tool };
|
|
251
|
+
}
|
|
252
|
+
// ── format ───────────────────────────────────────────────────────
|
|
253
|
+
if (tool === "prettier" || tool === "black" || tool === "gofmt" || tool === "rustfmt") {
|
|
254
|
+
return { command_type: "format", command_tool: tool };
|
|
255
|
+
}
|
|
256
|
+
if (/\b(format|fmt)\b/.test(lower) && (tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun")) {
|
|
257
|
+
return { command_type: "format", command_tool: tool };
|
|
258
|
+
}
|
|
259
|
+
// ── build ────────────────────────────────────────────────────────
|
|
260
|
+
if (tool === "tsc" || tool === "webpack" || tool === "rollup" || tool === "vite" || tool === "esbuild" || tool === "make") {
|
|
261
|
+
return { command_type: "build", command_tool: tool };
|
|
262
|
+
}
|
|
263
|
+
if (/\bbuild\b/.test(lower) && (tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun" || tool === "cargo" || tool === "go")) {
|
|
264
|
+
return { command_type: "build", command_tool: tool };
|
|
265
|
+
}
|
|
266
|
+
// ── run ──────────────────────────────────────────────────────────
|
|
267
|
+
// Generic execution — `npm run …` (with no recognised sub-verb),
|
|
268
|
+
// `node script.js`, `python -m foo`, etc.
|
|
269
|
+
if ((tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun") &&
|
|
270
|
+
/\brun\b/.test(lower)) {
|
|
271
|
+
return { command_type: "run", command_tool: tool };
|
|
272
|
+
}
|
|
273
|
+
if (tool === "node" || tool === "python" || tool === "python3" || tool === "deno" || tool === "bun") {
|
|
274
|
+
return { command_type: "run", command_tool: tool };
|
|
275
|
+
}
|
|
276
|
+
// Fallthrough — preserves the tool name so the dashboard can still
|
|
277
|
+
// chart "other" commands by their underlying binary.
|
|
278
|
+
return { command_type: "other", command_tool: tool || "" };
|
|
279
|
+
}
|
|
280
|
+
// ── Duration bucketiser ────────────────────────────────────────────────
|
|
281
|
+
/**
|
|
282
|
+
* Bucketise a latency_ms reading into the four fixed buckets the
|
|
283
|
+
* dashboard renders. Bucket edges (in ms):
|
|
284
|
+
*
|
|
285
|
+
* fast : [0, 1_000)
|
|
286
|
+
* medium : [1_000, 10_000)
|
|
287
|
+
* slow : [10_000, 60_000)
|
|
288
|
+
* timeout : [60_000, ∞)
|
|
289
|
+
*
|
|
290
|
+
* Defensive: negative and non-numeric inputs collapse to `fast` (the
|
|
291
|
+
* neutral bucket) rather than throwing, because hooks have been seen
|
|
292
|
+
* to emit `null` for very short calls.
|
|
293
|
+
*/
|
|
294
|
+
export function bucketizeDuration(latencyMs) {
|
|
295
|
+
const n = typeof latencyMs === "number" && Number.isFinite(latencyMs) ? latencyMs : 0;
|
|
296
|
+
if (n < 1_000)
|
|
297
|
+
return "fast";
|
|
298
|
+
if (n < 10_000)
|
|
299
|
+
return "medium";
|
|
300
|
+
if (n < 60_000)
|
|
301
|
+
return "slow";
|
|
302
|
+
return "timeout";
|
|
303
|
+
}
|