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/README.md CHANGED
@@ -220,7 +220,7 @@ flowchart LR
220
220
 
221
221
  | Tool | Strategy | Setup |
222
222
  |---|---|---|
223
- | **Claude Code** | HTTP hooks + JSONL transcript watching + process detection | `aisnitch setup claude-code` |
223
+ | **Claude Code** | Command hooks + JSONL transcript watching + process detection | `aisnitch setup claude-code` |
224
224
  | **OpenCode** | Local plugin + process detection | `aisnitch setup opencode` |
225
225
  | **Gemini CLI** | Command hooks + `logs.json` watching + process detection | `aisnitch setup gemini-cli` |
226
226
  | **Codex** | `codex-tui.log` parsing + process detection | `aisnitch setup codex` |
@@ -41,7 +41,7 @@ var import_commander = require("commander");
41
41
 
42
42
  // src/package-info.ts
43
43
  var AISNITCH_PACKAGE_NAME = "aisnitch";
44
- var AISNITCH_VERSION = "0.2.3";
44
+ var AISNITCH_VERSION = "0.2.12";
45
45
  var AISNITCH_DESCRIPTION = "Universal bridge for AI coding tool activity \u2014 capture, normalize, stream.";
46
46
 
47
47
  // src/core/events/schema.ts
@@ -876,9 +876,15 @@ var FileToolSetupBase = class {
876
876
  var ClaudeCodeSetup = class extends FileToolSetupBase {
877
877
  settingsPath;
878
878
  hookUrl;
879
+ scriptPath;
879
880
  constructor(httpPort, dependencies = {}) {
880
881
  super("claude-code", dependencies.binaryExists);
881
882
  this.settingsPath = dependencies.claudeSettingsPath ?? (0, import_node_path2.join)(dependencies.homeDirectory ?? (0, import_node_os2.homedir)(), ".claude", "settings.json");
883
+ this.scriptPath = (0, import_node_path2.join)(
884
+ dependencies.homeDirectory ?? (0, import_node_os2.homedir)(),
885
+ ".claude",
886
+ "aisnitch-forward.mjs"
887
+ );
882
888
  this.hookUrl = `http://localhost:${httpPort}/hooks/claude-code`;
883
889
  }
884
890
  async detect() {
@@ -887,7 +893,49 @@ var ClaudeCodeSetup = class extends FileToolSetupBase {
887
893
  getConfigPath() {
888
894
  return this.settingsPath;
889
895
  }
896
+ async computeDiff() {
897
+ const currentSettingsContent = await readOptionalFile(this.settingsPath);
898
+ const currentScriptContent = await readOptionalFile(this.scriptPath);
899
+ const nextSettingsContent = this.buildNextSettingsContent(currentSettingsContent);
900
+ const nextScriptContent = buildClaudeForwardScriptSource();
901
+ return [
902
+ renderColoredDiff(
903
+ this.settingsPath,
904
+ currentSettingsContent,
905
+ nextSettingsContent
906
+ ),
907
+ "",
908
+ renderColoredDiff(
909
+ this.scriptPath,
910
+ currentScriptContent,
911
+ nextScriptContent
912
+ )
913
+ ].join("\n");
914
+ }
915
+ async apply() {
916
+ const currentSettingsContent = await readOptionalFile(this.settingsPath);
917
+ const currentScriptContent = await readOptionalFile(this.scriptPath);
918
+ const nextSettingsContent = this.buildNextSettingsContent(currentSettingsContent);
919
+ const nextScriptContent = buildClaudeForwardScriptSource();
920
+ await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(this.settingsPath), { recursive: true });
921
+ await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(this.scriptPath), { recursive: true });
922
+ if (currentSettingsContent !== null) {
923
+ await (0, import_promises2.copyFile)(this.settingsPath, this.getFileBackupPath(this.settingsPath));
924
+ }
925
+ if (currentScriptContent !== null) {
926
+ await (0, import_promises2.copyFile)(this.scriptPath, this.getFileBackupPath(this.scriptPath));
927
+ }
928
+ await (0, import_promises2.writeFile)(this.settingsPath, nextSettingsContent, "utf8");
929
+ await (0, import_promises2.writeFile)(this.scriptPath, nextScriptContent, "utf8");
930
+ }
931
+ async revert() {
932
+ await restoreBackupOrRemove(this.settingsPath);
933
+ await restoreBackupOrRemove(this.scriptPath);
934
+ }
890
935
  buildNextContent(currentContent) {
936
+ return Promise.resolve(this.buildNextSettingsContent(currentContent));
937
+ }
938
+ buildNextSettingsContent(currentContent) {
891
939
  const parsedSettings = parseClaudeSettings(currentContent);
892
940
  const currentHooks = parsedSettings.hooks ?? {};
893
941
  const nextHooks = {
@@ -897,15 +945,19 @@ var ClaudeCodeSetup = class extends FileToolSetupBase {
897
945
  nextHooks[hookEventName] = ensureClaudeAISnitchHook(
898
946
  currentHooks[hookEventName] ?? [],
899
947
  hookEventName,
900
- this.hookUrl
948
+ this.hookUrl,
949
+ this.scriptPath
901
950
  );
902
951
  }
903
952
  const nextSettings = {
904
953
  ...parsedSettings,
905
954
  hooks: nextHooks
906
955
  };
907
- return Promise.resolve(`${JSON.stringify(nextSettings, null, 2)}
908
- `);
956
+ return `${JSON.stringify(nextSettings, null, 2)}
957
+ `;
958
+ }
959
+ getFileBackupPath(path) {
960
+ return `${path}.bak`;
909
961
  }
910
962
  };
911
963
  var OpenCodeSetup = class extends FileToolSetupBase {
@@ -1709,7 +1761,7 @@ async function defaultConfirm(_diff, prompt) {
1709
1761
  readlineInterface.close();
1710
1762
  }
1711
1763
  }
1712
- function ensureClaudeAISnitchHook(groups, hookEventName, hookUrl) {
1764
+ function ensureClaudeAISnitchHook(groups, hookEventName, hookUrl, scriptPath) {
1713
1765
  const clonedGroups = groups.map((group) => ({
1714
1766
  ...group,
1715
1767
  hooks: group.hooks.map((handler) => ({ ...handler }))
@@ -1717,35 +1769,113 @@ function ensureClaudeAISnitchHook(groups, hookEventName, hookUrl) {
1717
1769
  const matcher = hookEventName === "FileChanged" ? CLAUDE_FILE_CHANGED_MATCHER : void 0;
1718
1770
  const matchingGroup = clonedGroups.find((group) => group.matcher === matcher);
1719
1771
  if (matchingGroup) {
1720
- const existingHook = matchingGroup.hooks.find((handler) => isClaudeAISnitchHook(handler, hookUrl));
1772
+ matchingGroup.hooks = matchingGroup.hooks.filter(
1773
+ (handler) => !isLegacyClaudeAISnitchHttpHook(handler, hookUrl)
1774
+ );
1775
+ const existingHook = matchingGroup.hooks.find(
1776
+ (handler) => isClaudeAISnitchHook(handler, hookEventName, hookUrl, scriptPath)
1777
+ );
1721
1778
  if (existingHook) {
1722
1779
  if (existingHook.async !== true) {
1723
1780
  existingHook.async = true;
1724
1781
  }
1725
1782
  return clonedGroups;
1726
1783
  }
1727
- matchingGroup.hooks.push(createClaudeAISnitchHook(hookUrl));
1784
+ matchingGroup.hooks.push(
1785
+ createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath)
1786
+ );
1728
1787
  return clonedGroups;
1729
1788
  }
1730
1789
  const nextGroup = matcher === void 0 ? {
1731
- hooks: [createClaudeAISnitchHook(hookUrl)]
1790
+ hooks: [createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath)]
1732
1791
  } : {
1733
1792
  matcher,
1734
- hooks: [createClaudeAISnitchHook(hookUrl)]
1793
+ hooks: [createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath)]
1735
1794
  };
1736
1795
  clonedGroups.push(nextGroup);
1737
1796
  return clonedGroups;
1738
1797
  }
1739
- function createClaudeAISnitchHook(hookUrl) {
1798
+ function createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath) {
1740
1799
  return {
1741
1800
  async: true,
1742
- type: "http",
1743
- url: hookUrl
1801
+ command: buildClaudeForwardCommand(hookEventName, hookUrl, scriptPath),
1802
+ type: "command"
1744
1803
  };
1745
1804
  }
1746
- function isClaudeAISnitchHook(handler, hookUrl) {
1805
+ function isClaudeAISnitchHook(handler, hookEventName, hookUrl, scriptPath) {
1806
+ return handler.type === "command" && typeof handler.command === "string" && handler.command === buildClaudeForwardCommand(hookEventName, hookUrl, scriptPath);
1807
+ }
1808
+ function isLegacyClaudeAISnitchHttpHook(handler, hookUrl) {
1747
1809
  return handler.type === "http" && handler.url === hookUrl;
1748
1810
  }
1811
+ function buildClaudeForwardCommand(hookEventName, hookUrl, scriptPath) {
1812
+ return `node ${shellEscapeSingle(scriptPath)} ${hookEventName} ${shellEscapeSingle(hookUrl)}`;
1813
+ }
1814
+ function buildClaudeForwardScriptSource() {
1815
+ return `#!/usr/bin/env node
1816
+ /**
1817
+ * AISnitch Claude Code hook bridge
1818
+ *
1819
+ * \u{1F4D6} Claude Code currently exposes command hooks fed through stdin JSON.
1820
+ * This bridge keeps the Claude config valid while still forwarding every
1821
+ * selected hook event into the local AISnitch HTTP receiver.
1822
+ */
1823
+
1824
+ function isRecord(value) {
1825
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1826
+ }
1827
+
1828
+ async function readInput() {
1829
+ const chunks = [];
1830
+
1831
+ for await (const chunk of process.stdin) {
1832
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
1833
+ }
1834
+
1835
+ return chunks.join("");
1836
+ }
1837
+
1838
+ async function main() {
1839
+ const hookEventName = process.argv[2] ?? "unknown";
1840
+ const endpoint = process.argv[3] ?? "http://localhost:4821/hooks/claude-code";
1841
+ const rawInput = await readInput();
1842
+ let payload = {};
1843
+
1844
+ if (rawInput.trim().length > 0) {
1845
+ try {
1846
+ const parsedPayload = JSON.parse(rawInput);
1847
+
1848
+ if (isRecord(parsedPayload)) {
1849
+ payload = parsedPayload;
1850
+ }
1851
+ } catch {
1852
+ payload = {
1853
+ raw: rawInput
1854
+ };
1855
+ }
1856
+ }
1857
+
1858
+ try {
1859
+ await fetch(endpoint, {
1860
+ method: "POST",
1861
+ headers: {
1862
+ "content-type": "application/json"
1863
+ },
1864
+ body: JSON.stringify({
1865
+ ...payload,
1866
+ hook_event_name: hookEventName
1867
+ })
1868
+ });
1869
+ } catch {
1870
+ // Claude hooks must stay fire-and-forget for observability only.
1871
+ }
1872
+ }
1873
+
1874
+ void main().catch(() => {
1875
+ // Never bubble hook bridge failures back into Claude Code itself.
1876
+ });
1877
+ `;
1878
+ }
1749
1879
  function ensureGeminiAISnitchHook(groups, hookUrl) {
1750
1880
  const clonedGroups = groups.map((group) => ({
1751
1881
  ...group,
@@ -2260,6 +2390,9 @@ async function readOptionalFile(path) {
2260
2390
  throw error;
2261
2391
  }
2262
2392
  }
2393
+ function shellEscapeSingle(value) {
2394
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
2395
+ }
2263
2396
  async function restoreBackupOrRemove(path) {
2264
2397
  const backupPath = `${path}.bak`;
2265
2398
  if (await fileExists(backupPath)) {
@@ -5037,6 +5170,9 @@ var ClaudeCodeAdapter = class extends BaseAdapter {
5037
5170
  });
5038
5171
  const context = {
5039
5172
  cwd: getString(payload, "cwd"),
5173
+ // 📖 Pass process.env so the context detector can detect the terminal
5174
+ // from TERM_PROGRAM, ITERM_SESSION_ID, etc. — hooks don't carry env vars
5175
+ env: this.env ?? process.env,
5040
5176
  hookPayload: payload,
5041
5177
  pid: getNumber(payload, "pid"),
5042
5178
  sessionId,
@@ -5270,6 +5406,8 @@ function extractClaudeTranscriptObservations(payload, transcriptPath) {
5270
5406
  const tokensUsed = extractTokenUsage(payload);
5271
5407
  const rawPayload = payload;
5272
5408
  const sharedContext = {
5409
+ // 📖 Pass process.env so terminal detection works from transcript path too
5410
+ env: process.env,
5273
5411
  hookPayload: rawPayload,
5274
5412
  sessionId,
5275
5413
  source: "aisnitch://adapters/claude-code/transcript",
@@ -8926,6 +9064,8 @@ var OpenCodeAdapter = class extends BaseAdapter {
8926
9064
  });
8927
9065
  const context = {
8928
9066
  cwd: extractOpenCodeCwd(payload),
9067
+ // 📖 Pass process.env so the context detector can detect the terminal
9068
+ env: this.env ?? process.env,
8929
9069
  hookPayload: payload,
8930
9070
  pid: getNumber5(payload, "pid"),
8931
9071
  sessionId,
@@ -8936,6 +9076,8 @@ var OpenCodeAdapter = class extends BaseAdapter {
8936
9076
  cwd: context.cwd,
8937
9077
  errorMessage: extractOpenCodeErrorMessage(payload),
8938
9078
  errorType: extractOpenCodeErrorType(payload),
9079
+ // 📖 Extract model from payload — OpenCode may send it as "model" or nested in properties
9080
+ model: getString7(payload, "model") ?? getString7(getRecord6(payload.properties), "model"),
8939
9081
  project: extractOpenCodeProject(payload),
8940
9082
  raw: payload,
8941
9083
  toolInput: extractOpenCodeToolInput(payload),
@@ -9535,6 +9677,7 @@ var Pipeline = class {
9535
9677
 
9536
9678
  // src/tui/index.tsx
9537
9679
  var import_ink13 = require("ink");
9680
+ var import_fullscreen_ink = require("fullscreen-ink");
9538
9681
  var import_ws4 = __toESM(require("ws"), 1);
9539
9682
 
9540
9683
  // src/tui/App.tsx
@@ -11443,7 +11586,7 @@ function attachEventBusMonitor(eventBus, output) {
11443
11586
  // src/tui/index.tsx
11444
11587
  var import_jsx_runtime13 = require("react/jsx-runtime");
11445
11588
  async function renderForegroundTui(options) {
11446
- const app = (0, import_ink13.render)(
11589
+ const ink = (0, import_fullscreen_ink.withFullScreen)(
11447
11590
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
11448
11591
  App,
11449
11592
  {
@@ -11465,7 +11608,8 @@ async function renderForegroundTui(options) {
11465
11608
  }
11466
11609
  )
11467
11610
  );
11468
- await app.waitUntilExit();
11611
+ await ink.start();
11612
+ await ink.waitUntilExit;
11469
11613
  }
11470
11614
  async function renderManagedTui(options) {
11471
11615
  const app = (0, import_ink13.render)(