@iinm/plain-agent 1.8.2 → 1.8.4

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.
Files changed (86) hide show
  1. package/README.md +6 -2
  2. package/bin/plain +1 -1
  3. package/config/config.predefined.json +1 -1
  4. package/config/prompts.predefined/shortcuts/configure.md +1 -1
  5. package/dist/main.mjs +473 -0
  6. package/dist/main.mjs.map +7 -0
  7. package/package.json +5 -7
  8. package/src/agent.d.ts +0 -52
  9. package/src/agent.mjs +0 -204
  10. package/src/agentLoop.mjs +0 -419
  11. package/src/agentState.mjs +0 -41
  12. package/src/claudeCodePlugin.mjs +0 -164
  13. package/src/cliArgs.mjs +0 -175
  14. package/src/cliBatch.mjs +0 -144
  15. package/src/cliCommands.mjs +0 -283
  16. package/src/cliCompleter.mjs +0 -227
  17. package/src/cliCost.mjs +0 -309
  18. package/src/cliFormatter.mjs +0 -413
  19. package/src/cliInteractive.mjs +0 -526
  20. package/src/cliInterruptTransform.mjs +0 -51
  21. package/src/cliMuteTransform.mjs +0 -26
  22. package/src/cliPasteTransform.mjs +0 -183
  23. package/src/config.d.ts +0 -36
  24. package/src/config.mjs +0 -197
  25. package/src/context/loadAgentRoles.mjs +0 -283
  26. package/src/context/loadPrompts.mjs +0 -324
  27. package/src/context/loadUserMessageContext.mjs +0 -147
  28. package/src/costTracker.mjs +0 -210
  29. package/src/env.mjs +0 -44
  30. package/src/main.mjs +0 -278
  31. package/src/mcpClient.mjs +0 -351
  32. package/src/mcpIntegration.mjs +0 -160
  33. package/src/model.d.ts +0 -109
  34. package/src/modelCaller.mjs +0 -32
  35. package/src/modelDefinition.d.ts +0 -92
  36. package/src/prompt.mjs +0 -138
  37. package/src/providers/anthropic.d.ts +0 -248
  38. package/src/providers/anthropic.mjs +0 -587
  39. package/src/providers/bedrock.d.ts +0 -249
  40. package/src/providers/bedrock.mjs +0 -700
  41. package/src/providers/gemini.d.ts +0 -208
  42. package/src/providers/gemini.mjs +0 -754
  43. package/src/providers/openai.d.ts +0 -281
  44. package/src/providers/openai.mjs +0 -544
  45. package/src/providers/openaiCompatible.d.ts +0 -147
  46. package/src/providers/openaiCompatible.mjs +0 -652
  47. package/src/providers/platform/awsSigV4.mjs +0 -184
  48. package/src/providers/platform/azure.mjs +0 -42
  49. package/src/providers/platform/bedrock.mjs +0 -78
  50. package/src/providers/platform/googleCloud.mjs +0 -34
  51. package/src/subagent.mjs +0 -265
  52. package/src/tmpfile.mjs +0 -27
  53. package/src/tool.d.ts +0 -74
  54. package/src/toolExecutor.mjs +0 -236
  55. package/src/toolInputValidator.mjs +0 -183
  56. package/src/toolUseApprover.mjs +0 -99
  57. package/src/tools/askURL.mjs +0 -209
  58. package/src/tools/askWeb.mjs +0 -208
  59. package/src/tools/compactContext.d.ts +0 -4
  60. package/src/tools/compactContext.mjs +0 -87
  61. package/src/tools/delegateToSubagent.d.ts +0 -4
  62. package/src/tools/delegateToSubagent.mjs +0 -48
  63. package/src/tools/execCommand.d.ts +0 -22
  64. package/src/tools/execCommand.mjs +0 -200
  65. package/src/tools/patchFile.d.ts +0 -4
  66. package/src/tools/patchFile.mjs +0 -133
  67. package/src/tools/reportAsSubagent.d.ts +0 -3
  68. package/src/tools/reportAsSubagent.mjs +0 -44
  69. package/src/tools/tmuxCommand.d.ts +0 -14
  70. package/src/tools/tmuxCommand.mjs +0 -194
  71. package/src/tools/writeFile.d.ts +0 -4
  72. package/src/tools/writeFile.mjs +0 -56
  73. package/src/usageStore.mjs +0 -167
  74. package/src/utils/evalJSONConfig.mjs +0 -72
  75. package/src/utils/matchValue.d.ts +0 -6
  76. package/src/utils/matchValue.mjs +0 -40
  77. package/src/utils/noThrow.mjs +0 -31
  78. package/src/utils/notify.mjs +0 -29
  79. package/src/utils/parseFileRange.mjs +0 -18
  80. package/src/utils/readFileRange.mjs +0 -33
  81. package/src/utils/retryOnError.mjs +0 -41
  82. package/src/voiceInput.mjs +0 -61
  83. package/src/voiceInputGemini.mjs +0 -105
  84. package/src/voiceInputOpenAI.mjs +0 -104
  85. package/src/voiceInputSession.mjs +0 -543
  86. package/src/voiceToggleKey.mjs +0 -62
