aisnitch 0.2.5 → 0.2.13

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
@@ -756,7 +756,6 @@ var ANSI_RESET = "\x1B[0m";
756
756
  var ANSI_RED = "\x1B[31m";
757
757
  var ANSI_GREEN = "\x1B[32m";
758
758
  var ANSI_CYAN = "\x1B[36m";
759
- var CLAUDE_FILE_CHANGED_MATCHER = ".*";
760
759
  var CLAUDE_CODE_HOOK_EVENTS = [
761
760
  "SessionStart",
762
761
  "UserPromptSubmit",
@@ -767,15 +766,12 @@ var CLAUDE_CODE_HOOK_EVENTS = [
767
766
  "Notification",
768
767
  "SubagentStart",
769
768
  "SubagentStop",
770
- "TaskCreated",
771
769
  "TaskCompleted",
772
770
  "Stop",
773
771
  "StopFailure",
774
772
  "TeammateIdle",
775
773
  "InstructionsLoaded",
776
774
  "ConfigChange",
777
- "CwdChanged",
778
- "FileChanged",
779
775
  "WorktreeCreate",
780
776
  "WorktreeRemove",
781
777
  "PreCompact",
@@ -876,9 +872,15 @@ var FileToolSetupBase = class {
876
872
  var ClaudeCodeSetup = class extends FileToolSetupBase {
877
873
  settingsPath;
878
874
  hookUrl;
875
+ scriptPath;
879
876
  constructor(httpPort, dependencies = {}) {
880
877
  super("claude-code", dependencies.binaryExists);
881
878
  this.settingsPath = dependencies.claudeSettingsPath ?? (0, import_node_path2.join)(dependencies.homeDirectory ?? (0, import_node_os2.homedir)(), ".claude", "settings.json");
879
+ this.scriptPath = (0, import_node_path2.join)(
880
+ dependencies.homeDirectory ?? (0, import_node_os2.homedir)(),
881
+ ".claude",
882
+ "aisnitch-forward.mjs"
883
+ );
882
884
  this.hookUrl = `http://localhost:${httpPort}/hooks/claude-code`;
883
885
  }
884
886
  async detect() {
@@ -887,7 +889,49 @@ var ClaudeCodeSetup = class extends FileToolSetupBase {
887
889
  getConfigPath() {
888
890
  return this.settingsPath;
889
891
  }
892
+ async computeDiff() {
893
+ const currentSettingsContent = await readOptionalFile(this.settingsPath);
894
+ const currentScriptContent = await readOptionalFile(this.scriptPath);
895
+ const nextSettingsContent = this.buildNextSettingsContent(currentSettingsContent);
896
+ const nextScriptContent = buildClaudeForwardScriptSource();
897
+ return [
898
+ renderColoredDiff(
899
+ this.settingsPath,
900
+ currentSettingsContent,
901
+ nextSettingsContent
902
+ ),
903
+ "",
904
+ renderColoredDiff(
905
+ this.scriptPath,
906
+ currentScriptContent,
907
+ nextScriptContent
908
+ )
909
+ ].join("\n");
910
+ }
911
+ async apply() {
912
+ const currentSettingsContent = await readOptionalFile(this.settingsPath);
913
+ const currentScriptContent = await readOptionalFile(this.scriptPath);
914
+ const nextSettingsContent = this.buildNextSettingsContent(currentSettingsContent);
915
+ const nextScriptContent = buildClaudeForwardScriptSource();
916
+ await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(this.settingsPath), { recursive: true });
917
+ await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(this.scriptPath), { recursive: true });
918
+ if (currentSettingsContent !== null) {
919
+ await (0, import_promises2.copyFile)(this.settingsPath, this.getFileBackupPath(this.settingsPath));
920
+ }
921
+ if (currentScriptContent !== null) {
922
+ await (0, import_promises2.copyFile)(this.scriptPath, this.getFileBackupPath(this.scriptPath));
923
+ }
924
+ await (0, import_promises2.writeFile)(this.settingsPath, nextSettingsContent, "utf8");
925
+ await (0, import_promises2.writeFile)(this.scriptPath, nextScriptContent, "utf8");
926
+ }
927
+ async revert() {
928
+ await restoreBackupOrRemove(this.settingsPath);
929
+ await restoreBackupOrRemove(this.scriptPath);
930
+ }
890
931
  buildNextContent(currentContent) {
932
+ return Promise.resolve(this.buildNextSettingsContent(currentContent));
933
+ }
934
+ buildNextSettingsContent(currentContent) {
891
935
  const parsedSettings = parseClaudeSettings(currentContent);
892
936
  const currentHooks = parsedSettings.hooks ?? {};
893
937
  const nextHooks = {
@@ -897,15 +941,19 @@ var ClaudeCodeSetup = class extends FileToolSetupBase {
897
941
  nextHooks[hookEventName] = ensureClaudeAISnitchHook(
898
942
  currentHooks[hookEventName] ?? [],
899
943
  hookEventName,
900
- this.hookUrl
944
+ this.hookUrl,
945
+ this.scriptPath
901
946
  );
902
947
  }
903
948
  const nextSettings = {
904
949
  ...parsedSettings,
905
950
  hooks: nextHooks
906
951
  };
907
- return Promise.resolve(`${JSON.stringify(nextSettings, null, 2)}
908
- `);
952
+ return `${JSON.stringify(nextSettings, null, 2)}
953
+ `;
954
+ }
955
+ getFileBackupPath(path) {
956
+ return `${path}.bak`;
909
957
  }
910
958
  };
911
959
  var OpenCodeSetup = class extends FileToolSetupBase {
@@ -1709,43 +1757,116 @@ async function defaultConfirm(_diff, prompt) {
1709
1757
  readlineInterface.close();
1710
1758
  }
1711
1759
  }
1712
- function ensureClaudeAISnitchHook(groups, hookEventName, hookUrl) {
1760
+ function ensureClaudeAISnitchHook(groups, hookEventName, hookUrl, scriptPath) {
1713
1761
  const clonedGroups = groups.map((group) => ({
1714
1762
  ...group,
1715
1763
  hooks: group.hooks.map((handler) => ({ ...handler }))
1716
1764
  }));
1717
- const matcher = hookEventName === "FileChanged" ? CLAUDE_FILE_CHANGED_MATCHER : void 0;
1718
- const matchingGroup = clonedGroups.find((group) => group.matcher === matcher);
1765
+ const matchingGroup = clonedGroups.find((group) => group.matcher === void 0);
1719
1766
  if (matchingGroup) {
1720
- const existingHook = matchingGroup.hooks.find((handler) => isClaudeAISnitchHook(handler, hookUrl));
1767
+ matchingGroup.hooks = matchingGroup.hooks.filter(
1768
+ (handler) => !isLegacyClaudeAISnitchHttpHook(handler, hookUrl)
1769
+ );
1770
+ const existingHook = matchingGroup.hooks.find(
1771
+ (handler) => isClaudeAISnitchHook(handler, hookEventName, hookUrl, scriptPath)
1772
+ );
1721
1773
  if (existingHook) {
1722
1774
  if (existingHook.async !== true) {
1723
1775
  existingHook.async = true;
1724
1776
  }
1725
1777
  return clonedGroups;
1726
1778
  }
1727
- matchingGroup.hooks.push(createClaudeAISnitchHook(hookUrl));
1779
+ matchingGroup.hooks.push(
1780
+ createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath)
1781
+ );
1728
1782
  return clonedGroups;
1729
1783
  }
1730
- const nextGroup = matcher === void 0 ? {
1731
- hooks: [createClaudeAISnitchHook(hookUrl)]
1732
- } : {
1733
- matcher,
1734
- hooks: [createClaudeAISnitchHook(hookUrl)]
1735
- };
1736
- clonedGroups.push(nextGroup);
1784
+ clonedGroups.push({
1785
+ hooks: [createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath)]
1786
+ });
1737
1787
  return clonedGroups;
1738
1788
  }
1739
- function createClaudeAISnitchHook(hookUrl) {
1789
+ function createClaudeAISnitchHook(hookEventName, hookUrl, scriptPath) {
1740
1790
  return {
1741
1791
  async: true,
1742
- type: "http",
1743
- url: hookUrl
1792
+ command: buildClaudeForwardCommand(hookEventName, hookUrl, scriptPath),
1793
+ type: "command"
1744
1794
  };
1745
1795
  }
1746
- function isClaudeAISnitchHook(handler, hookUrl) {
1796
+ function isClaudeAISnitchHook(handler, hookEventName, hookUrl, scriptPath) {
1797
+ return handler.type === "command" && typeof handler.command === "string" && handler.command === buildClaudeForwardCommand(hookEventName, hookUrl, scriptPath);
1798
+ }
1799
+ function isLegacyClaudeAISnitchHttpHook(handler, hookUrl) {
1747
1800
  return handler.type === "http" && handler.url === hookUrl;
1748
1801
  }
1802
+ function buildClaudeForwardCommand(hookEventName, hookUrl, scriptPath) {
1803
+ return `node ${shellEscapeSingle(scriptPath)} ${hookEventName} ${shellEscapeSingle(hookUrl)}`;
1804
+ }
1805
+ function buildClaudeForwardScriptSource() {
1806
+ return `#!/usr/bin/env node
1807
+ /**
1808
+ * AISnitch Claude Code hook bridge
1809
+ *
1810
+ * \u{1F4D6} Claude Code currently exposes command hooks fed through stdin JSON.
1811
+ * This bridge keeps the Claude config valid while still forwarding every
1812
+ * selected hook event into the local AISnitch HTTP receiver.
1813
+ */
1814
+
1815
+ function isRecord(value) {
1816
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1817
+ }
1818
+
1819
+ async function readInput() {
1820
+ const chunks = [];
1821
+
1822
+ for await (const chunk of process.stdin) {
1823
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
1824
+ }
1825
+
1826
+ return chunks.join("");
1827
+ }
1828
+
1829
+ async function main() {
1830
+ const hookEventName = process.argv[2] ?? "unknown";
1831
+ const endpoint = process.argv[3] ?? "http://localhost:4821/hooks/claude-code";
1832
+ const rawInput = await readInput();
1833
+ let payload = {};
1834
+
1835
+ if (rawInput.trim().length > 0) {
1836
+ try {
1837
+ const parsedPayload = JSON.parse(rawInput);
1838
+
1839
+ if (isRecord(parsedPayload)) {
1840
+ payload = parsedPayload;
1841
+ }
1842
+ } catch {
1843
+ payload = {
1844
+ raw: rawInput
1845
+ };
1846
+ }
1847
+ }
1848
+
1849
+ try {
1850
+ await fetch(endpoint, {
1851
+ method: "POST",
1852
+ headers: {
1853
+ "content-type": "application/json"
1854
+ },
1855
+ body: JSON.stringify({
1856
+ ...payload,
1857
+ hook_event_name: hookEventName
1858
+ })
1859
+ });
1860
+ } catch {
1861
+ // Claude hooks must stay fire-and-forget for observability only.
1862
+ }
1863
+ }
1864
+
1865
+ void main().catch(() => {
1866
+ // Never bubble hook bridge failures back into Claude Code itself.
1867
+ });
1868
+ `;
1869
+ }
1749
1870
  function ensureGeminiAISnitchHook(groups, hookUrl) {
1750
1871
  const clonedGroups = groups.map((group) => ({
1751
1872
  ...group,
@@ -2260,6 +2381,9 @@ async function readOptionalFile(path) {
2260
2381
  throw error;
2261
2382
  }
2262
2383
  }
2384
+ function shellEscapeSingle(value) {
2385
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
2386
+ }
2263
2387
  async function restoreBackupOrRemove(path) {
2264
2388
  const backupPath = `${path}.bak`;
2265
2389
  if (await fileExists(backupPath)) {
@@ -9544,6 +9668,7 @@ var Pipeline = class {
9544
9668
 
9545
9669
  // src/tui/index.tsx
9546
9670
  var import_ink13 = require("ink");
9671
+ var import_fullscreen_ink = require("fullscreen-ink");
9547
9672
  var import_ws4 = __toESM(require("ws"), 1);
9548
9673
 
9549
9674
  // src/tui/App.tsx
@@ -11452,7 +11577,7 @@ function attachEventBusMonitor(eventBus, output) {
11452
11577
  // src/tui/index.tsx
11453
11578
  var import_jsx_runtime13 = require("react/jsx-runtime");
11454
11579
  async function renderForegroundTui(options) {
11455
- const app = (0, import_ink13.render)(
11580
+ const ink = (0, import_fullscreen_ink.withFullScreen)(
11456
11581
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
11457
11582
  App,
11458
11583
  {
@@ -11474,7 +11599,8 @@ async function renderForegroundTui(options) {
11474
11599
  }
11475
11600
  )
11476
11601
  );
11477
- await app.waitUntilExit();
11602
+ await ink.start();
11603
+ await ink.waitUntilExit;
11478
11604
  }
11479
11605
  async function renderManagedTui(options) {
11480
11606
  const app = (0, import_ink13.render)(