@buzzni/saycode-cli 0.1.4 → 0.1.5
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 +10 -3
- package/index.mjs +559 -254
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,11 @@ saycode chat -p <프로젝트> --voice-file ./prompt.wav
|
|
|
126
126
|
프롬프트에 쌓이지 않습니다. 권한 요청 때만 `◆ Action required — permission response only`로 바뀌고
|
|
127
127
|
해당 선택 입력을 받습니다. 에이전트 턴이 끝나 `Ready`가 표시된 뒤에만 일반 프롬프트가 열립니다.
|
|
128
128
|
|
|
129
|
+
이전 답변을 보려면 터미널 scrollback 단축키인 **`Shift+Page Up/Down`**을 사용합니다. Mac 노트북
|
|
130
|
+
키보드에서는 **`Shift+Fn+↑/↓`**입니다. `Page Up`만 누르면 readline/raw 입력으로 전달될 수 있어
|
|
131
|
+
스크롤되지 않습니다. CLI는 터미널의 기존 scrollback·복사·검색을 보존하기 위해 alternate screen을
|
|
132
|
+
사용하지 않습니다.
|
|
133
|
+
|
|
129
134
|
에이전트가 작업 중일 때 **`Ctrl+C`를 누르면 선택지가 뜹니다.**
|
|
130
135
|
한 글자만 누르면 됩니다 (Enter 불필요). `Ctrl+C`를 한 번 더 누르면 `d`와 같습니다 —
|
|
131
136
|
반복해서 누르는 건 "빨리 나가자"는 뜻이고, 나가는 건 되돌릴 수 있지만 작업을 죽이는 건
|
|
@@ -139,6 +144,8 @@ saycode chat -p <프로젝트> --voice-file ./prompt.wav
|
|
|
139
144
|
| `Ctrl+C` (유휴 중) | CLI를 종료합니다 |
|
|
140
145
|
| `/detach` | 프롬프트에서 치는 `d`의 별칭 |
|
|
141
146
|
| `/exit` | 대화를 떠납니다 (탐색기에서 왔다면 탐색기로 복귀) |
|
|
147
|
+
| `/model` | 다음 메시지부터 사용할 모델을 선택합니다 |
|
|
148
|
+
| `/effort` | 현재 모델의 effort를 다음 메시지부터 변경합니다 |
|
|
142
149
|
| `/voice` | 마이크로 다음 프롬프트를 녹음·전사합니다 |
|
|
143
150
|
| `/voice <audio-path>` | 기존 음성 파일을 다음 프롬프트로 전사합니다 |
|
|
144
151
|
|
|
@@ -179,11 +186,11 @@ GitHub Actions 가 태그로 발행합니다 (`.github/workflows/publish-saycode
|
|
|
179
186
|
|
|
180
187
|
```bash
|
|
181
188
|
# 1. 버전 올리기 — 데스크탑 앱 버전과 무관합니다
|
|
182
|
-
vi cli/manifest.json # "version": "0.1.
|
|
189
|
+
vi cli/manifest.json # "version": "0.1.5"
|
|
183
190
|
|
|
184
191
|
# 2. 커밋하고 태그를 밀면 끝
|
|
185
|
-
git tag -a saycode-cli-v0.1.
|
|
186
|
-
git push origin saycode-cli-v0.1.
|
|
192
|
+
git tag -a saycode-cli-v0.1.5 -m "Release saycode-cli 0.1.5"
|
|
193
|
+
git push origin saycode-cli-v0.1.5
|
|
187
194
|
```
|
|
188
195
|
|
|
189
196
|
워크플로는 발행 전에 이 순서로 막습니다:
|
package/index.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import require$$4$1 from "tls";
|
|
|
19
19
|
import require$$0$5 from "stream";
|
|
20
20
|
import require$$0$3 from "zlib";
|
|
21
21
|
import { readFile, stat, mkdtemp, access, rm, mkdir, appendFile, writeFile } from "node:fs/promises";
|
|
22
|
-
import { spawn } from "node:child_process";
|
|
22
|
+
import { execFile, spawn } from "node:child_process";
|
|
23
23
|
import { randomUUID } from "node:crypto";
|
|
24
24
|
function _mergeNamespaces(n, m) {
|
|
25
25
|
for (var i = 0; i < m.length; i++) {
|
|
@@ -40,7 +40,7 @@ function _mergeNamespaces(n, m) {
|
|
|
40
40
|
}
|
|
41
41
|
return Object.freeze(Object.defineProperty(n, Symbol.toStringTag, { value: "Module" }));
|
|
42
42
|
}
|
|
43
|
-
const version = "0.1.
|
|
43
|
+
const version = "0.1.5";
|
|
44
44
|
const manifest = {
|
|
45
45
|
version
|
|
46
46
|
};
|
|
@@ -294,9 +294,8 @@ In a chat
|
|
|
294
294
|
[Enter] keep watching
|
|
295
295
|
[d] detach — the agent keeps working
|
|
296
296
|
[s] stop the response
|
|
297
|
-
Ctrl+C
|
|
298
|
-
/
|
|
299
|
-
/exit leave the chat
|
|
297
|
+
Ctrl+C idle: leave Shift+Page Up/Down: scroll (Mac: Shift+Fn+↑/↓)
|
|
298
|
+
/model · /effort · /voice [path] · /detach · /exit live chat commands
|
|
300
299
|
|
|
301
300
|
Leaving never stops a remote session — it keeps running on the machine.`;
|
|
302
301
|
function getDefaultExportFromCjs(x) {
|
|
@@ -2802,6 +2801,26 @@ function coerceSessionModels(raw) {
|
|
|
2802
2801
|
}
|
|
2803
2802
|
return out.length ? out : void 0;
|
|
2804
2803
|
}
|
|
2804
|
+
function dedupeByKey(options) {
|
|
2805
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2806
|
+
const out = [];
|
|
2807
|
+
for (const option of options) {
|
|
2808
|
+
if (seen.has(option.key)) continue;
|
|
2809
|
+
seen.add(option.key);
|
|
2810
|
+
out.push(option);
|
|
2811
|
+
}
|
|
2812
|
+
return out;
|
|
2813
|
+
}
|
|
2814
|
+
function buildOpencodeModelOptions(models) {
|
|
2815
|
+
const options = [{ key: DEFAULT_MODEL_KEY, label: "Default" }];
|
|
2816
|
+
if (Array.isArray(models)) {
|
|
2817
|
+
for (const model of models) {
|
|
2818
|
+
if (!model || typeof model.code !== "string" || !model.code) continue;
|
|
2819
|
+
options.push({ key: model.code, label: model.value || model.code });
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return dedupeByKey(options);
|
|
2823
|
+
}
|
|
2805
2824
|
function hasModelCode(models, code) {
|
|
2806
2825
|
return Array.isArray(models) && models.some((model) => model?.code === code);
|
|
2807
2826
|
}
|
|
@@ -2809,6 +2828,10 @@ function normalizeOpencodeModel(selected, models) {
|
|
|
2809
2828
|
if (!selected || selected === DEFAULT_MODEL_KEY) return DEFAULT_MODEL_KEY;
|
|
2810
2829
|
return hasModelCode(models, selected) ? selected : DEFAULT_MODEL_KEY;
|
|
2811
2830
|
}
|
|
2831
|
+
function resolveOpencodeCurrentModel(currentModelCode, models) {
|
|
2832
|
+
if (currentModelCode && hasModelCode(models, currentModelCode)) return currentModelCode;
|
|
2833
|
+
return DEFAULT_MODEL_KEY;
|
|
2834
|
+
}
|
|
2812
2835
|
const MAX_MAPPING_COUNT = 1e3;
|
|
2813
2836
|
function buildProjectConversationsPath(projectId) {
|
|
2814
2837
|
const trimmed = projectId.trim();
|
|
@@ -24382,6 +24405,38 @@ function interruptMenuChoice(key) {
|
|
|
24382
24405
|
if (first === "s") return "stop";
|
|
24383
24406
|
return "watch";
|
|
24384
24407
|
}
|
|
24408
|
+
class ChatSession {
|
|
24409
|
+
state = createChatState();
|
|
24410
|
+
waiters = [];
|
|
24411
|
+
detached = false;
|
|
24412
|
+
get current() {
|
|
24413
|
+
return this.state;
|
|
24414
|
+
}
|
|
24415
|
+
get isDetached() {
|
|
24416
|
+
return this.detached;
|
|
24417
|
+
}
|
|
24418
|
+
detach() {
|
|
24419
|
+
this.detached = true;
|
|
24420
|
+
this.wakeAll();
|
|
24421
|
+
}
|
|
24422
|
+
wakeAll() {
|
|
24423
|
+
const waiting = this.waiters;
|
|
24424
|
+
this.waiters = [];
|
|
24425
|
+
for (const wake of waiting) wake();
|
|
24426
|
+
}
|
|
24427
|
+
apply(event) {
|
|
24428
|
+
const next = reduceChatState(this.state, event);
|
|
24429
|
+
if (next === this.state) return;
|
|
24430
|
+
this.state = next;
|
|
24431
|
+
this.wakeAll();
|
|
24432
|
+
}
|
|
24433
|
+
async waitForAttention() {
|
|
24434
|
+
while (this.state.phase === "responding" && !this.detached) {
|
|
24435
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
24436
|
+
}
|
|
24437
|
+
return this.state;
|
|
24438
|
+
}
|
|
24439
|
+
}
|
|
24385
24440
|
function chatPresentation(state) {
|
|
24386
24441
|
if (state.phase === "idle") {
|
|
24387
24442
|
return {
|
|
@@ -24403,6 +24458,213 @@ function chatPresentation(state) {
|
|
|
24403
24458
|
prompt: null
|
|
24404
24459
|
};
|
|
24405
24460
|
}
|
|
24461
|
+
function chatStartupHints() {
|
|
24462
|
+
return [
|
|
24463
|
+
"During a response, typing is locked. Ctrl+C opens response options.",
|
|
24464
|
+
"Scroll history: Shift+Page Up/Down · Mac laptops: Shift+Fn+↑/↓.",
|
|
24465
|
+
"At the prompt: /model · /effort · /voice to dictate · /detach to leave running · /exit to leave.\n"
|
|
24466
|
+
];
|
|
24467
|
+
}
|
|
24468
|
+
const claudeEffortOptions = [
|
|
24469
|
+
{ key: "low", label: "Low" },
|
|
24470
|
+
{ key: "medium", label: "Medium" },
|
|
24471
|
+
{ key: "high", label: "High" },
|
|
24472
|
+
{ key: "max", label: "Max" }
|
|
24473
|
+
];
|
|
24474
|
+
const claudeFableEffortOptions = [
|
|
24475
|
+
{ key: "low", label: "Low" },
|
|
24476
|
+
{ key: "medium", label: "Medium" },
|
|
24477
|
+
{ key: "high", label: "High" },
|
|
24478
|
+
{ key: "xhigh", label: "Xhigh" },
|
|
24479
|
+
{ key: "max", label: "Max" }
|
|
24480
|
+
];
|
|
24481
|
+
const codexEffortOptions = [
|
|
24482
|
+
{ key: "low", label: "Low" },
|
|
24483
|
+
{ key: "medium", label: "Medium" },
|
|
24484
|
+
{ key: "high", label: "High" },
|
|
24485
|
+
{ key: "xhigh", label: "Xhigh" }
|
|
24486
|
+
];
|
|
24487
|
+
const codexMaxEffortOptions = [
|
|
24488
|
+
...codexEffortOptions,
|
|
24489
|
+
{ key: "max", label: "Max" }
|
|
24490
|
+
];
|
|
24491
|
+
const codexUltraEffortOptions = [
|
|
24492
|
+
...codexMaxEffortOptions,
|
|
24493
|
+
{ key: "ultra", label: "Ultra" }
|
|
24494
|
+
];
|
|
24495
|
+
function getEffortOptionsForAgent(agent, model) {
|
|
24496
|
+
if (model === "default") return [];
|
|
24497
|
+
if (agent === "codex") {
|
|
24498
|
+
if (model === "gpt-5.6-sol" || model === "gpt-5.6-terra") return codexUltraEffortOptions;
|
|
24499
|
+
if (model === "gpt-5.6-luna") return codexMaxEffortOptions;
|
|
24500
|
+
return codexEffortOptions;
|
|
24501
|
+
}
|
|
24502
|
+
if (agent === "gemini" || agent === "opencode") return [];
|
|
24503
|
+
if (model === "claude-fable-5") return claudeFableEffortOptions;
|
|
24504
|
+
return claudeEffortOptions;
|
|
24505
|
+
}
|
|
24506
|
+
function defaultEffortForAgent(agent, model) {
|
|
24507
|
+
const options = getEffortOptionsForAgent(agent, model);
|
|
24508
|
+
if (options.length === 0) return null;
|
|
24509
|
+
if (agent === "codex") {
|
|
24510
|
+
if (model === "gpt-5.6-sol") return "low";
|
|
24511
|
+
if (model === "gpt-5.6-terra" || model === "gpt-5.6-luna") return "medium";
|
|
24512
|
+
}
|
|
24513
|
+
return options.some((option) => option.key === "high") ? "high" : options[options.length - 1].key;
|
|
24514
|
+
}
|
|
24515
|
+
const newSessionAgents = [
|
|
24516
|
+
{ id: "claude", label: "Claude", provider: "Anthropic" },
|
|
24517
|
+
{ id: "codex", label: "Codex", provider: "OpenAI" },
|
|
24518
|
+
{ id: "opencode", label: "opencode", provider: "opencode" }
|
|
24519
|
+
];
|
|
24520
|
+
const claudeModelOptions = [
|
|
24521
|
+
{ key: "default", label: "Default" },
|
|
24522
|
+
{ key: "claude-fable-5", label: "Fable 5" },
|
|
24523
|
+
{ key: "claude-opus-5", label: "Opus 5" },
|
|
24524
|
+
{ key: "claude-sonnet-5", label: "Sonnet 5" },
|
|
24525
|
+
{ key: "claude-haiku-4-5", label: "Haiku 4.5" }
|
|
24526
|
+
];
|
|
24527
|
+
const codexModelOptions = [
|
|
24528
|
+
{ key: "default", label: "Default" },
|
|
24529
|
+
{ key: "gpt-5.6-sol", label: "GPT-5.6 Sol" },
|
|
24530
|
+
{ key: "gpt-5.6-terra", label: "GPT-5.6 Terra" },
|
|
24531
|
+
{ key: "gpt-5.6-luna", label: "GPT-5.6 Luna" },
|
|
24532
|
+
{ key: "gpt-5.5", label: "gpt-5.5" },
|
|
24533
|
+
{ key: "gpt-5.4", label: "gpt-5.4" },
|
|
24534
|
+
{ key: "gpt-5.3-codex", label: "gpt-5.3-codex" },
|
|
24535
|
+
{ key: "gpt-5.2-codex", label: "gpt-5.2-codex" },
|
|
24536
|
+
{ key: "gpt-5.1-codex-max", label: "gpt-5.1-codex-max" },
|
|
24537
|
+
{ key: "gpt-5.2", label: "gpt-5.2" },
|
|
24538
|
+
{ key: "gpt-5.1-codex-mini", label: "gpt-5.1-codex-mini" }
|
|
24539
|
+
];
|
|
24540
|
+
const geminiModelOptions = [
|
|
24541
|
+
{ key: "gemini-3.1-pro-preview", label: "gemini 3.1 pro" },
|
|
24542
|
+
{ key: "gemini-3-flash-preview", label: "gemini 3 flash" },
|
|
24543
|
+
{ key: "gemini-3.1-flash-lite-preview", label: "gemini 3.1 flash lite" },
|
|
24544
|
+
{ key: "gemini-2.5-pro", label: "gemini 2.5 pro" },
|
|
24545
|
+
{ key: "gemini-2.5-flash", label: "gemini 2.5 flash" },
|
|
24546
|
+
{ key: "gemini-2.5-flash-lite", label: "gemini 2.5 flash lite" }
|
|
24547
|
+
];
|
|
24548
|
+
const opencodeModelOptions = [
|
|
24549
|
+
{ key: "default", label: "Default" }
|
|
24550
|
+
];
|
|
24551
|
+
function getModelsForAgent(agent) {
|
|
24552
|
+
if (agent === "codex") return codexModelOptions;
|
|
24553
|
+
if (agent === "gemini") return geminiModelOptions;
|
|
24554
|
+
if (agent === "opencode") return opencodeModelOptions;
|
|
24555
|
+
return claudeModelOptions;
|
|
24556
|
+
}
|
|
24557
|
+
function normalizeModelForAgent(agent, model) {
|
|
24558
|
+
if (agent === "opencode") return model || "default";
|
|
24559
|
+
const models = getModelsForAgent(agent);
|
|
24560
|
+
return models.some((option) => option.key === model) ? model : models[0]?.key ?? "default";
|
|
24561
|
+
}
|
|
24562
|
+
function normalizeModelSelectionOptions(agent, current, patch) {
|
|
24563
|
+
const model = normalizeModelForAgent(agent, patch.model ?? current.model);
|
|
24564
|
+
const efforts = getEffortOptionsForAgent(agent, model);
|
|
24565
|
+
const effort = patch.model !== void 0 ? defaultEffortForAgent(agent, model) : patch.effort !== void 0 && efforts.some((option) => option.key === patch.effort) ? patch.effort : current.effort && efforts.some((option) => option.key === current.effort) ? current.effort : defaultEffortForAgent(agent, model);
|
|
24566
|
+
return { model, effort };
|
|
24567
|
+
}
|
|
24568
|
+
function messageModelOptionsFromSelection(agent, options, availableModels) {
|
|
24569
|
+
if (agent === "opencode") {
|
|
24570
|
+
const model = normalizeOpencodeModel(options.model, availableModels);
|
|
24571
|
+
return { model: model !== "default" ? model : void 0, effort: null };
|
|
24572
|
+
}
|
|
24573
|
+
const normalized = normalizeModelSelectionOptions(agent, options, {});
|
|
24574
|
+
return {
|
|
24575
|
+
model: normalized.model !== "default" ? normalized.model : void 0,
|
|
24576
|
+
effort: normalized.effort
|
|
24577
|
+
};
|
|
24578
|
+
}
|
|
24579
|
+
function configurableAgent$1(value2) {
|
|
24580
|
+
return value2 === "claude" || value2 === "codex" || value2 === "gemini" || value2 === "opencode" ? value2 : null;
|
|
24581
|
+
}
|
|
24582
|
+
function createLiveChatSettings(input) {
|
|
24583
|
+
const agent = configurableAgent$1(input.agent);
|
|
24584
|
+
const models = [...input.models ?? []];
|
|
24585
|
+
if (!agent) return { agent: null, model: "default", effort: null, models };
|
|
24586
|
+
if (agent === "opencode") {
|
|
24587
|
+
const requested = input.model ?? resolveOpencodeCurrentModel(input.currentModelCode ?? void 0, models);
|
|
24588
|
+
return { agent, model: normalizeOpencodeModel(requested, models), effort: null, models };
|
|
24589
|
+
}
|
|
24590
|
+
let selection = normalizeModelSelectionOptions(
|
|
24591
|
+
agent,
|
|
24592
|
+
{ model: "default", effort: null },
|
|
24593
|
+
{ model: input.model ?? "default" }
|
|
24594
|
+
);
|
|
24595
|
+
if (input.effort) selection = normalizeModelSelectionOptions(agent, selection, { effort: input.effort });
|
|
24596
|
+
return { agent, ...selection, models };
|
|
24597
|
+
}
|
|
24598
|
+
function liveChatModelChoices(settings) {
|
|
24599
|
+
if (!settings.agent) return [];
|
|
24600
|
+
if (settings.agent === "opencode") return buildOpencodeModelOptions(settings.models);
|
|
24601
|
+
return getModelsForAgent(settings.agent);
|
|
24602
|
+
}
|
|
24603
|
+
function liveChatEffortChoices(settings) {
|
|
24604
|
+
if (!settings.agent) return [];
|
|
24605
|
+
return getEffortOptionsForAgent(settings.agent, settings.model);
|
|
24606
|
+
}
|
|
24607
|
+
function updateLiveChatSettings(current, patch) {
|
|
24608
|
+
if (!current.agent) return current;
|
|
24609
|
+
if (current.agent === "opencode") {
|
|
24610
|
+
if (patch.model === void 0) return current;
|
|
24611
|
+
return { ...current, model: normalizeOpencodeModel(patch.model, current.models), effort: null };
|
|
24612
|
+
}
|
|
24613
|
+
let selection = { model: current.model, effort: current.effort };
|
|
24614
|
+
if (patch.model !== void 0) {
|
|
24615
|
+
if (!liveChatModelChoices(current).some((option) => option.key === patch.model)) return current;
|
|
24616
|
+
selection = normalizeModelSelectionOptions(current.agent, selection, { model: patch.model });
|
|
24617
|
+
}
|
|
24618
|
+
if (patch.effort !== void 0) {
|
|
24619
|
+
const efforts = getEffortOptionsForAgent(current.agent, selection.model);
|
|
24620
|
+
if (patch.effort !== null && !efforts.some((option) => option.key === patch.effort)) return current;
|
|
24621
|
+
selection = {
|
|
24622
|
+
...selection,
|
|
24623
|
+
effort: patch.effort ?? defaultEffortForAgent(current.agent, selection.model)
|
|
24624
|
+
};
|
|
24625
|
+
}
|
|
24626
|
+
return { ...current, ...selection };
|
|
24627
|
+
}
|
|
24628
|
+
function observeLiveChatMessage(current, content) {
|
|
24629
|
+
if (content.role !== "user") return current;
|
|
24630
|
+
const model = typeof content.meta?.model === "string" ? content.meta.model : "default";
|
|
24631
|
+
const effort = typeof content.meta?.effort === "string" ? content.meta.effort : null;
|
|
24632
|
+
return updateLiveChatSettings(current, { model, effort });
|
|
24633
|
+
}
|
|
24634
|
+
function buildLiveChatUserMessage(text2, settings) {
|
|
24635
|
+
const modelOptions = settings.agent ? messageModelOptionsFromSelection(settings.agent, settings, settings.models) : {};
|
|
24636
|
+
return {
|
|
24637
|
+
role: "user",
|
|
24638
|
+
content: { type: "text", text: text2 },
|
|
24639
|
+
meta: {
|
|
24640
|
+
permissionMode: "default",
|
|
24641
|
+
...modelOptions.model ? { model: modelOptions.model } : {},
|
|
24642
|
+
...modelOptions.effort ? { effort: modelOptions.effort } : {}
|
|
24643
|
+
}
|
|
24644
|
+
};
|
|
24645
|
+
}
|
|
24646
|
+
function liveChatSettingsFromSessionRows(rows, sessionId, candidateKeys, fallbackAgent) {
|
|
24647
|
+
const row = rows.find((entry) => entry.id === sessionId);
|
|
24648
|
+
const metadata = decryptJsonWithCandidates(row?.metadata, candidateKeys);
|
|
24649
|
+
const agent = agentFromFlavor(typeof metadata?.flavor === "string" ? metadata.flavor : void 0) ?? fallbackAgent;
|
|
24650
|
+
const models = coerceSessionModels(metadata?.models) ?? [];
|
|
24651
|
+
return createLiveChatSettings({
|
|
24652
|
+
agent,
|
|
24653
|
+
models,
|
|
24654
|
+
currentModelCode: typeof metadata?.currentModelCode === "string" ? metadata.currentModelCode : null
|
|
24655
|
+
});
|
|
24656
|
+
}
|
|
24657
|
+
function needsLiveChatModelRefresh(settings) {
|
|
24658
|
+
return settings.agent === "opencode" && settings.models.length === 0;
|
|
24659
|
+
}
|
|
24660
|
+
function refreshLiveChatModelsFromSessionRows(current, rows, sessionId, candidateKeys) {
|
|
24661
|
+
if (!needsLiveChatModelRefresh(current)) return current;
|
|
24662
|
+
const row = rows.find((entry) => entry.id === sessionId);
|
|
24663
|
+
const metadata = decryptJsonWithCandidates(row?.metadata, candidateKeys);
|
|
24664
|
+
const models = coerceSessionModels(metadata?.models);
|
|
24665
|
+
if (!models?.length) return current;
|
|
24666
|
+
return createLiveChatSettings({ ...current, models });
|
|
24667
|
+
}
|
|
24406
24668
|
function describePermissionRequest(request) {
|
|
24407
24669
|
const summary = summarizeToolArgs(request.arguments ?? {});
|
|
24408
24670
|
if (summary) return `${request.toolName}(${summary})`;
|
|
@@ -24775,116 +25037,11 @@ async function runProjectConversations(context, query, all) {
|
|
|
24775
25037
|
! Some conversations could not be resolved${payload.warnings.length ? `: ${payload.warnings.join(", ")}` : "."}`);
|
|
24776
25038
|
}
|
|
24777
25039
|
}
|
|
24778
|
-
|
|
24779
|
-
|
|
24780
|
-
|
|
24781
|
-
|
|
24782
|
-
|
|
24783
|
-
];
|
|
24784
|
-
const claudeFableEffortOptions = [
|
|
24785
|
-
{ key: "low", label: "Low" },
|
|
24786
|
-
{ key: "medium", label: "Medium" },
|
|
24787
|
-
{ key: "high", label: "High" },
|
|
24788
|
-
{ key: "xhigh", label: "Xhigh" },
|
|
24789
|
-
{ key: "max", label: "Max" }
|
|
24790
|
-
];
|
|
24791
|
-
const codexEffortOptions = [
|
|
24792
|
-
{ key: "low", label: "Low" },
|
|
24793
|
-
{ key: "medium", label: "Medium" },
|
|
24794
|
-
{ key: "high", label: "High" },
|
|
24795
|
-
{ key: "xhigh", label: "Xhigh" }
|
|
24796
|
-
];
|
|
24797
|
-
const codexMaxEffortOptions = [
|
|
24798
|
-
...codexEffortOptions,
|
|
24799
|
-
{ key: "max", label: "Max" }
|
|
24800
|
-
];
|
|
24801
|
-
const codexUltraEffortOptions = [
|
|
24802
|
-
...codexMaxEffortOptions,
|
|
24803
|
-
{ key: "ultra", label: "Ultra" }
|
|
24804
|
-
];
|
|
24805
|
-
function getEffortOptionsForAgent(agent, model) {
|
|
24806
|
-
if (model === "default") return [];
|
|
24807
|
-
if (agent === "codex") {
|
|
24808
|
-
if (model === "gpt-5.6-sol" || model === "gpt-5.6-terra") return codexUltraEffortOptions;
|
|
24809
|
-
if (model === "gpt-5.6-luna") return codexMaxEffortOptions;
|
|
24810
|
-
return codexEffortOptions;
|
|
24811
|
-
}
|
|
24812
|
-
if (agent === "gemini" || agent === "opencode") return [];
|
|
24813
|
-
if (model === "claude-fable-5") return claudeFableEffortOptions;
|
|
24814
|
-
return claudeEffortOptions;
|
|
24815
|
-
}
|
|
24816
|
-
function defaultEffortForAgent(agent, model) {
|
|
24817
|
-
const options = getEffortOptionsForAgent(agent, model);
|
|
24818
|
-
if (options.length === 0) return null;
|
|
24819
|
-
if (agent === "codex") {
|
|
24820
|
-
if (model === "gpt-5.6-sol") return "low";
|
|
24821
|
-
if (model === "gpt-5.6-terra" || model === "gpt-5.6-luna") return "medium";
|
|
24822
|
-
}
|
|
24823
|
-
return options.some((option) => option.key === "high") ? "high" : options[options.length - 1].key;
|
|
24824
|
-
}
|
|
24825
|
-
const newSessionAgents = [
|
|
24826
|
-
{ id: "claude", label: "Claude", provider: "Anthropic" },
|
|
24827
|
-
{ id: "codex", label: "Codex", provider: "OpenAI" },
|
|
24828
|
-
{ id: "opencode", label: "opencode", provider: "opencode" }
|
|
24829
|
-
];
|
|
24830
|
-
const claudeModelOptions = [
|
|
24831
|
-
{ key: "default", label: "Default" },
|
|
24832
|
-
{ key: "claude-fable-5", label: "Fable 5" },
|
|
24833
|
-
{ key: "claude-opus-5", label: "Opus 5" },
|
|
24834
|
-
{ key: "claude-sonnet-5", label: "Sonnet 5" },
|
|
24835
|
-
{ key: "claude-haiku-4-5", label: "Haiku 4.5" }
|
|
24836
|
-
];
|
|
24837
|
-
const codexModelOptions = [
|
|
24838
|
-
{ key: "default", label: "Default" },
|
|
24839
|
-
{ key: "gpt-5.6-sol", label: "GPT-5.6 Sol" },
|
|
24840
|
-
{ key: "gpt-5.6-terra", label: "GPT-5.6 Terra" },
|
|
24841
|
-
{ key: "gpt-5.6-luna", label: "GPT-5.6 Luna" },
|
|
24842
|
-
{ key: "gpt-5.5", label: "gpt-5.5" },
|
|
24843
|
-
{ key: "gpt-5.4", label: "gpt-5.4" },
|
|
24844
|
-
{ key: "gpt-5.3-codex", label: "gpt-5.3-codex" },
|
|
24845
|
-
{ key: "gpt-5.2-codex", label: "gpt-5.2-codex" },
|
|
24846
|
-
{ key: "gpt-5.1-codex-max", label: "gpt-5.1-codex-max" },
|
|
24847
|
-
{ key: "gpt-5.2", label: "gpt-5.2" },
|
|
24848
|
-
{ key: "gpt-5.1-codex-mini", label: "gpt-5.1-codex-mini" }
|
|
24849
|
-
];
|
|
24850
|
-
const geminiModelOptions = [
|
|
24851
|
-
{ key: "gemini-3.1-pro-preview", label: "gemini 3.1 pro" },
|
|
24852
|
-
{ key: "gemini-3-flash-preview", label: "gemini 3 flash" },
|
|
24853
|
-
{ key: "gemini-3.1-flash-lite-preview", label: "gemini 3.1 flash lite" },
|
|
24854
|
-
{ key: "gemini-2.5-pro", label: "gemini 2.5 pro" },
|
|
24855
|
-
{ key: "gemini-2.5-flash", label: "gemini 2.5 flash" },
|
|
24856
|
-
{ key: "gemini-2.5-flash-lite", label: "gemini 2.5 flash lite" }
|
|
24857
|
-
];
|
|
24858
|
-
const opencodeModelOptions = [
|
|
24859
|
-
{ key: "default", label: "Default" }
|
|
24860
|
-
];
|
|
24861
|
-
function getModelsForAgent(agent) {
|
|
24862
|
-
if (agent === "codex") return codexModelOptions;
|
|
24863
|
-
if (agent === "gemini") return geminiModelOptions;
|
|
24864
|
-
if (agent === "opencode") return opencodeModelOptions;
|
|
24865
|
-
return claudeModelOptions;
|
|
24866
|
-
}
|
|
24867
|
-
function normalizeModelForAgent(agent, model) {
|
|
24868
|
-
if (agent === "opencode") return model || "default";
|
|
24869
|
-
const models = getModelsForAgent(agent);
|
|
24870
|
-
return models.some((option) => option.key === model) ? model : models[0]?.key ?? "default";
|
|
24871
|
-
}
|
|
24872
|
-
function normalizeModelSelectionOptions(agent, current, patch) {
|
|
24873
|
-
const model = normalizeModelForAgent(agent, patch.model ?? current.model);
|
|
24874
|
-
const efforts = getEffortOptionsForAgent(agent, model);
|
|
24875
|
-
const effort = patch.model !== void 0 ? defaultEffortForAgent(agent, model) : patch.effort !== void 0 && efforts.some((option) => option.key === patch.effort) ? patch.effort : current.effort && efforts.some((option) => option.key === current.effort) ? current.effort : defaultEffortForAgent(agent, model);
|
|
24876
|
-
return { model, effort };
|
|
24877
|
-
}
|
|
24878
|
-
function messageModelOptionsFromSelection(agent, options, availableModels) {
|
|
24879
|
-
if (agent === "opencode") {
|
|
24880
|
-
const model = normalizeOpencodeModel(options.model, availableModels);
|
|
24881
|
-
return { model: model !== "default" ? model : void 0, effort: null };
|
|
24882
|
-
}
|
|
24883
|
-
const normalized = normalizeModelSelectionOptions(agent, options, {});
|
|
24884
|
-
return {
|
|
24885
|
-
model: normalized.model !== "default" ? normalized.model : void 0,
|
|
24886
|
-
effort: normalized.effort
|
|
24887
|
-
};
|
|
25040
|
+
function worktreeTargetMismatch(record2, expected) {
|
|
25041
|
+
if (record2.projectId !== expected.projectId) return "project";
|
|
25042
|
+
if (record2.machineId !== expected.machineId) return "machine";
|
|
25043
|
+
if (expected.path !== void 0 && record2.path !== expected.path) return "path";
|
|
25044
|
+
return null;
|
|
24888
25045
|
}
|
|
24889
25046
|
const WORKTREE_NAME_ADJECTIVES = [
|
|
24890
25047
|
"bright",
|
|
@@ -24946,63 +25103,6 @@ function isWorktreePathWithinProjectRoot(projectRoot, worktreePath) {
|
|
|
24946
25103
|
if (segments.length < 1 || segments.length > MAX_MANAGED_WORKTREE_DEPTH) return false;
|
|
24947
25104
|
return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
24948
25105
|
}
|
|
24949
|
-
const DEFAULT_CLI_NEW_CONVERSATION_OPTIONS = {
|
|
24950
|
-
agent: "claude",
|
|
24951
|
-
model: "default",
|
|
24952
|
-
effort: null,
|
|
24953
|
-
useWorktree: false,
|
|
24954
|
-
worktreeName: "",
|
|
24955
|
-
worktreeBaseRef: "HEAD",
|
|
24956
|
-
files: [],
|
|
24957
|
-
voice: false,
|
|
24958
|
-
voiceFile: null
|
|
24959
|
-
};
|
|
24960
|
-
function normalizeNewConversationOptions(current, patch) {
|
|
24961
|
-
const agent = patch.agent ?? current.agent;
|
|
24962
|
-
const agentChanged = patch.agent !== void 0 && patch.agent !== current.agent;
|
|
24963
|
-
const currentSelection = agentChanged ? { model: "default", effort: null } : { model: current.model, effort: current.effort };
|
|
24964
|
-
const modelSelection = normalizeModelSelectionOptions(agent, currentSelection, {
|
|
24965
|
-
...patch.model !== void 0 ? { model: patch.model } : {}
|
|
24966
|
-
});
|
|
24967
|
-
const efforts = getEffortOptionsForAgent(agent, modelSelection.model);
|
|
24968
|
-
const requestedEffort = patch.effort !== void 0 ? patch.effort : modelSelection.effort;
|
|
24969
|
-
const effort = requestedEffort && efforts.some((option) => option.key === requestedEffort) ? requestedEffort : defaultEffortForAgent(agent, modelSelection.model);
|
|
24970
|
-
return {
|
|
24971
|
-
agent,
|
|
24972
|
-
model: modelSelection.model,
|
|
24973
|
-
effort,
|
|
24974
|
-
useWorktree: patch.useWorktree ?? current.useWorktree,
|
|
24975
|
-
worktreeName: patch.worktreeName !== void 0 ? sanitizeWorktreeName(patch.worktreeName) : current.worktreeName,
|
|
24976
|
-
worktreeBaseRef: (patch.worktreeBaseRef ?? current.worktreeBaseRef).trim() || "HEAD",
|
|
24977
|
-
files: patch.files ? [...patch.files] : [...current.files],
|
|
24978
|
-
voice: patch.voice ?? current.voice,
|
|
24979
|
-
voiceFile: patch.voiceFile !== void 0 ? patch.voiceFile : current.voiceFile
|
|
24980
|
-
};
|
|
24981
|
-
}
|
|
24982
|
-
function buildCliUserMessage(text2, options) {
|
|
24983
|
-
const modelOptions = messageModelOptionsFromSelection(options.agent, options);
|
|
24984
|
-
return {
|
|
24985
|
-
role: "user",
|
|
24986
|
-
content: { type: "text", text: text2 },
|
|
24987
|
-
meta: {
|
|
24988
|
-
permissionMode: "default",
|
|
24989
|
-
...modelOptions.model ? { model: modelOptions.model } : {},
|
|
24990
|
-
...modelOptions.effort ? { effort: modelOptions.effort } : {}
|
|
24991
|
-
}
|
|
24992
|
-
};
|
|
24993
|
-
}
|
|
24994
|
-
function modelChoices(agent) {
|
|
24995
|
-
return getModelsForAgent(agent);
|
|
24996
|
-
}
|
|
24997
|
-
function effortChoices(agent, model) {
|
|
24998
|
-
return getEffortOptionsForAgent(agent, normalizeModelForAgent(agent, model));
|
|
24999
|
-
}
|
|
25000
|
-
function worktreeTargetMismatch(record2, expected) {
|
|
25001
|
-
if (record2.projectId !== expected.projectId) return "project";
|
|
25002
|
-
if (record2.machineId !== expected.machineId) return "machine";
|
|
25003
|
-
if (expected.path !== void 0 && record2.path !== expected.path) return "path";
|
|
25004
|
-
return null;
|
|
25005
|
-
}
|
|
25006
25106
|
async function createWorktreeOnMachine(input) {
|
|
25007
25107
|
const record2 = await input.client.createProjectWorktree(input.projectId, {
|
|
25008
25108
|
name: input.name,
|
|
@@ -25187,6 +25287,45 @@ function uploadCliAttachment(transport, machineId, workspaceRoot, file, determin
|
|
|
25187
25287
|
function prepareCliMessageWithAttachments(text2, files, uploadFile) {
|
|
25188
25288
|
return preparePersonalChatMessageWithAttachments({ text: text2, files }, { uploadFile });
|
|
25189
25289
|
}
|
|
25290
|
+
const DEFAULT_CLI_NEW_CONVERSATION_OPTIONS = {
|
|
25291
|
+
agent: "claude",
|
|
25292
|
+
model: "default",
|
|
25293
|
+
effort: null,
|
|
25294
|
+
useWorktree: false,
|
|
25295
|
+
worktreeName: "",
|
|
25296
|
+
worktreeBaseRef: "HEAD",
|
|
25297
|
+
files: [],
|
|
25298
|
+
voice: false,
|
|
25299
|
+
voiceFile: null
|
|
25300
|
+
};
|
|
25301
|
+
function normalizeNewConversationOptions(current, patch) {
|
|
25302
|
+
const agent = patch.agent ?? current.agent;
|
|
25303
|
+
const agentChanged = patch.agent !== void 0 && patch.agent !== current.agent;
|
|
25304
|
+
const currentSelection = agentChanged ? { model: "default", effort: null } : { model: current.model, effort: current.effort };
|
|
25305
|
+
const modelSelection = normalizeModelSelectionOptions(agent, currentSelection, {
|
|
25306
|
+
...patch.model !== void 0 ? { model: patch.model } : {}
|
|
25307
|
+
});
|
|
25308
|
+
const efforts = getEffortOptionsForAgent(agent, modelSelection.model);
|
|
25309
|
+
const requestedEffort = patch.effort !== void 0 ? patch.effort : modelSelection.effort;
|
|
25310
|
+
const effort = requestedEffort && efforts.some((option) => option.key === requestedEffort) ? requestedEffort : defaultEffortForAgent(agent, modelSelection.model);
|
|
25311
|
+
return {
|
|
25312
|
+
agent,
|
|
25313
|
+
model: modelSelection.model,
|
|
25314
|
+
effort,
|
|
25315
|
+
useWorktree: patch.useWorktree ?? current.useWorktree,
|
|
25316
|
+
worktreeName: patch.worktreeName !== void 0 ? sanitizeWorktreeName(patch.worktreeName) : current.worktreeName,
|
|
25317
|
+
worktreeBaseRef: (patch.worktreeBaseRef ?? current.worktreeBaseRef).trim() || "HEAD",
|
|
25318
|
+
files: patch.files ? [...patch.files] : [...current.files],
|
|
25319
|
+
voice: patch.voice ?? current.voice,
|
|
25320
|
+
voiceFile: patch.voiceFile !== void 0 ? patch.voiceFile : current.voiceFile
|
|
25321
|
+
};
|
|
25322
|
+
}
|
|
25323
|
+
function modelChoices(agent) {
|
|
25324
|
+
return getModelsForAgent(agent);
|
|
25325
|
+
}
|
|
25326
|
+
function effortChoices(agent, model) {
|
|
25327
|
+
return getEffortOptionsForAgent(agent, normalizeModelForAgent(agent, model));
|
|
25328
|
+
}
|
|
25190
25329
|
function choiceIndex(answer, length, currentIndex) {
|
|
25191
25330
|
const trimmed = answer?.trim() ?? "";
|
|
25192
25331
|
if (!trimmed) return Math.max(0, currentIndex);
|
|
@@ -25250,26 +25389,87 @@ function resolveTranscriptionOutcome(text2) {
|
|
|
25250
25389
|
if (!cleaned) return { errorKind: "no-speech" };
|
|
25251
25390
|
return { transcript: cleaned };
|
|
25252
25391
|
}
|
|
25253
|
-
function
|
|
25392
|
+
function findDefaultInput(value2) {
|
|
25393
|
+
if (Array.isArray(value2)) {
|
|
25394
|
+
for (const entry of value2) {
|
|
25395
|
+
const found = findDefaultInput(entry);
|
|
25396
|
+
if (found) return found;
|
|
25397
|
+
}
|
|
25398
|
+
return null;
|
|
25399
|
+
}
|
|
25400
|
+
if (!value2 || typeof value2 !== "object") return null;
|
|
25401
|
+
const record2 = value2;
|
|
25402
|
+
if (record2.coreaudio_default_audio_input_device === "spaudio_yes") {
|
|
25403
|
+
return typeof record2._name === "string" ? record2._name : null;
|
|
25404
|
+
}
|
|
25405
|
+
return findDefaultInput(Object.values(record2));
|
|
25406
|
+
}
|
|
25407
|
+
function defaultMacAudioInput(profile) {
|
|
25408
|
+
return findDefaultInput(profile);
|
|
25409
|
+
}
|
|
25410
|
+
async function lookupMacAudioInput() {
|
|
25411
|
+
return new Promise((resolve) => {
|
|
25412
|
+
execFile(
|
|
25413
|
+
"/usr/sbin/system_profiler",
|
|
25414
|
+
["SPAudioDataType", "-json"],
|
|
25415
|
+
{ encoding: "utf8", timeout: 5e3 },
|
|
25416
|
+
(error, stdout) => {
|
|
25417
|
+
if (error) {
|
|
25418
|
+
resolve(null);
|
|
25419
|
+
return;
|
|
25420
|
+
}
|
|
25421
|
+
try {
|
|
25422
|
+
resolve(defaultMacAudioInput(JSON.parse(stdout)));
|
|
25423
|
+
} catch {
|
|
25424
|
+
resolve(null);
|
|
25425
|
+
}
|
|
25426
|
+
}
|
|
25427
|
+
);
|
|
25428
|
+
});
|
|
25429
|
+
}
|
|
25430
|
+
function createMacAudioInputResolver(lookup2 = lookupMacAudioInput) {
|
|
25431
|
+
let cached = null;
|
|
25432
|
+
return () => {
|
|
25433
|
+
cached ??= lookup2().catch(() => null);
|
|
25434
|
+
return cached;
|
|
25435
|
+
};
|
|
25436
|
+
}
|
|
25437
|
+
const resolveMacAudioInput = createMacAudioInputResolver();
|
|
25438
|
+
function ffmpegRecorderSpec(platform2, macInput) {
|
|
25439
|
+
if (platform2 === "darwin") {
|
|
25440
|
+
return {
|
|
25441
|
+
command: "ffmpeg",
|
|
25442
|
+
args: (path) => ["-nostdin", "-loglevel", "error", "-f", "avfoundation", "-i", `:${macInput}`, "-ac", "1", "-ar", "16000", "-y", path]
|
|
25443
|
+
};
|
|
25444
|
+
}
|
|
25445
|
+
return {
|
|
25446
|
+
command: "ffmpeg",
|
|
25447
|
+
args: (path) => ["-nostdin", "-loglevel", "error", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", "-y", path]
|
|
25448
|
+
};
|
|
25449
|
+
}
|
|
25450
|
+
function recorderSpecs(platform2, available, macInput = "0") {
|
|
25254
25451
|
if (platform2 === "linux" && available.has("arecord")) {
|
|
25255
|
-
return { command: "arecord", args: (path) => ["-q", "-f", "S16_LE", "-r", "16000", "-c", "1", path] };
|
|
25452
|
+
return [{ command: "arecord", args: (path) => ["-q", "-f", "S16_LE", "-r", "16000", "-c", "1", path] }];
|
|
25256
25453
|
}
|
|
25257
25454
|
if (available.has("rec")) {
|
|
25258
|
-
return { command: "rec", args: (path) => ["-q", "-c", "1", "-r", "16000", path] };
|
|
25455
|
+
return [{ command: "rec", args: (path) => ["-q", "-c", "1", "-r", "16000", path] }];
|
|
25259
25456
|
}
|
|
25260
25457
|
if (available.has("ffmpeg")) {
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25265
|
-
|
|
25458
|
+
const preferred = ffmpegRecorderSpec(platform2, macInput);
|
|
25459
|
+
return platform2 === "darwin" && macInput !== "0" ? [preferred, ffmpegRecorderSpec(platform2, "0")] : [preferred];
|
|
25460
|
+
}
|
|
25461
|
+
return [];
|
|
25462
|
+
}
|
|
25463
|
+
async function startRecorderWithFallback(specs, start) {
|
|
25464
|
+
let lastError = new Error("No recorder candidate was provided.");
|
|
25465
|
+
for (const spec of specs) {
|
|
25466
|
+
try {
|
|
25467
|
+
return await start(spec);
|
|
25468
|
+
} catch (error) {
|
|
25469
|
+
lastError = error;
|
|
25266
25470
|
}
|
|
25267
|
-
return {
|
|
25268
|
-
command: "ffmpeg",
|
|
25269
|
-
args: (path) => ["-nostdin", "-loglevel", "error", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", "-y", path]
|
|
25270
|
-
};
|
|
25271
25471
|
}
|
|
25272
|
-
|
|
25472
|
+
throw lastError;
|
|
25273
25473
|
}
|
|
25274
25474
|
async function installedCommands(env) {
|
|
25275
25475
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -25304,6 +25504,41 @@ function waitForSpawn(child) {
|
|
|
25304
25504
|
child.once("error", onError);
|
|
25305
25505
|
});
|
|
25306
25506
|
}
|
|
25507
|
+
function waitForRecorderStartup(child) {
|
|
25508
|
+
return new Promise((resolve, reject) => {
|
|
25509
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
25510
|
+
reject(new Error(`Recorder exited during startup (${child.exitCode ?? child.signalCode}).`));
|
|
25511
|
+
return;
|
|
25512
|
+
}
|
|
25513
|
+
let stderr = "";
|
|
25514
|
+
const timer = setTimeout(() => finish(), 300);
|
|
25515
|
+
const finish = (error) => {
|
|
25516
|
+
clearTimeout(timer);
|
|
25517
|
+
child.stderr?.off("data", onData);
|
|
25518
|
+
child.off("close", onClose);
|
|
25519
|
+
child.off("error", onError);
|
|
25520
|
+
if (error) reject(error);
|
|
25521
|
+
else resolve();
|
|
25522
|
+
};
|
|
25523
|
+
const fail = (message) => {
|
|
25524
|
+
child.kill("SIGTERM");
|
|
25525
|
+
finish(new Error(message));
|
|
25526
|
+
};
|
|
25527
|
+
const onData = (chunk) => {
|
|
25528
|
+
stderr = `${stderr}${chunk.toString()}`.slice(-4e3);
|
|
25529
|
+
if (/device not found|failed to create av capture input device|error opening input/i.test(stderr)) {
|
|
25530
|
+
fail(stderr.trim() || "Recorder failed to open its input device.");
|
|
25531
|
+
}
|
|
25532
|
+
};
|
|
25533
|
+
const onClose = (code, signal) => {
|
|
25534
|
+
finish(new Error(stderr.trim() || `Recorder exited during startup (${code ?? signal ?? "unknown"}).`));
|
|
25535
|
+
};
|
|
25536
|
+
const onError = (error) => finish(error);
|
|
25537
|
+
child.stderr?.on("data", onData);
|
|
25538
|
+
child.once("close", onClose);
|
|
25539
|
+
child.once("error", onError);
|
|
25540
|
+
});
|
|
25541
|
+
}
|
|
25307
25542
|
function waitForClose(child) {
|
|
25308
25543
|
return new Promise((resolve) => {
|
|
25309
25544
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
@@ -25322,16 +25557,19 @@ function waitForClose(child) {
|
|
|
25322
25557
|
}
|
|
25323
25558
|
async function recordCliVoice(waitForStop, input = {}) {
|
|
25324
25559
|
const platform2 = input.platform ?? process.platform;
|
|
25325
|
-
const
|
|
25326
|
-
|
|
25327
|
-
|
|
25328
|
-
}
|
|
25560
|
+
const macInput = platform2 === "darwin" ? await resolveMacAudioInput() : null;
|
|
25561
|
+
const specs = recorderSpecs(platform2, await installedCommands(input.env ?? process.env), macInput ?? "0");
|
|
25562
|
+
if (specs.length === 0) throw new Error("No supported microphone recorder was found. Install ffmpeg/SoX/arecord or use `--voice-file <audio>` instead.");
|
|
25329
25563
|
const directory = await mkdtemp(join(tmpdir(), "saycode-voice-"));
|
|
25330
25564
|
const path = join(directory, "prompt.wav");
|
|
25331
25565
|
const cleanup = () => rm(directory, { recursive: true, force: true });
|
|
25332
25566
|
try {
|
|
25333
|
-
const child =
|
|
25334
|
-
|
|
25567
|
+
const child = await startRecorderWithFallback(specs, async (spec) => {
|
|
25568
|
+
const candidate = spawn(spec.command, spec.args(path), { stdio: ["ignore", "ignore", "pipe"] });
|
|
25569
|
+
await waitForSpawn(candidate);
|
|
25570
|
+
if (specs.length > 1) await waitForRecorderStartup(candidate);
|
|
25571
|
+
return candidate;
|
|
25572
|
+
});
|
|
25335
25573
|
await waitForStop();
|
|
25336
25574
|
child.kill("SIGINT");
|
|
25337
25575
|
await waitForClose(child);
|
|
@@ -25935,6 +26173,92 @@ async function flushCliUsageAnalytics() {
|
|
|
25935
26173
|
if (!current) return;
|
|
25936
26174
|
await flushUsageAnalyticsWithin(() => current.flush(), CLI_USAGE_ANALYTICS_FLUSH_TIMEOUT_MS);
|
|
25937
26175
|
}
|
|
26176
|
+
async function sendCliChatPrompt(context, sessionId, text2, settings) {
|
|
26177
|
+
const content = buildLiveChatUserMessage(text2, settings);
|
|
26178
|
+
const key = new Uint8Array(Buffer.from(context.syncCredential.secret, "base64"));
|
|
26179
|
+
const localId = `saycode-cli-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
26180
|
+
await context.rest.sendMessage(sessionId, localId, encodeMessageContent(content, key));
|
|
26181
|
+
return localId;
|
|
26182
|
+
}
|
|
26183
|
+
class ChatStatusController {
|
|
26184
|
+
constructor(input, onInterrupt, output) {
|
|
26185
|
+
this.input = input;
|
|
26186
|
+
this.onInterrupt = onInterrupt;
|
|
26187
|
+
this.output = output;
|
|
26188
|
+
}
|
|
26189
|
+
shownInputMode = null;
|
|
26190
|
+
apply(chat, event) {
|
|
26191
|
+
chat.apply(event);
|
|
26192
|
+
this.sync(chat.current);
|
|
26193
|
+
}
|
|
26194
|
+
sync(state) {
|
|
26195
|
+
const presentation = chatPresentation(state);
|
|
26196
|
+
const previousInputMode = this.shownInputMode;
|
|
26197
|
+
if (presentation.inputMode === "locked") this.input.lock(this.onInterrupt);
|
|
26198
|
+
if (presentation.inputMode !== "locked") {
|
|
26199
|
+
if (presentation.inputMode === "permission" && previousInputMode === "prompt") {
|
|
26200
|
+
this.input.cancelQuestion();
|
|
26201
|
+
}
|
|
26202
|
+
this.input.unlock();
|
|
26203
|
+
}
|
|
26204
|
+
if (presentation.inputMode === previousInputMode) return;
|
|
26205
|
+
this.shownInputMode = presentation.inputMode;
|
|
26206
|
+
this.output(presentation);
|
|
26207
|
+
}
|
|
26208
|
+
}
|
|
26209
|
+
async function choose(label, choices, selected, ask) {
|
|
26210
|
+
if (choices.length === 0) return null;
|
|
26211
|
+
const currentIndex = Math.max(0, choices.findIndex((entry) => entry.key === selected));
|
|
26212
|
+
const index = choiceIndex(await ask(choiceQuestion(label, choices, currentIndex)), choices.length, currentIndex);
|
|
26213
|
+
return choices[index]?.key ?? null;
|
|
26214
|
+
}
|
|
26215
|
+
async function promptLiveChatModel(current, ask) {
|
|
26216
|
+
const model = await choose("Model", liveChatModelChoices(current), current.model, ask);
|
|
26217
|
+
if (!model) return current;
|
|
26218
|
+
let settings = updateLiveChatSettings(current, { model });
|
|
26219
|
+
const effort = await choose("Effort", liveChatEffortChoices(settings), settings.effort, ask);
|
|
26220
|
+
if (effort) settings = updateLiveChatSettings(settings, { effort });
|
|
26221
|
+
return settings;
|
|
26222
|
+
}
|
|
26223
|
+
async function promptLiveChatEffort(current, ask) {
|
|
26224
|
+
const effort = await choose("Effort", liveChatEffortChoices(current), current.effort, ask);
|
|
26225
|
+
return effort ? updateLiveChatSettings(current, { effort }) : current;
|
|
26226
|
+
}
|
|
26227
|
+
function liveChatSettingsLabel(settings) {
|
|
26228
|
+
const model = liveChatModelChoices(settings).find((choice) => choice.key === settings.model)?.label ?? settings.model;
|
|
26229
|
+
const effort = liveChatEffortChoices(settings).find((choice) => choice.key === settings.effort)?.label;
|
|
26230
|
+
return effort ? `${model} · ${effort}` : model;
|
|
26231
|
+
}
|
|
26232
|
+
function parseVoiceChatCommand(prompt) {
|
|
26233
|
+
if (prompt !== "/voice" && !prompt.startsWith("/voice ")) return null;
|
|
26234
|
+
return { voiceFile: prompt.slice("/voice".length).trim() || null };
|
|
26235
|
+
}
|
|
26236
|
+
async function handleLiveChatCommand(prompt, current, ask) {
|
|
26237
|
+
if (prompt !== "/model" && prompt !== "/effort") return { handled: false, settings: current };
|
|
26238
|
+
if (!current.agent) {
|
|
26239
|
+
return {
|
|
26240
|
+
handled: true,
|
|
26241
|
+
settings: current,
|
|
26242
|
+
message: "Model controls are not available for this session agent."
|
|
26243
|
+
};
|
|
26244
|
+
}
|
|
26245
|
+
if (prompt === "/effort" && current.model === "default") {
|
|
26246
|
+
return {
|
|
26247
|
+
handled: true,
|
|
26248
|
+
settings: current,
|
|
26249
|
+
message: "Choose a model with /model before selecting effort."
|
|
26250
|
+
};
|
|
26251
|
+
}
|
|
26252
|
+
if (prompt === "/effort" && liveChatEffortChoices(current).length === 0) {
|
|
26253
|
+
return {
|
|
26254
|
+
handled: true,
|
|
26255
|
+
settings: current,
|
|
26256
|
+
message: "Effort controls are not available for the current model."
|
|
26257
|
+
};
|
|
26258
|
+
}
|
|
26259
|
+
const settings = prompt === "/model" ? await promptLiveChatModel(current, ask) : await promptLiveChatEffort(current, ask);
|
|
26260
|
+
return { handled: true, settings, message: `Next message: ${liveChatSettingsLabel(settings)}` };
|
|
26261
|
+
}
|
|
25938
26262
|
const DETACH_COMMAND = "/detach";
|
|
25939
26263
|
async function spawnSession(transport, machineId, directory, agent) {
|
|
25940
26264
|
const result = await transport.rpc(machineId, "spawn-happy-session", { directory, agent }, 6e4);
|
|
@@ -25963,47 +26287,6 @@ async function replyToPermission(transport, sessionId, request, answer) {
|
|
|
25963
26287
|
...answer === "approve-always" ? { mode: "bypassPermissions" } : {}
|
|
25964
26288
|
});
|
|
25965
26289
|
}
|
|
25966
|
-
async function sendPrompt(context, sessionId, text2, options) {
|
|
25967
|
-
const content = options ? buildCliUserMessage(text2, options) : { role: "user", content: { type: "text", text: text2 }, meta: { permissionMode: "default" } };
|
|
25968
|
-
const key = new Uint8Array(Buffer.from(context.syncCredential.secret, "base64"));
|
|
25969
|
-
const localId = `saycode-cli-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
25970
|
-
await context.rest.sendMessage(sessionId, localId, encodeMessageContent(content, key));
|
|
25971
|
-
return localId;
|
|
25972
|
-
}
|
|
25973
|
-
class ChatSession {
|
|
25974
|
-
state = createChatState();
|
|
25975
|
-
waiters = [];
|
|
25976
|
-
detached = false;
|
|
25977
|
-
get current() {
|
|
25978
|
-
return this.state;
|
|
25979
|
-
}
|
|
25980
|
-
get isDetached() {
|
|
25981
|
-
return this.detached;
|
|
25982
|
-
}
|
|
25983
|
-
/** Leave without touching the remote turn — it keeps running on the machine. */
|
|
25984
|
-
detach() {
|
|
25985
|
-
this.detached = true;
|
|
25986
|
-
this.wakeAll();
|
|
25987
|
-
}
|
|
25988
|
-
wakeAll() {
|
|
25989
|
-
const waiting = this.waiters;
|
|
25990
|
-
this.waiters = [];
|
|
25991
|
-
for (const wake of waiting) wake();
|
|
25992
|
-
}
|
|
25993
|
-
apply(event) {
|
|
25994
|
-
const next = reduceChatState(this.state, event);
|
|
25995
|
-
if (next === this.state) return;
|
|
25996
|
-
this.state = next;
|
|
25997
|
-
this.wakeAll();
|
|
25998
|
-
}
|
|
25999
|
-
/** Resolves when the turn ends, a permission needs an answer, or on detach. */
|
|
26000
|
-
async waitForAttention() {
|
|
26001
|
-
while (this.state.phase === "responding" && !this.detached) {
|
|
26002
|
-
await new Promise((resolve) => this.waiters.push(resolve));
|
|
26003
|
-
}
|
|
26004
|
-
return this.state;
|
|
26005
|
-
}
|
|
26006
|
-
}
|
|
26007
26290
|
async function resolveSpawnTarget(context, options) {
|
|
26008
26291
|
if (options.project) {
|
|
26009
26292
|
const project = await resolveProject(context, options.project);
|
|
@@ -26079,8 +26362,13 @@ async function runChat(context, options, providedTransport) {
|
|
|
26079
26362
|
}
|
|
26080
26363
|
}
|
|
26081
26364
|
context.out.raw(`Session ${sessionId}`);
|
|
26082
|
-
|
|
26083
|
-
|
|
26365
|
+
for (const hint of chatStartupHints()) context.out.raw(hint);
|
|
26366
|
+
let liveSettings = newConversationOptions ? createLiveChatSettings(newConversationOptions) : liveChatSettingsFromSessionRows(
|
|
26367
|
+
await context.rest.listSessions().catch(() => []),
|
|
26368
|
+
sessionId,
|
|
26369
|
+
[decodeBase64(context.syncCredential.secret)],
|
|
26370
|
+
options.agent
|
|
26371
|
+
);
|
|
26084
26372
|
const chat = new ChatSession();
|
|
26085
26373
|
const renderer = createSessionEventRenderer(terminalContentWidth(process.stdout.columns));
|
|
26086
26374
|
const answered = /* @__PURE__ */ new Set();
|
|
@@ -26092,7 +26380,10 @@ async function runChat(context, options, providedTransport) {
|
|
|
26092
26380
|
transport,
|
|
26093
26381
|
sessionId,
|
|
26094
26382
|
resumeHistoryCount({ resuming, ...options.since !== void 0 ? { since: options.since } : {} }),
|
|
26095
|
-
(content) =>
|
|
26383
|
+
(content) => {
|
|
26384
|
+
chat.apply({ type: "content", content });
|
|
26385
|
+
liveSettings = observeLiveChatMessage(liveSettings, content);
|
|
26386
|
+
},
|
|
26096
26387
|
renderer
|
|
26097
26388
|
);
|
|
26098
26389
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -26123,6 +26414,11 @@ Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
|
26123
26414
|
context.out.raw("…still working.");
|
|
26124
26415
|
});
|
|
26125
26416
|
};
|
|
26417
|
+
const status = new ChatStatusController(input, onSigint, (presentation) => {
|
|
26418
|
+
const style = presentation.inputMode === "prompt" ? "status" : presentation.inputMode === "permission" ? "tool" : "thinking";
|
|
26419
|
+
context.out.line({ style, text: presentation.status });
|
|
26420
|
+
});
|
|
26421
|
+
status.sync(chat.current);
|
|
26126
26422
|
const stopFollowing = followSession({
|
|
26127
26423
|
context,
|
|
26128
26424
|
transport,
|
|
@@ -26132,8 +26428,8 @@ Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
|
26132
26428
|
renderer,
|
|
26133
26429
|
onContent: (content) => {
|
|
26134
26430
|
printContent(context, content, renderer);
|
|
26135
|
-
|
|
26136
|
-
|
|
26431
|
+
status.apply(chat, { type: "content", content });
|
|
26432
|
+
liveSettings = observeLiveChatMessage(liveSettings, content);
|
|
26137
26433
|
},
|
|
26138
26434
|
onUpdate: (update) => {
|
|
26139
26435
|
const cipher = readSessionAgentState(update, sessionId);
|
|
@@ -26146,8 +26442,7 @@ Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
|
26146
26442
|
}
|
|
26147
26443
|
for (const request of pendingPermissionRequestsFromState(decoded)) {
|
|
26148
26444
|
if (answered.has(request.requestId)) continue;
|
|
26149
|
-
|
|
26150
|
-
input.cancelQuestion();
|
|
26445
|
+
status.apply(chat, { type: "permission-requested", request });
|
|
26151
26446
|
}
|
|
26152
26447
|
}
|
|
26153
26448
|
});
|
|
@@ -26156,25 +26451,18 @@ Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
|
26156
26451
|
try {
|
|
26157
26452
|
for (; ; ) {
|
|
26158
26453
|
if (chat.current.phase !== "idle") {
|
|
26159
|
-
if (chat.current.phase === "responding") {
|
|
26160
|
-
context.out.line({ style: "thinking", text: chatPresentation(chat.current).status });
|
|
26161
|
-
input.lock(onSigint);
|
|
26162
|
-
}
|
|
26163
26454
|
for (; ; ) {
|
|
26164
26455
|
const state = await chat.waitForAttention();
|
|
26165
26456
|
if (chat.isDetached) break;
|
|
26166
26457
|
if (state.phase !== "awaiting-permission" || !state.pendingPermission) break;
|
|
26167
26458
|
const request = state.pendingPermission;
|
|
26168
|
-
context.out.line({ style: "tool", text: chatPresentation(state).status });
|
|
26169
26459
|
let answer = parsePermissionAnswer(await input.ask(`${permissionPromptText(request)} `) ?? "n");
|
|
26170
26460
|
while (answer === "unknown") {
|
|
26171
26461
|
answer = parsePermissionAnswer(await input.ask("Please answer y, n, or a. ") ?? "n");
|
|
26172
26462
|
}
|
|
26173
26463
|
answered.add(request.requestId);
|
|
26174
26464
|
await replyToPermission(transport, sessionId, request, answer);
|
|
26175
|
-
|
|
26176
|
-
context.out.line({ style: "thinking", text: chatPresentation(chat.current).status });
|
|
26177
|
-
input.lock(onSigint);
|
|
26465
|
+
status.apply(chat, { type: "permission-answered" });
|
|
26178
26466
|
}
|
|
26179
26467
|
if (chat.isDetached) {
|
|
26180
26468
|
outcome = "detached";
|
|
@@ -26183,7 +26471,7 @@ Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
|
26183
26471
|
continue;
|
|
26184
26472
|
}
|
|
26185
26473
|
const ready = chatPresentation(chat.current);
|
|
26186
|
-
|
|
26474
|
+
status.sync(chat.current);
|
|
26187
26475
|
let prompt = "";
|
|
26188
26476
|
if (pendingVoice) {
|
|
26189
26477
|
const voiceFile = typeof pendingVoice === "string" ? pendingVoice : null;
|
|
@@ -26210,10 +26498,28 @@ Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
|
26210
26498
|
outcome = "detached";
|
|
26211
26499
|
break;
|
|
26212
26500
|
}
|
|
26213
|
-
if (prompt === "/
|
|
26214
|
-
|
|
26501
|
+
if (prompt === "/model" && needsLiveChatModelRefresh(liveSettings)) {
|
|
26502
|
+
liveSettings = refreshLiveChatModelsFromSessionRows(
|
|
26503
|
+
liveSettings,
|
|
26504
|
+
await context.rest.listSessions().catch(() => []),
|
|
26505
|
+
sessionId,
|
|
26506
|
+
[decodeBase64(context.syncCredential.secret)]
|
|
26507
|
+
);
|
|
26508
|
+
}
|
|
26509
|
+
const liveCommand = await handleLiveChatCommand(
|
|
26510
|
+
prompt,
|
|
26511
|
+
liveSettings,
|
|
26512
|
+
(question) => input.ask(question)
|
|
26513
|
+
);
|
|
26514
|
+
if (liveCommand.handled) {
|
|
26515
|
+
liveSettings = liveCommand.settings;
|
|
26516
|
+
if (liveCommand.message) context.out.raw(liveCommand.message);
|
|
26517
|
+
continue;
|
|
26518
|
+
}
|
|
26519
|
+
const voiceCommand = parseVoiceChatCommand(prompt);
|
|
26520
|
+
if (voiceCommand) {
|
|
26215
26521
|
try {
|
|
26216
|
-
prompt = await captureCliVoicePrompt(context, voiceFile, async () => {
|
|
26522
|
+
prompt = await captureCliVoicePrompt(context, voiceCommand.voiceFile, async () => {
|
|
26217
26523
|
await input.ask("Press Enter to stop recording. ");
|
|
26218
26524
|
});
|
|
26219
26525
|
context.out.raw(`Transcript: ${prompt}`);
|
|
@@ -26235,9 +26541,8 @@ Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
|
26235
26541
|
}
|
|
26236
26542
|
}
|
|
26237
26543
|
if (prompt === "") continue;
|
|
26238
|
-
|
|
26239
|
-
|
|
26240
|
-
ownLocalIds.add(await sendPrompt(context, sessionId, prompt, newConversationOptions));
|
|
26544
|
+
status.apply(chat, { type: "message-sent" });
|
|
26545
|
+
ownLocalIds.add(await sendCliChatPrompt(context, sessionId, prompt, liveSettings));
|
|
26241
26546
|
await recordCliUsage("message_sent");
|
|
26242
26547
|
}
|
|
26243
26548
|
} finally {
|