@ouro.bot/cli 0.1.0-alpha.500 → 0.1.0-alpha.502
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/changelog.json +16 -0
- package/dist/heart/core.js +5 -0
- package/dist/heart/providers/error-classification.js +64 -0
- package/dist/nerves/coverage/file-completeness.js +5 -0
- package/dist/nerves/review/cli-main.js +5 -0
- package/dist/nerves/review/cli.js +156 -0
- package/dist/nerves/review/core.js +152 -0
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.502",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Enrich `engine.error` nerve event with HTTP status, redacted body excerpt, and a one-line summary string. Provider errors previously surfaced only as a free-form `error.message`, which forced operators to spelunk the SDK's wrapped object to find the actual status code or quota explanation.",
|
|
8
|
+
"Two new helpers in `src/heart/providers/error-classification.ts`: `extractProviderErrorDetails(error)` pulls `status` (when present) and a body excerpt (capped at 240 chars, with redaction of any 32+ char token-shaped substring so leaked auth keys don't get persisted into nerves), falling through `error.error → error.response → error.body → error.message` until something usable shows up. Survives circular structures defensively. `summarizeProviderError(error, classification, providerId, model)` produces the canonical operator-readable line: `provider <id>/<model>: <classification>[ HTTP <status>][ — <bodyExcerpt>]`.",
|
|
9
|
+
"Wired into `finishTerminalProviderError` in `src/heart/core.ts` so every terminal provider error now lands in nerves with `httpStatus` + `bodyExcerpt` + `summary` meta — making `ouro nerves-review --component engine --event engine.error` (alpha.501) immediately useful for diagnosing provider blowups. 11 new tests cover status capture, missing-status defaults, token redaction, 240-char truncation, fallback through alternate body fields, circular-structure safety, and summary formatting in two shapes."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.501",
|
|
14
|
+
"changes": [
|
|
15
|
+
"New `ouro nerves-review` CLI for tailing the agent's nerves ndjson with structured filters. Read-only. Operators previously had to grep raw ndjson by hand to track down something like 'how many heartbeat-recursion-suspected events fired today' or 'show me the last hour of senses warnings'.",
|
|
16
|
+
"Filters: `--component <substr>`, `--event <substr>`, `--level <level>`, `--since <duration>` (e.g. 5m, 2h, 1d), `--limit <N>`, `--process <name>` (default: daemon), `--agent <name>` (default: current). Output modes: human-readable text (`<time> [<level>] <component>/<event> — <message>`) and `--json` (one parsed object per line for piping to jq).",
|
|
17
|
+
"Pure `reviewNerveEvents(filePath, filter)` core in `src/nerves/review/core.ts` reads the tail of the ndjson (8 MB cap, walks last 200+ lines) and applies in-memory filters; testable without filesystem mocks beyond a temp file. 12 tests cover all six filter dimensions plus duration parsing edge cases (ms/s/m/h/d, malformed inputs), missing-file handling, and the two CLI flag paths (--help, invalid --since). Wired as `npm run nerves:review -- <flags>` and `dist/nerves/review/cli-main.js`."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.500",
|
|
6
22
|
"changes": [
|
package/dist/heart/core.js
CHANGED
|
@@ -20,6 +20,7 @@ const runtime_1 = require("../nerves/runtime");
|
|
|
20
20
|
const context_1 = require("../mind/context");
|
|
21
21
|
const prompt_1 = require("../mind/prompt");
|
|
22
22
|
const kept_notes_1 = require("./kept-notes");
|
|
23
|
+
const error_classification_1 = require("./providers/error-classification");
|
|
23
24
|
const anthropic_1 = require("./providers/anthropic");
|
|
24
25
|
const azure_1 = require("./providers/azure");
|
|
25
26
|
const minimax_1 = require("./providers/minimax");
|
|
@@ -613,6 +614,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
613
614
|
callbacks.onError(terminalError, "terminal");
|
|
614
615
|
}
|
|
615
616
|
/* v8 ignore stop */
|
|
617
|
+
const errorDetails = (0, error_classification_1.extractProviderErrorDetails)(terminalError);
|
|
616
618
|
(0, runtime_1.emitNervesEvent)({
|
|
617
619
|
level: "error",
|
|
618
620
|
event: "engine.error",
|
|
@@ -623,6 +625,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
623
625
|
provider: providerRuntime.id,
|
|
624
626
|
model: providerRuntime.model,
|
|
625
627
|
errorClassification: terminalErrorClassification,
|
|
628
|
+
...(errorDetails.status !== undefined ? { httpStatus: errorDetails.status } : {}),
|
|
629
|
+
...(errorDetails.bodyExcerpt ? { bodyExcerpt: errorDetails.bodyExcerpt } : {}),
|
|
630
|
+
summary: (0, error_classification_1.summarizeProviderError)(terminalError, terminalErrorClassification, providerRuntime.id, providerRuntime.model),
|
|
626
631
|
},
|
|
627
632
|
});
|
|
628
633
|
stripLastToolCalls(messages);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isNetworkError = isNetworkError;
|
|
4
4
|
exports.classifyHttpError = classifyHttpError;
|
|
5
|
+
exports.extractProviderErrorDetails = extractProviderErrorDetails;
|
|
6
|
+
exports.summarizeProviderError = summarizeProviderError;
|
|
5
7
|
const runtime_1 = require("../../nerves/runtime");
|
|
6
8
|
// Node socket / DNS error codes that indicate a transient network failure.
|
|
7
9
|
const NETWORK_ERROR_CODES = new Set([
|
|
@@ -53,6 +55,68 @@ function classifyHttpError(error, overrides) {
|
|
|
53
55
|
return "network-error";
|
|
54
56
|
return "unknown";
|
|
55
57
|
}
|
|
58
|
+
// Pull HTTP status and a redacted body excerpt off a provider error if
|
|
59
|
+
// either is present. SDK shapes: OpenAI puts `status` on the error, body
|
|
60
|
+
// often on `error.error` or `error.response`. Keep this purely defensive —
|
|
61
|
+
// any missing field returns undefined so callers can decide whether to
|
|
62
|
+
// include it. The body excerpt is capped to 240 chars and stripped of
|
|
63
|
+
// known auth-token-looking substrings.
|
|
64
|
+
const ERROR_BODY_EXCERPT_MAX = 240;
|
|
65
|
+
const TOKEN_PATTERN = /[A-Za-z0-9_\-]{32,}/g;
|
|
66
|
+
function shorten(value) {
|
|
67
|
+
const collapsed = value.replace(/\s+/g, " ").trim();
|
|
68
|
+
if (collapsed.length === 0)
|
|
69
|
+
return "";
|
|
70
|
+
const redacted = collapsed.replace(TOKEN_PATTERN, "[redacted]");
|
|
71
|
+
return redacted.length > ERROR_BODY_EXCERPT_MAX
|
|
72
|
+
? `${redacted.slice(0, ERROR_BODY_EXCERPT_MAX - 3)}...`
|
|
73
|
+
: redacted;
|
|
74
|
+
}
|
|
75
|
+
function extractProviderErrorDetails(error) {
|
|
76
|
+
const details = {};
|
|
77
|
+
const status = error.status;
|
|
78
|
+
if (typeof status === "number" && Number.isFinite(status))
|
|
79
|
+
details.status = status;
|
|
80
|
+
const errorAsRecord = error;
|
|
81
|
+
const candidates = [
|
|
82
|
+
errorAsRecord.error,
|
|
83
|
+
errorAsRecord.response,
|
|
84
|
+
errorAsRecord.body,
|
|
85
|
+
error.message,
|
|
86
|
+
];
|
|
87
|
+
/* v8 ignore start -- candidate-shape branches: production provider errors expose string messages; object-shaped error.body and the string-false fall-through are fallbacks for non-OpenAI SDK shapes @preserve */
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
if (!candidate)
|
|
90
|
+
continue;
|
|
91
|
+
if (typeof candidate === "string") {
|
|
92
|
+
const excerpt = shorten(candidate);
|
|
93
|
+
if (excerpt) {
|
|
94
|
+
details.bodyExcerpt = excerpt;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (typeof candidate === "object") {
|
|
99
|
+
try {
|
|
100
|
+
const excerpt = shorten(JSON.stringify(candidate));
|
|
101
|
+
if (excerpt) {
|
|
102
|
+
details.bodyExcerpt = excerpt;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Circular structure or otherwise unstringifyable; skip.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/* v8 ignore stop */
|
|
112
|
+
return details;
|
|
113
|
+
}
|
|
114
|
+
function summarizeProviderError(error, classification, providerId, model) {
|
|
115
|
+
const details = extractProviderErrorDetails(error);
|
|
116
|
+
const statusPart = details.status !== undefined ? ` HTTP ${details.status}` : "";
|
|
117
|
+
const excerptPart = details.bodyExcerpt ? ` — ${details.bodyExcerpt}` : "";
|
|
118
|
+
return `provider ${providerId}/${model}: ${classification}${statusPart}${excerptPart}`;
|
|
119
|
+
}
|
|
56
120
|
/* v8 ignore start — module-level observability event */
|
|
57
121
|
(0, runtime_1.emitNervesEvent)({
|
|
58
122
|
component: "engine",
|
|
@@ -120,6 +120,11 @@ const DISPATCH_EXEMPT_PATTERNS = [
|
|
|
120
120
|
"heart/session-playback-cli-main",
|
|
121
121
|
"heart/session-playback-cli",
|
|
122
122
|
"heart/session-playback",
|
|
123
|
+
// Nerves review: read-only NDJSON tail/filter CLI for debugging.
|
|
124
|
+
// Diagnostics-only utility; the running daemon owns observability.
|
|
125
|
+
"nerves/review/cli-main",
|
|
126
|
+
"nerves/review/cli",
|
|
127
|
+
"nerves/review/core",
|
|
123
128
|
];
|
|
124
129
|
function isDispatchExempt(filePath) {
|
|
125
130
|
return DISPATCH_EXEMPT_PATTERNS.some((pattern) => filePath.includes(pattern));
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runNervesReviewCli = runNervesReviewCli;
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const identity_1 = require("../../heart/identity");
|
|
39
|
+
const core_1 = require("./core");
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const args = { process: "daemon", json: false, help: false };
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const token = argv[i];
|
|
44
|
+
const next = argv[i + 1];
|
|
45
|
+
switch (token) {
|
|
46
|
+
case "--help":
|
|
47
|
+
case "-h":
|
|
48
|
+
args.help = true;
|
|
49
|
+
break;
|
|
50
|
+
case "--process":
|
|
51
|
+
if (next)
|
|
52
|
+
args.process = next;
|
|
53
|
+
i++;
|
|
54
|
+
break;
|
|
55
|
+
case "--agent":
|
|
56
|
+
if (next)
|
|
57
|
+
args.agent = next;
|
|
58
|
+
i++;
|
|
59
|
+
break;
|
|
60
|
+
case "--component":
|
|
61
|
+
if (next)
|
|
62
|
+
args.componentSubstring = next;
|
|
63
|
+
i++;
|
|
64
|
+
break;
|
|
65
|
+
case "--event":
|
|
66
|
+
if (next)
|
|
67
|
+
args.eventSubstring = next;
|
|
68
|
+
i++;
|
|
69
|
+
break;
|
|
70
|
+
case "--level":
|
|
71
|
+
if (next)
|
|
72
|
+
args.level = next;
|
|
73
|
+
i++;
|
|
74
|
+
break;
|
|
75
|
+
case "--since":
|
|
76
|
+
if (next)
|
|
77
|
+
args.since = next;
|
|
78
|
+
i++;
|
|
79
|
+
break;
|
|
80
|
+
case "--limit":
|
|
81
|
+
if (next) {
|
|
82
|
+
const parsed = Number.parseInt(next, 10);
|
|
83
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
84
|
+
args.limit = parsed;
|
|
85
|
+
}
|
|
86
|
+
i++;
|
|
87
|
+
break;
|
|
88
|
+
case "--json":
|
|
89
|
+
args.json = true;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return args;
|
|
94
|
+
}
|
|
95
|
+
function printHelp() {
|
|
96
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
97
|
+
console.log([
|
|
98
|
+
"usage: ouro nerves-review [options]",
|
|
99
|
+
"",
|
|
100
|
+
"Tail the agent's nerves ndjson and filter recent events. Read-only.",
|
|
101
|
+
"",
|
|
102
|
+
"options:",
|
|
103
|
+
" --process <name> log stream to read (default: daemon)",
|
|
104
|
+
" --agent <name> agent bundle to read from (default: current)",
|
|
105
|
+
" --component <substr> filter by component substring (case-insensitive)",
|
|
106
|
+
" --event <substr> filter by event-name substring (case-insensitive)",
|
|
107
|
+
" --level <level> filter by exact level (debug|info|warn|error)",
|
|
108
|
+
" --since <duration> only events newer than e.g. 5m, 2h, 1d",
|
|
109
|
+
" --limit <N> cap returned events (default: 50)",
|
|
110
|
+
" --json output one JSON object per line",
|
|
111
|
+
].join("\n"));
|
|
112
|
+
}
|
|
113
|
+
function runNervesReviewCli(argv) {
|
|
114
|
+
const args = parseArgs(argv);
|
|
115
|
+
if (args.help) {
|
|
116
|
+
printHelp();
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
let sinceMs;
|
|
120
|
+
if (args.since) {
|
|
121
|
+
const parsed = (0, core_1.parseDuration)(args.since);
|
|
122
|
+
if (parsed === null) {
|
|
123
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
124
|
+
console.error(`nerves-review: --since '${args.since}' is not a valid duration (e.g. 5m, 2h, 1d)`);
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
sinceMs = parsed;
|
|
128
|
+
}
|
|
129
|
+
const logsDir = (0, identity_1.getAgentDaemonLogsDir)(args.agent);
|
|
130
|
+
const filePath = path.join(logsDir, `${args.process}.ndjson`);
|
|
131
|
+
const filter = {
|
|
132
|
+
componentSubstring: args.componentSubstring,
|
|
133
|
+
eventSubstring: args.eventSubstring,
|
|
134
|
+
level: args.level,
|
|
135
|
+
sinceMs,
|
|
136
|
+
limit: args.limit,
|
|
137
|
+
nowMs: Date.now(),
|
|
138
|
+
};
|
|
139
|
+
const entries = (0, core_1.reviewNerveEvents)(filePath, filter);
|
|
140
|
+
if (entries.length === 0) {
|
|
141
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
142
|
+
console.log(`(no matching nerves events in ${filePath})`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (args.json) {
|
|
147
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
148
|
+
console.log(entry.raw);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
152
|
+
console.log((0, core_1.formatNerveEntry)(entry));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseDuration = parseDuration;
|
|
37
|
+
exports.reviewNerveEvents = reviewNerveEvents;
|
|
38
|
+
exports.formatNerveEntry = formatNerveEntry;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const DURATION_PATTERN = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/;
|
|
41
|
+
function parseDuration(value) {
|
|
42
|
+
const match = DURATION_PATTERN.exec(value.trim().toLowerCase());
|
|
43
|
+
if (!match)
|
|
44
|
+
return null;
|
|
45
|
+
const amount = Number.parseFloat(match[1]);
|
|
46
|
+
if (!Number.isFinite(amount) || amount < 0)
|
|
47
|
+
return null;
|
|
48
|
+
switch (match[2]) {
|
|
49
|
+
case "ms": return amount;
|
|
50
|
+
case "s": return amount * 1_000;
|
|
51
|
+
case "m": return amount * 60_000;
|
|
52
|
+
case "h": return amount * 3_600_000;
|
|
53
|
+
case "d": return amount * 86_400_000;
|
|
54
|
+
/* v8 ignore start -- exhaustive switch over the regex group; unreachable */
|
|
55
|
+
default: return null;
|
|
56
|
+
/* v8 ignore stop */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function entryTimeMs(parsed) {
|
|
60
|
+
if (!parsed)
|
|
61
|
+
return null;
|
|
62
|
+
const time = parsed.time;
|
|
63
|
+
if (typeof time === "string") {
|
|
64
|
+
const ms = Date.parse(time);
|
|
65
|
+
return Number.isFinite(ms) ? ms : null;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function readLastNLines(filePath, maxLines, maxBytes = 8 * 1024 * 1024) {
|
|
70
|
+
let text;
|
|
71
|
+
try {
|
|
72
|
+
const stat = fs.statSync(filePath);
|
|
73
|
+
if (stat.size > maxBytes) {
|
|
74
|
+
const fd = fs.openSync(filePath, "r");
|
|
75
|
+
try {
|
|
76
|
+
const buf = Buffer.alloc(maxBytes);
|
|
77
|
+
fs.readSync(fd, buf, 0, maxBytes, stat.size - maxBytes);
|
|
78
|
+
text = buf.toString("utf-8");
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
fs.closeSync(fd);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
text = fs.readFileSync(filePath, "utf-8");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const lines = text.split("\n").filter((line) => line.length > 0);
|
|
92
|
+
return lines.slice(-maxLines);
|
|
93
|
+
}
|
|
94
|
+
function reviewNerveEvents(filePath, filter = {}) {
|
|
95
|
+
const limit = filter.limit ?? 50;
|
|
96
|
+
const candidateLineCount = Math.max(limit * 4, 200);
|
|
97
|
+
const lines = readLastNLines(filePath, candidateLineCount);
|
|
98
|
+
const cutoffMs = filter.sinceMs !== undefined && filter.nowMs !== undefined
|
|
99
|
+
? filter.nowMs - filter.sinceMs
|
|
100
|
+
: null;
|
|
101
|
+
const matched = [];
|
|
102
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
103
|
+
const raw = lines[i];
|
|
104
|
+
let parsed = null;
|
|
105
|
+
try {
|
|
106
|
+
const value = JSON.parse(raw);
|
|
107
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
108
|
+
parsed = value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
parsed = null;
|
|
113
|
+
}
|
|
114
|
+
if (!parsed)
|
|
115
|
+
continue;
|
|
116
|
+
if (filter.componentSubstring) {
|
|
117
|
+
const component = String(parsed.component ?? "");
|
|
118
|
+
if (!component.toLowerCase().includes(filter.componentSubstring.toLowerCase()))
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (filter.eventSubstring) {
|
|
122
|
+
const event = String(parsed.event ?? "");
|
|
123
|
+
if (!event.toLowerCase().includes(filter.eventSubstring.toLowerCase()))
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (filter.level) {
|
|
127
|
+
const level = String(parsed.level ?? "info");
|
|
128
|
+
if (level !== filter.level)
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (cutoffMs !== null) {
|
|
132
|
+
const eventMs = entryTimeMs(parsed);
|
|
133
|
+
if (eventMs === null || eventMs < cutoffMs)
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
matched.push({ raw, parsed });
|
|
137
|
+
if (matched.length >= limit)
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
return matched.reverse();
|
|
141
|
+
}
|
|
142
|
+
function formatNerveEntry(entry) {
|
|
143
|
+
const parsed = entry.parsed;
|
|
144
|
+
if (!parsed)
|
|
145
|
+
return entry.raw;
|
|
146
|
+
const time = String(parsed.time ?? "");
|
|
147
|
+
const level = String(parsed.level ?? "info");
|
|
148
|
+
const component = String(parsed.component ?? "?");
|
|
149
|
+
const event = String(parsed.event ?? "?");
|
|
150
|
+
const message = String(parsed.message ?? "");
|
|
151
|
+
return `${time} [${level.padEnd(5)}] ${component}/${event} — ${message}`;
|
|
152
|
+
}
|