@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.
- package/dist/{chunk-4M2ZRGQR.js → chunk-BHXNKH3U.js} +5 -1
- package/dist/cli.js +1 -1
- package/dist/serve.js +78 -14
- package/package.json +1 -1
|
@@ -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
package/dist/serve.js
CHANGED
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
systemPathResolver,
|
|
79
79
|
withLegacyEnv,
|
|
80
80
|
writeFileAtomic
|
|
81
|
-
} from "./chunk-
|
|
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) =>
|
|
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
|
|
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;
|