@@ -1,526 +0,0 @@
1
- /**
2
- * @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "./agent"
3
- * @import { ClaudeCodePlugin } from "./claudeCodePlugin.mjs"
4
- * @import { VoiceInputConfig, VoiceSession } from "./voiceInput.mjs"
5
- */
6
-
7
- import readline from "node:readline";
8
- import { styleText } from "node:util";
9
- import { createCommandHandler } from "./cliCommands.mjs";
10
- import { createCompleter, SLASH_COMMANDS } from "./cliCompleter.mjs";
11
- import {
12
- formatCostSummary,
13
- formatProviderTokenUsage,
14
- printMessage,
15
- } from "./cliFormatter.mjs";
16
- import { createInterruptTransform } from "./cliInterruptTransform.mjs";
17
- import { createMuteTransform } from "./cliMuteTransform.mjs";
18
- import { createPasteHandler } from "./cliPasteTransform.mjs";
19
- import { appendUsageRecord, buildUsageRecord } from "./usageStore.mjs";
20
- import { notify } from "./utils/notify.mjs";
21
- import { parseVoiceToggleKey, startVoiceSession } from "./voiceInput.mjs";
22
-
23
- const HELP_MESSAGE = [
24
- "Commands:",
25
- ...SLASH_COMMANDS.map(
26
- (cmd) => ` ${cmd.name.padEnd(13)} - ${cmd.description}`,
27
- ),
28
- "",
29
- "Multi-line Input Syntax:",
30
- ' """ - Start/stop multi-line input mode',
31
- "",
32
- "File Input Syntax:",
33
- " !path/to/file - Read content from a file",
34
- " !path/to/file:N - Read line N from a file",
35
- " !path/to/file:N-M - Read lines N to M from a file",
36
- "",
37
- "References (use within input content):",
38
- " @path/to/file - Reference content from another file",
39
- " @path/to/file:N - Reference line N from another file",
40
- " @path/to/file:N-M - Reference lines N to M from another file",
41
- "",
42
- "Image Attachments (use within input content):",
43
- " @path/to/image.png - Attach an image (png, jpg, jpeg, gif, webp)",
44
- " @'path/with spaces.png' - Quote paths that include spaces",
45
- " @path/with\\ spaces.png - Escape spaces with a backslash",
46
- ]
47
- .join("\n")
48
- .trim()
49
- .replace(/^[^ ].*:/gm, (m) => styleText("bold", m))
50
- .replace(/^ {2}\/.+?(?= - )/gm, (m) => styleText("cyan", m))
51
- .replace(/^ {2}.+?(?= - )/gm, (m) => styleText("blue", m));
52
-
53
- /**
54
- * @typedef {object} CliOptions
55
- * @property {UserEventEmitter} userEventEmitter
56
- * @property {AgentEventEmitter} agentEventEmitter
57
- * @property {AgentCommands} agentCommands
58
- * @property {string} sessionId
59
- * @property {string} modelName
60
- * @property {{ command: string; args?: string[] } | undefined} notifyCmd
61
- * @property {boolean} sandbox
62
- * @property {() => Promise<void>} onStop
63
- * @property {ClaudeCodePlugin[]} [claudeCodePlugins]
64
- * @property {VoiceInputConfig} [voiceInput]
65
- */
66
-
67
- /**
68
- * Persist the session's cost summary to the usage log.
69
- * Failures are logged but never thrown so exit is not blocked.
70
- *
71
- * @param {import("./costTracker.mjs").CostSummary} summary
72
- * @param {{ sessionId: string, modelName: string }} meta
73
- */
74
- async function persistUsage(summary, { sessionId, modelName }) {
75
- try {
76
- const record = buildUsageRecord({
77
- sessionId,
78
- mode: "interactive",
79
- modelName,
80
- workingDir: process.cwd(),
81
- costSummary: summary,
82
- });
83
- if (!record) return;
84
- await appendUsageRecord(record);
85
- } catch (err) {
86
- const message = err instanceof Error ? err.message : String(err);
87
- console.error(
88
- styleText("yellow", `Warning: failed to record usage: ${message}`),
89
- );
90
- }
91
- }
92
-
93
- /**
94
- * @param {CliOptions} options
95
- */
96
- export function startInteractiveSession({
97
- userEventEmitter,
98
- agentEventEmitter,
99
- agentCommands,
100
- sessionId,
101
- modelName,
102
- notifyCmd,
103
- sandbox,
104
- onStop,
105
- claudeCodePlugins,
106
- voiceInput,
107
- }) {
108
- /** @type {{ turn: boolean, multiLineBuffer: string[] | null, subagentName: string }} */
109
- const state = {
110
- turn: true,
111
- multiLineBuffer: null,
112
- subagentName: "",
113
- };
114
-
115
- /**
116
- * Active voice input session, or null when not recording.
117
- * @type {{ session: VoiceSession, startCursor: number, transcriptLength: number } | null}
118
- */
119
- let voice = null;
120
-
121
- // Parse the voice toggle key once at startup so misconfiguration fails
122
- // loudly instead of silently falling back.
123
- const voiceToggle = parseVoiceToggleKey(voiceInput?.toggleKey);
124
-
125
- const getCliPrompt = (subagentName = "", flashMessage = "") =>
126
- [
127
- "",
128
- styleText(
129
- ["white", "bgGray"],
130
- [
131
- ...(subagentName ? [`[${subagentName}]`] : []),
132
- `session: ${sessionId} | model: ${modelName} | sandbox: ${sandbox ? "on" : "off"}`,
133
- ].join(" "),
134
- ),
135
- ...(flashMessage ? [flashMessage] : []),
136
- "> ",
137
- ].join("\n");
138
-
139
- // Cleanup handler to disable bracketed paste mode on exit
140
- const cleanup = () => {
141
- if (process.stdout.isTTY) {
142
- process.stdout.write("\x1b[?2004l");
143
- }
144
- };
145
-
146
- // Handle exit signals
147
- let isExiting = false;
148
- const handleExit = async () => {
149
- if (isExiting) return;
150
- isExiting = true;
151
-
152
- cleanup();
153
- const summary = agentCommands.getCostSummary();
154
- console.log();
155
- console.log(formatCostSummary(summary));
156
- await persistUsage(summary, { sessionId, modelName });
157
- await onStop();
158
- process.exit(0);
159
- };
160
-
161
- // Double-press Ctrl-D exit confirmation
162
- let lastCtrlDAttempt = 0;
163
- const EXIT_CONFIRM_TIMEOUT = 1500;
164
-
165
- /** @type {import("node:readline").Interface} */
166
- let cli;
167
-
168
- /**
169
- * Clear the current readline input line and redraw the prompt.
170
- * Also aborts multi-line input mode if active.
171
- */
172
- const resetInput = () => {
173
- if (state.multiLineBuffer !== null) {
174
- state.multiLineBuffer = null;
175
- cli.setPrompt(currentCliPrompt);
176
- }
177
- cli.write(null, { ctrl: true, name: "a" }); // move to line start
178
- cli.write(null, { ctrl: true, name: "k" }); // delete to line end
179
- cli.prompt();
180
- };
181
-
182
- const stopVoiceSession = async () => {
183
- if (!voice) return;
184
- const current = voice;
185
- voice = null;
186
- await current.session.stop();
187
- cli.setPrompt(currentCliPrompt);
188
- // @ts-expect-error - internal property
189
- cli._refreshLine?.();
190
- };
191
-
192
- const handleVoiceToggle = () => {
193
- // Ignore while the agent is working.
194
- if (!state.turn) return;
195
-
196
- if (voice) {
197
- stopVoiceSession();
198
- return;
199
- }
200
-
201
- if (!voiceInput) {
202
- cli.setPrompt(
203
- getCliPrompt(
204
- state.subagentName,
205
- styleText(
206
- "yellow",
207
- `Voice input not configured. Set \`voiceInput\` in your config to enable ${voiceToggle.label}.`,
208
- ),
209
- ),
210
- );
211
- cli.prompt(true);
212
- return;
213
- }
214
-
215
- const startCursor = cli.cursor;
216
- const session = startVoiceSession({
217
- config: voiceInput,
218
- callbacks: {
219
- onTranscript: (delta) => {
220
- if (!voice) return;
221
- const insertAt = voice.startCursor + voice.transcriptLength;
222
- // Insert delta at the recording's insertion point. User input is
223
- // swallowed while recording, so the buffer around `insertAt` is
224
- // stable.
225
- const before = cli.line.slice(0, insertAt);
226
- const after = cli.line.slice(insertAt);
227
- // `line` and `cursor` are declared readonly in the Node typings but
228
- // are writable at runtime — the existing code already patches
229
- // `_refreshLine` in the same way.
230
- const mutableCli = /** @type {{ line: string, cursor: number }} */ (
231
- /** @type {unknown} */ (cli)
232
- );
233
- mutableCli.line = before + delta + after;
234
- mutableCli.cursor = insertAt + delta.length;
235
- voice.transcriptLength += delta.length;
236
- // @ts-expect-error - internal property
237
- cli._refreshLine?.();
238
- },
239
- onError: (err) => {
240
- voice = null;
241
- cli.setPrompt(
242
- getCliPrompt(
243
- state.subagentName,
244
- styleText("red", `Voice input error: ${err.message}`),
245
- ),
246
- );
247
- cli.prompt(true);
248
- },
249
- onClose: () => {
250
- if (!voice) return;
251
- voice = null;
252
- cli.setPrompt(currentCliPrompt);
253
- // @ts-expect-error - internal property
254
- cli._refreshLine?.();
255
- },
256
- },
257
- });
258
- voice = { session, startCursor, transcriptLength: 0 };
259
- cli.setPrompt(
260
- getCliPrompt(
261
- state.subagentName,
262
- styleText(["red", "bold"], `● REC (${voiceToggle.label} to stop)`),
263
- ),
264
- );
265
- // @ts-expect-error - internal property
266
- cli._refreshLine?.();
267
- };
268
-
269
- const handleCtrlC = () => {
270
- // Stop voice recording first if active.
271
- if (voice) {
272
- stopVoiceSession();
273
- return;
274
- }
275
-
276
- // Agent turn: pause auto-approve; do not clear input.
277
- if (!state.turn) {
278
- agentCommands.pauseAutoApprove();
279
- console.log(
280
- styleText(
281
- "yellow",
282
- "\n\n⚠️ Ctrl-C: Auto-approve paused. Finishing current tool...\nPress Ctrl-D twice to exit.\n",
283
- ),
284
- );
285
- return;
286
- }
287
-
288
- // User turn: clear current input. On empty input, show exit hint.
289
- const hasInput = cli.line.length > 0 || state.multiLineBuffer !== null;
290
- if (hasInput) {
291
- resetInput();
292
- } else {
293
- cli.setPrompt(
294
- getCliPrompt(
295
- state.subagentName,
296
- styleText("yellow", "Press Ctrl-D twice to exit"),
297
- ),
298
- );
299
- cli.prompt();
300
- }
301
- // Reset Ctrl-D confirmation when Ctrl-C is pressed
302
- lastCtrlDAttempt = 0;
303
- };
304
-
305
- const handleCtrlD = () => {
306
- // User turn with non-empty input: ignore Ctrl-D entirely.
307
- if (state.turn && (cli.line.length > 0 || state.multiLineBuffer !== null)) {
308
- return;
309
- }
310
-
311
- const now = Date.now();
312
- if (now - lastCtrlDAttempt < EXIT_CONFIRM_TIMEOUT) {
313
- handleExit();
314
- return;
315
- }
316
- lastCtrlDAttempt = now;
317
- if (state.turn) {
318
- cli.setPrompt(
319
- getCliPrompt(
320
- state.subagentName,
321
- styleText("yellow", "Press Ctrl-D again to exit."),
322
- ),
323
- );
324
- cli.prompt();
325
- } else {
326
- console.log(styleText("yellow", "\n\n⚠️ Press Ctrl-D again to exit.\n"));
327
- }
328
- };
329
-
330
- // Pre-readline pipeline:
331
- // stdin -> interrupt (Ctrl-C / Ctrl-D) -> mute (voice recording) -> paste (bracketed paste) -> readline
332
- const interrupt = createInterruptTransform({
333
- onCtrlC: handleCtrlC,
334
- onCtrlD: handleCtrlD,
335
- onVoiceToggle: handleVoiceToggle,
336
- voiceToggleByte: voiceToggle.byte,
337
- });
338
- // While a voice session is recording, swallow all stdin bytes other than
339
- // Ctrl-C / Ctrl-D / the voice toggle key so transcript insertion stays
340
- // consistent.
341
- const mute = createMuteTransform({ isMuted: () => voice !== null });
342
- const paste = createPasteHandler();
343
-
344
- process.stdin.pipe(interrupt).pipe(mute).pipe(paste.transform);
345
-
346
- // Enable bracketed paste mode
347
- if (process.stdout.isTTY) {
348
- process.stdout.write("\x1b[?2004h");
349
- }
350
-
351
- let currentCliPrompt = getCliPrompt();
352
- cli = readline.createInterface({
353
- input: paste.transform,
354
- output: process.stdout,
355
- prompt: currentCliPrompt,
356
- completer: createCompleter(() => cli, claudeCodePlugins),
357
- });
358
-
359
- // Disable automatic prompt redraw on resize during agent turn
360
- // @ts-expect-error - internal property
361
- const originalRefreshLine = cli._refreshLine?.bind(cli);
362
- if (originalRefreshLine) {
363
- // @ts-expect-error - internal property
364
- cli._refreshLine = (...args) => {
365
- if (state.turn) {
366
- originalRefreshLine(...args);
367
- }
368
- };
369
- }
370
-
371
- readline.emitKeypressEvents(process.stdin);
372
- if (process.stdin.isTTY) {
373
- process.stdin.setRawMode(true);
374
- }
375
-
376
- // Handle readline close (e.g., stdin closed externally)
377
- cli.on("close", handleExit);
378
-
379
- const handleCommand = createCommandHandler({
380
- agentCommands,
381
- userEventEmitter,
382
- claudeCodePlugins,
383
- helpMessage: HELP_MESSAGE,
384
- });
385
-
386
- /**
387
- * Process the complete user input.
388
- * @param {string} input
389
- * @returns {Promise<void>}
390
- */
391
- async function processInput(input) {
392
- // Prevent concurrent input processing from multi-line paste
393
- state.turn = false;
394
-
395
- // Resolve paste placeholders to original content
396
- const resolvedInput = paste.resolvePlaceholders(input);
397
- const inputTrimmed = resolvedInput.trim();
398
-
399
- if (inputTrimmed.length === 0) {
400
- state.turn = true;
401
- cli.prompt();
402
- return;
403
- }
404
-
405
- cli.setPrompt(currentCliPrompt);
406
-
407
- const result = await handleCommand(inputTrimmed);
408
- if (result === "prompt") {
409
- state.turn = true;
410
- cli.prompt();
411
- }
412
- }
413
-
414
- cli.on("line", async (lineInput) => {
415
- if (!state.turn) {
416
- console.warn(
417
- styleText(
418
- "yellow",
419
- `\nAgent is working. Ignore input: ${lineInput.trim()}`,
420
- ),
421
- );
422
- return;
423
- }
424
-
425
- // Check for multi-line delimiter
426
- if (lineInput.trim() === '"""') {
427
- if (state.multiLineBuffer === null) {
428
- state.multiLineBuffer = [];
429
- cli.setPrompt(styleText("gray", "... "));
430
- cli.prompt();
431
- return;
432
- }
433
-
434
- const combined = state.multiLineBuffer.join("\n");
435
- state.multiLineBuffer = null;
436
- cli.setPrompt(currentCliPrompt);
437
-
438
- await processInput(combined);
439
- return;
440
- }
441
-
442
- // Accumulate lines if in multi-line mode
443
- if (state.multiLineBuffer !== null) {
444
- state.multiLineBuffer.push(lineInput);
445
- cli.prompt();
446
- return;
447
- }
448
-
449
- await processInput(lineInput);
450
- });
451
-
452
- agentEventEmitter.on("partialMessageContent", (partialContent) => {
453
- if (partialContent.position === "start") {
454
- const subagentPrefix = state.subagentName
455
- ? styleText("cyan", `[${state.subagentName}]\n`)
456
- : "";
457
- const partialContentStr = styleText("gray", `<${partialContent.type}>`);
458
- console.log(`\n${subagentPrefix}${partialContentStr}`);
459
- }
460
- if (partialContent.content) {
461
- if (partialContent.type === "tool_use") {
462
- process.stdout.write(styleText("gray", partialContent.content));
463
- } else {
464
- process.stdout.write(partialContent.content);
465
- }
466
- }
467
- if (partialContent.position === "stop") {
468
- console.log(styleText("gray", `\n</${partialContent.type}>`));
469
- }
470
- });
471
-
472
- agentEventEmitter.on("message", (message) => {
473
- printMessage(message);
474
- });
475
-
476
- agentEventEmitter.on("toolUseRequest", () => {
477
- cli.setPrompt(
478
- getCliPrompt(
479
- state.subagentName,
480
- styleText(
481
- "yellow",
482
- "Approve tool calls? (y = allow once, Y = allow in this session, or feedback)",
483
- ),
484
- ),
485
- );
486
- });
487
-
488
- agentEventEmitter.on("subagentSwitched", (subagent) => {
489
- state.subagentName = subagent?.name ?? "";
490
- currentCliPrompt = getCliPrompt(state.subagentName);
491
- cli.setPrompt(currentCliPrompt);
492
- });
493
-
494
- agentEventEmitter.on("providerTokenUsage", (usage) => {
495
- console.log(formatProviderTokenUsage(usage));
496
- });
497
-
498
- agentEventEmitter.on("error", (error) => {
499
- console.log(
500
- styleText(
501
- "red",
502
- `\nError: message=${error.message}, stack=${error.stack}`,
503
- ),
504
- );
505
- });
506
-
507
- agentEventEmitter.on("turnEnd", async () => {
508
- const err = notify(notifyCmd);
509
- if (err) {
510
- console.error(
511
- styleText("yellow", `\nNotification error: ${err.message}`),
512
- );
513
- }
514
- // 暫定対応: token usageのconsole出力を確実にflushするため、次のevent loop tickまで遅延
515
- await new Promise((resolve) => setTimeout(resolve, 0));
516
-
517
- state.turn = true;
518
- cli.prompt();
519
- });
520
-
521
- cli.prompt();
522
-
523
- // Register cleanup handlers
524
- process.on("exit", cleanup);
525
- process.on("SIGTERM", cleanup);
526
- }
@@ -1,51 +0,0 @@
1
- import { Transform } from "node:stream";
2
-
3
- /**
4
- * Create a Transform that intercepts Ctrl-C (0x03), Ctrl-D (0x04), and an
5
- * optional "voice toggle" byte (default Ctrl-O, 0x0f). When one of those
6
- * bytes is seen anywhere in a chunk, the corresponding callback is invoked
7
- * and the entire chunk is dropped so that downstream consumers (e.g.
8
- * readline) never observe it. All other input flows through unchanged.
9
- *
10
- * Priority when multiple handled bytes appear in the same chunk:
11
- * Ctrl-C > Ctrl-D > voice toggle.
12
- *
13
- * @param {object} handlers
14
- * @param {() => void} handlers.onCtrlC - Called when Ctrl-C is detected
15
- * @param {() => void} handlers.onCtrlD - Called when Ctrl-D is detected
16
- * @param {() => void} [handlers.onVoiceToggle]
17
- * Called when the voice toggle byte is detected.
18
- * @param {number} [handlers.voiceToggleByte]
19
- * Byte value for the voice toggle key. Defaults to 0x0f (Ctrl-O).
20
- * @returns {Transform}
21
- */
22
- export function createInterruptTransform({
23
- onCtrlC,
24
- onCtrlD,
25
- onVoiceToggle,
26
- voiceToggleByte = 0x0f,
27
- }) {
28
- const voiceToggleChar = String.fromCharCode(voiceToggleByte);
29
- return new Transform({
30
- transform(chunk, _encoding, callback) {
31
- const data = chunk.toString("utf8");
32
- if (data.includes("\x03")) {
33
- onCtrlC();
34
- callback();
35
- return;
36
- }
37
- if (data.includes("\x04")) {
38
- onCtrlD();
39
- callback();
40
- return;
41
- }
42
- if (onVoiceToggle && data.includes(voiceToggleChar)) {
43
- onVoiceToggle();
44
- callback();
45
- return;
46
- }
47
- this.push(chunk);
48
- callback();
49
- },
50
- });
51
- }
@@ -1,26 +0,0 @@
1
- import { Transform } from "node:stream";
2
-
3
- /**
4
- * Create a Transform that swallows all chunks while `isMuted()` returns true,
5
- * and passes them through unchanged while it returns false.
6
- *
7
- * Intended to sit between `createInterruptTransform` and the paste handler so
8
- * that callers can fully silence regular stdin input during special modes
9
- * (e.g. while a voice input session is recording) without coupling that
10
- * concern to the interrupt-detection logic.
11
- *
12
- * @param {object} options
13
- * @param {() => boolean} options.isMuted
14
- * Called for each incoming chunk; when true the chunk is dropped.
15
- * @returns {Transform}
16
- */
17
- export function createMuteTransform({ isMuted }) {
18
- return new Transform({
19
- transform(chunk, _encoding, callback) {
20
- if (!isMuted()) {
21
- this.push(chunk);
22
- }
23
- callback();
24
- },
25
- });
26
- }