tabclaw 0.1.0 → 0.1.2

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
@@ -1,13 +1,28 @@
1
1
  # Tabclaw
2
2
 
3
- Tabclaw is a local gateway for AI assistant event hooks and notification channels. It helps connect hook-enabled agents to channels like Telegram while keeping runtime state under `~/.tabclaw`.
3
+ Tabclaw is a local gateway that bridges AI programming assistants (like Claude Code) with chat platforms (like Telegram). It listens for agent events and forwards them to your channels in real time.
4
4
 
5
5
  ## What it does
6
6
 
7
- - Runs a local gateway daemon for hook-based agent integrations
8
- - Attaches supported agents such as `claude` to the gateway
9
- - Routes session events, permission requests, and status updates to configured channels
10
- - Provides CLI commands for daemon control, status, logs, and agent attachment
7
+ ### Overview
8
+
9
+ - Runs a local HTTP gateway daemon to capture agent hook events
10
+ - Attaches hook-enabled agents to the gateway for event streaming
11
+ - Routes session events, permission requests, and task notifications to configured channels
12
+ - Manages agent-initiated tool execution permissions via chat platform interactions
13
+
14
+ ### Claude Code + Telegram integration
15
+
16
+ The core workflow bridges Claude Code with Telegram:
17
+
18
+ 1. **Session Monitoring** — Tabclaw listens for Claude Code session start/end events and notifies you on Telegram
19
+ 2. **Permission Control** — When Claude requests to execute a tool, you receive a permission prompt on Telegram with **Allow/Deny** buttons
20
+ 3. **Real-time Decisions** — Click the button in Telegram to grant or deny the tool execution
21
+ 4. **Task Completion** — Receive final notifications when tasks complete, including execution duration, token usage, and results
22
+
23
+ ![Claude + Telegram workflow](assets/image_01.jpg)
24
+
25
+ ![Permission request and task completion on Telegram](assets/image_02.jpg)
11
26
 
12
27
  ## Installation
13
28
 
@@ -61,9 +76,30 @@ tabclaw gateway status
61
76
 
62
77
  ## Configuration
63
78
 
64
- Copy `config.example.toml` to `~/.tabclaw/config.toml` and fill in your values.
79
+ Most configuration is handled by `tabclaw init` interactive wizard, which guides you through:
80
+
81
+ 1. Setting up your Claude Code hook server port
82
+ 2. Configuring Telegram bot credentials
83
+ 3. Testing channel connectivity
84
+
85
+ For manual setup or advanced options, edit `~/.tabclaw/config.toml`. Key sections:
86
+
87
+ **General settings:**
88
+ - `default_agent` — default agent to use (currently `claude`)
89
+ - `hook_server_port` — local port for receiving agent hooks (default `9876`)
90
+ - `log_level` — logging verbosity (`debug`, `info`, `warn`, `error`)
91
+
92
+ **Telegram channel:**
93
+ - `bot_token` — your Telegram bot token from BotFather
94
+ - `allowed_users` — list of Telegram user IDs allowed to interact with the bot (e.g., `[123456789, 987654321]`)
95
+ - `notify_types` — which event types to forward (`permission_request`, `finished`, `session_start`, `error`, etc.)
96
+
97
+ **Claude agent:**
98
+ - `adapter` — integration method (always `hooks`)
99
+ - `binary` — Claude Code binary name
100
+ - `permission_mode` — how to handle tool permissions
65
101
 
66
- Example:
102
+ Example `~/.tabclaw/config.toml`:
67
103
 
