@halfwhey/claudraband-core 0.2.0 → 0.4.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.
package/README.md CHANGED
@@ -1,149 +1,161 @@
1
1
  <div align="center">
2
2
 
3
- # claudraband
3
+ # Claudraband
4
4
 
5
5
  Claude Code for the power user
6
6
 
7
- > Experimental: this project is still evolving and parts of it may break as Claude Code and ACP clients change.
7
+ > Experimental: this project is still evolving as Claude Code and ACP clients change.
8
8
 
9
- [Quick start](#quick-start) •
10
9
  [CLI](docs/cli.md) •
11
10
  [Library](docs/library.md) •
12
- [Examples](#examples) •
13
- [Troubleshooting](#troubleshooting)
11
+ [Daemon API](docs/daemon-api.md) •
12
+ [Examples](examples/)
14
13
 
15
14
  </div>
16
15
 
17
- `claudraband` wraps a Claude Code TUI in a controlled terminal to enable extended workflows. This project provides:
16
+ `claudraband` wraps the official Claude Code TUI in a controlled terminal so you can keep sessions alive, resume them later, answer pending prompts, expose them through a daemon, or drive them through ACP.
18
17
 
19
- - Resumable non-interactive workflows. Essentially `claude -p` with session support: `cband continue <session-id> 'what was the result of the research?'`
20
- - HTTP server to remotely control a Claude Code session: `cband serve --port 1234`
21
- - ACP server to use with alternative frontends such as Zed or Toad (https://github.com/batrachianai/toad): `cband acp --model haiku`
22
- - TypeScript library so you can integrate these workflows into your own application.
18
+ It provides:
23
19
 
24
- ## Caveats
20
+ - Resumable non-interactive workflows. Essentially claude -p with session support: cband continue <session-id> 'what was the result of the research?'
21
+ - An http daemon for remote or headless session control
22
+ - An ACP server for editor and alternate frontend integration
23
+ - A TypeScript library for building these workflows into your own tools
24
+
25
+ Caveats
25
26
 
26
27
  - This is not a replacement for the Claude SDK. It is geared toward personal, ad-hoc usage.
27
28
  - We do not touch OAuth and we do not bypass the Claude Code TUI. You must authenticate through Claude Code, and every interaction runs through a real Claude Code session.
28
29
 
29
- ## Requirements
30
+
31
+ ## Setup
32
+
33
+ Requirements:
30
34
 
31
35
  - Node.js or Bun
32
36
  - An already authenticated Claude Code
33
- - `tmux` if you want visible persistent local sessions
37
+ - `tmux` for the first-class local and daemon-backed workflow
34
38
 
35
- ## Install
39
+ Install or run:
36
40
 
37
41
  ```sh
38
- # run without installing globally
42
+ # one-off
39
43
  npx @halfwhey/claudraband "review the staged diff"
44
+ bunx @halfwhey/claudraband "review the staged diff"
40
45
 
41
- # or install it once
46
+ # install once
42
47
  npm install -g @halfwhey/claudraband
43
48
  ```
44
49
 
45
- If you prefer Bun:
46
-
47
- ```sh
48
- bunx @halfwhey/claudraband "review the staged diff"
49
- ```
50
-
51
- `claudraband` installs a pinned Claude Code version, `@anthropic-ai/claude-code@2.1.96`, as a dependency for compatibility. It will be bumped over time. If you need to point at a different Claude binary for debugging or compatibility work, set `CLAUDRABAND_CLAUDE_PATH`.
50
+ The package installs both `claudraband` and `cband`. `cband` is the recommended shorthand. The package bundles Claude Code `@anthropic-ai/claude-code@2.1.96`; set `CLAUDRABAND_CLAUDE_PATH` if you need to override the binary.
52
51
 
52
+ ## Quick Start
53
53
 
54
- ## Quick start
54
+ The two first-class paths are local `tmux` sessions and daemon-backed sessions.
55
55
 
56
- The package installs both `cband` and `claudraband`. The shorter `cband` binary is the recommended CLI. The two first-class ways to use `cband` are:
57
-
58
- - local persistent sessions with `tmux`
59
- - headless persistent sessions with `serve`
60
-
61
- ### Visible persistent sessions with `tmux`
56
+ ### Local persistent sessions
62
57
 
63
58
  ```sh
64
59
  cband "audit the last commit and tell me what looks risky"
65
-
66
60
  cband sessions
67
-
68
61
  cband continue <session-id> "keep going"
69
-
70
- # if Claude is waiting on a choice
71
62
  cband continue <session-id> --select 2
72
- cband continue <session-id> --select 3 "xyz"
73
63
  ```
74
64
 
75
- ### Headless persistent sessions with `serve`
65
+ ### Daemon-backed sessions
76
66
 
77
67
  ```sh
78
- cband serve --host 127.0.0.1 --backend xterm --port 7842
68
+ cband serve --host 127.0.0.1 --port 7842
79
69
  cband --connect localhost:7842 "start a migration plan"
80
70
  cband attach <session-id>
81
71
  cband continue <session-id> --select 2
82
72
  ```
83
73
 
84
- Use `--connect` only when starting a new daemon-backed session. After that, `continue`, `attach`, and `sessions` route through the tracked session automatically. `attach` is especially useful here because it gives you a simple REPL for a headless xterm.js session.
85
-
86
- ### Using the CLI without tmux or server
87
-
88
- If you run `cband "..."` without `tmux` and without `--connect`, `cband` falls back to a local headless `xterm.js` session. That mode is useful for one-off runs, but it is not a good default for interactive follow-up because the session is not kept alive between commands. It only works properly when Claude itself can proceed without an interactive permission prompt. Use one of:
74
+ The daemon defaults to using `tmux` as the terminal runtime, just like the local path. Use `--connect` only when creating a new daemon-backed session; after that, tracked `continue`, `attach`, and `sessions` route through the recorded live owner automatically.
89
75
 
90
- - `-c "--dangerously-skip-permissions"`
91
- - `--permission-mode bypassPermissions`
76
+ ## Experimental Xterm Backend
92
77
 
93
- Without `tmux` or server mode, `cband` shuts down Claude Code after each command finishes and starts it again on the next one. If the last output was a question for the user, that question will not survive well across the next resume. Interactive question flows work best with persistent sessions.
78
+ `--backend xterm` exists for local or daemon use, but it is experimental and slower than `tmux`. Use it when you need a headless fallback, not as the default path for long-lived interactive work. See [docs/cli.md](docs/cli.md) for current caveats and backend behavior.
94
79
 
95
- ### ACP and editor integration
80
+ ## ACP
96
81
 
97
82
  Use ACP when another tool wants to drive Claude through `claudraband`.
98
83
 
99
84
  ```sh
100
85
  cband acp --model opus
101
86
 
102
- # for example with toad
87
+ # example: toad
103
88
  uvx --from batrachian-toad toad acp 'cband acp -c "--model haiku"'
104
89
  ```
105
90
 
106
- Some ACP clients still have limitations around resuming existing sessions. `claudraband` itself supports session follow and resume as part of the ACP protocol, but the frontend you put on top may not expose all of that yet.
91
+ Editor and ACP client support varies by frontend, but `claudraband` itself supports session follow and resume through ACP.
107
92
 
108
- ## Session model
93
+ ## Session Model
109
94
 
110
95
  Live sessions are tracked in `~/.claudraband/`.
111
96
 
112
- - `cband sessions` shows only live tracked sessions, either hosted by tmux or the xterm.js daemon.
113
- - `continue` can resume an existing Claude Code session even when it is no longer live, but `attach` only works on live sessions.
114
- - `sessions close ...` closes live sessions hosted by tmux or the xterm.js daemon
115
- - `sessions close --all` will close all live sessions controlled by Claudraband
97
+ - `cband sessions` lists live tracked sessions
98
+ - `continue` can resume an existing Claude Code session even when it is no longer live
99
+ - `attach` only works on live sessions
100
+ - `sessions close ...` closes live tracked sessions, either local or daemon-backed
116
101
 
117
102
  ## Examples
118
103
 
119
104
  ### Self-interrogation
120
105
 
121
- I have a Claude Code hook that saves the session id that was involved in a commit so I can ask it questions about the commit later. In this workflow, Claude can use `claudraband` to interrogate that older session and justify the choices it made.
106
+ Claude can interrogate an older Claude session and justify the choices it made.
122
107
 
123
108
  ![Claude interrogating an older Claude session through claudraband](assets/self-interrogate.png)
124
109
 
125
110
  ### Toad via ACP
126
111
 
127
- Toad can use `claudraband acp` to be an alternative frontend for Claude Code.
112
+ Toad can use `claudraband acp` as an alternative frontend for Claude Code.
128
113
 
129
114
  ![Toad using claudraband ACP as an alternative frontend](assets/toad-acp.png)
130
115
 
131
- That UI is backed by a real Claude Code pane underneath.
116
+ That UI is still backed by a real Claude Code pane underneath.
132
117
 
133
118
  ![Backing Claude Code pane for the Toad ACP session](assets/toad-claude-pane.png)
134
119
 
135
120
  ### Zed via ACP
136
121
 
137
- Zed can also use `claudraband acp` to be an alternative frontend.
122
+ Zed can also use `claudraband acp` as an alternative frontend.
138
123
 
139
124
  ![Zed using claudraband ACP as an alternative frontend](assets/zed-acp.png)
140
125
 
141
- ## Library use
126
+ ## Library
142
127
 
143
- Runnable TypeScript examples live in [`examples/`](examples):
128
+ Runnable TypeScript examples live in [`examples/`](examples/):
144
129
 
145
- - [`examples/code-review.ts`](examples/code-review.ts) starts a session, asks for a review, and prints the result
146
- - [`examples/multi-session.ts`](examples/multi-session.ts) runs multiple Claude sessions in parallel
147
- - [`examples/session-journal.ts`](examples/session-journal.ts) resumes a session and writes a simple journal
130
+ - [`examples/code-review.ts`](examples/code-review.ts)
131
+ - [`examples/multi-session.ts`](examples/multi-session.ts)
132
+ - [`examples/session-journal.ts`](examples/session-journal.ts)
148
133
 
149
- For the full TypeScript API, see [docs/library.md](docs/library.md).
134
+ For the full API, see [docs/library.md](docs/library.md). For CLI details, see [docs/cli.md](docs/cli.md). For raw daemon endpoints, see [docs/daemon-api.md](docs/daemon-api.md).
135
+
136
+ ## Cheat Sheet
137
+
138
+ ```sh
139
+ # install or run once
140
+ npx @halfwhey/claudraband "review the staged diff"
141
+ bunx @halfwhey/claudraband "review the staged diff"
142
+ npm install -g @halfwhey/claudraband
143
+
144
+ # local persistent sessions
145
+ cband "audit the last commit"
146
+ cband sessions
147
+ cband sessions close --all # close all claudraband controlled sessions
148
+ cband continue <session-id> "keep going"
149
+
150
+ # answer pending prompts
151
+ cband continue <session-id> --select 2
152
+ cband continue <session-id> --select 3 "xyz"
153
+
154
+ # daemon mode
155
+ cband serve --host 127.0.0.1 --port 7842
156
+ cband --connect localhost:7842 "start a migration plan"
157
+ cband attach <session-id>
158
+
159
+ # ACP
160
+ cband acp --model opus
161
+ ```
@@ -41,6 +41,9 @@ export declare class ClaudeWrapper implements Wrapper {
41
41
  /**
42
42
  * Poll the terminal until Claude Code is ready to accept input.
43
43
  * Looks for "INSERT" in the status bar, which indicates the TUI has loaded.
44
+ *
45
+ * Also handles the "trust this folder" prompt that appears before any JSONL
46
+ * is written when Claude Code runs in a directory for the first time.
44
47
  */
45
48
  private waitForReady;
46
49
  stop(): Promise<void>;
package/dist/index.js CHANGED
@@ -1096,6 +1096,9 @@ class ClaudeWrapper {
1096
1096
  if (pane.includes("INSERT") || pane.includes("NORMAL")) {
1097
1097
  return;
1098
1098
  }
1099
+ if (pane.includes("Yes, I trust this folder") && pane.includes("No, exit") || pane.includes("Bypass Permissions mode") && pane.includes("Yes, I accept")) {
1100
+ return;
1101
+ }
1099
1102
  } catch {}
1100
1103
  await new Promise((r) => setTimeout(r, POLL_MS));
1101
1104
  }
@@ -1178,6 +1181,9 @@ function parseClaudeArgs(args) {
1178
1181
  }
1179
1182
  // src/claude/inspect.ts
1180
1183
  import { readFile } from "node:fs/promises";
1184
+ function normalizeCapturedPane(paneText) {
1185
+ return paneText.replace(/\r/g, "").replace(/\x1b\[(\d+)C/g, (_match, count) => " ".repeat(Number.parseInt(count, 10) || 0)).replace(/\x1b\[[0-9;]*[ABDHJKf]/g, "").replace(/\x1b\[\?[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
1186
+ }
1181
1187
  async function hasPendingQuestion(jsonlPath) {
1182
1188
  let data;
1183
1189
  try {
@@ -1211,10 +1217,20 @@ async function hasPendingQuestion(jsonlPath) {
1211
1217
  return pendingIds.size > 0;
1212
1218
  }
1213
1219
  function parseNativePermissionPrompt(paneText) {
1214
- const questionMatch = paneText.match(/(?:^|\n)\s*(Do you want to [^\n]+\?)/);
1220
+ const normalizedPane = normalizeCapturedPane(paneText);
1221
+ const isTrustPrompt = normalizedPane.includes("Yes, I trust this folder") && normalizedPane.includes("No, exit");
1222
+ const isBypassPrompt = normalizedPane.includes("Bypass Permissions mode") && normalizedPane.includes("Yes, I accept");
1223
+ let questionMatch;
1224
+ if (isTrustPrompt) {
1225
+ questionMatch = normalizedPane.match(/(Is this a project you created[^\n]*)/);
1226
+ } else if (isBypassPrompt) {
1227
+ questionMatch = normalizedPane.match(/(you accept all responsibility[^\n]*)/);
1228
+ } else {
1229
+ questionMatch = normalizedPane.match(/(?:^|\n)\s*(Do you want to [^\n]+\?)/);
1230
+ }
1215
1231
  if (!questionMatch)
1216
1232
  return null;
1217
- const afterQuestion = paneText.slice(paneText.indexOf(questionMatch[1]) + questionMatch[1].length);
1233
+ const afterQuestion = normalizedPane.slice(normalizedPane.indexOf(questionMatch[1]) + questionMatch[1].length);
1218
1234
  const optionRegex = /(?:❯\s*)?(\d+)\.\s+(.+)/g;
1219
1235
  const options = [];
1220
1236
  let match;
@@ -1389,6 +1405,10 @@ var TERMINAL_BACKENDS = [
1389
1405
  description: "Run Claude Code in a headless xterm-backed PTY"
1390
1406
  }
1391
1407
  ];
1408
+ function isRejectingNativeOption(label) {
1409
+ const normalized = label.trim().toLowerCase();
1410
+ return normalized.startsWith("no") || normalized.startsWith("reject");
1411
+ }
1392
1412
  function makeDefaultLogger() {
1393
1413
  const noop = () => {};
1394
1414
  return {
@@ -1510,6 +1530,8 @@ class ClaudrabandSessionImpl {
1510
1530
  _permissionMode;
1511
1531
  allowTextResponses;
1512
1532
  sessionOwner;
1533
+ lastNativePermissionFingerprint = null;
1534
+ lastNativePermissionOutcome = "none";
1513
1535
  constructor(wrapper, sessionId, cwd, backend, model, permissionMode, allowTextResponses, logger, onPermissionRequest, lifetime, sessionOwner) {
1514
1536
  this.sessionId = sessionId;
1515
1537
  this.cwd = cwd;
@@ -1544,7 +1566,20 @@ class ClaudrabandSessionImpl {
1544
1566
  const prompt = this.newPromptWaiter(text);
1545
1567
  this.activePrompt = prompt;
1546
1568
  this.logger.info("prompt received", "sid", this.sessionId, "length", text.length);
1547
- await this.wrapper.send(text);
1569
+ const startupState = await this.prepareForInput(text);
1570
+ if (startupState === "blocked") {
1571
+ this.activePrompt = null;
1572
+ this.promptAbortController = null;
1573
+ return { stopReason: "end_turn" };
1574
+ }
1575
+ if (startupState === "cancelled") {
1576
+ this.activePrompt = null;
1577
+ this.promptAbortController = null;
1578
+ return { stopReason: "cancelled" };
1579
+ }
1580
+ if (startupState !== "consumed") {
1581
+ await this.wrapper.send(text);
1582
+ }
1548
1583
  try {
1549
1584
  const stopReason = await this.waitForPromptCompletion(prompt, controller.signal);
1550
1585
  this.logger.info("prompt completed", "sid", this.sessionId, "stop_reason", stopReason);
@@ -1567,6 +1602,17 @@ class ClaudrabandSessionImpl {
1567
1602
  const prompt = this.newPromptWaiter("");
1568
1603
  prompt.matchedUserEcho = true;
1569
1604
  this.activePrompt = prompt;
1605
+ const startupState = await this.prepareForInput(null);
1606
+ if (startupState === "blocked") {
1607
+ this.activePrompt = null;
1608
+ this.promptAbortController = null;
1609
+ return { stopReason: "end_turn" };
1610
+ }
1611
+ if (startupState === "cancelled") {
1612
+ this.activePrompt = null;
1613
+ this.promptAbortController = null;
1614
+ return { stopReason: "cancelled" };
1615
+ }
1570
1616
  this.logger.info("awaitTurn", "sid", this.sessionId);
1571
1617
  try {
1572
1618
  const stopReason = await this.waitForPromptCompletion(prompt, controller.signal);
@@ -1590,6 +1636,26 @@ class ClaudrabandSessionImpl {
1590
1636
  const prompt = this.newPromptWaiter("");
1591
1637
  prompt.matchedUserEcho = true;
1592
1638
  this.activePrompt = prompt;
1639
+ const startupState = await this.prepareForInput(text);
1640
+ if (startupState === "blocked") {
1641
+ this.activePrompt = null;
1642
+ this.promptAbortController = null;
1643
+ return { stopReason: "end_turn" };
1644
+ }
1645
+ if (startupState === "cancelled") {
1646
+ this.activePrompt = null;
1647
+ this.promptAbortController = null;
1648
+ return { stopReason: "cancelled" };
1649
+ }
1650
+ if (startupState === "consumed") {
1651
+ if (this.activePrompt === prompt) {
1652
+ this.activePrompt = null;
1653
+ }
1654
+ if (this.promptAbortController === controller) {
1655
+ this.promptAbortController = null;
1656
+ }
1657
+ return { stopReason: "end_turn" };
1658
+ }
1593
1659
  this.logger.info("sendAndAwaitTurn", "sid", this.sessionId, "length", text.length);
1594
1660
  await this.wrapper.send(text);
1595
1661
  try {
@@ -1608,6 +1674,21 @@ class ClaudrabandSessionImpl {
1608
1674
  send(text) {
1609
1675
  return this.wrapper.send(text);
1610
1676
  }
1677
+ async prepareForInput(intendedText) {
1678
+ const handled = await this.pollNativePermission(null, intendedText);
1679
+ if (handled === "handled" || handled === "consumed" || handled === "pending_clear") {
1680
+ const ready = await this.waitForInsertMode();
1681
+ if (!ready) {
1682
+ return this.wrapper.isProcessAlive() ? "blocked" : "cancelled";
1683
+ }
1684
+ return handled === "consumed" ? "consumed" : "ready";
1685
+ }
1686
+ const stillBlocking = hasPendingNativePrompt(await this.wrapper.capturePane().catch(() => ""));
1687
+ if (stillBlocking) {
1688
+ return "blocked";
1689
+ }
1690
+ return "ready";
1691
+ }
1611
1692
  async interrupt() {
1612
1693
  this.promptAbortController?.abort();
1613
1694
  await this.wrapper.interrupt();
@@ -1927,7 +2008,7 @@ class ClaudrabandSessionImpl {
1927
2008
  }
1928
2009
  if (prompt.pendingTools > 0) {
1929
2010
  const handled = await this.pollNativePermission(prompt.lastPendingTool);
1930
- if (handled) {
2011
+ if (handled === "handled" || handled === "consumed") {
1931
2012
  prompt.pendingTools = Math.max(0, prompt.pendingTools - 1);
1932
2013
  prompt.gotResponse = true;
1933
2014
  prompt.lastPendingTool = null;
@@ -1942,16 +2023,30 @@ class ClaudrabandSessionImpl {
1942
2023
  }
1943
2024
  }
1944
2025
  }
1945
- async pollNativePermission(pendingTool) {
2026
+ async pollNativePermission(pendingTool, intendedText = null) {
1946
2027
  let paneText;
1947
2028
  try {
1948
2029
  paneText = await this.wrapper.capturePane();
1949
2030
  } catch {
1950
- return false;
2031
+ return "none";
1951
2032
  }
1952
2033
  const prompt = parseNativePermissionPrompt(paneText);
1953
- if (!prompt)
1954
- return false;
2034
+ if (!prompt) {
2035
+ this.lastNativePermissionFingerprint = null;
2036
+ this.lastNativePermissionOutcome = "none";
2037
+ return "none";
2038
+ }
2039
+ const fingerprint = `${prompt.question}
2040
+ ${prompt.options.map((option) => `${option.number}:${option.label}`).join(`
2041
+ `)}`;
2042
+ if (this.lastNativePermissionFingerprint === fingerprint) {
2043
+ if (this.lastNativePermissionOutcome === "handled" || this.lastNativePermissionOutcome === "consumed") {
2044
+ return "pending_clear";
2045
+ }
2046
+ if (this.lastNativePermissionOutcome !== "deferred") {
2047
+ return this.lastNativePermissionOutcome;
2048
+ }
2049
+ }
1955
2050
  const decision = await this.resolvePermission({
1956
2051
  source: "native_prompt",
1957
2052
  sessionId: this.sessionId,
@@ -1971,19 +2066,62 @@ class ClaudrabandSessionImpl {
1971
2066
  name: opt.label
1972
2067
  }))
1973
2068
  });
2069
+ let outcome;
1974
2070
  if (decision.outcome === "deferred") {
1975
- return false;
2071
+ outcome = "deferred";
2072
+ } else if (decision.outcome === "cancelled") {
2073
+ await this.safeInterruptNativePermission();
2074
+ outcome = "handled";
2075
+ } else if (decision.outcome === "text") {
2076
+ await this.safeSendNativePermission(decision.text, false);
2077
+ outcome = decision.text === intendedText ? "consumed" : "handled";
2078
+ } else {
2079
+ const selected = prompt.options.find((option) => option.number === decision.optionId);
2080
+ const tolerateExit = selected ? isRejectingNativeOption(selected.label) : false;
2081
+ await this.safeSendNativePermission(decision.optionId, tolerateExit);
2082
+ outcome = decision.optionId === intendedText ? "consumed" : "handled";
2083
+ }
2084
+ this.lastNativePermissionFingerprint = fingerprint;
2085
+ this.lastNativePermissionOutcome = outcome;
2086
+ return outcome;
2087
+ }
2088
+ async safeSendNativePermission(input, tolerateExit) {
2089
+ try {
2090
+ await this.wrapper.send(input);
2091
+ } catch (error) {
2092
+ if (tolerateExit && !this.wrapper.isProcessAlive()) {
2093
+ this.logger.debug("native permission send ignored after pane exit", "sid", this.sessionId);
2094
+ return;
2095
+ }
2096
+ throw error;
1976
2097
  }
1977
- if (decision.outcome === "cancelled") {
2098
+ }
2099
+ async safeInterruptNativePermission() {
2100
+ try {
1978
2101
  await this.wrapper.interrupt();
1979
- return true;
2102
+ } catch (error) {
2103
+ if (!this.wrapper.isProcessAlive()) {
2104
+ this.logger.debug("native permission interrupt ignored after pane exit", "sid", this.sessionId);
2105
+ return;
2106
+ }
2107
+ throw error;
1980
2108
  }
1981
- if (decision.outcome === "text") {
1982
- await this.wrapper.send(decision.text);
1983
- return true;
2109
+ }
2110
+ async waitForInsertMode(timeoutMs = 15000) {
2111
+ const POLL_MS = 300;
2112
+ const start = Date.now();
2113
+ while (Date.now() - start < timeoutMs) {
2114
+ if (!this.wrapper.isProcessAlive()) {
2115
+ return false;
2116
+ }
2117
+ try {
2118
+ const pane = await this.wrapper.capturePane();
2119
+ if (pane.includes("INSERT") || pane.includes("NORMAL"))
2120
+ return true;
2121
+ } catch {}
2122
+ await new Promise((r) => setTimeout(r, POLL_MS));
1984
2123
  }
1985
- await this.wrapper.send(decision.optionId);
1986
- return true;
2124
+ return false;
1987
2125
  }
1988
2126
  async handleUserQuestion(ev) {
1989
2127
  const parsed = parseAskUserQuestion(ev.toolInput);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halfwhey/claudraband-core",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript runtime for controlling local Claude Code sessions through a real terminal.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",