agent-trace 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/agent-trace.cjs +1216 -17
  2. package/package.json +1 -1
package/agent-trace.cjs CHANGED
@@ -24,9 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // packages/runtime/src/standalone-entry.ts
27
- var import_node_path3 = __toESM(require("node:path"));
28
- var import_node_os2 = __toESM(require("node:os"));
29
- var import_node_fs2 = __toESM(require("node:fs"));
27
+ var import_node_path5 = __toESM(require("node:path"));
28
+ var import_node_os3 = __toESM(require("node:os"));
29
+ var import_node_fs5 = __toESM(require("node:fs"));
30
30
 
31
31
  // packages/dashboard/src/web-server.ts
32
32
  var import_node_http = __toESM(require("node:http"));
@@ -2202,32 +2202,32 @@ function isEventSource(value) {
2202
2202
  function isPrivacyTier(value) {
2203
2203
  return PRIVACY_TIERS.some((tier) => tier === value);
2204
2204
  }
2205
- function addError(errors, path4, message) {
2206
- errors.push(`${path4}: ${message}`);
2205
+ function addError(errors, path6, message) {
2206
+ errors.push(`${path6}: ${message}`);
2207
2207
  }
2208
- function readRequiredString(source, key, errors, path4) {
2208
+ function readRequiredString(source, key, errors, path6) {
2209
2209
  const value = source[key];
2210
2210
  if (!isNonEmptyString(value)) {
2211
- addError(errors, path4, "must be a non-empty string");
2211
+ addError(errors, path6, "must be a non-empty string");
2212
2212
  return void 0;
2213
2213
  }
2214
2214
  return value;
2215
2215
  }
2216
- function readOptionalString(source, key, errors, path4) {
2216
+ function readOptionalString(source, key, errors, path6) {
2217
2217
  const value = source[key];
2218
2218
  if (value === void 0) {
2219
2219
  return void 0;
2220
2220
  }
2221
2221
  if (!isNonEmptyString(value)) {
2222
- addError(errors, path4, "must be a non-empty string when provided");
2222
+ addError(errors, path6, "must be a non-empty string when provided");
2223
2223
  return void 0;
2224
2224
  }
2225
2225
  return value;
2226
2226
  }
2227
- function readRequiredIsoDate(source, key, errors, path4) {
2227
+ function readRequiredIsoDate(source, key, errors, path6) {
2228
2228
  const value = source[key];
2229
2229
  if (!isValidIsoDate(value)) {
2230
- addError(errors, path4, "must be a valid ISO-8601 date");
2230
+ addError(errors, path6, "must be a valid ISO-8601 date");
2231
2231
  return void 0;
2232
2232
  }
2233
2233
  return value;
@@ -3021,9 +3021,9 @@ function readStringArray(record, key) {
3021
3021
  }
3022
3022
  return output;
3023
3023
  }
3024
- function readNestedString(record, path4) {
3024
+ function readNestedString(record, path6) {
3025
3025
  let current = record;
3026
- for (const key of path4) {
3026
+ for (const key of path6) {
3027
3027
  const asObject = asRecord3(current);
3028
3028
  if (asObject === void 0) {
3029
3029
  return void 0;
@@ -4343,6 +4343,1126 @@ function createSqliteBackedRuntime(options) {
4343
4343
  };
4344
4344
  }
4345
4345
 
4346
+ // packages/cli/src/args.ts
4347
+ function isCliCommand(value) {
4348
+ return value === "init" || value === "status" || value === "hook-handler";
4349
+ }
4350
+ function parsePrivacyTier(value) {
4351
+ if (value === void 0) {
4352
+ return void 0;
4353
+ }
4354
+ if (value === "1" || value === "2" || value === "3") {
4355
+ return Number(value);
4356
+ }
4357
+ return void 0;
4358
+ }
4359
+ function parseArgs(argv) {
4360
+ const commandCandidate = argv[2];
4361
+ const command = isCliCommand(commandCandidate) ? commandCandidate : void 0;
4362
+ let configDir;
4363
+ let collectorUrl;
4364
+ let privacyTier;
4365
+ let installHooks;
4366
+ let forward = false;
4367
+ for (let i = 3; i < argv.length; i += 1) {
4368
+ const token = argv[i];
4369
+ if (token === "--forward") {
4370
+ forward = true;
4371
+ continue;
4372
+ }
4373
+ if (token === "--install-hooks") {
4374
+ installHooks = true;
4375
+ continue;
4376
+ }
4377
+ if (token === "--no-install-hooks") {
4378
+ installHooks = false;
4379
+ continue;
4380
+ }
4381
+ if (token === "--config-dir") {
4382
+ const value = argv[i + 1];
4383
+ if (typeof value === "string" && value.length > 0) {
4384
+ configDir = value;
4385
+ }
4386
+ i += 1;
4387
+ continue;
4388
+ }
4389
+ if (token === "--collector-url") {
4390
+ const value = argv[i + 1];
4391
+ if (typeof value === "string" && value.length > 0) {
4392
+ collectorUrl = value;
4393
+ }
4394
+ i += 1;
4395
+ continue;
4396
+ }
4397
+ if (token === "--privacy-tier") {
4398
+ const value = argv[i + 1];
4399
+ privacyTier = parsePrivacyTier(value);
4400
+ i += 1;
4401
+ continue;
4402
+ }
4403
+ }
4404
+ return {
4405
+ command,
4406
+ ...configDir !== void 0 ? { configDir } : {},
4407
+ ...collectorUrl !== void 0 ? { collectorUrl } : {},
4408
+ ...privacyTier !== void 0 ? { privacyTier } : {},
4409
+ ...installHooks !== void 0 ? { installHooks } : {},
4410
+ ...forward ? { forward: true } : {}
4411
+ };
4412
+ }
4413
+
4414
+ // packages/cli/src/claude-hooks.ts
4415
+ var HOOK_EVENTS = [
4416
+ "SessionStart",
4417
+ "SessionEnd",
4418
+ "PostToolUse",
4419
+ "Stop",
4420
+ "TaskCompleted"
4421
+ ];
4422
+ function buildClaudeHookConfig(hookCommand, generatedAt) {
4423
+ return {
4424
+ version: "1.0",
4425
+ generatedAt,
4426
+ hooks: HOOK_EVENTS.map((event) => ({
4427
+ event,
4428
+ command: hookCommand
4429
+ }))
4430
+ };
4431
+ }
4432
+
4433
+ // packages/cli/src/config-store.ts
4434
+ var import_node_fs2 = __toESM(require("node:fs"));
4435
+ var import_node_os2 = __toESM(require("node:os"));
4436
+ var import_node_path3 = __toESM(require("node:path"));
4437
+ var CONFIG_FILE_NAME = "agent-trace.json";
4438
+ var CLAUDE_HOOKS_FILE_NAME = "agent-trace-claude-hooks.json";
4439
+ var CLAUDE_GLOBAL_SETTINGS_FILE_NAME = "settings.json";
4440
+ var CLAUDE_LOCAL_SETTINGS_FILE_NAME = "settings.local.json";
4441
+ function ensurePrivacyTier(value) {
4442
+ return value === 1 || value === 2 || value === 3;
4443
+ }
4444
+ function parseConfig(raw) {
4445
+ const parsedUnknown = JSON.parse(raw);
4446
+ if (typeof parsedUnknown !== "object" || parsedUnknown === null) {
4447
+ return void 0;
4448
+ }
4449
+ const parsed = parsedUnknown;
4450
+ if (parsed["version"] !== "1.0") {
4451
+ return void 0;
4452
+ }
4453
+ if (typeof parsed["collectorUrl"] !== "string" || parsed["collectorUrl"].length === 0) {
4454
+ return void 0;
4455
+ }
4456
+ if (!ensurePrivacyTier(parsed["privacyTier"])) {
4457
+ return void 0;
4458
+ }
4459
+ if (typeof parsed["hookCommand"] !== "string" || parsed["hookCommand"].length === 0) {
4460
+ return void 0;
4461
+ }
4462
+ if (typeof parsed["updatedAt"] !== "string" || parsed["updatedAt"].length === 0) {
4463
+ return void 0;
4464
+ }
4465
+ return {
4466
+ version: "1.0",
4467
+ collectorUrl: parsed["collectorUrl"],
4468
+ privacyTier: parsed["privacyTier"],
4469
+ hookCommand: parsed["hookCommand"],
4470
+ updatedAt: parsed["updatedAt"]
4471
+ };
4472
+ }
4473
+ function isRecord2(value) {
4474
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4475
+ }
4476
+ function isGlobalClaudeConfigDir(configDir) {
4477
+ const globalClaudeDir = import_node_path3.default.join(import_node_os2.default.homedir(), ".claude");
4478
+ return import_node_path3.default.resolve(configDir) === import_node_path3.default.resolve(globalClaudeDir);
4479
+ }
4480
+ function readSettingsDocument(filePath) {
4481
+ if (!import_node_fs2.default.existsSync(filePath)) {
4482
+ return {
4483
+ exists: false,
4484
+ settings: {}
4485
+ };
4486
+ }
4487
+ try {
4488
+ const parsed = JSON.parse(import_node_fs2.default.readFileSync(filePath, "utf8"));
4489
+ if (!isRecord2(parsed)) {
4490
+ return {
4491
+ exists: true,
4492
+ settings: {}
4493
+ };
4494
+ }
4495
+ return {
4496
+ exists: true,
4497
+ settings: parsed
4498
+ };
4499
+ } catch {
4500
+ return {
4501
+ exists: true,
4502
+ settings: {}
4503
+ };
4504
+ }
4505
+ }
4506
+ function toMutableHooks(settings) {
4507
+ const mutableHooks = {};
4508
+ const hooks = settings["hooks"];
4509
+ if (!isRecord2(hooks)) {
4510
+ return mutableHooks;
4511
+ }
4512
+ for (const [eventName, value] of Object.entries(hooks)) {
4513
+ if (Array.isArray(value)) {
4514
+ mutableHooks[eventName] = [...value];
4515
+ continue;
4516
+ }
4517
+ mutableHooks[eventName] = [];
4518
+ }
4519
+ return mutableHooks;
4520
+ }
4521
+ function toMutableEnv(settings) {
4522
+ const mutableEnv = {};
4523
+ const env = settings["env"];
4524
+ if (!isRecord2(env)) {
4525
+ return mutableEnv;
4526
+ }
4527
+ for (const [key, value] of Object.entries(env)) {
4528
+ if (typeof value === "string") {
4529
+ mutableEnv[key] = value;
4530
+ }
4531
+ }
4532
+ return mutableEnv;
4533
+ }
4534
+ function hasCommandOnEvent(entries, expectedCommand) {
4535
+ return entries.some((entry) => {
4536
+ if (!isRecord2(entry)) {
4537
+ return false;
4538
+ }
4539
+ const hooks = entry["hooks"];
4540
+ if (!Array.isArray(hooks)) {
4541
+ return false;
4542
+ }
4543
+ return hooks.some((hook) => {
4544
+ if (!isRecord2(hook)) {
4545
+ return false;
4546
+ }
4547
+ return hook["type"] === "command" && hook["command"] === expectedCommand;
4548
+ });
4549
+ });
4550
+ }
4551
+ function createCommandEntry(command) {
4552
+ const commandHook = {
4553
+ type: "command",
4554
+ command,
4555
+ timeout: 10
4556
+ };
4557
+ return {
4558
+ hooks: [commandHook]
4559
+ };
4560
+ }
4561
+ function areHooksInstalled(hooksMap, config) {
4562
+ return config.hooks.every((entry) => {
4563
+ const entries = hooksMap[entry.event] ?? [];
4564
+ return hasCommandOnEvent(entries, entry.command);
4565
+ });
4566
+ }
4567
+ function toSettingsDocument(settings, hooksMap, envMap) {
4568
+ const hooks = hooksMap;
4569
+ const env = envMap;
4570
+ return {
4571
+ ...settings,
4572
+ hooks,
4573
+ ...Object.keys(env).length > 0 ? { env } : {}
4574
+ };
4575
+ }
4576
+ var FileCliConfigStore = class {
4577
+ resolveConfigDir(configDirOverride) {
4578
+ if (configDirOverride !== void 0 && configDirOverride.length > 0) {
4579
+ return configDirOverride;
4580
+ }
4581
+ const fromEnv = process.env["AGENT_TRACE_CONFIG_DIR"];
4582
+ if (typeof fromEnv === "string" && fromEnv.length > 0) {
4583
+ return fromEnv;
4584
+ }
4585
+ return import_node_path3.default.join(import_node_os2.default.homedir(), ".claude");
4586
+ }
4587
+ resolveConfigPath(configDirOverride) {
4588
+ return import_node_path3.default.join(this.resolveConfigDir(configDirOverride), CONFIG_FILE_NAME);
4589
+ }
4590
+ resolveHooksPath(configDirOverride) {
4591
+ return import_node_path3.default.join(this.resolveConfigDir(configDirOverride), CLAUDE_HOOKS_FILE_NAME);
4592
+ }
4593
+ resolveClaudeSettingsPath(configDirOverride) {
4594
+ const configDir = this.resolveConfigDir(configDirOverride);
4595
+ const settingsFileName = isGlobalClaudeConfigDir(configDir) ? CLAUDE_GLOBAL_SETTINGS_FILE_NAME : CLAUDE_LOCAL_SETTINGS_FILE_NAME;
4596
+ return import_node_path3.default.join(configDir, settingsFileName);
4597
+ }
4598
+ readConfig(configDirOverride) {
4599
+ const configPath = this.resolveConfigPath(configDirOverride);
4600
+ if (!import_node_fs2.default.existsSync(configPath)) {
4601
+ return void 0;
4602
+ }
4603
+ const raw = import_node_fs2.default.readFileSync(configPath, "utf8");
4604
+ return parseConfig(raw);
4605
+ }
4606
+ writeConfig(config, configDirOverride) {
4607
+ const configDir = this.resolveConfigDir(configDirOverride);
4608
+ import_node_fs2.default.mkdirSync(configDir, { recursive: true });
4609
+ const configPath = this.resolveConfigPath(configDirOverride);
4610
+ import_node_fs2.default.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
4611
+ `, "utf8");
4612
+ return configPath;
4613
+ }
4614
+ writeClaudeHooks(config, configDirOverride) {
4615
+ const configDir = this.resolveConfigDir(configDirOverride);
4616
+ import_node_fs2.default.mkdirSync(configDir, { recursive: true });
4617
+ const hooksPath = this.resolveHooksPath(configDirOverride);
4618
+ import_node_fs2.default.writeFileSync(hooksPath, `${JSON.stringify(config, null, 2)}
4619
+ `, "utf8");
4620
+ return hooksPath;
4621
+ }
4622
+ installClaudeHooks(config, configDirOverride, settingsEnv = {}) {
4623
+ const configDir = this.resolveConfigDir(configDirOverride);
4624
+ import_node_fs2.default.mkdirSync(configDir, { recursive: true });
4625
+ const settingsPath = this.resolveClaudeSettingsPath(configDirOverride);
4626
+ const read = readSettingsDocument(settingsPath);
4627
+ const hooksMap = toMutableHooks(read.settings);
4628
+ const envMap = toMutableEnv(read.settings);
4629
+ let changed = !read.exists;
4630
+ for (const hook of config.hooks) {
4631
+ const eventEntries = hooksMap[hook.event] ?? [];
4632
+ if (!hasCommandOnEvent(eventEntries, hook.command)) {
4633
+ eventEntries.push(createCommandEntry(hook.command));
4634
+ hooksMap[hook.event] = eventEntries;
4635
+ changed = true;
4636
+ }
4637
+ }
4638
+ Object.entries(settingsEnv).forEach(([key, value]) => {
4639
+ if (envMap[key] !== value) {
4640
+ envMap[key] = value;
4641
+ changed = true;
4642
+ }
4643
+ });
4644
+ const installed = areHooksInstalled(hooksMap, config);
4645
+ if (changed) {
4646
+ const updatedSettings = toSettingsDocument(read.settings, hooksMap, envMap);
4647
+ import_node_fs2.default.writeFileSync(settingsPath, `${JSON.stringify(updatedSettings, null, 2)}
4648
+ `, "utf8");
4649
+ }
4650
+ return {
4651
+ settingsPath,
4652
+ installed
4653
+ };
4654
+ }
4655
+ isClaudeHooksInstalled(config, configDirOverride) {
4656
+ const settingsPath = this.resolveClaudeSettingsPath(configDirOverride);
4657
+ const read = readSettingsDocument(settingsPath);
4658
+ if (!read.exists) {
4659
+ return false;
4660
+ }
4661
+ const hooksMap = toMutableHooks(read.settings);
4662
+ return areHooksInstalled(hooksMap, config);
4663
+ }
4664
+ };
4665
+
4666
+ // packages/cli/src/init.ts
4667
+ function ensurePrivacyTierOrDefault(value) {
4668
+ if (value === void 0) {
4669
+ return 2;
4670
+ }
4671
+ return value;
4672
+ }
4673
+ function nowIso(inputNowIso) {
4674
+ if (inputNowIso !== void 0) {
4675
+ return inputNowIso;
4676
+ }
4677
+ return (/* @__PURE__ */ new Date()).toISOString();
4678
+ }
4679
+ function shouldInstallHooks(value) {
4680
+ if (value === void 0) {
4681
+ return true;
4682
+ }
4683
+ return value;
4684
+ }
4685
+ function deriveOtelLogsEndpoint(collectorUrl) {
4686
+ try {
4687
+ const parsed = new URL(collectorUrl);
4688
+ parsed.port = "4717";
4689
+ parsed.pathname = "";
4690
+ parsed.search = "";
4691
+ parsed.hash = "";
4692
+ const value = parsed.toString();
4693
+ return value.endsWith("/") ? value.slice(0, -1) : value;
4694
+ } catch {
4695
+ return "http://127.0.0.1:4717";
4696
+ }
4697
+ }
4698
+ function buildTelemetryEnv(collectorUrl, privacyTier) {
4699
+ const enablePromptAndToolDetail = privacyTier >= 2;
4700
+ const otelLogsEndpoint = deriveOtelLogsEndpoint(collectorUrl);
4701
+ return {
4702
+ CLAUDE_CODE_ENABLE_TELEMETRY: "1",
4703
+ OTEL_METRICS_EXPORTER: "none",
4704
+ OTEL_LOGS_EXPORTER: "otlp",
4705
+ OTEL_EXPORTER_OTLP_PROTOCOL: "grpc",
4706
+ OTEL_EXPORTER_OTLP_ENDPOINT: otelLogsEndpoint,
4707
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: otelLogsEndpoint,
4708
+ OTEL_LOG_USER_PROMPTS: enablePromptAndToolDetail ? "1" : "0",
4709
+ OTEL_LOG_TOOL_DETAILS: enablePromptAndToolDetail ? "1" : "0"
4710
+ };
4711
+ }
4712
+ function runInit(input, store = new FileCliConfigStore()) {
4713
+ const timestamp = nowIso(input.nowIso);
4714
+ const config = {
4715
+ version: "1.0",
4716
+ collectorUrl: input.collectorUrl ?? "http://127.0.0.1:8317/v1/hooks",
4717
+ privacyTier: ensurePrivacyTierOrDefault(input.privacyTier),
4718
+ hookCommand: "agent-trace hook-handler --forward",
4719
+ updatedAt: timestamp
4720
+ };
4721
+ const telemetryEnv = buildTelemetryEnv(config.collectorUrl, config.privacyTier);
4722
+ const hooks = buildClaudeHookConfig(config.hookCommand, timestamp);
4723
+ const configPath = store.writeConfig(config, input.configDir);
4724
+ const hooksPath = store.writeClaudeHooks(hooks, input.configDir);
4725
+ const installResult = shouldInstallHooks(input.installHooks) ? store.installClaudeHooks(hooks, input.configDir, telemetryEnv) : {
4726
+ settingsPath: store.resolveClaudeSettingsPath(input.configDir),
4727
+ installed: store.isClaudeHooksInstalled(hooks, input.configDir)
4728
+ };
4729
+ return {
4730
+ ok: true,
4731
+ configPath,
4732
+ hooksPath,
4733
+ settingsPath: installResult.settingsPath,
4734
+ settingsHooksInstalled: installResult.installed,
4735
+ config,
4736
+ hooks
4737
+ };
4738
+ }
4739
+
4740
+ // packages/cli/src/status.ts
4741
+ var import_node_fs3 = __toESM(require("node:fs"));
4742
+ function runStatus(configDir, store = new FileCliConfigStore()) {
4743
+ const configPath = store.resolveConfigPath(configDir);
4744
+ const hooksPath = store.resolveHooksPath(configDir);
4745
+ const config = store.readConfig(configDir);
4746
+ if (config === void 0) {
4747
+ return {
4748
+ ok: false,
4749
+ message: "agent-trace config not found",
4750
+ configPath
4751
+ };
4752
+ }
4753
+ const settingsPath = store.resolveClaudeSettingsPath(configDir);
4754
+ const expectedHooks = buildClaudeHookConfig(config.hookCommand, config.updatedAt);
4755
+ return {
4756
+ ok: true,
4757
+ message: "agent-trace config found",
4758
+ configPath,
4759
+ hooksPath,
4760
+ hooksConfigured: import_node_fs3.default.existsSync(hooksPath),
4761
+ settingsPath,
4762
+ settingsHooksInstalled: store.isClaudeHooksInstalled(expectedHooks, configDir),
4763
+ config
4764
+ };
4765
+ }
4766
+
4767
+ // packages/cli/src/hook-handler.ts
4768
+ var import_node_crypto4 = __toESM(require("node:crypto"));
4769
+ var import_node_child_process = require("node:child_process");
4770
+ var import_node_fs4 = __toESM(require("node:fs"));
4771
+ var import_node_path4 = __toESM(require("node:path"));
4772
+ function isIsoDate2(value) {
4773
+ if (Number.isNaN(Date.parse(value))) {
4774
+ return false;
4775
+ }
4776
+ return value.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(value);
4777
+ }
4778
+ function parseHookPayload(rawStdin) {
4779
+ const trimmed = rawStdin.trim();
4780
+ if (trimmed.length === 0) {
4781
+ return void 0;
4782
+ }
4783
+ const parsed = JSON.parse(trimmed);
4784
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4785
+ return void 0;
4786
+ }
4787
+ return parsed;
4788
+ }
4789
+ function readString5(record, key) {
4790
+ const value = record[key];
4791
+ if (typeof value === "string" && value.length > 0) {
4792
+ return value;
4793
+ }
4794
+ return void 0;
4795
+ }
4796
+ function readNumber4(record, key) {
4797
+ const value = record[key];
4798
+ if (typeof value === "number" && Number.isFinite(value)) {
4799
+ return value;
4800
+ }
4801
+ return void 0;
4802
+ }
4803
+ function readStringArray3(record, key) {
4804
+ const value = record[key];
4805
+ if (!Array.isArray(value)) {
4806
+ return void 0;
4807
+ }
4808
+ const output = [];
4809
+ value.forEach((item) => {
4810
+ if (typeof item === "string" && item.length > 0) {
4811
+ output.push(item);
4812
+ }
4813
+ });
4814
+ if (output.length === 0) {
4815
+ return void 0;
4816
+ }
4817
+ return output;
4818
+ }
4819
+ function readNestedString2(record, path6) {
4820
+ let current = record;
4821
+ for (const key of path6) {
4822
+ if (typeof current !== "object" || current === null || Array.isArray(current)) {
4823
+ return void 0;
4824
+ }
4825
+ current = current[key];
4826
+ }
4827
+ if (typeof current === "string" && current.length > 0) {
4828
+ return current;
4829
+ }
4830
+ return void 0;
4831
+ }
4832
+ function pickCommand2(payload) {
4833
+ const record = payload;
4834
+ return readString5(record, "command") ?? readString5(record, "bash_command") ?? readString5(record, "bashCommand") ?? readNestedString2(record, ["tool_input", "command"]);
4835
+ }
4836
+ function pickToolName2(payload) {
4837
+ const record = payload;
4838
+ return readString5(record, "tool_name") ?? readString5(record, "toolName");
4839
+ }
4840
+ function isGitBashPayload(payload) {
4841
+ const toolName = pickToolName2(payload);
4842
+ const command = pickCommand2(payload);
4843
+ if (toolName === void 0 || command === void 0) {
4844
+ return false;
4845
+ }
4846
+ const cmd = command.trim();
4847
+ return toolName.toLowerCase() === "bash" && (cmd.startsWith("git ") || cmd.startsWith("gh pr ") || cmd.includes("gh pr create"));
4848
+ }
4849
+ function isSessionEndEvent(payload) {
4850
+ const eventType = pickEventType(payload).toLowerCase();
4851
+ return eventType === "session_end" || eventType === "sessionend" || eventType === "stop" || eventType === "task_completed" || eventType === "taskcompleted";
4852
+ }
4853
+ function isSessionStartEvent(payload) {
4854
+ const eventType = pickEventType(payload).toLowerCase();
4855
+ return eventType === "session_start" || eventType === "sessionstart" || eventType === "startup";
4856
+ }
4857
+ function shouldAttemptGitEnrichment(payload) {
4858
+ return isGitBashPayload(payload) || isSessionStartEvent(payload) || isSessionEndEvent(payload);
4859
+ }
4860
+ function isGitCommitCommand(command) {
4861
+ return /\bgit\s+commit\b/.test(command);
4862
+ }
4863
+ function parseCommitMessage2(command) {
4864
+ const regex = /(?:^|\s)-m\s+["']([^"']+)["']/;
4865
+ const match = command.match(regex);
4866
+ if (match?.[1] === void 0 || match[1].length === 0) {
4867
+ return void 0;
4868
+ }
4869
+ const message = match[1];
4870
+ if (message.startsWith("$(") || message.startsWith("`")) {
4871
+ return void 0;
4872
+ }
4873
+ return message;
4874
+ }
4875
+ function extractPrUrl(payload) {
4876
+ const record = payload;
4877
+ const output = readString5(record, "tool_response") ?? readString5(record, "toolResponse") ?? readString5(record, "stdout") ?? readString5(record, "output");
4878
+ const command = pickCommand2(payload);
4879
+ const combined = [command, output].filter((s) => s !== void 0).join("\n");
4880
+ if (combined.length === 0) return void 0;
4881
+ const prUrlMatch = combined.match(/https:\/\/github\.com\/[^\s"']+\/pull\/\d+/);
4882
+ if (prUrlMatch !== null && prUrlMatch[0] !== void 0) return prUrlMatch[0];
4883
+ return void 0;
4884
+ }
4885
+ function parsePrFromUrl(url) {
4886
+ const match = url.match(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
4887
+ if (match === null || match[1] === void 0 || match[2] === void 0) return void 0;
4888
+ const prNumber = Number.parseInt(match[2], 10);
4889
+ if (!Number.isFinite(prNumber)) return void 0;
4890
+ return { repo: match[1], prNumber };
4891
+ }
4892
+ function pickRepositoryPath(payload) {
4893
+ const record = payload;
4894
+ return readString5(record, "project_path") ?? readString5(record, "projectPath") ?? readString5(record, "cwd") ?? readString5(record, "working_directory") ?? readString5(record, "workingDirectory");
4895
+ }
4896
+ function toUniqueStrings3(values) {
4897
+ const seen = /* @__PURE__ */ new Set();
4898
+ const output = [];
4899
+ values.forEach((value) => {
4900
+ if (value.length === 0 || seen.has(value)) {
4901
+ return;
4902
+ }
4903
+ seen.add(value);
4904
+ output.push(value);
4905
+ });
4906
+ return output;
4907
+ }
4908
+ function parseIntSafe(raw) {
4909
+ const parsed = Number.parseInt(raw, 10);
4910
+ if (!Number.isFinite(parsed) || parsed < 0) {
4911
+ return void 0;
4912
+ }
4913
+ return parsed;
4914
+ }
4915
+ function parseNumstatOutput(raw) {
4916
+ const lines = raw.split(/\r?\n/);
4917
+ let linesAdded = 0;
4918
+ let linesRemoved = 0;
4919
+ const filesChanged = [];
4920
+ let matched = false;
4921
+ lines.forEach((line) => {
4922
+ const match = line.trim().match(/^(\d+|-)\s+(\d+|-)\s+(.+)$/);
4923
+ if (match === null) {
4924
+ return;
4925
+ }
4926
+ const addedRaw = match[1];
4927
+ const removedRaw = match[2];
4928
+ const filePath = match[3]?.trim();
4929
+ if (filePath === void 0 || filePath.length === 0) {
4930
+ return;
4931
+ }
4932
+ if (addedRaw !== void 0 && addedRaw !== "-") {
4933
+ linesAdded += parseIntSafe(addedRaw) ?? 0;
4934
+ }
4935
+ if (removedRaw !== void 0 && removedRaw !== "-") {
4936
+ linesRemoved += parseIntSafe(removedRaw) ?? 0;
4937
+ }
4938
+ filesChanged.push(filePath);
4939
+ matched = true;
4940
+ });
4941
+ if (!matched) {
4942
+ return void 0;
4943
+ }
4944
+ return {
4945
+ linesAdded,
4946
+ linesRemoved,
4947
+ filesChanged: toUniqueStrings3(filesChanged)
4948
+ };
4949
+ }
4950
+ function stableStringify(payload) {
4951
+ const keys = Object.keys(payload).sort();
4952
+ const record = {};
4953
+ keys.forEach((key) => {
4954
+ record[key] = payload[key];
4955
+ });
4956
+ return JSON.stringify(record);
4957
+ }
4958
+ function buildEventId3(payload, now) {
4959
+ const material = `${now}:${stableStringify(payload)}`;
4960
+ return import_node_crypto4.default.createHash("sha256").update(material).digest("hex");
4961
+ }
4962
+ function pickSessionId(payload) {
4963
+ const fromSnake = payload["session_id"];
4964
+ if (typeof fromSnake === "string" && fromSnake.length > 0) {
4965
+ return fromSnake;
4966
+ }
4967
+ const fromCamel = payload["sessionId"];
4968
+ if (typeof fromCamel === "string" && fromCamel.length > 0) {
4969
+ return fromCamel;
4970
+ }
4971
+ return "unknown_session";
4972
+ }
4973
+ function pickPromptId2(payload) {
4974
+ const fromSnake = payload["prompt_id"];
4975
+ if (typeof fromSnake === "string" && fromSnake.length > 0) {
4976
+ return fromSnake;
4977
+ }
4978
+ const fromCamel = payload["promptId"];
4979
+ if (typeof fromCamel === "string" && fromCamel.length > 0) {
4980
+ return fromCamel;
4981
+ }
4982
+ const fromMessageId = payload["message_id"];
4983
+ if (typeof fromMessageId === "string" && fromMessageId.length > 0) {
4984
+ return fromMessageId;
4985
+ }
4986
+ const fromMessageIdCamel = payload["messageId"];
4987
+ if (typeof fromMessageIdCamel === "string" && fromMessageIdCamel.length > 0) {
4988
+ return fromMessageIdCamel;
4989
+ }
4990
+ return void 0;
4991
+ }
4992
+ function pickEventType(payload) {
4993
+ const event = payload["event"];
4994
+ if (typeof event === "string" && event.length > 0) {
4995
+ return event;
4996
+ }
4997
+ const type = payload["type"];
4998
+ if (typeof type === "string" && type.length > 0) {
4999
+ return type;
5000
+ }
5001
+ const hookEventNameSnake = payload["hook_event_name"];
5002
+ if (typeof hookEventNameSnake === "string" && hookEventNameSnake.length > 0) {
5003
+ return hookEventNameSnake;
5004
+ }
5005
+ const hookEventNameCamel = payload["hookEventName"];
5006
+ if (typeof hookEventNameCamel === "string" && hookEventNameCamel.length > 0) {
5007
+ return hookEventNameCamel;
5008
+ }
5009
+ const hookNameSnake = payload["hook_name"];
5010
+ if (typeof hookNameSnake === "string" && hookNameSnake.length > 0) {
5011
+ return hookNameSnake;
5012
+ }
5013
+ const hookNameCamel = payload["hookName"];
5014
+ if (typeof hookNameCamel === "string" && hookNameCamel.length > 0) {
5015
+ return hookNameCamel;
5016
+ }
5017
+ const hook = payload["hook"];
5018
+ if (typeof hook === "string" && hook.length > 0) {
5019
+ return hook;
5020
+ }
5021
+ return "hook_event";
5022
+ }
5023
+ function pickTimestamp(payload, now) {
5024
+ const value = payload["timestamp"];
5025
+ if (typeof value === "string" && value.length > 0) {
5026
+ return value;
5027
+ }
5028
+ return now;
5029
+ }
5030
+ function getPrivacyTier(store, configDir) {
5031
+ const config = store.readConfig(configDir);
5032
+ if (config === void 0) {
5033
+ return 2;
5034
+ }
5035
+ return config.privacyTier;
5036
+ }
5037
+ function getCollectorUrl(store, configDir, collectorUrlOverride) {
5038
+ if (collectorUrlOverride !== void 0 && collectorUrlOverride.length > 0) {
5039
+ return collectorUrlOverride;
5040
+ }
5041
+ const config = store.readConfig(configDir);
5042
+ if (config !== void 0) {
5043
+ return config.collectorUrl;
5044
+ }
5045
+ return "http://127.0.0.1:8317/v1/hooks";
5046
+ }
5047
+ function normalizeBaseline(baseline) {
5048
+ return {
5049
+ ...baseline.repositoryPath !== void 0 ? { repositoryPath: baseline.repositoryPath } : {},
5050
+ linesAdded: Math.max(0, Math.trunc(baseline.linesAdded)),
5051
+ linesRemoved: Math.max(0, Math.trunc(baseline.linesRemoved)),
5052
+ filesChanged: toUniqueStrings3(baseline.filesChanged),
5053
+ capturedAt: baseline.capturedAt
5054
+ };
5055
+ }
5056
+ function isHookSessionBaseline(value) {
5057
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
5058
+ return false;
5059
+ }
5060
+ const record = value;
5061
+ if (typeof record["linesAdded"] !== "number" || typeof record["linesRemoved"] !== "number") {
5062
+ return false;
5063
+ }
5064
+ if (typeof record["capturedAt"] !== "string") {
5065
+ return false;
5066
+ }
5067
+ if (!Array.isArray(record["filesChanged"])) {
5068
+ return false;
5069
+ }
5070
+ return record["filesChanged"].every((entry) => typeof entry === "string");
5071
+ }
5072
+ function parseBaselineFile(raw) {
5073
+ const parsed = JSON.parse(raw);
5074
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5075
+ return {
5076
+ sessions: {}
5077
+ };
5078
+ }
5079
+ const root = parsed;
5080
+ const sessionsRaw = root["sessions"];
5081
+ if (typeof sessionsRaw !== "object" || sessionsRaw === null || Array.isArray(sessionsRaw)) {
5082
+ return {
5083
+ sessions: {}
5084
+ };
5085
+ }
5086
+ const sessionsRecord = sessionsRaw;
5087
+ const sessions = {};
5088
+ Object.keys(sessionsRecord).forEach((sessionId) => {
5089
+ const candidate = sessionsRecord[sessionId];
5090
+ if (!isHookSessionBaseline(candidate)) {
5091
+ return;
5092
+ }
5093
+ sessions[sessionId] = normalizeBaseline(candidate);
5094
+ });
5095
+ return {
5096
+ sessions
5097
+ };
5098
+ }
5099
+ var FileHookSessionBaselineStore = class {
5100
+ baselinePath;
5101
+ constructor(store, configDir) {
5102
+ this.baselinePath = import_node_path4.default.join(store.resolveConfigDir(configDir), "hook-session-baselines.json");
5103
+ }
5104
+ readState() {
5105
+ try {
5106
+ if (!import_node_fs4.default.existsSync(this.baselinePath)) {
5107
+ return {
5108
+ sessions: {}
5109
+ };
5110
+ }
5111
+ const raw = import_node_fs4.default.readFileSync(this.baselinePath, "utf8");
5112
+ return parseBaselineFile(raw);
5113
+ } catch {
5114
+ return {
5115
+ sessions: {}
5116
+ };
5117
+ }
5118
+ }
5119
+ writeState(state) {
5120
+ const dir = import_node_path4.default.dirname(this.baselinePath);
5121
+ import_node_fs4.default.mkdirSync(dir, { recursive: true });
5122
+ import_node_fs4.default.writeFileSync(this.baselinePath, JSON.stringify(state, null, 2), "utf8");
5123
+ }
5124
+ read(sessionId) {
5125
+ const state = this.readState();
5126
+ return state.sessions[sessionId];
5127
+ }
5128
+ write(sessionId, baseline) {
5129
+ const state = this.readState();
5130
+ const next = {
5131
+ sessions: {
5132
+ ...state.sessions,
5133
+ [sessionId]: normalizeBaseline(baseline)
5134
+ }
5135
+ };
5136
+ this.writeState(next);
5137
+ }
5138
+ delete(sessionId) {
5139
+ const state = this.readState();
5140
+ if (!(sessionId in state.sessions)) {
5141
+ return;
5142
+ }
5143
+ const sessions = {
5144
+ ...state.sessions
5145
+ };
5146
+ delete sessions[sessionId];
5147
+ this.writeState({
5148
+ sessions
5149
+ });
5150
+ }
5151
+ };
5152
+ function runGitCommand(args, repositoryPath) {
5153
+ try {
5154
+ const output = (0, import_node_child_process.execFileSync)("git", [...args], {
5155
+ ...repositoryPath !== void 0 ? { cwd: repositoryPath } : {},
5156
+ encoding: "utf8",
5157
+ stdio: ["ignore", "pipe", "ignore"]
5158
+ });
5159
+ const trimmed = output.trim();
5160
+ if (trimmed.length === 0) {
5161
+ return void 0;
5162
+ }
5163
+ return trimmed;
5164
+ } catch {
5165
+ return void 0;
5166
+ }
5167
+ }
5168
+ var ShellHookGitContextProvider = class {
5169
+ readContext(request) {
5170
+ const branch = runGitCommand(["rev-parse", "--abbrev-ref", "HEAD"], request.repositoryPath);
5171
+ const headSha = runGitCommand(["rev-parse", "HEAD"], request.repositoryPath);
5172
+ const diffStatsRaw = request.includeDiffStats && request.diffSource === "working_tree" ? runGitCommand(["diff", "--numstat", "HEAD"], request.repositoryPath) : request.includeDiffStats ? runGitCommand(["show", "--numstat", "--format="], request.repositoryPath) : void 0;
5173
+ const diffStats = diffStatsRaw === void 0 ? void 0 : parseNumstatOutput(diffStatsRaw);
5174
+ const commitMessage = request.includeCommitMessage === true ? runGitCommand(["log", "-1", "--format=%B"], request.repositoryPath) : void 0;
5175
+ if (branch === void 0 && headSha === void 0 && commitMessage === void 0 && diffStats?.linesAdded === void 0 && diffStats?.linesRemoved === void 0 && diffStats?.filesChanged === void 0) {
5176
+ return void 0;
5177
+ }
5178
+ return {
5179
+ ...branch !== void 0 ? { branch } : {},
5180
+ ...headSha !== void 0 ? { headSha } : {},
5181
+ ...commitMessage !== void 0 ? { commitMessage } : {},
5182
+ ...diffStats?.linesAdded !== void 0 ? { linesAdded: diffStats.linesAdded } : {},
5183
+ ...diffStats?.linesRemoved !== void 0 ? { linesRemoved: diffStats.linesRemoved } : {},
5184
+ ...diffStats?.filesChanged !== void 0 ? { filesChanged: diffStats.filesChanged } : {}
5185
+ };
5186
+ }
5187
+ };
5188
+ function enrichHookPayloadWithGitContext(payload, provider, baselineStore, now) {
5189
+ if (!shouldAttemptGitEnrichment(payload)) {
5190
+ return {
5191
+ payload,
5192
+ enriched: false,
5193
+ usedSessionBaselineDelta: false
5194
+ };
5195
+ }
5196
+ const record = payload;
5197
+ const command = pickCommand2(payload);
5198
+ const sessionStartEvent = isSessionStartEvent(payload);
5199
+ const sessionEndEvent = isSessionEndEvent(payload);
5200
+ const isCommit = command !== void 0 && isGitCommitCommand(command) && !sessionStartEvent && !sessionEndEvent;
5201
+ const includeDiffStats = isCommit || sessionStartEvent || sessionEndEvent;
5202
+ const diffSource = sessionStartEvent || sessionEndEvent ? "working_tree" : "head_commit";
5203
+ const repositoryPath = pickRepositoryPath(payload);
5204
+ const sessionId = pickSessionId(payload);
5205
+ const contextRequest = {
5206
+ includeDiffStats,
5207
+ ...includeDiffStats ? { diffSource } : {},
5208
+ ...isCommit ? { includeCommitMessage: true } : {},
5209
+ ...repositoryPath !== void 0 ? { repositoryPath } : {}
5210
+ };
5211
+ const gitContext = provider.readContext(contextRequest);
5212
+ let usedSessionBaselineDelta = false;
5213
+ if (sessionStartEvent && sessionId !== "unknown_session" && gitContext !== void 0) {
5214
+ baselineStore.write(sessionId, {
5215
+ ...repositoryPath !== void 0 ? { repositoryPath } : {},
5216
+ linesAdded: gitContext.linesAdded ?? 0,
5217
+ linesRemoved: gitContext.linesRemoved ?? 0,
5218
+ filesChanged: gitContext.filesChanged ?? [],
5219
+ capturedAt: now
5220
+ });
5221
+ }
5222
+ let linesAdded = gitContext?.linesAdded;
5223
+ let linesRemoved = gitContext?.linesRemoved;
5224
+ let filesChanged = gitContext?.filesChanged;
5225
+ if (sessionStartEvent) {
5226
+ linesAdded = void 0;
5227
+ linesRemoved = void 0;
5228
+ filesChanged = void 0;
5229
+ }
5230
+ if (sessionEndEvent && sessionId !== "unknown_session") {
5231
+ const baseline = baselineStore.read(sessionId);
5232
+ if (baseline !== void 0 && gitContext !== void 0) {
5233
+ const currentLinesAdded = gitContext.linesAdded ?? 0;
5234
+ const currentLinesRemoved = gitContext.linesRemoved ?? 0;
5235
+ linesAdded = Math.max(0, currentLinesAdded - baseline.linesAdded);
5236
+ linesRemoved = Math.max(0, currentLinesRemoved - baseline.linesRemoved);
5237
+ const currentFiles = gitContext.filesChanged ?? [];
5238
+ if (currentFiles.length > 0) {
5239
+ const baselineFiles = new Set(baseline.filesChanged);
5240
+ const deltaFiles = currentFiles.filter((filePath) => !baselineFiles.has(filePath));
5241
+ filesChanged = deltaFiles.length > 0 ? deltaFiles : currentFiles;
5242
+ }
5243
+ usedSessionBaselineDelta = true;
5244
+ }
5245
+ baselineStore.delete(sessionId);
5246
+ }
5247
+ const patch = {};
5248
+ if (isCommit) {
5249
+ patch["is_commit"] = true;
5250
+ }
5251
+ const commitMessageFromCommand = command === void 0 ? void 0 : parseCommitMessage2(command);
5252
+ const commitMessageFromGit = gitContext?.commitMessage;
5253
+ const commitMessage = commitMessageFromCommand ?? commitMessageFromGit;
5254
+ const existingCommitMessage = readString5(record, "commit_message") ?? readString5(record, "commitMessage");
5255
+ if (existingCommitMessage === void 0 && commitMessage !== void 0) {
5256
+ patch["commit_message"] = commitMessage;
5257
+ }
5258
+ const existingBranch = readString5(record, "git_branch") ?? readString5(record, "gitBranch");
5259
+ if (existingBranch === void 0 && gitContext?.branch !== void 0) {
5260
+ patch["git_branch"] = gitContext.branch;
5261
+ }
5262
+ const existingCommitSha = readString5(record, "commit_sha") ?? readString5(record, "commitSha");
5263
+ if (existingCommitSha === void 0 && gitContext?.headSha !== void 0) {
5264
+ patch["commit_sha"] = gitContext.headSha;
5265
+ }
5266
+ const existingLinesAdded = readNumber4(record, "lines_added") ?? readNumber4(record, "linesAdded");
5267
+ if (existingLinesAdded === void 0 && linesAdded !== void 0) {
5268
+ patch["lines_added"] = linesAdded;
5269
+ }
5270
+ const existingLinesRemoved = readNumber4(record, "lines_removed") ?? readNumber4(record, "linesRemoved");
5271
+ if (existingLinesRemoved === void 0 && linesRemoved !== void 0) {
5272
+ patch["lines_removed"] = linesRemoved;
5273
+ }
5274
+ const existingFilesChanged = readStringArray3(record, "files_changed") ?? readStringArray3(record, "filesChanged");
5275
+ if (existingFilesChanged === void 0 && filesChanged !== void 0) {
5276
+ patch["files_changed"] = filesChanged;
5277
+ }
5278
+ const prUrl = extractPrUrl(payload);
5279
+ if (prUrl !== void 0) {
5280
+ const existingPrUrl = readString5(record, "pr_url") ?? readString5(record, "prUrl");
5281
+ if (existingPrUrl === void 0) {
5282
+ patch["pr_url"] = prUrl;
5283
+ const parsed = parsePrFromUrl(prUrl);
5284
+ if (parsed !== void 0) {
5285
+ patch["pr_repo"] = parsed.repo;
5286
+ patch["pr_number"] = parsed.prNumber;
5287
+ }
5288
+ }
5289
+ }
5290
+ if (Object.keys(patch).length === 0) {
5291
+ return {
5292
+ payload,
5293
+ enriched: false,
5294
+ usedSessionBaselineDelta
5295
+ };
5296
+ }
5297
+ return {
5298
+ payload: {
5299
+ ...payload,
5300
+ ...patch
5301
+ },
5302
+ enriched: true,
5303
+ usedSessionBaselineDelta
5304
+ };
5305
+ }
5306
+ function toEnvelope(payload, privacyTier, now, extraAttributes = {}) {
5307
+ const promptId = pickPromptId2(payload);
5308
+ const eventType = pickEventType(payload);
5309
+ const envelope = {
5310
+ schemaVersion: "1.0",
5311
+ source: "hook",
5312
+ sourceVersion: "agent-trace-cli-v0.1",
5313
+ eventId: buildEventId3(payload, now),
5314
+ sessionId: pickSessionId(payload),
5315
+ ...promptId !== void 0 ? { promptId } : {},
5316
+ eventType,
5317
+ eventTimestamp: pickTimestamp(payload, now),
5318
+ ingestedAt: now,
5319
+ privacyTier,
5320
+ payload,
5321
+ attributes: {
5322
+ hook_name: eventType,
5323
+ ...extraAttributes
5324
+ }
5325
+ };
5326
+ return envelope;
5327
+ }
5328
+ function validateEnvelope(envelope) {
5329
+ const errors = [];
5330
+ if (envelope.schemaVersion !== "1.0") {
5331
+ errors.push("schemaVersion must equal 1.0");
5332
+ }
5333
+ if (envelope.source !== "hook") {
5334
+ errors.push("source must equal hook");
5335
+ }
5336
+ if (envelope.eventId.length === 0) {
5337
+ errors.push("eventId must be non-empty");
5338
+ }
5339
+ if (envelope.sessionId.length === 0) {
5340
+ errors.push("sessionId must be non-empty");
5341
+ }
5342
+ if (envelope.eventType.length === 0) {
5343
+ errors.push("eventType must be non-empty");
5344
+ }
5345
+ if (!isIsoDate2(envelope.eventTimestamp)) {
5346
+ errors.push("eventTimestamp must be ISO-8601");
5347
+ }
5348
+ if (!isIsoDate2(envelope.ingestedAt)) {
5349
+ errors.push("ingestedAt must be ISO-8601");
5350
+ }
5351
+ if (envelope.privacyTier !== 1 && envelope.privacyTier !== 2 && envelope.privacyTier !== 3) {
5352
+ errors.push("privacyTier must be 1, 2, or 3");
5353
+ }
5354
+ return errors;
5355
+ }
5356
+ function runHookHandler(input, store = new FileCliConfigStore(), gitContextProvider = new ShellHookGitContextProvider()) {
5357
+ let payload;
5358
+ try {
5359
+ payload = parseHookPayload(input.rawStdin);
5360
+ } catch {
5361
+ return {
5362
+ ok: false,
5363
+ errors: ["hook payload is not valid JSON"]
5364
+ };
5365
+ }
5366
+ if (payload === void 0) {
5367
+ return {
5368
+ ok: false,
5369
+ errors: ["hook payload is empty or invalid"]
5370
+ };
5371
+ }
5372
+ const now = input.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
5373
+ const privacyTier = getPrivacyTier(store, input.configDir);
5374
+ const baselineStore = new FileHookSessionBaselineStore(store, input.configDir);
5375
+ const enrichment = enrichHookPayloadWithGitContext(payload, gitContextProvider, baselineStore, now);
5376
+ const envelope = toEnvelope(enrichment.payload, privacyTier, now, {
5377
+ ...enrichment.enriched ? { git_enriched: "1" } : {},
5378
+ ...enrichment.usedSessionBaselineDelta ? { git_session_delta: "1" } : {}
5379
+ });
5380
+ const errors = validateEnvelope(envelope);
5381
+ if (errors.length > 0) {
5382
+ return {
5383
+ ok: false,
5384
+ errors
5385
+ };
5386
+ }
5387
+ return {
5388
+ ok: true,
5389
+ envelope
5390
+ };
5391
+ }
5392
+ var FetchCollectorHttpClient = class {
5393
+ async postJson(url, payload) {
5394
+ try {
5395
+ const response = await fetch(url, {
5396
+ method: "POST",
5397
+ headers: {
5398
+ "Content-Type": "application/json"
5399
+ },
5400
+ body: JSON.stringify(payload)
5401
+ });
5402
+ const body = await response.text();
5403
+ return {
5404
+ ok: true,
5405
+ statusCode: response.status,
5406
+ body
5407
+ };
5408
+ } catch (error) {
5409
+ return {
5410
+ ok: false,
5411
+ statusCode: 0,
5412
+ body: "",
5413
+ error: String(error)
5414
+ };
5415
+ }
5416
+ }
5417
+ };
5418
+ function isSuccessStatus(statusCode) {
5419
+ return statusCode >= 200 && statusCode < 300;
5420
+ }
5421
+ async function runHookHandlerAndForward(input, client = new FetchCollectorHttpClient(), store = new FileCliConfigStore(), gitContextProvider = new ShellHookGitContextProvider()) {
5422
+ const hookResult = runHookHandler(
5423
+ {
5424
+ rawStdin: input.rawStdin,
5425
+ ...input.configDir !== void 0 ? { configDir: input.configDir } : {},
5426
+ ...input.nowIso !== void 0 ? { nowIso: input.nowIso } : {}
5427
+ },
5428
+ store,
5429
+ gitContextProvider
5430
+ );
5431
+ if (!hookResult.ok) {
5432
+ return {
5433
+ ok: false,
5434
+ errors: hookResult.errors
5435
+ };
5436
+ }
5437
+ const collectorUrl = getCollectorUrl(store, input.configDir, input.collectorUrl);
5438
+ const postResult = await client.postJson(collectorUrl, hookResult.envelope);
5439
+ if (!postResult.ok) {
5440
+ return {
5441
+ ok: false,
5442
+ envelope: hookResult.envelope,
5443
+ errors: [postResult.error ?? "failed to send hook event to collector"]
5444
+ };
5445
+ }
5446
+ if (!isSuccessStatus(postResult.statusCode)) {
5447
+ return {
5448
+ ok: false,
5449
+ envelope: hookResult.envelope,
5450
+ statusCode: postResult.statusCode,
5451
+ errors: [
5452
+ `collector returned status ${String(postResult.statusCode)}`,
5453
+ ...postResult.body.length > 0 ? [postResult.body] : []
5454
+ ]
5455
+ };
5456
+ }
5457
+ return {
5458
+ ok: true,
5459
+ envelope: hookResult.envelope,
5460
+ collectorUrl,
5461
+ statusCode: postResult.statusCode,
5462
+ body: postResult.body
5463
+ };
5464
+ }
5465
+
4346
5466
  // packages/runtime/src/standalone-entry.ts
4347
5467
  function readNumberEnv(name, fallback) {
4348
5468
  const raw = process.env[name];
@@ -4358,11 +5478,77 @@ function readPrivacyTierEnv(name) {
4358
5478
  return 2;
4359
5479
  }
4360
5480
  function resolveDefaultSqlitePath() {
4361
- const dataDir = import_node_path3.default.join(import_node_os2.default.homedir(), ".agent-trace");
4362
- import_node_fs2.default.mkdirSync(dataDir, { recursive: true });
4363
- return import_node_path3.default.join(dataDir, "data.db");
5481
+ const dataDir = import_node_path5.default.join(import_node_os3.default.homedir(), ".agent-trace");
5482
+ import_node_fs5.default.mkdirSync(dataDir, { recursive: true });
5483
+ return import_node_path5.default.join(dataDir, "data.db");
4364
5484
  }
4365
- async function main() {
5485
+ async function readStdin() {
5486
+ return new Promise((resolve, reject) => {
5487
+ let buffer = "";
5488
+ process.stdin.setEncoding("utf8");
5489
+ process.stdin.on("data", (chunk) => {
5490
+ buffer += chunk;
5491
+ });
5492
+ process.stdin.on("end", () => resolve(buffer));
5493
+ process.stdin.on("error", (error) => reject(error));
5494
+ });
5495
+ }
5496
+ function printUsage() {
5497
+ process.stdout.write(
5498
+ "\nusage: agent-trace <command>\n\ncommands:\n (none) start the server (collector + api + dashboard)\n init configure Claude Code hooks\n status check if hooks are installed\n hook-handler process a hook event from stdin\n\noptions:\n --collector-url <url> collector endpoint (default: http://127.0.0.1:8317/v1/hooks)\n --privacy-tier <1|2|3> privacy tier (default: 2)\n --install-hooks install hooks into Claude settings (default for init)\n --no-install-hooks skip hook installation\n --forward forward hook event to collector (hook-handler)\n --config-dir <path> config directory (default: ~/.claude)\n\n"
5499
+ );
5500
+ }
5501
+ async function handleCliCommand(args) {
5502
+ const command = args.command;
5503
+ if (command === "init") {
5504
+ const result2 = runInit({
5505
+ ...args.configDir !== void 0 ? { configDir: args.configDir } : {},
5506
+ ...args.collectorUrl !== void 0 ? { collectorUrl: args.collectorUrl } : {},
5507
+ ...args.privacyTier !== void 0 ? { privacyTier: args.privacyTier } : {},
5508
+ ...args.installHooks !== void 0 ? { installHooks: args.installHooks } : {}
5509
+ });
5510
+ process.stdout.write(`${JSON.stringify(result2, null, 2)}
5511
+ `);
5512
+ return;
5513
+ }
5514
+ if (command === "status") {
5515
+ const result2 = runStatus(args.configDir);
5516
+ process.stdout.write(`${JSON.stringify(result2, null, 2)}
5517
+ `);
5518
+ process.exitCode = result2.ok ? 0 : 1;
5519
+ return;
5520
+ }
5521
+ const rawStdin = await readStdin();
5522
+ if (args.forward === true) {
5523
+ const result2 = await runHookHandlerAndForward({
5524
+ rawStdin,
5525
+ ...args.configDir !== void 0 ? { configDir: args.configDir } : {},
5526
+ ...args.collectorUrl !== void 0 ? { collectorUrl: args.collectorUrl } : {}
5527
+ });
5528
+ if (!result2.ok) {
5529
+ process.stderr.write(`${JSON.stringify(result2, null, 2)}
5530
+ `);
5531
+ process.exitCode = 1;
5532
+ return;
5533
+ }
5534
+ process.stdout.write(`${JSON.stringify(result2, null, 2)}
5535
+ `);
5536
+ return;
5537
+ }
5538
+ const result = runHookHandler({
5539
+ rawStdin,
5540
+ ...args.configDir !== void 0 ? { configDir: args.configDir } : {}
5541
+ });
5542
+ if (!result.ok) {
5543
+ process.stderr.write(`${JSON.stringify(result, null, 2)}
5544
+ `);
5545
+ process.exitCode = 1;
5546
+ return;
5547
+ }
5548
+ process.stdout.write(`${JSON.stringify(result.envelope, null, 2)}
5549
+ `);
5550
+ }
5551
+ async function startServer() {
4366
5552
  const host = process.env["RUNTIME_HOST"] ?? "127.0.0.1";
4367
5553
  const collectorPort = readNumberEnv("COLLECTOR_PORT", 8317);
4368
5554
  const apiPort = readNumberEnv("API_PORT", 8318);
@@ -4455,6 +5641,19 @@ async function main() {
4455
5641
  void shutdown();
4456
5642
  });
4457
5643
  }
5644
+ async function main() {
5645
+ const args = parseArgs(process.argv);
5646
+ const command = args.command;
5647
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
5648
+ printUsage();
5649
+ return;
5650
+ }
5651
+ if (command === "init" || command === "status" || command === "hook-handler") {
5652
+ await handleCliCommand(args);
5653
+ return;
5654
+ }
5655
+ await startServer();
5656
+ }
4458
5657
  void main().catch((error) => {
4459
5658
  process.stderr.write(`${String(error)}
4460
5659
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-trace",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Self-hosted observability for AI coding agents. One command, zero config.",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {