@tarfa/cli 0.1.7 → 0.1.8

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.
@@ -5526,6 +5526,9 @@ var clientMessageSchema = z5.discriminatedUnion("type", [
5526
5526
  z5.object({ type: z5.literal("voiceStart"), workspaceId: z5.string(), id: z5.string() }),
5527
5527
  z5.object({ type: z5.literal("voiceChunk"), id: z5.string(), data: z5.string() }),
5528
5528
  z5.object({ type: z5.literal("voiceEnd"), id: z5.string() }),
5529
+ // Throw the dictation away: Claude writes nothing. Sent only to an agent whose authResult names
5530
+ // the `voiceCancel` feature (an older one would drop it, and Claude would type what it heard).
5531
+ z5.object({ type: z5.literal("voiceCancel"), id: z5.string() }),
5529
5532
  // The text of Claude's last reply in a workspace, read aloud on the phone. Sent only to an agent
5530
5533
  // whose authResult names the `lastReply` feature.
5531
5534
  z5.object({ type: z5.literal("lastReply"), workspaceId: z5.string() }),
@@ -5554,7 +5557,8 @@ var AGENT_FEATURES = {
5554
5557
  lastReply: "lastReply",
5555
5558
  dictationKeys: "dictationKeys",
5556
5559
  setDictationKey: "setDictationKey",
5557
- addresses: "addresses"
5560
+ addresses: "addresses",
5561
+ voiceCancel: "voiceCancel"
5558
5562
  };
5559
5563
  var agentMessageSchema = z5.discriminatedUnion("type", [
5560
5564
  // `credential` is what authenticated the session (`token` only from an agent from before token
package/dist/cli.js CHANGED
@@ -73,7 +73,7 @@ import {
73
73
  withEnrolmentMode,
74
74
  withLegacyEnv,
75
75
  workspaceLink
76
- } from "./chunk-4M2ZRGQR.js";
76
+ } from "./chunk-BHXNKH3U.js";
77
77
 
78
78
  // src/cli.ts
79
79
  import { randomBytes } from "node:crypto";
package/dist/serve.js CHANGED
@@ -78,7 +78,7 @@ import {
78
78
  systemPathResolver,
79
79
  withLegacyEnv,
80
80
  writeFileAtomic
81
- } from "./chunk-4M2ZRGQR.js";
81
+ } from "./chunk-BHXNKH3U.js";
82
82
 
83
83
  // src/serve.ts
84
84
  import { homedir as homedir2 } from "node:os";
@@ -91,6 +91,21 @@ var TAIL_MS = 500;
91
91
  var IDLE_MS = 5e3;
92
92
  var MAX_MS = 5 * 6e4;
93
93
  var DRAIN_MS = LEAD_MS + 1e4;
94
+ var CANCEL_POLL_MS = 50;
95
+ var CANCEL_WAIT_MS = 1500;
96
+ var NOT_LISTENING = "Dictation not cancelled: Claude was not listening";
97
+ var CANCEL_KEY = "";
98
+ function claudeVoiceListening(screen) {
99
+ const lines = screen.split("\n");
100
+ let prompt = -1;
101
+ for (let i = lines.length - 1; i >= 0; i--) {
102
+ if (lines[i].startsWith("\u276F")) {
103
+ prompt = i;
104
+ break;
105
+ }
106
+ }
107
+ return prompt >= 2 && lines[prompt - 2].trim() === "listening\u2026";
108
+ }
94
109
  function voiceSettingsProblem(settings) {
95
110
  const s = typeof settings === "object" && settings !== null ? settings : {};
96
111
  const enabled = s.voice?.enabled ?? s.voiceEnabled;
@@ -102,10 +117,11 @@ function voiceSettingsProblem(settings) {
102
117
  }
103
118
  var errorText = (e) => e instanceof Error ? e.message : String(e);
104
119
  var Dictation = class {
105
- constructor(deps, pressSpace, release) {
120
+ constructor(deps, pressSpace, release, pane) {
106
121
  this.deps = deps;
107
122
  this.pressSpace = pressSpace;
108
123
  this.release = release;
124
+ this.pane = pane;
109
125
  this.done = new Promise((resolve) => this.resolve = resolve);
110
126
  this.max = setTimeout(() => this.abort("Dictation stopped: it ran past 5 minutes"), MAX_MS);
111
127
  this.touch();
@@ -114,11 +130,13 @@ var Dictation = class {
114
130
  deps;
115
131
  pressSpace;
116
132
  release;
133
+ pane;
117
134
  done;
118
135
  resolve;
119
136
  queue = [];
120
137
  stream = null;
121
138
  ended = false;
139
+ cancelling = false;
122
140
  finished = false;
123
141
  hold;
124
142
  lead;
@@ -132,7 +150,7 @@ var Dictation = class {
132
150
  else this.queue.push(pcm);
133
151
  }
134
152
  end() {
135
- if (this.finished || this.ended) return;
153
+ if (this.finished || this.ended || this.cancelling) return;
136
154
  this.ended = true;
137
155
  clearTimeout(this.idle);
138
156
  if (this.stream) this.drain(this.stream);
@@ -141,9 +159,39 @@ var Dictation = class {
141
159
  this.stream?.abort();
142
160
  this.finish(reason);
143
161
  }
162
+ cancel() {
163
+ if (this.finished || this.ended || this.cancelling) return;
164
+ this.cancelling = true;
165
+ clearTimeout(this.lead);
166
+ clearTimeout(this.idle);
167
+ this.queue = [];
168
+ this.stream?.abort();
169
+ this.stream = null;
170
+ if (!this.hold) return this.finish(null);
171
+ void this.cancelOnceListening(Date.now());
172
+ }
173
+ // The hold goes on until Claude shows it records, so the key lands while it still listens.
174
+ async cancelOnceListening(since) {
175
+ const pane = this.pane;
176
+ if (!pane) return this.finish(NOT_LISTENING);
177
+ while (!this.finished) {
178
+ const listening = await pane.listening().catch(() => false);
179
+ if (this.finished) return;
180
+ if (listening) {
181
+ clearInterval(this.hold);
182
+ try {
183
+ pane.pressCancel();
184
+ } catch {
185
+ }
186
+ return this.finish(null);
187
+ }
188
+ if (Date.now() - since >= CANCEL_WAIT_MS) return this.finish(NOT_LISTENING);
189
+ await new Promise((resolve) => setTimeout(resolve, CANCEL_POLL_MS));
190
+ }
191
+ }
144
192
  async begin() {
145
193
  const problem = voiceSettingsProblem(await this.deps.settings.read().catch(() => null));
146
- if (this.finished) return;
194
+ if (this.finished || this.cancelling) return;
147
195
  if (problem) return this.finish(problem);
148
196
  const press = () => {
149
197
  try {
@@ -156,7 +204,7 @@ var Dictation = class {
156
204
  this.lead = setTimeout(() => this.play(), LEAD_MS);
157
205
  }
158
206
  play() {
159
- if (this.finished) return;
207
+ if (this.finished || this.cancelling) return;
160
208
  let stream;
161
209
  try {
162
210
  stream = this.deps.player.open();
@@ -164,7 +212,9 @@ var Dictation = class {
164
212
  return this.finish(errorText(e));
165
213
  }
166
214
  this.stream = stream;
167
- stream.played.catch((e) => this.finish(errorText(e)));
215
+ stream.played.catch((e) => {
216
+ if (!this.cancelling) this.finish(errorText(e));
217
+ });
168
218
  for (const pcm of this.queue.splice(0)) stream.write(pcm);
169
219
  if (this.ended) this.drain(stream);
170
220
  }
@@ -201,13 +251,13 @@ var ClaudeVoice = class {
201
251
  available() {
202
252
  return this.deps.player.available();
203
253
  }
204
- start(pressSpace) {
254
+ start(pressSpace, pane) {
205
255
  if (this.busy) {
206
256
  const done = Promise.resolve("Claude voice is already in use on this machine");
207
- return { write: () => void 0, end: () => void 0, abort: () => void 0, done };
257
+ return { write: () => void 0, end: () => void 0, abort: () => void 0, cancel: () => void 0, done };
208
258
  }
209
259
  this.busy = true;
210
- return new Dictation(this.deps, pressSpace, () => this.busy = false);
260
+ return new Dictation(this.deps, pressSpace, () => this.busy = false, pane);
211
261
  }
212
262
  };
213
263
 
@@ -1288,7 +1338,7 @@ var PRE_AUTH_MAX_MESSAGE_BYTES = 8 * 1024;
1288
1338
  var MAX_AUTH_ATTEMPTS = 3;
1289
1339
  var MAX_HELD_MESSAGES = 16;
1290
1340
  var PAIRING_SECRET = /^[A-Za-z0-9_-]{43}$/;
1291
- var FEATURES = [AGENT_FEATURES.lastReply, AGENT_FEATURES.dictationKeys, AGENT_FEATURES.setDictationKey];
1341
+ var FEATURES = [AGENT_FEATURES.lastReply, AGENT_FEATURES.dictationKeys, AGENT_FEATURES.setDictationKey, AGENT_FEATURES.voiceCancel];
1292
1342
  var NoiseClientConnection = class {
1293
1343
  constructor(socket, staticPrivate, options) {
1294
1344
  this.socket = socket;
@@ -2517,7 +2567,7 @@ var ConnectionHandler = class {
2517
2567
  case "voiceStart": {
2518
2568
  const { workspaceId, id } = message;
2519
2569
  this.cancelVoice("Superseded by a newer dictation");
2520
- const voice = { id, workspaceId, session: null, queue: [], ended: false, cancelled: null };
2570
+ const voice = { id, workspaceId, session: null, queue: [], ended: false, cancelled: null, discarded: false };
2521
2571
  this.voice = voice;
2522
2572
  const reply = (error) => {
2523
2573
  if (this.voice === voice) this.voice = null;
@@ -2528,9 +2578,15 @@ var ConnectionHandler = class {
2528
2578
  const tmuxSession = (await this.deps.tmux.listSessions(this.deps.config.sessionPrefix)).find((s) => s.name === workspaceId);
2529
2579
  if (!tmuxSession || !runsClaude(tmuxSession.command)) return reply("Claude voice works only in a Claude Code workspace");
2530
2580
  if (tmuxSession.program?.state === "blocked") return reply("Answer Claude\u2019s question first, then dictate");
2581
+ if (voice.discarded) return reply(null);
2531
2582
  if (voice.cancelled) return reply(voice.cancelled);
2532
- const session = this.deps.claudeVoice.start(() => {
2533
- if (this.attachment?.workspaceId === workspaceId) this.attachment.session.write(" ");
2583
+ const typeInto = (data) => {
2584
+ if (this.attachment?.workspaceId === workspaceId) this.attachment.session.write(data);
2585
+ };
2586
+ const tmux2 = this.deps.tmux;
2587
+ const session = this.deps.claudeVoice.start(() => typeInto(" "), {
2588
+ listening: async () => !!tmux2.capturePane && claudeVoiceListening(await tmux2.capturePane(workspaceId, 0)),
2589
+ pressCancel: () => typeInto(CANCEL_KEY)
2534
2590
  });
2535
2591
  voice.session = session;
2536
2592
  for (const pcm of voice.queue.splice(0)) session.write(pcm);
@@ -2546,9 +2602,17 @@ var ConnectionHandler = class {
2546
2602
  else voice.queue.push(pcm);
2547
2603
  return;
2548
2604
  }
2605
+ case "voiceCancel": {
2606
+ const voice = this.voice;
2607
+ if (voice?.id !== message.id || voice.ended) return;
2608
+ voice.ended = true;
2609
+ voice.discarded = true;
2610
+ voice.session?.cancel();
2611
+ return;
2612
+ }
2549
2613
  case "voiceEnd": {
2550
2614
  const voice = this.voice;
2551
- if (voice?.id !== message.id) return;
2615
+ if (voice?.id !== message.id || voice.discarded) return;
2552
2616
  voice.ended = true;
2553
2617
  voice.session?.end();
2554
2618
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarfa/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Tarfa agent and CLI: serves your Claude Code terminal sessions (tmux workspaces) to the Tarfa phone app.",
5
5
  "repository": {
6
6
  "type": "git",