aisnitch 0.2.4 → 0.2.12

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/cli/index.js CHANGED
@@ -8,7 +8,7 @@ import { Command, InvalidArgumentError } from "commander";
8
8
 
9
9
  // src/package-info.ts
10
10
  var AISNITCH_PACKAGE_NAME = "aisnitch";
11
- var AISNITCH_VERSION = "0.2.3";
11
+ var AISNITCH_VERSION = "0.2.12";
12
12
  var AISNITCH_DESCRIPTION = "Universal bridge for AI coding tool activity \u2014 capture, normalize, stream.";
13
13
 
14
14
  // src/core/events/schema.ts
@@ -843,9 +843,15 @@ var FileToolSetupBase = class {
843
843
  var ClaudeCodeSetup = class extends FileToolSetupBase {
844
844
  settingsPath;
845
845
  hookUrl;
846
+ scriptPath;
846
847
  constructor(httpPort, dependencies = {}) {
847
848
  super("claude-code", dependencies.binaryExists);
848
849
  this.settingsPath = dependencies.claudeSettingsPath ?? join2(dependencies.homeDirectory ?? homedir2(), ".claude", "settings.json");
850
+ this.scriptPath = join2(
851
+ dependencies.homeDirectory ?? homedir2(),
852
+ ".claude",
853
+ "aisnitch-forward.mjs"
854
+ );
849
855
  this.hookUrl = `http://localhost:${httpPort}/hooks/claude-code`;
850
856
  }
851
857
  async detect() {
@@ -854,7 +860,49 @@ var ClaudeCodeSetup = class extends FileToolSetupBase {
854
860
  getConfigPath() {
855
861
  return this.settingsPath;
856
862
  }
863
+ async computeDiff() {
864
+ const currentSettingsContent = await readOptionalFile(this.settingsPath);
865
+ const currentScriptContent = await readOptionalFile(this.scriptPath);
866
+ const nextSettingsContent = this.buildNextSettingsContent(currentSettingsContent);
867
+ const nextScriptContent = buildClaudeForwardScriptSource();
868
+ return [
869
+ renderColoredDiff(
870
+ this.settingsPath,
871
+ currentSettingsContent,
872
+ nextSettingsContent
873
+ ),
874
+ "",
875
+ renderColoredDiff(
876
+ this.scriptPath,
877
+ currentScriptContent,
878
+ nextScriptContent
879
+ )
880
+ ].join("\n");
881
+ }
882
+ async apply() {
883
+ const currentSettingsContent = await readOptionalFile(this.settingsPath);
884
+ const currentScriptContent = await readOptionalFile(this.scriptPath);
885
+ const nextSettingsContent = this.buildNextSettingsContent(currentSettingsContent);
886
+ const nextScriptContent = buildClaudeForwardScriptSource();
887
+ await mkdir2(dirname2(this.settingsPath), { recursive: true });
888
+ await mkdir2(dirname2(this.scriptPath), { recursive: true });
889
+ if (currentSettingsContent !== null) {
890
+ await copyFile(this.settingsPath, this.getFileBackupPath(this.settingsPath));
891
+ }
892
+ if (currentScriptContent !== null) {
893
+ await copyFile(this.scriptPath, this.getFileBackupPath(this.scriptPath));
894
+ }
895
+ await writeFile2(this.settingsPath, nextSettingsContent, "utf8");
896
+ await writeFile2(this.scriptPath, nextScriptContent, "utf8");
897
+ }
898
+ async revert() {
899
+ await restoreBackupOrRemove(this.settingsPath);
900
+ await restoreBackupOrRemove(this.scriptPath);
901
+ }
857
902
  buildNextContent(currentContent) {
903
+ return Promise.resolve(this.buildNextSettingsContent(currentContent));
904
+ }
905
+ buildNextSettingsContent(currentContent) {
858
906
  const parsedSettings = parseClaudeSettings(currentContent);
859
907
  const currentHooks = parsedSettings.hooks ?? {};
860
908
  const nextHooks = {
@@ -864,15 +912,19 @@ var ClaudeCodeSetup = class extends FileToolSetupBase {
864
912
  nextHooks[hookEventName] = ensureClaudeAISnitchHook(
865
913
  currentHooks[hookEventName] ?? [],
866
914
  hookEventName,
867
- this.hookUrl
915
+ this.hookUrl,
916
+ this.scriptPath
868
917
  );
869
918
  }
870
919
  const nextSettings = {
871
920
  ...parsedSettings,
872
921
  hooks: nextHooks
873
922
  };
874
- return Promise.resolve(`${JSON.stringify(nextSettings, null, 2)}
875
- `);
923
+ return `${JSON.stringify(nextSettings, null, 2)}
924
+ `;
925
+ }
926
+ getFileBackupPath(path) {
927
+ return `${path}.bak`;
876
928
  }
877
929
  };
878
930
  var OpenCodeSetup = class extends FileToolSetupBase {
@@ -1676,7 +1728,7 @@ async function defaultConfirm(_diff, prompt) {
1676
1728
  readlineInterface.close();
1677
1729
  }
1678
1730
  }
1679
- function ensureClaudeAISnitchHook(groups, hookEventName, hookUrl) {
1731
+ function ensureClaudeAISnitchHook(groups, hookEventName, hookUrl, scriptPath) {
1680
1732
  const clonedGroups = groups.map((group) => ({
1681
1733
  ...group,
1682
1734
  hooks: group.hooks.map((handler) => ({ ...handler }))
@@ -1684,35 +1736,113 @@ function ensureClaudeAISnitchHook(groups, hookEventName, hookUrl) {
1684
1736
  const matcher = hookEventName === "FileChanged" ? CLAUDE_FILE_CHANGED_MATCHER : void 0;
1685
1737
  const matchingGroup = clonedGroups.find((group) => group.matcher === matcher);
1686
1738
  if (matchingGroup) {
1687
- const existingHook = matchingGroup.hooks.find((handler) => isClaudeAISnitchHook(handler, hookUrl));
1739
+ matchingGroup.hooks = matchingGroup.hooks.filter(
1740
+ (handler) => !isLegacyClaudeAISnitchHttpHook(handler, hookUrl)
1741
+ );
1742
+ const existingHook = matchingGroup.hooks.find(
1743
+ (handler) => isClaudeAISnitchHook(handler, hookEventName, hookUrl, scriptPath)
1744
+ );
1688
1745
  if (existingHook) {
1689
1746
  if (existingHook.async !== true) {
1690
1747
  existingHook.async = true;
1691
1748
  }
1692
1749
  return clonedGroups;
1693
1750
  }
1694
- matchingGroup.hooks.push(createClaudeAISnitchHook(hookUrl));
1751
+ matchingGroup.hooks.push(
1752
+ createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath)
1753
+ );
1695
1754
  return clonedGroups;
1696
1755
  }
1697
1756
  const nextGroup = matcher === void 0 ? {
1698
- hooks: [createClaudeAISnitchHook(hookUrl)]
1757
+ hooks: [createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath)]
1699
1758
  } : {
1700
1759
  matcher,
1701
- hooks: [createClaudeAISnitchHook(hookUrl)]
1760
+ hooks: [createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath)]
1702
1761
  };
1703
1762
  clonedGroups.push(nextGroup);
1704
1763
  return clonedGroups;
1705
1764
  }
1706
- function createClaudeAISnitchHook(hookUrl) {
1765
+ function createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath) {
1707
1766
  return {
1708
1767
  async: true,
1709
- type: "http",
1710
- url: hookUrl
1768
+ command: buildClaudeForwardCommand(hookEventName, hookUrl, scriptPath),
1769
+ type: "command"
1711
1770
  };
1712
1771
  }
1713
- function isClaudeAISnitchHook(handler, hookUrl) {
1772
+ function isClaudeAISnitchHook(handler, hookEventName, hookUrl, scriptPath) {
1773
+ return handler.type === "command" && typeof handler.command === "string" && handler.command === buildClaudeForwardCommand(hookEventName, hookUrl, scriptPath);
1774
+ }
1775
+ function isLegacyClaudeAISnitchHttpHook(handler, hookUrl) {
1714
1776
  return handler.type === "http" && handler.url === hookUrl;
1715
1777
  }
1778
+ function buildClaudeForwardCommand(hookEventName, hookUrl, scriptPath) {
1779
+ return `node ${shellEscapeSingle(scriptPath)} ${hookEventName} ${shellEscapeSingle(hookUrl)}`;
1780
+ }
1781
+ function buildClaudeForwardScriptSource() {
1782
+ return `#!/usr/bin/env node
1783
+ /**
1784
+ * AISnitch Claude Code hook bridge
1785
+ *
1786
+ * \u{1F4D6} Claude Code currently exposes command hooks fed through stdin JSON.
1787
+ * This bridge keeps the Claude config valid while still forwarding every
1788
+ * selected hook event into the local AISnitch HTTP receiver.
1789
+ */
1790
+
1791
+ function isRecord(value) {
1792
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1793
+ }
1794
+
1795
+ async function readInput() {
1796
+ const chunks = [];
1797
+
1798
+ for await (const chunk of process.stdin) {
1799
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
1800
+ }
1801
+
1802
+ return chunks.join("");
1803
+ }
1804
+
1805
+ async function main() {
1806
+ const hookEventName = process.argv[2] ?? "unknown";
1807
+ const endpoint = process.argv[3] ?? "http://localhost:4821/hooks/claude-code";
1808
+ const rawInput = await readInput();
1809
+ let payload = {};
1810
+
1811
+ if (rawInput.trim().length > 0) {
1812
+ try {
1813
+ const parsedPayload = JSON.parse(rawInput);
1814
+
1815
+ if (isRecord(parsedPayload)) {
1816
+ payload = parsedPayload;
1817
+ }
1818
+ } catch {
1819
+ payload = {
1820
+ raw: rawInput
1821
+ };
1822
+ }
1823
+ }
1824
+
1825
+ try {
1826
+ await fetch(endpoint, {
1827
+ method: "POST",
1828
+ headers: {
1829
+ "content-type": "application/json"
1830
+ },
1831
+ body: JSON.stringify({
1832
+ ...payload,
1833
+ hook_event_name: hookEventName
1834
+ })
1835
+ });
1836
+ } catch {
1837
+ // Claude hooks must stay fire-and-forget for observability only.
1838
+ }
1839
+ }
1840
+
1841
+ void main().catch(() => {
1842
+ // Never bubble hook bridge failures back into Claude Code itself.
1843
+ });
1844
+ `;
1845
+ }
1716
1846
  function ensureGeminiAISnitchHook(groups, hookUrl) {
1717
1847
  const clonedGroups = groups.map((group) => ({
1718
1848
  ...group,
@@ -2227,6 +2357,9 @@ async function readOptionalFile(path) {
2227
2357
  throw error;
2228
2358
  }
2229
2359
  }
2360
+ function shellEscapeSingle(value) {
2361
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
2362
+ }
2230
2363
  async function restoreBackupOrRemove(path) {
2231
2364
  const backupPath = `${path}.bak`;
2232
2365
  if (await fileExists(backupPath)) {
@@ -5006,6 +5139,9 @@ var ClaudeCodeAdapter = class extends BaseAdapter {
5006
5139
  });
5007
5140
  const context = {
5008
5141
  cwd: getString(payload, "cwd"),
5142
+ // 📖 Pass process.env so the context detector can detect the terminal
5143
+ // from TERM_PROGRAM, ITERM_SESSION_ID, etc. — hooks don't carry env vars
5144
+ env: this.env ?? process.env,
5009
5145
  hookPayload: payload,
5010
5146
  pid: getNumber(payload, "pid"),
5011
5147
  sessionId,
@@ -5239,6 +5375,8 @@ function extractClaudeTranscriptObservations(payload, transcriptPath) {
5239
5375
  const tokensUsed = extractTokenUsage(payload);
5240
5376
  const rawPayload = payload;
5241
5377
  const sharedContext = {
5378
+ // 📖 Pass process.env so terminal detection works from transcript path too
5379
+ env: process.env,
5242
5380
  hookPayload: rawPayload,
5243
5381
  sessionId,
5244
5382
  source: "aisnitch://adapters/claude-code/transcript",
@@ -8895,6 +9033,8 @@ var OpenCodeAdapter = class extends BaseAdapter {
8895
9033
  });
8896
9034
  const context = {
8897
9035
  cwd: extractOpenCodeCwd(payload),
9036
+ // 📖 Pass process.env so the context detector can detect the terminal
9037
+ env: this.env ?? process.env,
8898
9038
  hookPayload: payload,
8899
9039
  pid: getNumber5(payload, "pid"),
8900
9040
  sessionId,
@@ -8905,6 +9045,8 @@ var OpenCodeAdapter = class extends BaseAdapter {
8905
9045
  cwd: context.cwd,
8906
9046
  errorMessage: extractOpenCodeErrorMessage(payload),
8907
9047
  errorType: extractOpenCodeErrorType(payload),
9048
+ // 📖 Extract model from payload — OpenCode may send it as "model" or nested in properties
9049
+ model: getString7(payload, "model") ?? getString7(getRecord6(payload.properties), "model"),
8908
9050
  project: extractOpenCodeProject(payload),
8909
9051
  raw: payload,
8910
9052
  toolInput: extractOpenCodeToolInput(payload),
@@ -9504,6 +9646,7 @@ var Pipeline = class {
9504
9646
 
9505
9647
  // src/tui/index.tsx
9506
9648
  import { render } from "ink";
9649
+ import { withFullScreen } from "fullscreen-ink";
9507
9650
  import WebSocket4 from "ws";
9508
9651
 
9509
9652
  // src/tui/App.tsx
@@ -11412,7 +11555,7 @@ function attachEventBusMonitor(eventBus, output) {
11412
11555
  // src/tui/index.tsx
11413
11556
  import { jsx as jsx13 } from "react/jsx-runtime";
11414
11557
  async function renderForegroundTui(options) {
11415
- const app = render(
11558
+ const ink = withFullScreen(
11416
11559
  /* @__PURE__ */ jsx13(
11417
11560
  App,
11418
11561
  {
@@ -11434,7 +11577,8 @@ async function renderForegroundTui(options) {
11434
11577
  }
11435
11578
  )
11436
11579
  );
11437
- await app.waitUntilExit();
11580
+ await ink.start();
11581
+ await ink.waitUntilExit;
11438
11582
  }
11439
11583
  async function renderManagedTui(options) {
11440
11584
  const app = render(