@iinm/plain-agent 1.10.2 → 1.10.3
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 +2 -2
- package/package.json +1 -1
- package/src/{cliBatch.mjs → cli/batch.mjs} +3 -3
- package/src/{cliCommands.mjs → cli/commands.mjs} +13 -11
- package/src/{cliCompleter.mjs → cli/completer.mjs} +4 -4
- package/src/{cliCost.mjs → cli/cost.mjs} +3 -3
- package/src/cli/formatter.mjs +997 -0
- package/src/{cliInteractive.mjs → cli/interactive.mjs} +48 -14
- package/src/cli/tableDetector.mjs +228 -0
- package/src/config.d.ts +1 -1
- package/src/main.mjs +5 -5
- package/src/{mcpIntegration.mjs → mcp/integration.mjs} +7 -7
- package/src/tools/patchFile.mjs +18 -12
- package/src/{voiceInputGemini.mjs → voice/gemini.mjs} +2 -5
- package/src/voice/input.mjs +29 -0
- package/src/{voiceInputOpenAI.mjs → voice/openai.mjs} +15 -17
- package/src/cliFormatter.mjs +0 -573
- package/src/voiceInput.mjs +0 -61
- /package/src/{cliArgs.mjs → cli/args.mjs} +0 -0
- /package/src/{cliInterruptTransform.mjs → cli/interruptTransform.mjs} +0 -0
- /package/src/{cliMuteTransform.mjs → cli/muteTransform.mjs} +0 -0
- /package/src/{cliPasteTransform.mjs → cli/pasteTransform.mjs} +0 -0
- /package/src/{mcpClient.mjs → mcp/client.mjs} +0 -0
- /package/src/{voiceInputSession.mjs → voice/session.mjs} +0 -0
- /package/src/{voiceToggleKey.mjs → voice/toggleKey.mjs} +0 -0
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "
|
|
3
|
-
* @import { ClaudeCodePlugin } from "
|
|
4
|
-
* @import { VoiceInputConfig
|
|
2
|
+
* @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "../agent"
|
|
3
|
+
* @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs"
|
|
4
|
+
* @import { VoiceInputConfig } from "../voice/input.mjs"
|
|
5
|
+
* @import { VoiceSession } from "../voice/session.mjs"
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import readline from "node:readline";
|
|
8
9
|
import { styleText } from "node:util";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import { appendUsageRecord, buildUsageRecord } from "../usageStore.mjs";
|
|
11
|
+
import { createSequentialExecutor } from "../utils/createSequentialExecutor.mjs";
|
|
12
|
+
import { notify } from "../utils/notify.mjs";
|
|
13
|
+
import { startVoiceSession } from "../voice/input.mjs";
|
|
14
|
+
import { parseVoiceToggleKey } from "../voice/toggleKey.mjs";
|
|
15
|
+
import { createCommandHandler } from "./commands.mjs";
|
|
16
|
+
import { createCompleter, SLASH_COMMANDS } from "./completer.mjs";
|
|
11
17
|
import {
|
|
12
18
|
formatCostSummary,
|
|
13
19
|
formatProviderTokenUsage,
|
|
14
20
|
printMessage,
|
|
15
|
-
} from "./
|
|
16
|
-
import { createInterruptTransform } from "./
|
|
17
|
-
import { createMuteTransform } from "./
|
|
18
|
-
import { createPasteHandler } from "./
|
|
19
|
-
import {
|
|
20
|
-
import { createSequentialExecutor } from "./utils/createSequentialExecutor.mjs";
|
|
21
|
-
import { notify } from "./utils/notify.mjs";
|
|
22
|
-
import { parseVoiceToggleKey, startVoiceSession } from "./voiceInput.mjs";
|
|
21
|
+
} from "./formatter.mjs";
|
|
22
|
+
import { createInterruptTransform } from "./interruptTransform.mjs";
|
|
23
|
+
import { createMuteTransform } from "./muteTransform.mjs";
|
|
24
|
+
import { createPasteHandler } from "./pasteTransform.mjs";
|
|
25
|
+
import { createTableDetector } from "./tableDetector.mjs";
|
|
23
26
|
|
|
24
27
|
const HELP_MESSAGE = [
|
|
25
28
|
"Commands:",
|
|
@@ -70,7 +73,7 @@ const HELP_MESSAGE = [
|
|
|
70
73
|
* Persist the session's cost summary to the usage log.
|
|
71
74
|
* Failures are logged but never thrown so exit is not blocked.
|
|
72
75
|
*
|
|
73
|
-
* @param {import("
|
|
76
|
+
* @param {import("../costTracker.mjs").CostSummary} summary
|
|
74
77
|
* @param {{ sessionId: string, modelName: string, startTime: Date }} meta
|
|
75
78
|
*/
|
|
76
79
|
async function persistUsage(summary, { sessionId, modelName, startTime }) {
|
|
@@ -122,6 +125,9 @@ export function startInteractiveSession({
|
|
|
122
125
|
*/
|
|
123
126
|
let voice = null;
|
|
124
127
|
|
|
128
|
+
// Create the table buffer instance for this session
|
|
129
|
+
const tableBuffer = createTableBuffer();
|
|
130
|
+
|
|
125
131
|
// Parse the voice toggle key once at startup so misconfiguration fails
|
|
126
132
|
// loudly instead of silently falling back.
|
|
127
133
|
const voiceToggle = parseVoiceToggleKey(voiceInput?.toggleKey);
|
|
@@ -465,6 +471,8 @@ export function startInteractiveSession({
|
|
|
465
471
|
if (partialContent.content) {
|
|
466
472
|
if (partialContent.type === "tool_use") {
|
|
467
473
|
process.stdout.write(styleText("gray", partialContent.content));
|
|
474
|
+
} else if (partialContent.type === "text") {
|
|
475
|
+
tableBuffer.feed(partialContent.content);
|
|
468
476
|
} else {
|
|
469
477
|
process.stdout.write(partialContent.content);
|
|
470
478
|
}
|
|
@@ -520,6 +528,9 @@ export function startInteractiveSession({
|
|
|
520
528
|
});
|
|
521
529
|
|
|
522
530
|
agentEventEmitter.on("turnEnd", async () => {
|
|
531
|
+
// Flush any remaining table buffer content
|
|
532
|
+
tableBuffer.forceFlush();
|
|
533
|
+
|
|
523
534
|
const err = notify(notifyCmd);
|
|
524
535
|
if (err) {
|
|
525
536
|
console.error(
|
|
@@ -543,3 +554,26 @@ export function startInteractiveSession({
|
|
|
543
554
|
process.on("exit", cleanup);
|
|
544
555
|
process.on("SIGTERM", cleanup);
|
|
545
556
|
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Creates a table buffer for detecting and formatting markdown tables
|
|
560
|
+
* in streaming text output.
|
|
561
|
+
* Thin shell: delegates pure logic to createTableDetector and handles I/O.
|
|
562
|
+
*/
|
|
563
|
+
function createTableBuffer() {
|
|
564
|
+
const detector = createTableDetector();
|
|
565
|
+
|
|
566
|
+
function feed(/** @type {string} */ chunk) {
|
|
567
|
+
const { output, warnings } = detector.feed(chunk);
|
|
568
|
+
for (const s of output) process.stdout.write(s);
|
|
569
|
+
for (const w of warnings) console.error(styleText("yellow", w));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function forceFlush() {
|
|
573
|
+
const { output, warnings } = detector.forceFlush();
|
|
574
|
+
for (const s of output) process.stdout.write(s);
|
|
575
|
+
for (const w of warnings) console.error(styleText("yellow", w));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return { feed, forceFlush };
|
|
579
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { formatMarkdownTable } from "./formatter.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{ output: string[], warnings: string[] }} DetectorResult
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a table detector for detecting and formatting markdown tables
|
|
9
|
+
* in streaming text output. This is a pure logic module with no I/O side effects.
|
|
10
|
+
*
|
|
11
|
+
* @param {(lines: string[], maxWidth?: number) => string} [formatTable=formatMarkdownTable] - Table formatting function (injectable for testing)
|
|
12
|
+
* @param {number} [maxWidth] - Maximum terminal display width (defaults to process.stdout.columns - 4 or 80)
|
|
13
|
+
* @returns {{ feed: (chunk: string) => DetectorResult, forceFlush: () => DetectorResult }}
|
|
14
|
+
*/
|
|
15
|
+
export function createTableDetector(
|
|
16
|
+
formatTable = formatMarkdownTable,
|
|
17
|
+
maxWidth = process.stdout.columns ? process.stdout.columns - 4 : 80,
|
|
18
|
+
) {
|
|
19
|
+
/** @type {string} - Accumulated incomplete line */
|
|
20
|
+
let pendingLine = "";
|
|
21
|
+
/** @type {string[]} - Lines of the current table being detected */
|
|
22
|
+
const tableLines = [];
|
|
23
|
+
/** @type {boolean} - Inside a code block (```) */
|
|
24
|
+
let inCodeBlock = false;
|
|
25
|
+
const MAX_TABLE_LINES = 200;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Feed a text chunk to the detector.
|
|
29
|
+
* @param {string} chunk
|
|
30
|
+
* @returns {DetectorResult}
|
|
31
|
+
*/
|
|
32
|
+
function feed(chunk) {
|
|
33
|
+
if (chunk.length === 0) return { output: [], warnings: [] };
|
|
34
|
+
pendingLine += chunk;
|
|
35
|
+
|
|
36
|
+
/** @type {string[]} */
|
|
37
|
+
const output = [];
|
|
38
|
+
/** @type {string[]} */
|
|
39
|
+
const warnings = [];
|
|
40
|
+
|
|
41
|
+
// Process complete lines (those containing newlines)
|
|
42
|
+
while (pendingLine.includes("\n")) {
|
|
43
|
+
const idx = pendingLine.indexOf("\n");
|
|
44
|
+
const line = pendingLine.slice(0, idx); // Exclude the newline
|
|
45
|
+
pendingLine = pendingLine.slice(idx + 1);
|
|
46
|
+
const result = processLine(`${line}\n`); // Add newline back for output
|
|
47
|
+
output.push(...result.output);
|
|
48
|
+
warnings.push(...result.warnings);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If not buffering a table and pendingLine has no pipe, output immediately
|
|
52
|
+
// This ensures non-table text is streamed without delay
|
|
53
|
+
if (tableLines.length === 0 && !pendingLine.includes("|")) {
|
|
54
|
+
output.push(pendingLine);
|
|
55
|
+
pendingLine = "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { output, warnings };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Force flush any pending content (call on turn end).
|
|
63
|
+
* @returns {DetectorResult}
|
|
64
|
+
*/
|
|
65
|
+
function forceFlush() {
|
|
66
|
+
/** @type {string[]} */
|
|
67
|
+
const output = [];
|
|
68
|
+
/** @type {string[]} */
|
|
69
|
+
const warnings = [];
|
|
70
|
+
|
|
71
|
+
// Process any remaining pending line
|
|
72
|
+
if (pendingLine.length > 0) {
|
|
73
|
+
// If we have a table buffer, add pending line to it or output directly
|
|
74
|
+
if (tableLines.length > 0) {
|
|
75
|
+
tableLines.push(`${pendingLine}\n`);
|
|
76
|
+
} else {
|
|
77
|
+
output.push(pendingLine);
|
|
78
|
+
}
|
|
79
|
+
pendingLine = "";
|
|
80
|
+
}
|
|
81
|
+
const flushResult = flushTable();
|
|
82
|
+
output.push(...flushResult.output);
|
|
83
|
+
warnings.push(...flushResult.warnings);
|
|
84
|
+
|
|
85
|
+
return { output, warnings };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Process a complete line.
|
|
90
|
+
* @param {string} line - Line including trailing newline
|
|
91
|
+
* @returns {DetectorResult}
|
|
92
|
+
*/
|
|
93
|
+
function processLine(line) {
|
|
94
|
+
/** @type {string[]} */
|
|
95
|
+
const output = [];
|
|
96
|
+
/** @type {string[]} */
|
|
97
|
+
const warnings = [];
|
|
98
|
+
|
|
99
|
+
// Code block detection
|
|
100
|
+
if (line.trimStart().startsWith("```")) {
|
|
101
|
+
inCodeBlock = !inCodeBlock;
|
|
102
|
+
const flushResult = flushTable(); // Code block terminates any ongoing table
|
|
103
|
+
output.push(...flushResult.output);
|
|
104
|
+
warnings.push(...flushResult.warnings);
|
|
105
|
+
output.push(line);
|
|
106
|
+
return { output, warnings };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (inCodeBlock) {
|
|
110
|
+
output.push(line);
|
|
111
|
+
return { output, warnings };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Table start: line begins with pipe
|
|
115
|
+
if (isTableStart(line)) {
|
|
116
|
+
tableLines.push(line);
|
|
117
|
+
|
|
118
|
+
// Buffer limit check
|
|
119
|
+
if (tableLines.length > MAX_TABLE_LINES) {
|
|
120
|
+
const flushResult = flushTableAsIs();
|
|
121
|
+
output.push(...flushResult.output);
|
|
122
|
+
warnings.push(...flushResult.warnings);
|
|
123
|
+
}
|
|
124
|
+
return { output, warnings };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Table continuation: line contains pipe (for rows without leading pipe)
|
|
128
|
+
if (tableLines.length > 0 && isTableContinuation(line)) {
|
|
129
|
+
tableLines.push(line);
|
|
130
|
+
if (tableLines.length > MAX_TABLE_LINES) {
|
|
131
|
+
const flushResult = flushTableAsIs();
|
|
132
|
+
output.push(...flushResult.output);
|
|
133
|
+
warnings.push(...flushResult.warnings);
|
|
134
|
+
}
|
|
135
|
+
return { output, warnings };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Table ended: format and flush buffer, then output current line
|
|
139
|
+
const flushResult = flushTable();
|
|
140
|
+
output.push(...flushResult.output);
|
|
141
|
+
warnings.push(...flushResult.warnings);
|
|
142
|
+
output.push(line);
|
|
143
|
+
return { output, warnings };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Flush table buffer with formatting.
|
|
148
|
+
* @returns {DetectorResult}
|
|
149
|
+
*/
|
|
150
|
+
function flushTable() {
|
|
151
|
+
if (tableLines.length === 0) return { output: [], warnings: [] };
|
|
152
|
+
|
|
153
|
+
/** @type {string[]} */
|
|
154
|
+
const output = [];
|
|
155
|
+
/** @type {string[]} */
|
|
156
|
+
const warnings = [];
|
|
157
|
+
|
|
158
|
+
// Separate trailing empty lines (preserve spacing after table)
|
|
159
|
+
/** @type {string[]} */
|
|
160
|
+
const trailingEmpty = [];
|
|
161
|
+
while (tableLines.length > 0 && tableLines.at(-1)?.trim() === "") {
|
|
162
|
+
const line = tableLines.pop();
|
|
163
|
+
if (line !== undefined) trailingEmpty.unshift(line);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (tableLines.length > 0) {
|
|
167
|
+
// Remove trailing newlines for formatting, then add them back
|
|
168
|
+
const rawLines = tableLines.map((l) =>
|
|
169
|
+
l.endsWith("\n") ? l.slice(0, -1) : l,
|
|
170
|
+
);
|
|
171
|
+
try {
|
|
172
|
+
const formatted = formatTable(rawLines, maxWidth);
|
|
173
|
+
output.push(`${formatted}\n`);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
// Fallback: output raw lines if formatting fails
|
|
176
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
177
|
+
warnings.push(`Warning: Table formatting failed: ${message}`);
|
|
178
|
+
for (const line of tableLines) {
|
|
179
|
+
output.push(line);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
tableLines.length = 0;
|
|
185
|
+
|
|
186
|
+
// Output trailing empty lines
|
|
187
|
+
for (const empty of trailingEmpty) {
|
|
188
|
+
output.push(empty);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { output, warnings };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Flush table buffer without formatting (for oversized tables).
|
|
196
|
+
* @returns {DetectorResult}
|
|
197
|
+
*/
|
|
198
|
+
function flushTableAsIs() {
|
|
199
|
+
if (tableLines.length === 0) return { output: [], warnings: [] };
|
|
200
|
+
const output = [...tableLines];
|
|
201
|
+
tableLines.length = 0;
|
|
202
|
+
return { output, warnings: [] };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check if a line starts a table.
|
|
207
|
+
* @param {string} line
|
|
208
|
+
* @returns {boolean}
|
|
209
|
+
*/
|
|
210
|
+
function isTableStart(line) {
|
|
211
|
+
const trimmed = line.trimStart();
|
|
212
|
+
return trimmed.startsWith("|");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if a line continues a table.
|
|
217
|
+
* This is a heuristic: any line containing a pipe character is considered
|
|
218
|
+
* a potential table row. This may produce false positives for non-table
|
|
219
|
+
* content with pipes (e.g., "Choose A | B | C").
|
|
220
|
+
* @param {string} line
|
|
221
|
+
* @returns {boolean}
|
|
222
|
+
*/
|
|
223
|
+
function isTableContinuation(line) {
|
|
224
|
+
return line.includes("|");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { feed, forceFlush };
|
|
228
|
+
}
|
package/src/config.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
WebSearchToolGeminiOptions,
|
|
11
11
|
WebSearchToolGeminiVertexAIOptions,
|
|
12
12
|
} from "./tools/webSearch.mjs";
|
|
13
|
-
import { VoiceInputConfig } from "./
|
|
13
|
+
import { VoiceInputConfig } from "./voice/input.mjs";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* JSON-serializable webFetch configuration.
|
package/src/main.mjs
CHANGED
|
@@ -10,15 +10,15 @@ import {
|
|
|
10
10
|
installClaudeCodePlugins,
|
|
11
11
|
resolvePluginPaths,
|
|
12
12
|
} from "./claudeCodePlugin.mjs";
|
|
13
|
-
import { parseCliArgs, printHelp } from "./
|
|
14
|
-
import { startBatchSession } from "./
|
|
15
|
-
import { runCostCommand } from "./
|
|
16
|
-
import { startInteractiveSession } from "./
|
|
13
|
+
import { parseCliArgs, printHelp } from "./cli/args.mjs";
|
|
14
|
+
import { startBatchSession } from "./cli/batch.mjs";
|
|
15
|
+
import { runCostCommand } from "./cli/cost.mjs";
|
|
16
|
+
import { startInteractiveSession } from "./cli/interactive.mjs";
|
|
17
17
|
import { loadAppConfig } from "./config.mjs";
|
|
18
18
|
import { loadAgentRoles } from "./context/loadAgentRoles.mjs";
|
|
19
19
|
import { loadPrompts } from "./context/loadPrompts.mjs";
|
|
20
20
|
import { AGENT_PROJECT_METADATA_DIR, USER_NAME } from "./env.mjs";
|
|
21
|
-
import { setupMCPServer } from "./
|
|
21
|
+
import { setupMCPServer } from "./mcp/integration.mjs";
|
|
22
22
|
import { createModelCaller } from "./modelCaller.mjs";
|
|
23
23
|
import { createPrompt } from "./prompt.mjs";
|
|
24
24
|
import { listSessions, loadSession } from "./sessionStore.mjs";
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { StructuredToolResultContent, Tool, ToolImplementation } from "
|
|
3
|
-
* @import { MCPServerConfig } from "
|
|
2
|
+
* @import { StructuredToolResultContent, Tool, ToolImplementation } from "../tool";
|
|
3
|
+
* @import { MCPServerConfig } from "../config";
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { mkdir } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { AGENT_PROJECT_METADATA_DIR } from "
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { AGENT_PROJECT_METADATA_DIR } from "../env.mjs";
|
|
9
|
+
import { writeTmpFile } from "../tmpfile.mjs";
|
|
10
|
+
import { noThrow } from "../utils/noThrow.mjs";
|
|
11
|
+
import { createMCPClient } from "./client.mjs";
|
|
12
12
|
|
|
13
|
-
/** @typedef {import("./
|
|
13
|
+
/** @typedef {import("./client.mjs").MCPClient} MCPClient */
|
|
14
14
|
|
|
15
15
|
const OUTPUT_MAX_LENGTH = 1024 * 8;
|
|
16
16
|
|
package/src/tools/patchFile.mjs
CHANGED
|
@@ -27,18 +27,16 @@ export function createPatchFileTool(
|
|
|
27
27
|
},
|
|
28
28
|
patch: {
|
|
29
29
|
description: `
|
|
30
|
-
Format:
|
|
31
|
-
|
|
30
|
+
Format — a single patch string may contain multiple blocks:
|
|
31
|
+
>>> ${nonce} {start}:{startHash}-{end}:{endHash}
|
|
32
32
|
new content
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@@@ ${nonce} {N}:{afterHash}+
|
|
33
|
+
<<< ${nonce}
|
|
34
|
+
>>> ${nonce} {N}:{afterHash}+
|
|
36
35
|
inserted content
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@@@ ${nonce} 0+
|
|
36
|
+
<<< ${nonce}
|
|
37
|
+
>>> ${nonce} 0+
|
|
40
38
|
prepended content
|
|
41
|
-
|
|
39
|
+
<<< ${nonce}
|
|
42
40
|
|
|
43
41
|
- The nonce "${nonce}" is constant; always use the exact value shown above.
|
|
44
42
|
- Line numbers are 1-indexed and refer to the original file; "{start}-{end}" is inclusive.
|
|
@@ -63,7 +61,7 @@ prepended content
|
|
|
63
61
|
const blocks = parseBlocks(patch, nonce);
|
|
64
62
|
if (blocks.length === 0) {
|
|
65
63
|
throw new Error(
|
|
66
|
-
`No patch blocks found. Each block must start with "
|
|
64
|
+
`No patch blocks found. Each block must start with ">>> ${nonce} ..." and end with "<<< ${nonce}".`,
|
|
67
65
|
);
|
|
68
66
|
}
|
|
69
67
|
|
|
@@ -93,8 +91,8 @@ prepended content
|
|
|
93
91
|
* @returns {PatchBlock[]}
|
|
94
92
|
*/
|
|
95
93
|
export function parseBlocks(patch, nonce) {
|
|
96
|
-
const openPrefix =
|
|
97
|
-
const closeMarker =
|
|
94
|
+
const openPrefix = `>>> ${nonce} `;
|
|
95
|
+
const closeMarker = `<<< ${nonce}`;
|
|
98
96
|
const lines = patch.split("\n");
|
|
99
97
|
|
|
100
98
|
/** @type {PatchBlock[]} */
|
|
@@ -124,6 +122,14 @@ export function parseBlocks(patch, nonce) {
|
|
|
124
122
|
);
|
|
125
123
|
}
|
|
126
124
|
const body = lines.slice(i + 1, closeIdx);
|
|
125
|
+
const nestedOpen = body.findIndex((l) => l.startsWith(openPrefix));
|
|
126
|
+
if (nestedOpen !== -1) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Unclosed block "${openPrefix}${headerArgs}": found another open marker "${body[nestedOpen]}" ` +
|
|
129
|
+
`at line ${i + 1 + nestedOpen + 1} of patch before the close marker. ` +
|
|
130
|
+
`Did you forget "${closeMarker}" to close the previous block?`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
127
133
|
if (header.op === "insert" && body.length === 0) {
|
|
128
134
|
throw new Error(
|
|
129
135
|
`Insert block "${openPrefix}${headerArgs}" has empty body. Use a replace block to delete content.`,
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isObjectLike,
|
|
3
|
-
startWebSocketVoiceSession,
|
|
4
|
-
} from "./voiceInputSession.mjs";
|
|
1
|
+
import { isObjectLike, startWebSocketVoiceSession } from "./session.mjs";
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
|
-
* @import { VoiceProviderHooks, VoiceRecorderConfig, VoiceSession, VoiceSessionCallbacks } from "./
|
|
4
|
+
* @import { VoiceProviderHooks, VoiceRecorderConfig, VoiceSession, VoiceSessionCallbacks } from "./session.mjs"
|
|
8
5
|
*/
|
|
9
6
|
|
|
10
7
|
/**
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { startGeminiVoiceSession } from "./gemini.mjs";
|
|
2
|
+
import { startOpenAIVoiceSession } from "./openai.mjs";
|
|
3
|
+
import { failVoiceSessionAsync } from "./session.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import("./openai.mjs").VoiceInputOpenAIConfig | import("./gemini.mjs").VoiceInputGeminiConfig} VoiceInputConfig
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Start a voice input session. Dispatches to the provider-specific
|
|
10
|
+
* implementation based on `config.provider`.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} options
|
|
13
|
+
* @param {VoiceInputConfig} options.config
|
|
14
|
+
* @param {import("./session.mjs").VoiceSessionCallbacks} options.callbacks
|
|
15
|
+
* @returns {import("./session.mjs").VoiceSession}
|
|
16
|
+
*/
|
|
17
|
+
export function startVoiceSession({ config, callbacks }) {
|
|
18
|
+
if (config.provider === "openai") {
|
|
19
|
+
return startOpenAIVoiceSession({ config, callbacks });
|
|
20
|
+
}
|
|
21
|
+
if (config.provider === "gemini") {
|
|
22
|
+
return startGeminiVoiceSession({ config, callbacks });
|
|
23
|
+
}
|
|
24
|
+
const provider = /** @type {{ provider: string }} */ (config).provider;
|
|
25
|
+
return failVoiceSessionAsync(
|
|
26
|
+
callbacks,
|
|
27
|
+
new Error(`Unsupported voiceInput.provider: ${provider}`),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -1,24 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isObjectLike,
|
|
3
|
-
startWebSocketVoiceSession,
|
|
4
|
-
} from "./voiceInputSession.mjs";
|
|
1
|
+
import { isObjectLike, startWebSocketVoiceSession } from "./session.mjs";
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
|
-
* @import { VoiceProviderHooks, VoiceRecorderConfig, VoiceSession, VoiceSessionCallbacks } from "./
|
|
4
|
+
* @import { VoiceProviderHooks, VoiceRecorderConfig, VoiceSession, VoiceSessionCallbacks } from "./session.mjs"
|
|
8
5
|
*/
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
8
|
* @typedef {Object} VoiceInputOpenAIConfig
|
|
12
9
|
* @property {"openai"} provider
|
|
13
10
|
* @property {string} apiKey
|
|
14
|
-
* @property {string} [model] - Defaults to "gpt-
|
|
11
|
+
* @property {string} [model] - Transcription model. Defaults to "gpt-realtime-whisper".
|
|
15
12
|
* @property {string} [language] - ISO-639-1 code (e.g. "ja", "en"). Improves accuracy and latency when set.
|
|
16
13
|
* @property {string} [baseURL]
|
|
17
14
|
* @property {VoiceRecorderConfig} [recorder]
|
|
18
15
|
* @property {string} [toggleKey] - "ctrl-<char>". Defaults to "ctrl-o".
|
|
19
16
|
*/
|
|
20
17
|
|
|
21
|
-
const
|
|
18
|
+
const OPENAI_DEFAULT_TRANSCRIPTION_MODEL = "gpt-realtime-whisper";
|
|
22
19
|
const OPENAI_DEFAULT_WS = "wss://api.openai.com/v1/realtime";
|
|
23
20
|
const OPENAI_SAMPLE_RATE = 24000;
|
|
24
21
|
const OPENAI_LABEL = "OpenAI Realtime";
|
|
@@ -46,31 +43,32 @@ export function startOpenAIVoiceSession({ config, callbacks }) {
|
|
|
46
43
|
return {
|
|
47
44
|
headers: {
|
|
48
45
|
Authorization: `Bearer ${config.apiKey}`,
|
|
49
|
-
"OpenAI-Beta": "realtime=v1",
|
|
50
46
|
},
|
|
51
47
|
};
|
|
52
48
|
},
|
|
53
49
|
buildSetupMessage(config) {
|
|
54
|
-
const model = config.model ??
|
|
50
|
+
const model = config.model ?? OPENAI_DEFAULT_TRANSCRIPTION_MODEL;
|
|
55
51
|
/** @type {{ model: string, language?: string }} */
|
|
56
52
|
const transcription = { model };
|
|
57
53
|
if (config.language) transcription.language = config.language;
|
|
58
|
-
// The `?intent=transcription` endpoint uses the flat transcription-session
|
|
59
|
-
// schema, not the nested `session.audio.input.*` realtime schema.
|
|
60
54
|
return {
|
|
61
|
-
type: "
|
|
55
|
+
type: "session.update",
|
|
62
56
|
session: {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
type: "transcription",
|
|
58
|
+
audio: {
|
|
59
|
+
input: {
|
|
60
|
+
format: { type: "audio/pcm", rate: OPENAI_SAMPLE_RATE },
|
|
61
|
+
transcription,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
66
64
|
},
|
|
67
65
|
};
|
|
68
66
|
},
|
|
69
67
|
isReadyMessage(message) {
|
|
70
68
|
return (
|
|
71
69
|
isObjectLike(message) &&
|
|
72
|
-
(message.type === "
|
|
73
|
-
message.type === "
|
|
70
|
+
(message.type === "session.created" ||
|
|
71
|
+
message.type === "session.updated")
|
|
74
72
|
);
|
|
75
73
|
},
|
|
76
74
|
extractError(message) {
|