68
104
  ```toml
69
105
  [general]
@@ -74,7 +110,8 @@ hook_server_port = 9876
74
110
  [channel.telegram]
75
111
  enabled = true
76
112
  bot_token = "YOUR_BOT_TOKEN"
77
- allowed_users = [123456789]
113
+ allowed_users = ["123456789"]
114
+ notify_types = ["permission_request", "finished", "session_start", "error"]
78
115
 
79
116
  [agent.claude]
80
117
  adapter = "hooks"
package/dist/cli/index.js CHANGED
@@ -95,6 +95,16 @@ function getOrCreateHookToken() {
95
95
  function getHookToken() {
96
96
  return loadCredentials()?.hookToken ?? null;
97
97
  }
98
+ function resetCredentials() {
99
+ const newToken = generateToken();
100
+ const credentials = {
101
+ version: CREDENTIALS_VERSION,
102
+ hookToken: newToken,
103
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
104
+ };
105
+ saveCredentials(credentials);
106
+ return newToken;
107
+ }
98
108
 
99
109
  // src/cli/attach.ts
100
110
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync } from "fs";
@@ -235,6 +245,7 @@ var en_default = {
235
245
  detected: "Claude Code detected.",
236
246
  hooksInstalled: "Hooks already installed.",
237
247
  hooksNotInstalled: "Hooks not installed.",
248
+ hooksTokenMismatch: "Hooks have outdated token - reconfiguration required.",
238
249
  actionQuestion: "Select action:",
239
250
  injectAction: "Inject hooks configuration",
240
251
  updateAction: "Update hooks configuration",
@@ -603,44 +614,75 @@ function detectClaude() {
603
614
  settingsPath
604
615
  };
605
616
  }
617
+ const currentToken = getHookToken();
606
618
  let hooked = false;
619
+ let tokenMismatch = false;
607
620
  if (existsSync4(settingsPath)) {
608
621
  try {
609
622
  const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
610
- hooked = isTabclawHooked(settings);
623
+ const hookStatus = checkTabclawHooks(settings, currentToken);
624
+ hooked = hookStatus.matched;
625
+ tokenMismatch = hookStatus.mismatched;
611
626
  } catch {
612
627
  }
613
628
  }
629
+ let message;
630
+ if (hooked) {
631
+ message = "Claude Code hooks already configured.";
632
+ } else if (tokenMismatch) {
633
+ message = "Claude Code hooks have mismatched token. Reconfiguration required.";
634
+ } else {
635
+ message = "Claude Code detected.";
636
+ }
614
637
  return {
615
638
  supported: true,
616
639
  hooked,
617
- message: hooked ? "Claude Code hooks already configured." : "Claude Code detected.",
640
+ tokenMismatch,
641
+ message,
618
642
  binaryPath,
619
643
  settingsPath
620
644
  };
621
645
  }
622
- function isTabclawHooked(settings) {
646
+ function checkTabclawHooks(settings, expectedToken) {
623
647
  const hooks = settings.hooks;
624
- if (!hooks) return false;
648
+ if (!hooks) return { matched: false, mismatched: false };
649
+ let hasAnyTabclawHook = false;
650
+ let hasMatchingToken = false;
625
651
  for (const key of Object.keys(hooks)) {
626
652
  const entries = hooks[key];
627
653
  for (const entry of entries) {
628
654
  if (!entry || typeof entry !== "object") continue;
629
655
  const e = entry;
630
656
  const inner = e.hooks;
631
- if (Array.isArray(inner) && inner.some((h) => isHttpHookWithTabclawUrl(h))) {
632
- return true;
657
+ if (Array.isArray(inner)) {
658
+ for (const h of inner) {
659
+ const hookInfo = extractTabclawHookInfo(h);
660
+ if (hookInfo.isTabclaw) {
661
+ hasAnyTabclawHook = true;
662
+ if (expectedToken && hookInfo.token === expectedToken) {
663
+ hasMatchingToken = true;
664
+ }
665
+ }
666
+ }
633
667
  }
634
668
  }
635
669
  }
636
- return false;
670
+ return {
671
+ matched: hasMatchingToken,
672
+ mismatched: hasAnyTabclawHook && !hasMatchingToken
673
+ };
637
674
  }
638
- function isHttpHookWithTabclawUrl(entry) {
639
- if (!entry || typeof entry !== "object") return false;
675
+ function extractTabclawHookInfo(entry) {
676
+ if (!entry || typeof entry !== "object") return { isTabclaw: false };
640
677
  const e = entry;
641
- if (e.type !== "http") return false;
678
+ if (e.type !== "http") return { isTabclaw: false };
642
679
  const url = e.url;
643
- return typeof url === "string" && url.includes("localhost") && url.includes("/hook/");
680
+ if (typeof url !== "string") return { isTabclaw: false };
681
+ const match = url.match(/^http:\/\/localhost:\d+\/hook\/([^\/]+)/);
682
+ if (match) {
683
+ return { isTabclaw: true, token: match[1] };
684
+ }
685
+ return { isTabclaw: false };
644
686
  }
645
687
 
646
688
  // src/cli/wizard/prompts.ts
@@ -745,6 +787,8 @@ ${t("wizard.section.cli")}
745
787
  console.log(pc.gray(` Gateway: http://localhost:${port}/hook/${token ? token.substring(0, 8) + "..." : "unknown"}`));
746
788
  if (detection.hooked) {
747
789
  console.log(pc.green(` ${t("wizard.cli.hooksInstalled")}`));
790
+ } else if (detection.tokenMismatch) {
791
+ console.log(pc.red(` ${t("wizard.cli.hooksTokenMismatch")}`));
748
792
  } else {
749
793
  console.log(pc.yellow(` ${t("wizard.cli.hooksNotInstalled")}`));
750
794
  }
@@ -874,7 +918,10 @@ async function stepCliMenu(state) {
874
918
  ${t("wizard.section.cli")}
875
919
  `));
876
920
  const cliStatus = state.itemStatus.get("claude") || "unconfigured";
877
- const label = getChannelStatusLabel(cliStatus);
921
+ let label = getChannelStatusLabel(cliStatus);
922
+ if (state.claudeDetection?.tokenMismatch) {
923
+ label = pc2.yellow("[update needed]");
924
+ }
878
925
  const choice = await prompts.channelMenuSelect([{
879
926
  name: "claude",
880
927
  status: label
@@ -1323,6 +1370,11 @@ var initCommand = new Command2("init").description("Initialize tabclaw configura
1323
1370
  return;
1324
1371
  }
1325
1372
  }
1373
+ if (options.force) {
1374
+ resetCredentials();
1375
+ } else {
1376
+ getOrCreateHookToken();
1377
+ }
1326
1378
  const wizard = new WizardEngine();
1327
1379
  await wizard.run();
1328
1380
  });