@aaac/observability 0.1.8 → 0.1.10
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/dist/{chunk-BL7QW46X.js → chunk-3H35HIMU.js} +75 -8
- package/dist/chunk-3H35HIMU.js.map +1 -0
- package/dist/cli/index.js +243 -11
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +59 -2
- package/dist/index.js +5 -1
- package/package.json +1 -1
- package/dist/chunk-BL7QW46X.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -3,13 +3,18 @@ import {
|
|
|
3
3
|
DEFAULT_DB_PATH,
|
|
4
4
|
SqliteQueryAdapter,
|
|
5
5
|
createPipeline,
|
|
6
|
+
emitHumanInstruction,
|
|
7
|
+
emitPromotionPr,
|
|
8
|
+
emitQualityGateResult,
|
|
9
|
+
evaluateMapping,
|
|
6
10
|
generateId,
|
|
7
|
-
isoToUnixNano
|
|
8
|
-
|
|
11
|
+
isoToUnixNano,
|
|
12
|
+
loadEventMappingConfig
|
|
13
|
+
} from "../chunk-3H35HIMU.js";
|
|
9
14
|
|
|
10
15
|
// src/cli/index.ts
|
|
11
16
|
import { readFileSync } from "fs";
|
|
12
|
-
import { dirname, resolve } from "path";
|
|
17
|
+
import { dirname as dirname2, resolve } from "path";
|
|
13
18
|
import { fileURLToPath } from "url";
|
|
14
19
|
|
|
15
20
|
// src/generated/program.ts
|
|
@@ -88,6 +93,34 @@ command_sets:
|
|
|
88
93
|
stderr:
|
|
89
94
|
format: text
|
|
90
95
|
|
|
96
|
+
record-hook:
|
|
97
|
+
summary: Record a Cursor/git hook event \u2014 parses stdin JSON, resolves event_mapping into 3-axis spans/links, injects session_id, and emits human-events (fail-open)
|
|
98
|
+
arguments:
|
|
99
|
+
- name: hook-name
|
|
100
|
+
index: 0
|
|
101
|
+
required: true
|
|
102
|
+
schema: { type: string }
|
|
103
|
+
description: "Cursor/git hook name (e.g. afterFileEdit, subagentStart, beforeShellExecution)"
|
|
104
|
+
options:
|
|
105
|
+
- name: mapping-config
|
|
106
|
+
schema: { type: string }
|
|
107
|
+
description: "Path to event-mapping.json (default: .agent-logs/config/event-mapping.json)"
|
|
108
|
+
- name: db
|
|
109
|
+
schema: { type: string }
|
|
110
|
+
description: Path to observability SQLite database (overrides default)
|
|
111
|
+
exits:
|
|
112
|
+
'0':
|
|
113
|
+
description: Hook recorded (or no-op when stdin/mapping unavailable) \u2014 always fail-open
|
|
114
|
+
stdout:
|
|
115
|
+
format: json
|
|
116
|
+
schema:
|
|
117
|
+
type: object
|
|
118
|
+
properties:
|
|
119
|
+
recorded:
|
|
120
|
+
type: integer
|
|
121
|
+
fallback:
|
|
122
|
+
type: boolean
|
|
123
|
+
|
|
91
124
|
query:
|
|
92
125
|
summary: Query the observability store via QueryAdapter
|
|
93
126
|
options:
|
|
@@ -137,7 +170,7 @@ command_sets:
|
|
|
137
170
|
stderr:
|
|
138
171
|
format: text
|
|
139
172
|
`;
|
|
140
|
-
var CONTRACT_JSON_STR = '{\n "cli_contracts": "0.1.0",\n "info": {\n "title": "AaaC Observability CLI",\n "version": "0.1.0",\n "description": "aaac-observ \u2014 external event registration and query for @aaac/observability"\n },\n "command_sets": {\n "aaac-observ": {\n "summary": "aaac-observ \u2014 register external events and query the observability store",\n "executable": "aaac-observ",\n "commands": {\n "record": {\n "summary": "Register an external event into the observability pipeline (thin entrypoint for EventCollector.registerExternalEvent)",\n "options": [\n {\n "name": "event-type",\n "aliases": [\n "t"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Event type (e.g. promotion.commit, process.edit, agent.session)"\n },\n {\n "name": "lifecycle",\n "aliases": [\n "l"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "open",\n "close",\n "event",\n "instant"\n ]\n },\n "description": "Event lifecycle phase: open | close | event | instant"\n },\n {\n "name": "span-id",\n "aliases": [\n "s"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Span ID (auto-generated if omitted)"\n },\n {\n "name": "parent-span-id",\n "schema": {\n "type": "string"\n },\n "description": "Parent span ID for hierarchy"\n },\n {\n "name": "session-id",\n "schema": {\n "type": "string"\n },\n "description": "Session ID to associate with the event"\n },\n {\n "name": "trace-id",\n "schema": {\n "type": "string"\n },\n "description": "Trace ID for distributed tracing"\n },\n {\n "name": "source",\n "schema": {\n "type": "string"\n },\n "description": "Event source identifier (default: external)"\n },\n {\n "name": "attr",\n "schema": {\n "type": "array",\n "items": {\n "type": "string"\n }\n },\n "description": "Attribute as key=value (repeatable, e.g. --attr git.commit=abc123)"\n },\n {\n "name": "link",\n "schema": {\n "type": "array",\n "items": {\n "type": "string"\n }\n },\n "description": "Cross-axis link as targetSpanId:linkType[:targetTraceId] (repeatable)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Event registered successfully",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object",\n "properties": {\n "eventId": {\n "type": "string"\n },\n "spanId": {\n "type": "string"\n }\n }\n }\n }\n },\n "1": {\n "description": "Registration failed (validation error or write error)",\n "stderr": {\n "format": "text"\n }\n }\n }\n },\n "query": {\n "summary": "Query the observability store via QueryAdapter",\n "options": [\n {\n "name": "kind",\n "aliases": [\n "k"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "trace",\n "span",\n "search",\n "links"\n ]\n },\n "description": "Query kind: trace | span | search | links"\n },\n {\n "name": "trace-id",\n "schema": {\n "type": "string"\n },\n "description": "Trace ID for trace queries (--kind trace)"\n },\n {\n "name": "span-id",\n "aliases": [\n "s"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Span ID for span/links queries (--kind span or --kind links)"\n },\n {\n "name": "event-type",\n "schema": {\n "type": "string"\n },\n "description": "Event type filter for search queries (--kind search)"\n },\n {\n "name": "task-id",\n "schema": {\n "type": "string"\n },\n "description": "Task ID filter for search queries (--kind search)"\n },\n {\n "name": "from",\n "schema": {\n "type": "string"\n },\n "description": "Start time in ISO 8601 format for search queries (--kind search)"\n },\n {\n "name": "to",\n "schema": {\n "type": "string"\n },\n "description": "End time in ISO 8601 format for search queries (--kind search)"\n },\n {\n "name": "direction",\n "aliases": [\n "d"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "forward",\n "reverse",\n "both"\n ]\n },\n "description": "Link traversal direction for links queries (default: both)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Query succeeded \u2014 results as JSON array",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object"\n }\n }\n },\n "1": {\n "description": "Query failed",\n "stderr": {\n "format": "text"\n }\n }\n }\n }\n }\n }\n }\n}';
|
|
173
|
+
var CONTRACT_JSON_STR = '{\n "cli_contracts": "0.1.0",\n "info": {\n "title": "AaaC Observability CLI",\n "version": "0.1.0",\n "description": "aaac-observ \u2014 external event registration and query for @aaac/observability"\n },\n "command_sets": {\n "aaac-observ": {\n "summary": "aaac-observ \u2014 register external events and query the observability store",\n "executable": "aaac-observ",\n "commands": {\n "record": {\n "summary": "Register an external event into the observability pipeline (thin entrypoint for EventCollector.registerExternalEvent)",\n "options": [\n {\n "name": "event-type",\n "aliases": [\n "t"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Event type (e.g. promotion.commit, process.edit, agent.session)"\n },\n {\n "name": "lifecycle",\n "aliases": [\n "l"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "open",\n "close",\n "event",\n "instant"\n ]\n },\n "description": "Event lifecycle phase: open | close | event | instant"\n },\n {\n "name": "span-id",\n "aliases": [\n "s"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Span ID (auto-generated if omitted)"\n },\n {\n "name": "parent-span-id",\n "schema": {\n "type": "string"\n },\n "description": "Parent span ID for hierarchy"\n },\n {\n "name": "session-id",\n "schema": {\n "type": "string"\n },\n "description": "Session ID to associate with the event"\n },\n {\n "name": "trace-id",\n "schema": {\n "type": "string"\n },\n "description": "Trace ID for distributed tracing"\n },\n {\n "name": "source",\n "schema": {\n "type": "string"\n },\n "description": "Event source identifier (default: external)"\n },\n {\n "name": "attr",\n "schema": {\n "type": "array",\n "items": {\n "type": "string"\n }\n },\n "description": "Attribute as key=value (repeatable, e.g. --attr git.commit=abc123)"\n },\n {\n "name": "link",\n "schema": {\n "type": "array",\n "items": {\n "type": "string"\n }\n },\n "description": "Cross-axis link as targetSpanId:linkType[:targetTraceId] (repeatable)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Event registered successfully",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object",\n "properties": {\n "eventId": {\n "type": "string"\n },\n "spanId": {\n "type": "string"\n }\n }\n }\n }\n },\n "1": {\n "description": "Registration failed (validation error or write error)",\n "stderr": {\n "format": "text"\n }\n }\n }\n },\n "record-hook": {\n "summary": "Record a Cursor/git hook event \u2014 parses stdin JSON, resolves event_mapping into 3-axis spans/links, injects session_id, and emits human-events (fail-open)",\n "arguments": [\n {\n "name": "hook-name",\n "index": 0,\n "required": true,\n "schema": {\n "type": "string"\n },\n "description": "Cursor/git hook name (e.g. afterFileEdit, subagentStart, beforeShellExecution)"\n }\n ],\n "options": [\n {\n "name": "mapping-config",\n "schema": {\n "type": "string"\n },\n "description": "Path to event-mapping.json (default: .agent-logs/config/event-mapping.json)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Hook recorded (or no-op when stdin/mapping unavailable) \u2014 always fail-open",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object",\n "properties": {\n "recorded": {\n "type": "integer"\n },\n "fallback": {\n "type": "boolean"\n }\n }\n }\n }\n }\n }\n },\n "query": {\n "summary": "Query the observability store via QueryAdapter",\n "options": [\n {\n "name": "kind",\n "aliases": [\n "k"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "trace",\n "span",\n "search",\n "links"\n ]\n },\n "description": "Query kind: trace | span | search | links"\n },\n {\n "name": "trace-id",\n "schema": {\n "type": "string"\n },\n "description": "Trace ID for trace queries (--kind trace)"\n },\n {\n "name": "span-id",\n "aliases": [\n "s"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Span ID for span/links queries (--kind span or --kind links)"\n },\n {\n "name": "event-type",\n "schema": {\n "type": "string"\n },\n "description": "Event type filter for search queries (--kind search)"\n },\n {\n "name": "task-id",\n "schema": {\n "type": "string"\n },\n "description": "Task ID filter for search queries (--kind search)"\n },\n {\n "name": "from",\n "schema": {\n "type": "string"\n },\n "description": "Start time in ISO 8601 format for search queries (--kind search)"\n },\n {\n "name": "to",\n "schema": {\n "type": "string"\n },\n "description": "End time in ISO 8601 format for search queries (--kind search)"\n },\n {\n "name": "direction",\n "aliases": [\n "d"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "forward",\n "reverse",\n "both"\n ]\n },\n "description": "Link traversal direction for links queries (default: both)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Query succeeded \u2014 results as JSON array",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object"\n }\n }\n },\n "1": {\n "description": "Query failed",\n "stderr": {\n "format": "text"\n }\n }\n }\n }\n }\n }\n }\n}';
|
|
141
174
|
|
|
142
175
|
// src/generated/program.ts
|
|
143
176
|
function createProgram(handlers2, version) {
|
|
@@ -146,6 +179,9 @@ function createProgram(handlers2, version) {
|
|
|
146
179
|
program2.command("record").description("Register an external event into the observability pipeline (thin entrypoint for EventCollector.registerExternalEvent)").option("-t, --event-type <value>", "Event type (e.g. promotion.commit, process.edit, agent.session)").option("-l, --lifecycle <value>", "Event lifecycle phase: open | close | event | instant").option("-s, --span-id <value>", "Span ID (auto-generated if omitted)").option("--parent-span-id <value>", "Parent span ID for hierarchy").option("--session-id <value>", "Session ID to associate with the event").option("--trace-id <value>", "Trace ID for distributed tracing").option("--source <value>", "Event source identifier (default: external)").option("--attr <value>", "Attribute as key=value (repeatable, e.g. --attr git.commit=abc123)").option("--link <value>", "Cross-axis link as targetSpanId:linkType[:targetTraceId] (repeatable)").option("--db <value>", "Path to observability SQLite database (overrides default)").action(async (opts, cmd) => {
|
|
147
180
|
await handlers2.record(opts, cmd.optsWithGlobals());
|
|
148
181
|
});
|
|
182
|
+
program2.command("record-hook").description("Record a Cursor/git hook event \u2014 parses stdin JSON, resolves event_mapping into 3-axis spans/links, injects session_id, and emits human-events (fail-open)").argument("<hook-name>", "Cursor/git hook name (e.g. afterFileEdit, subagentStart, beforeShellExecution)").option("--mapping-config <value>", "Path to event-mapping.json (default: .agent-logs/config/event-mapping.json)").option("--db <value>", "Path to observability SQLite database (overrides default)").action(async (hookName, opts, cmd) => {
|
|
183
|
+
await handlers2.recordHook(hookName, opts, cmd.optsWithGlobals());
|
|
184
|
+
});
|
|
149
185
|
program2.command("query").description("Query the observability store via QueryAdapter").option("-k, --kind <value>", "Query kind: trace | span | search | links").option("--trace-id <value>", "Trace ID for trace queries (--kind trace)").option("-s, --span-id <value>", "Span ID for span/links queries (--kind span or --kind links)").option("--event-type <value>", "Event type filter for search queries (--kind search)").option("--task-id <value>", "Task ID filter for search queries (--kind search)").option("--from <value>", "Start time in ISO 8601 format for search queries (--kind search)").option("--to <value>", "End time in ISO 8601 format for search queries (--kind search)").option("-d, --direction <value>", "Link traversal direction for links queries (default: both)").option("--db <value>", "Path to observability SQLite database (overrides default)").action(async (opts, cmd) => {
|
|
150
186
|
await handlers2.query(opts, cmd.optsWithGlobals());
|
|
151
187
|
});
|
|
@@ -166,7 +202,7 @@ function createProgram(handlers2, version) {
|
|
|
166
202
|
type: "cli-contracts/extract",
|
|
167
203
|
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
168
204
|
specVersion: doc.cli_contracts ?? "0.1.0",
|
|
169
|
-
commands: ["aaac-observ.record", "aaac-observ.query"]
|
|
205
|
+
commands: ["aaac-observ.record", "aaac-observ.record-hook", "aaac-observ.query"]
|
|
170
206
|
};
|
|
171
207
|
}
|
|
172
208
|
Object.assign(out, doc);
|
|
@@ -183,7 +219,7 @@ function createProgram(handlers2, version) {
|
|
|
183
219
|
yamlLines.push("extractedAt: " + (/* @__PURE__ */ new Date()).toISOString());
|
|
184
220
|
yamlLines.push("spec_version: " + (doc.cli_contracts ?? "0.1.0"));
|
|
185
221
|
yamlLines.push("commands:");
|
|
186
|
-
for (const id of ["aaac-observ.record", "aaac-observ.query"]) {
|
|
222
|
+
for (const id of ["aaac-observ.record", "aaac-observ.record-hook", "aaac-observ.query"]) {
|
|
187
223
|
yamlLines.push(" - " + id);
|
|
188
224
|
}
|
|
189
225
|
}
|
|
@@ -296,10 +332,14 @@ async function handleRecord(options, _parentOpts) {
|
|
|
296
332
|
afterCorrelate: (event) => {
|
|
297
333
|
capturedEventId = event.id;
|
|
298
334
|
return event;
|
|
299
|
-
}
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
335
|
+
}
|
|
336
|
+
// Enable the default enrichment rules (R1–R5). This is the path the git
|
|
337
|
+
// post-commit hook uses to record promotion.commit (close) with
|
|
338
|
+
// committed_files; R5 (cross-axis) must run here to add the
|
|
339
|
+
// materializes_as_commit link to task spans whose modified files intersect
|
|
340
|
+
// the commit. R3/R4 accumulation persists in the SQLite-backed cache of the
|
|
341
|
+
// same db file, so R5 sees it across the process boundary (#143 / #140).
|
|
342
|
+
// Enricher is fail-open, so a rule error never blocks recording.
|
|
303
343
|
});
|
|
304
344
|
try {
|
|
305
345
|
collector.registerExternalEvent({
|
|
@@ -324,6 +364,197 @@ async function handleRecord(options, _parentOpts) {
|
|
|
324
364
|
process.stdout.write(JSON.stringify(result) + "\n");
|
|
325
365
|
}
|
|
326
366
|
|
|
367
|
+
// src/cli/record-hook.ts
|
|
368
|
+
import { mkdir, rename, unlink, writeFile } from "fs/promises";
|
|
369
|
+
import { dirname, join } from "path";
|
|
370
|
+
var GIT_CONTEXT_COMMAND_RE = /\bgit\s+(?:commit|push|merge)\b/;
|
|
371
|
+
var GIT_HOOK_NAMES = /* @__PURE__ */ new Set([
|
|
372
|
+
"pre-commit",
|
|
373
|
+
"post-commit",
|
|
374
|
+
"pre-push",
|
|
375
|
+
"post-merge",
|
|
376
|
+
"post-checkout"
|
|
377
|
+
]);
|
|
378
|
+
function defaultMappingConfigPath(dbPath) {
|
|
379
|
+
return join(dirname(dbPath), "config", "event-mapping.json");
|
|
380
|
+
}
|
|
381
|
+
function contextFilePath(dbPath) {
|
|
382
|
+
return join(dirname(dbPath), ".observ-context.json");
|
|
383
|
+
}
|
|
384
|
+
async function readStdin() {
|
|
385
|
+
if (process.stdin.isTTY) return "";
|
|
386
|
+
return new Promise((resolve2) => {
|
|
387
|
+
let data = "";
|
|
388
|
+
let settled = false;
|
|
389
|
+
const finish = () => {
|
|
390
|
+
if (settled) return;
|
|
391
|
+
settled = true;
|
|
392
|
+
resolve2(data);
|
|
393
|
+
};
|
|
394
|
+
try {
|
|
395
|
+
process.stdin.setEncoding("utf8");
|
|
396
|
+
process.stdin.on("data", (chunk) => {
|
|
397
|
+
data += chunk;
|
|
398
|
+
});
|
|
399
|
+
process.stdin.on("end", finish);
|
|
400
|
+
process.stdin.on("error", finish);
|
|
401
|
+
process.stdin.on("close", finish);
|
|
402
|
+
const t = setTimeout(finish, 1e3);
|
|
403
|
+
if (typeof t.unref === "function") t.unref();
|
|
404
|
+
} catch {
|
|
405
|
+
finish();
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function parseHookInput(raw) {
|
|
410
|
+
const trimmed = raw.trim();
|
|
411
|
+
if (!trimmed) return {};
|
|
412
|
+
try {
|
|
413
|
+
const parsed = JSON.parse(trimmed);
|
|
414
|
+
return parsed !== null && typeof parsed === "object" ? parsed : {};
|
|
415
|
+
} catch {
|
|
416
|
+
return {};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function asString(value) {
|
|
420
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
421
|
+
}
|
|
422
|
+
function asNumber(value) {
|
|
423
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
424
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
425
|
+
const n = Number(value);
|
|
426
|
+
if (Number.isFinite(n)) return n;
|
|
427
|
+
}
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
430
|
+
function emitMappedSpans(collector, hook, rule, hookInput, sessionId) {
|
|
431
|
+
const { spans, links } = evaluateMapping(rule, hookInput);
|
|
432
|
+
const linksBySpan = /* @__PURE__ */ new Map();
|
|
433
|
+
for (const link of links) {
|
|
434
|
+
const arr = linksBySpan.get(link.fromSpanId) ?? [];
|
|
435
|
+
arr.push({ targetSpanId: link.toSpanId, linkType: link.linkType });
|
|
436
|
+
linksBySpan.set(link.fromSpanId, arr);
|
|
437
|
+
}
|
|
438
|
+
const parentConversationId = hook === "subagentStart" ? asString(hookInput.parent_conversation_id) : void 0;
|
|
439
|
+
for (const span of spans) {
|
|
440
|
+
const attributes = { ...span.attributes };
|
|
441
|
+
if (sessionId) attributes["session_id"] = sessionId;
|
|
442
|
+
if (parentConversationId) {
|
|
443
|
+
attributes["parent_conversation_id"] = parentConversationId;
|
|
444
|
+
}
|
|
445
|
+
collector.emit({
|
|
446
|
+
source: "cursor-hook",
|
|
447
|
+
eventType: span.eventType,
|
|
448
|
+
lifecycle: span.lifecycle,
|
|
449
|
+
spanId: span.spanId,
|
|
450
|
+
parentSpanId: span.parentSpanId,
|
|
451
|
+
attributes,
|
|
452
|
+
links: linksBySpan.get(span.spanId) ?? []
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return spans.length;
|
|
456
|
+
}
|
|
457
|
+
function emitGenericFallback(collector, hook, sessionId) {
|
|
458
|
+
const eventType = (GIT_HOOK_NAMES.has(hook) ? "git." : "cursor.") + hook;
|
|
459
|
+
const attributes = { "hook.name": hook };
|
|
460
|
+
if (sessionId) attributes["session_id"] = sessionId;
|
|
461
|
+
collector.emit({
|
|
462
|
+
source: "cursor-hook",
|
|
463
|
+
eventType,
|
|
464
|
+
lifecycle: "instant",
|
|
465
|
+
attributes
|
|
466
|
+
});
|
|
467
|
+
return 1;
|
|
468
|
+
}
|
|
469
|
+
function emitHumanEvents(collector, hook, hookInput, sessionId) {
|
|
470
|
+
let count = 0;
|
|
471
|
+
if (hook === "beforeSubmitPrompt") {
|
|
472
|
+
const prompt = asString(hookInput.prompt);
|
|
473
|
+
if (prompt !== void 0) {
|
|
474
|
+
const attachments = Array.isArray(hookInput.attachments) ? hookInput.attachments : [];
|
|
475
|
+
emitHumanInstruction(collector, { sessionId, prompt, attachments });
|
|
476
|
+
count += 1;
|
|
477
|
+
}
|
|
478
|
+
} else if (hook === "afterShellExecution") {
|
|
479
|
+
const command = asString(hookInput.command);
|
|
480
|
+
if (command !== void 0) {
|
|
481
|
+
const exitCode = asNumber(hookInput.exit_code) ?? asNumber(hookInput.exitCode) ?? 0;
|
|
482
|
+
const durationMs = asNumber(hookInput.duration);
|
|
483
|
+
if (emitQualityGateResult(collector, { sessionId, command, exitCode, durationMs })) {
|
|
484
|
+
count += 1;
|
|
485
|
+
}
|
|
486
|
+
const output = asString(hookInput.output) ?? "";
|
|
487
|
+
if (emitPromotionPr(collector, { sessionId, command, output })) {
|
|
488
|
+
count += 1;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return count;
|
|
493
|
+
}
|
|
494
|
+
async function manageContextFile(hook, hookInput, sessionId, dbPath) {
|
|
495
|
+
const command = asString(hookInput.command) ?? "";
|
|
496
|
+
const isGitContextCommand = GIT_CONTEXT_COMMAND_RE.test(command);
|
|
497
|
+
const file = contextFilePath(dbPath);
|
|
498
|
+
if (hook === "beforeShellExecution" && isGitContextCommand) {
|
|
499
|
+
await mkdir(dirname(file), { recursive: true });
|
|
500
|
+
const payload = JSON.stringify({
|
|
501
|
+
session_id: sessionId || null,
|
|
502
|
+
nonce: generateId(),
|
|
503
|
+
created_at_ms: Date.now()
|
|
504
|
+
}) + "\n";
|
|
505
|
+
const tmp = `${file}.${process.pid}.${generateId()}.tmp`;
|
|
506
|
+
await writeFile(tmp, payload, "utf8");
|
|
507
|
+
await rename(tmp, file);
|
|
508
|
+
} else if (hook === "afterShellExecution" && isGitContextCommand) {
|
|
509
|
+
await unlink(file).catch(() => {
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function runRecordHook(args) {
|
|
514
|
+
const { hook, hookInput, dbPath, mappingConfigPath } = args;
|
|
515
|
+
const sessionId = asString(hookInput.conversation_id) ?? "";
|
|
516
|
+
await manageContextFile(hook, hookInput, sessionId, dbPath).catch(() => {
|
|
517
|
+
});
|
|
518
|
+
let config;
|
|
519
|
+
try {
|
|
520
|
+
config = await loadEventMappingConfig(mappingConfigPath);
|
|
521
|
+
} catch {
|
|
522
|
+
config = void 0;
|
|
523
|
+
}
|
|
524
|
+
const rule = config?.mappings?.[hook];
|
|
525
|
+
let recorded = 0;
|
|
526
|
+
let fallback = false;
|
|
527
|
+
const { collector, sink } = createPipeline({ dbPath });
|
|
528
|
+
try {
|
|
529
|
+
if (rule) {
|
|
530
|
+
recorded += emitMappedSpans(collector, hook, rule, hookInput, sessionId);
|
|
531
|
+
} else {
|
|
532
|
+
recorded += emitGenericFallback(collector, hook, sessionId);
|
|
533
|
+
fallback = true;
|
|
534
|
+
}
|
|
535
|
+
recorded += emitHumanEvents(collector, hook, hookInput, sessionId);
|
|
536
|
+
} finally {
|
|
537
|
+
sink.close();
|
|
538
|
+
}
|
|
539
|
+
return { recorded, fallback };
|
|
540
|
+
}
|
|
541
|
+
async function handleRecordHook(hookName, options, _parentOpts) {
|
|
542
|
+
const hook = hookName ?? "unknown";
|
|
543
|
+
const dbPath = options.db ?? DEFAULT_DB_PATH;
|
|
544
|
+
const mappingConfigPath = options.mappingConfig ?? defaultMappingConfigPath(dbPath);
|
|
545
|
+
let result = { recorded: 0, fallback: false };
|
|
546
|
+
try {
|
|
547
|
+
const hookInput = parseHookInput(await readStdin());
|
|
548
|
+
result = await runRecordHook({ hook, hookInput, dbPath, mappingConfigPath });
|
|
549
|
+
} catch (err) {
|
|
550
|
+
process.stderr.write(
|
|
551
|
+
`record-hook: ${err instanceof Error ? err.message : String(err)}
|
|
552
|
+
`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
556
|
+
}
|
|
557
|
+
|
|
327
558
|
// src/cli/query.ts
|
|
328
559
|
function bigIntReplacer(_key, value) {
|
|
329
560
|
return typeof value === "bigint" ? value.toString() : value;
|
|
@@ -412,11 +643,12 @@ async function handleQuery(options, _parentOpts) {
|
|
|
412
643
|
// src/cli/handlers.ts
|
|
413
644
|
var handlers = {
|
|
414
645
|
record: handleRecord,
|
|
646
|
+
recordHook: handleRecordHook,
|
|
415
647
|
query: handleQuery
|
|
416
648
|
};
|
|
417
649
|
|
|
418
650
|
// src/cli/index.ts
|
|
419
|
-
var __dirname =
|
|
651
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
420
652
|
var pkg = JSON.parse(
|
|
421
653
|
readFileSync(resolve(__dirname, "../../package.json"), "utf8")
|
|
422
654
|
);
|