@buzzni/saycode-cli 0.1.1 → 0.1.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 +34 -9
- package/index.mjs +1961 -202
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
|
-
import { homedir } from "node:os";
|
|
3
|
+
import { homedir, tmpdir, platform } from "node:os";
|
|
4
4
|
import require$$0$4, { Buffer as Buffer$1 } from "buffer";
|
|
5
5
|
import require$$0 from "crypto";
|
|
6
|
-
import { mkdirSync, writeFileSync,
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
6
|
+
import { readFileSync, mkdirSync, writeFileSync, rmSync, chmodSync, constants as constants$1 } from "node:fs";
|
|
7
|
+
import { dirname, join, basename, extname, delimiter } from "node:path";
|
|
8
8
|
import require$$0$1 from "fs";
|
|
9
9
|
import require$$1 from "url";
|
|
10
10
|
import require$$2 from "child_process";
|
|
@@ -18,6 +18,9 @@ import require$$3$1 from "net";
|
|
|
18
18
|
import require$$4$1 from "tls";
|
|
19
19
|
import require$$0$5 from "stream";
|
|
20
20
|
import require$$0$3 from "zlib";
|
|
21
|
+
import { readFile, stat, mkdtemp, access, rm, mkdir, appendFile, writeFile } from "node:fs/promises";
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { randomUUID } from "node:crypto";
|
|
21
24
|
function _mergeNamespaces(n, m) {
|
|
22
25
|
for (var i = 0; i < m.length; i++) {
|
|
23
26
|
const e = m[i];
|
|
@@ -37,7 +40,7 @@ function _mergeNamespaces(n, m) {
|
|
|
37
40
|
}
|
|
38
41
|
return Object.freeze(Object.defineProperty(n, Symbol.toStringTag, { value: "Module" }));
|
|
39
42
|
}
|
|
40
|
-
const version = "0.1.
|
|
43
|
+
const version = "0.1.3";
|
|
41
44
|
const manifest = {
|
|
42
45
|
version
|
|
43
46
|
};
|
|
@@ -58,8 +61,11 @@ function takeNumber(argv, index, flag) {
|
|
|
58
61
|
if (!Number.isFinite(parsed) || parsed < 0) throw new ArgError(`${flag} needs a non-negative number, got "${raw}"`);
|
|
59
62
|
return parsed;
|
|
60
63
|
}
|
|
64
|
+
function hasExplicitNewConversationOptions(flags) {
|
|
65
|
+
return flags.agent !== void 0 || flags.model !== void 0 || flags.effort !== void 0 || flags.useWorktree === true || flags.worktreeName !== void 0 || flags.worktreeBaseRef !== void 0 || flags.files.length > 0 || flags.voice === true || flags.voiceFile !== void 0;
|
|
66
|
+
}
|
|
61
67
|
function collectFlags(argv, startIndex) {
|
|
62
|
-
const flags = { all: false };
|
|
68
|
+
const flags = { all: false, files: [] };
|
|
63
69
|
const positionals = [];
|
|
64
70
|
for (let index = startIndex; index < argv.length; index += 1) {
|
|
65
71
|
const token = argv[index];
|
|
@@ -98,6 +104,36 @@ function collectFlags(argv, startIndex) {
|
|
|
98
104
|
index += 1;
|
|
99
105
|
break;
|
|
100
106
|
}
|
|
107
|
+
case "--model":
|
|
108
|
+
flags.model = takeValue(argv, index, token);
|
|
109
|
+
index += 1;
|
|
110
|
+
break;
|
|
111
|
+
case "--effort":
|
|
112
|
+
flags.effort = takeValue(argv, index, token);
|
|
113
|
+
index += 1;
|
|
114
|
+
break;
|
|
115
|
+
case "--worktree":
|
|
116
|
+
flags.useWorktree = true;
|
|
117
|
+
break;
|
|
118
|
+
case "--worktree-name":
|
|
119
|
+
flags.worktreeName = takeValue(argv, index, token);
|
|
120
|
+
index += 1;
|
|
121
|
+
break;
|
|
122
|
+
case "--worktree-base":
|
|
123
|
+
flags.worktreeBaseRef = takeValue(argv, index, token);
|
|
124
|
+
index += 1;
|
|
125
|
+
break;
|
|
126
|
+
case "--file":
|
|
127
|
+
flags.files.push(takeValue(argv, index, token));
|
|
128
|
+
index += 1;
|
|
129
|
+
break;
|
|
130
|
+
case "--voice":
|
|
131
|
+
flags.voice = true;
|
|
132
|
+
break;
|
|
133
|
+
case "--voice-file":
|
|
134
|
+
flags.voiceFile = takeValue(argv, index, token);
|
|
135
|
+
index += 1;
|
|
136
|
+
break;
|
|
101
137
|
case "--since":
|
|
102
138
|
flags.since = takeNumber(argv, index, token);
|
|
103
139
|
index += 1;
|
|
@@ -161,12 +197,35 @@ function parseArgs(argv, interactive = false) {
|
|
|
161
197
|
case "chat": {
|
|
162
198
|
const agent = flags.agent ?? "claude";
|
|
163
199
|
const since = flags.since !== void 0 ? { since: flags.since } : {};
|
|
164
|
-
|
|
165
|
-
if (flags.
|
|
200
|
+
const hasNewConversationOptions = hasExplicitNewConversationOptions(flags);
|
|
201
|
+
if ((flags.worktreeName !== void 0 || flags.worktreeBaseRef !== void 0) && !flags.useWorktree) {
|
|
202
|
+
throw new ArgError("--worktree-name and --worktree-base require --worktree");
|
|
203
|
+
}
|
|
204
|
+
if (flags.voice && flags.voiceFile) {
|
|
205
|
+
throw new ArgError("--voice and --voice-file cannot be used together");
|
|
206
|
+
}
|
|
207
|
+
const newConversation = {
|
|
208
|
+
...flags.model ? { model: flags.model } : {},
|
|
209
|
+
...flags.effort ? { effort: flags.effort } : {},
|
|
210
|
+
...flags.useWorktree ? { useWorktree: true } : {},
|
|
211
|
+
...flags.worktreeName ? { worktreeName: flags.worktreeName } : {},
|
|
212
|
+
...flags.worktreeBaseRef ? { worktreeBaseRef: flags.worktreeBaseRef } : {},
|
|
213
|
+
...flags.files.length > 0 ? { files: flags.files } : {},
|
|
214
|
+
...flags.voice ? { voice: true } : {},
|
|
215
|
+
...flags.voiceFile ? { voiceFile: flags.voiceFile } : {},
|
|
216
|
+
...interactive && !hasNewConversationOptions ? { promptForOptions: true } : {}
|
|
217
|
+
};
|
|
218
|
+
if (flags.sessionId) {
|
|
219
|
+
if (hasNewConversationOptions) {
|
|
220
|
+
throw new ArgError("agent, model, effort, worktree, file, and voice options only apply when starting a new session");
|
|
221
|
+
}
|
|
222
|
+
return { command: "chat", sessionId: flags.sessionId, agent, ...ws, ...since };
|
|
223
|
+
}
|
|
224
|
+
if (flags.project) return { command: "chat", project: flags.project, agent, ...ws, ...newConversation };
|
|
166
225
|
if (!flags.machine || !flags.directory) {
|
|
167
226
|
throw new ArgError("chat needs -s <session>, -p <project>, or both -m <machine> and -d <directory>");
|
|
168
227
|
}
|
|
169
|
-
return { command: "chat", machine: flags.machine, directory: flags.directory, agent, ...ws };
|
|
228
|
+
return { command: "chat", machine: flags.machine, directory: flags.directory, agent, ...ws, ...newConversation };
|
|
170
229
|
}
|
|
171
230
|
case "watch":
|
|
172
231
|
return { command: "watch", intervalSeconds: flags.intervalSeconds ?? DEFAULT_WATCH_INTERVAL_SECONDS, ...ws };
|
|
@@ -206,6 +265,14 @@ Options
|
|
|
206
265
|
-d, --directory <path> Working directory on that machine
|
|
207
266
|
-s, --session <id> Session id (see \`saycode ls\`)
|
|
208
267
|
--agent <name> claude (default), codex, gemini, openclaw, opencode
|
|
268
|
+
--model <name> Model for new-session messages (Default if omitted)
|
|
269
|
+
--effort <level> Model effort (validated against agent + model)
|
|
270
|
+
--worktree Create an isolated worktree for a project chat
|
|
271
|
+
--worktree-name <n> Optional worktree/branch label
|
|
272
|
+
--worktree-base <ref> Base ref for the worktree (default HEAD)
|
|
273
|
+
--file <path> Attach a local file to the first prompt (repeatable)
|
|
274
|
+
--voice Record and transcribe the first prompt
|
|
275
|
+
--voice-file <path> Transcribe an existing audio file as the first prompt
|
|
209
276
|
--since <n> Replay the last n messages before following
|
|
210
277
|
-i, --interval <seconds> watch refresh interval (default 10)
|
|
211
278
|
-a, --all Include finished sessions
|
|
@@ -2438,22 +2505,22 @@ function requireNaclFast() {
|
|
|
2438
2505
|
randombytes = fn;
|
|
2439
2506
|
};
|
|
2440
2507
|
(function() {
|
|
2441
|
-
var
|
|
2442
|
-
if (
|
|
2508
|
+
var crypto2 = typeof self !== "undefined" ? self.crypto || self.msCrypto : null;
|
|
2509
|
+
if (crypto2 && crypto2.getRandomValues) {
|
|
2443
2510
|
var QUOTA = 65536;
|
|
2444
2511
|
nacl2.setPRNG(function(x, n) {
|
|
2445
2512
|
var i, v = new Uint8Array(n);
|
|
2446
2513
|
for (i = 0; i < n; i += QUOTA) {
|
|
2447
|
-
|
|
2514
|
+
crypto2.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA)));
|
|
2448
2515
|
}
|
|
2449
2516
|
for (i = 0; i < n; i++) x[i] = v[i];
|
|
2450
2517
|
cleanup(v);
|
|
2451
2518
|
});
|
|
2452
2519
|
} else if (typeof commonjsRequire !== "undefined") {
|
|
2453
|
-
|
|
2454
|
-
if (
|
|
2520
|
+
crypto2 = require$$0;
|
|
2521
|
+
if (crypto2 && crypto2.randomBytes) {
|
|
2455
2522
|
nacl2.setPRNG(function(x, n) {
|
|
2456
|
-
var i, v =
|
|
2523
|
+
var i, v = crypto2.randomBytes(n);
|
|
2457
2524
|
for (i = 0; i < n; i++) x[i] = v[i];
|
|
2458
2525
|
cleanup(v);
|
|
2459
2526
|
});
|
|
@@ -2709,6 +2776,7 @@ function parseOpenConversationsPayload(value2) {
|
|
|
2709
2776
|
truncated: value2.truncated || rawActive.length > OPEN_CONVERSATION_ACTIVE_LIMIT || rawRecent.length > OPEN_CONVERSATION_RECENT_LIMIT
|
|
2710
2777
|
};
|
|
2711
2778
|
}
|
|
2779
|
+
const DEFAULT_MODEL_KEY = "default";
|
|
2712
2780
|
const FLAVOR_TO_AGENT = {
|
|
2713
2781
|
claude: "claude",
|
|
2714
2782
|
codex: "codex",
|
|
@@ -2734,6 +2802,13 @@ function coerceSessionModels(raw) {
|
|
|
2734
2802
|
}
|
|
2735
2803
|
return out.length ? out : void 0;
|
|
2736
2804
|
}
|
|
2805
|
+
function hasModelCode(models, code) {
|
|
2806
|
+
return Array.isArray(models) && models.some((model) => model?.code === code);
|
|
2807
|
+
}
|
|
2808
|
+
function normalizeOpencodeModel(selected, models) {
|
|
2809
|
+
if (!selected || selected === DEFAULT_MODEL_KEY) return DEFAULT_MODEL_KEY;
|
|
2810
|
+
return hasModelCode(models, selected) ? selected : DEFAULT_MODEL_KEY;
|
|
2811
|
+
}
|
|
2737
2812
|
const MAX_MAPPING_COUNT = 1e3;
|
|
2738
2813
|
function buildProjectConversationsPath(projectId) {
|
|
2739
2814
|
const trimmed = projectId.trim();
|
|
@@ -13411,12 +13486,12 @@ function normalizeMachine(row, candidateKeys) {
|
|
|
13411
13486
|
const daemonState = decryptJsonWithCandidates(row.daemonState, candidateKeys);
|
|
13412
13487
|
const resources = row.resources ?? parseMachineResources(daemonState);
|
|
13413
13488
|
const host = row.host || registry?.host || (typeof decrypted.host === "string" ? decrypted.host : row.id.slice(0, 8));
|
|
13414
|
-
const
|
|
13489
|
+
const platform2 = row.platform || registry?.platform || (typeof decrypted.platform === "string" ? decrypted.platform : "unknown");
|
|
13415
13490
|
return {
|
|
13416
13491
|
id: row.id,
|
|
13417
13492
|
accountId: row.accountId,
|
|
13418
13493
|
host,
|
|
13419
|
-
platform,
|
|
13494
|
+
platform: platform2,
|
|
13420
13495
|
online: typeof row.online === "boolean" ? row.online : !!row.active,
|
|
13421
13496
|
// Unknown last-seen must render as "unknown", not "just now" — a machine the
|
|
13422
13497
|
// server can't vouch for should never look freshly alive.
|
|
@@ -14926,9 +15001,9 @@ function credentialSearchPaths(input) {
|
|
|
14926
15001
|
{ source: "happy_local", path: joinPath(input.homeDir, ".happy_local", "access.key") }
|
|
14927
15002
|
];
|
|
14928
15003
|
}
|
|
14929
|
-
function selectFirstValidCredential(candidates,
|
|
15004
|
+
function selectFirstValidCredential(candidates, readFile2) {
|
|
14930
15005
|
for (const candidate of candidates) {
|
|
14931
|
-
const raw =
|
|
15006
|
+
const raw = readFile2(candidate.path);
|
|
14932
15007
|
if (raw === null) continue;
|
|
14933
15008
|
const credential = parseAccessKeyFile(raw);
|
|
14934
15009
|
if (!credential) continue;
|
|
@@ -14995,8 +15070,11 @@ function readConfig(homeDir) {
|
|
|
14995
15070
|
try {
|
|
14996
15071
|
const parsed = JSON.parse(readFileSync(configPath(homeDir), "utf8"));
|
|
14997
15072
|
if (!parsed || typeof parsed !== "object") return {};
|
|
14998
|
-
const { workspaceId } = parsed;
|
|
14999
|
-
return
|
|
15073
|
+
const { workspaceId, usageAnalyticsEnabled } = parsed;
|
|
15074
|
+
return {
|
|
15075
|
+
...typeof workspaceId === "string" && workspaceId ? { workspaceId } : {},
|
|
15076
|
+
...typeof usageAnalyticsEnabled === "boolean" ? { usageAnalyticsEnabled } : {}
|
|
15077
|
+
};
|
|
15000
15078
|
} catch {
|
|
15001
15079
|
return {};
|
|
15002
15080
|
}
|
|
@@ -15055,7 +15133,7 @@ function requireXMLHttpRequest() {
|
|
|
15055
15133
|
hasRequiredXMLHttpRequest = 1;
|
|
15056
15134
|
var fs = require$$0$1;
|
|
15057
15135
|
var Url = require$$1;
|
|
15058
|
-
var
|
|
15136
|
+
var spawn2 = require$$2.spawn;
|
|
15059
15137
|
XMLHttpRequest_1 = XMLHttpRequest2;
|
|
15060
15138
|
XMLHttpRequest2.XMLHttpRequest = XMLHttpRequest2;
|
|
15061
15139
|
function XMLHttpRequest2(opts) {
|
|
@@ -15350,7 +15428,7 @@ function requireXMLHttpRequest() {
|
|
|
15350
15428
|
var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
|
|
15351
15429
|
fs.writeFileSync(syncFile, "", "utf8");
|
|
15352
15430
|
var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
|
|
15353
|
-
var syncProc =
|
|
15431
|
+
var syncProc = spawn2(process.argv[0], ["-e", execString]);
|
|
15354
15432
|
while (fs.existsSync(syncFile)) {
|
|
15355
15433
|
}
|
|
15356
15434
|
self2.responseText = fs.readFileSync(contentFile, "utf8");
|
|
@@ -16115,8 +16193,8 @@ function requireCommon() {
|
|
|
16115
16193
|
}
|
|
16116
16194
|
return debug2;
|
|
16117
16195
|
}
|
|
16118
|
-
function extend(namespace,
|
|
16119
|
-
const newDebug = createDebug(this.namespace + (typeof
|
|
16196
|
+
function extend(namespace, delimiter2) {
|
|
16197
|
+
const newDebug = createDebug(this.namespace + (typeof delimiter2 === "undefined" ? ":" : delimiter2) + namespace);
|
|
16120
16198
|
newDebug.log = this.log;
|
|
16121
16199
|
return newDebug;
|
|
16122
16200
|
}
|
|
@@ -23536,6 +23614,13 @@ class ApiUnavailableError extends Error {
|
|
|
23536
23614
|
].join("\n"));
|
|
23537
23615
|
}
|
|
23538
23616
|
}
|
|
23617
|
+
class CliHttpError extends Error {
|
|
23618
|
+
constructor(status, body, message) {
|
|
23619
|
+
super(message);
|
|
23620
|
+
this.status = status;
|
|
23621
|
+
this.body = body;
|
|
23622
|
+
}
|
|
23623
|
+
}
|
|
23539
23624
|
class CliRest {
|
|
23540
23625
|
constructor(credential, baseUrl = DEFAULT_SERVER_URL) {
|
|
23541
23626
|
this.credential = credential;
|
|
@@ -23550,6 +23635,40 @@ class CliRest {
|
|
|
23550
23635
|
if (!apiToken) throw new ApiUnavailableError();
|
|
23551
23636
|
return this.request(path, { method: "GET" }, apiToken);
|
|
23552
23637
|
}
|
|
23638
|
+
async createProjectWorktree(projectId, input) {
|
|
23639
|
+
const data = await this.apiMutation(
|
|
23640
|
+
`/api/projects/${encodeURIComponent(projectId)}/worktrees`,
|
|
23641
|
+
{ method: "POST", body: buildWorktreeMutationBody(input) }
|
|
23642
|
+
);
|
|
23643
|
+
return data.worktree;
|
|
23644
|
+
}
|
|
23645
|
+
async deleteProjectWorktree(projectId, worktreeId, force) {
|
|
23646
|
+
await this.apiMutation(
|
|
23647
|
+
`/api/projects/${encodeURIComponent(projectId)}/worktrees/${encodeURIComponent(worktreeId)}`,
|
|
23648
|
+
{ method: "DELETE", body: JSON.stringify({ force }) }
|
|
23649
|
+
);
|
|
23650
|
+
}
|
|
23651
|
+
async assignSessionToProject(projectId, sessionId, worktreeId) {
|
|
23652
|
+
await this.apiMutation(`/api/projects/${encodeURIComponent(projectId)}/sessions`, {
|
|
23653
|
+
method: "POST",
|
|
23654
|
+
body: JSON.stringify({ sessionId, ...worktreeId ? { worktreeId } : {} })
|
|
23655
|
+
});
|
|
23656
|
+
}
|
|
23657
|
+
async transcribeAudio(audio, options) {
|
|
23658
|
+
const form = new FormData();
|
|
23659
|
+
form.append("audio", audio, options.fileName);
|
|
23660
|
+
form.append("mimeType", options.mimeType);
|
|
23661
|
+
const data = await this.apiMutation(
|
|
23662
|
+
"/api/transcriptions",
|
|
23663
|
+
{ method: "POST", body: form }
|
|
23664
|
+
);
|
|
23665
|
+
return { text: (data.text ?? data.transcript ?? data.transcription ?? "").trim() };
|
|
23666
|
+
}
|
|
23667
|
+
async apiMutation(path, init) {
|
|
23668
|
+
const apiToken = this.credential.apiToken;
|
|
23669
|
+
if (!apiToken) throw new ApiUnavailableError();
|
|
23670
|
+
return this.request(path, init, apiToken);
|
|
23671
|
+
}
|
|
23553
23672
|
/** A sync-boundary client bound to a different workspace identity. */
|
|
23554
23673
|
withSyncCredential(credential) {
|
|
23555
23674
|
return new CliRest({ ...credential, ...this.credential.apiToken ? { apiToken: this.credential.apiToken } : {} }, this.baseUrl);
|
|
@@ -23565,7 +23684,7 @@ class CliRest {
|
|
|
23565
23684
|
/** Newest-last page of a session transcript, used for `attach --since`. */
|
|
23566
23685
|
async recentMessages(sessionId, limit) {
|
|
23567
23686
|
const data = await this.get(
|
|
23568
|
-
`/v3/sessions/${encodeURIComponent(sessionId)}/messages
|
|
23687
|
+
`/v3/sessions/${encodeURIComponent(sessionId)}/messages?${new URLSearchParams({ limit: String(limit), before_seq: "2147483647" })}`
|
|
23569
23688
|
);
|
|
23570
23689
|
return data.messages ?? [];
|
|
23571
23690
|
}
|
|
@@ -23582,19 +23701,25 @@ class CliRest {
|
|
|
23582
23701
|
const controller = new AbortController();
|
|
23583
23702
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
23584
23703
|
try {
|
|
23704
|
+
const inheritedHeaders = new Headers(init.headers);
|
|
23705
|
+
const headers = Object.fromEntries(inheritedHeaders.entries());
|
|
23706
|
+
const isForm = typeof FormData !== "undefined" && init.body instanceof FormData;
|
|
23707
|
+
if (!isForm && !inheritedHeaders.has("Content-Type")) headers["Content-Type"] = "application/json";
|
|
23708
|
+
headers.Authorization = `Bearer ${tokenOverride ?? this.credential.token}`;
|
|
23585
23709
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
23586
23710
|
...init,
|
|
23587
23711
|
signal: controller.signal,
|
|
23588
|
-
headers
|
|
23589
|
-
"Content-Type": "application/json",
|
|
23590
|
-
Authorization: `Bearer ${tokenOverride ?? this.credential.token}`,
|
|
23591
|
-
...init.headers
|
|
23592
|
-
}
|
|
23712
|
+
headers
|
|
23593
23713
|
});
|
|
23714
|
+
const text2 = await response.text();
|
|
23594
23715
|
if (!response.ok) {
|
|
23595
|
-
throw new
|
|
23716
|
+
throw new CliHttpError(
|
|
23717
|
+
response.status,
|
|
23718
|
+
text2,
|
|
23719
|
+
`${init.method ?? "GET"} ${path} failed: ${response.status} ${response.statusText}`
|
|
23720
|
+
);
|
|
23596
23721
|
}
|
|
23597
|
-
return
|
|
23722
|
+
return text2 ? JSON.parse(text2) : {};
|
|
23598
23723
|
} finally {
|
|
23599
23724
|
clearTimeout(timer);
|
|
23600
23725
|
}
|
|
@@ -23625,17 +23750,34 @@ const RESET = "\x1B[0m";
|
|
|
23625
23750
|
const STYLE_CODES = {
|
|
23626
23751
|
user: "\x1B[1;36m",
|
|
23627
23752
|
thinking: "\x1B[2;37m",
|
|
23628
|
-
tool: "\x1B[33m",
|
|
23753
|
+
tool: "\x1B[2;33m",
|
|
23629
23754
|
progress: "\x1B[2;37m",
|
|
23630
23755
|
status: "\x1B[32m",
|
|
23631
|
-
error: "\x1B[31m"
|
|
23756
|
+
error: "\x1B[31m",
|
|
23757
|
+
heading: "\x1B[1;36m",
|
|
23758
|
+
quote: "\x1B[2;37m",
|
|
23759
|
+
code: "\x1B[36m"
|
|
23632
23760
|
// `text` is intentionally absent: agent prose is the main content and reads
|
|
23633
23761
|
// best in the terminal's own foreground colour.
|
|
23634
23762
|
};
|
|
23763
|
+
const INLINE_STYLE_CODES = {
|
|
23764
|
+
strong: "\x1B[1m",
|
|
23765
|
+
emphasis: "\x1B[3m",
|
|
23766
|
+
inlineCode: "\x1B[36m",
|
|
23767
|
+
strike: "\x1B[9m",
|
|
23768
|
+
link: "\x1B[4;36m"
|
|
23769
|
+
};
|
|
23770
|
+
function formatSpans(spans, baseCode) {
|
|
23771
|
+
return spans.map((span) => {
|
|
23772
|
+
if (!span.style) return span.text;
|
|
23773
|
+
return `${INLINE_STYLE_CODES[span.style]}${span.text}${baseCode ?? RESET}`;
|
|
23774
|
+
}).join("");
|
|
23775
|
+
}
|
|
23635
23776
|
function formatLine(line, colour) {
|
|
23636
23777
|
if (line.style === "divider") return "";
|
|
23637
23778
|
const code = colour ? STYLE_CODES[line.style] : void 0;
|
|
23638
|
-
|
|
23779
|
+
const text2 = colour && line.spans ? formatSpans(line.spans, code) : line.text;
|
|
23780
|
+
return code ? `${code}${text2}${RESET}` : text2;
|
|
23639
23781
|
}
|
|
23640
23782
|
function colourEnabled(env, isTty) {
|
|
23641
23783
|
if (env.NO_COLOR !== void 0) return false;
|
|
@@ -23854,7 +23996,171 @@ function runLogout(out) {
|
|
|
23854
23996
|
}
|
|
23855
23997
|
out.raw("No stored Saycode credentials. Happy credentials (~/.happy*/access.key) are untouched.");
|
|
23856
23998
|
}
|
|
23999
|
+
function isCombining(codePoint, character) {
|
|
24000
|
+
return codePoint === 8205 || new RegExp("\\p{Mark}", "u").test(character);
|
|
24001
|
+
}
|
|
24002
|
+
function isWide(codePoint) {
|
|
24003
|
+
return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 129791 || codePoint >= 131072 && codePoint <= 262141);
|
|
24004
|
+
}
|
|
24005
|
+
function displayWidth(text2) {
|
|
24006
|
+
let width = 0;
|
|
24007
|
+
for (const character of text2) {
|
|
24008
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
24009
|
+
if (codePoint < 32 || codePoint >= 127 && codePoint < 160 || isCombining(codePoint, character)) continue;
|
|
24010
|
+
width += isWide(codePoint) ? 2 : 1;
|
|
24011
|
+
}
|
|
24012
|
+
return width;
|
|
24013
|
+
}
|
|
24014
|
+
function styledToken(source, marker, style) {
|
|
24015
|
+
if (!source.startsWith(marker)) return null;
|
|
24016
|
+
const end = source.indexOf(marker, marker.length);
|
|
24017
|
+
if (end < marker.length + 1) return null;
|
|
24018
|
+
const text2 = source.slice(marker.length, end);
|
|
24019
|
+
if (text2.trim() === "") return null;
|
|
24020
|
+
return { length: end + marker.length, spans: [{ style, text: text2 }] };
|
|
24021
|
+
}
|
|
24022
|
+
function tokenAt(source) {
|
|
24023
|
+
const escaped = source.match(/^\\([\\`*_[\]~()])/);
|
|
24024
|
+
if (escaped) return { length: escaped[0].length, spans: [{ text: escaped[1] ?? "" }] };
|
|
24025
|
+
const code = source.match(/^`([^`\n]+)`/);
|
|
24026
|
+
if (code) return { length: code[0].length, spans: [{ style: "inlineCode", text: code[1] ?? "" }] };
|
|
24027
|
+
const link = source.match(/^\[([^\]\n]+)]\((https?:\/\/[^\s)\n]+)\)/);
|
|
24028
|
+
if (link) {
|
|
24029
|
+
const text2 = link[1] ?? "";
|
|
24030
|
+
const href = link[2] ?? "";
|
|
24031
|
+
return {
|
|
24032
|
+
length: link[0].length,
|
|
24033
|
+
spans: [{ href, style: "link", text: text2 }, { text: ` (${href})` }]
|
|
24034
|
+
};
|
|
24035
|
+
}
|
|
24036
|
+
return styledToken(source, "**", "strong") ?? styledToken(source, "~~", "strike") ?? (source.startsWith("**") ? null : styledToken(source, "*", "emphasis"));
|
|
24037
|
+
}
|
|
24038
|
+
function appendSpan(spans, span) {
|
|
24039
|
+
if (span.text === "") return;
|
|
24040
|
+
const previous = spans.at(-1);
|
|
24041
|
+
if (previous && previous.style === span.style && previous.href === span.href) {
|
|
24042
|
+
previous.text += span.text;
|
|
24043
|
+
return;
|
|
24044
|
+
}
|
|
24045
|
+
spans.push({ ...span });
|
|
24046
|
+
}
|
|
24047
|
+
function parseInlineMarkdown(source) {
|
|
24048
|
+
const spans = [];
|
|
24049
|
+
let offset = 0;
|
|
24050
|
+
while (offset < source.length) {
|
|
24051
|
+
const token = tokenAt(source.slice(offset));
|
|
24052
|
+
if (token) {
|
|
24053
|
+
for (const span of token.spans) appendSpan(spans, span);
|
|
24054
|
+
offset += token.length;
|
|
24055
|
+
continue;
|
|
24056
|
+
}
|
|
24057
|
+
const character = String.fromCodePoint(source.codePointAt(offset) ?? 0);
|
|
24058
|
+
appendSpan(spans, { text: character });
|
|
24059
|
+
offset += character.length;
|
|
24060
|
+
}
|
|
24061
|
+
return spans;
|
|
24062
|
+
}
|
|
24063
|
+
function finishLine(spans) {
|
|
24064
|
+
while (spans.at(-1)?.text.endsWith(" ")) {
|
|
24065
|
+
const last = spans.at(-1);
|
|
24066
|
+
if (!last) break;
|
|
24067
|
+
last.text = last.text.trimEnd();
|
|
24068
|
+
if (last.text === "") spans.pop();
|
|
24069
|
+
}
|
|
24070
|
+
return { text: spans.map((span) => span.text).join(""), spans };
|
|
24071
|
+
}
|
|
24072
|
+
function renderInlineMarkdown(source, width) {
|
|
24073
|
+
const parsed = parseInlineMarkdown(source);
|
|
24074
|
+
const lines = [];
|
|
24075
|
+
let current = [];
|
|
24076
|
+
let currentWidth = 0;
|
|
24077
|
+
for (const span of parsed) {
|
|
24078
|
+
for (const character of span.text) {
|
|
24079
|
+
const characterWidth = displayWidth(character);
|
|
24080
|
+
if (current.length > 0 && currentWidth + characterWidth > width) {
|
|
24081
|
+
lines.push(finishLine(current));
|
|
24082
|
+
current = [];
|
|
24083
|
+
currentWidth = 0;
|
|
24084
|
+
}
|
|
24085
|
+
if (current.length === 0 && character === " ") continue;
|
|
24086
|
+
appendSpan(current, { ...span, text: character });
|
|
24087
|
+
currentWidth += characterWidth;
|
|
24088
|
+
}
|
|
24089
|
+
}
|
|
24090
|
+
if (current.length > 0 || lines.length === 0) lines.push(finishLine(current));
|
|
24091
|
+
return lines;
|
|
24092
|
+
}
|
|
24093
|
+
function withPrefix(style, prefix, line) {
|
|
24094
|
+
if (!line.spans.some((span) => span.style)) return { style, text: `${prefix}${line.text}` };
|
|
24095
|
+
return { style, text: `${prefix}${line.text}`, spans: [...prefix ? [{ text: prefix }] : [], ...line.spans] };
|
|
24096
|
+
}
|
|
24097
|
+
class TerminalMarkdownRenderer {
|
|
24098
|
+
constructor(width) {
|
|
24099
|
+
this.width = width;
|
|
24100
|
+
}
|
|
24101
|
+
pending = "";
|
|
24102
|
+
inFence = false;
|
|
24103
|
+
get hasPending() {
|
|
24104
|
+
return this.pending !== "";
|
|
24105
|
+
}
|
|
24106
|
+
push(chunk) {
|
|
24107
|
+
const normalized = `${this.pending}${chunk}`.replace(/\r\n?/g, "\n");
|
|
24108
|
+
const sourceLines = normalized.split("\n");
|
|
24109
|
+
this.pending = sourceLines.pop() ?? "";
|
|
24110
|
+
return sourceLines.flatMap((line) => this.renderLine(line));
|
|
24111
|
+
}
|
|
24112
|
+
flush() {
|
|
24113
|
+
const pending = this.pending;
|
|
24114
|
+
this.pending = "";
|
|
24115
|
+
const lines = pending === "" ? [] : this.renderLine(pending);
|
|
24116
|
+
if (!this.inFence) return lines;
|
|
24117
|
+
this.inFence = false;
|
|
24118
|
+
return [...lines, { style: "code", text: "└─" }];
|
|
24119
|
+
}
|
|
24120
|
+
prefixed(style, prefix, content, continuation = " ") {
|
|
24121
|
+
const firstWidth = Math.max(1, this.width - displayWidth(prefix));
|
|
24122
|
+
const wrapped = renderInlineMarkdown(content, firstWidth);
|
|
24123
|
+
const lines = [withPrefix(style, prefix, wrapped[0] ?? { spans: [], text: "" })];
|
|
24124
|
+
for (const part of wrapped.slice(1)) {
|
|
24125
|
+
lines.push(withPrefix(style, continuation, part));
|
|
24126
|
+
}
|
|
24127
|
+
return lines;
|
|
24128
|
+
}
|
|
24129
|
+
renderLine(source) {
|
|
24130
|
+
const fence = source.match(/^\s*```\s*([^`]*)$/);
|
|
24131
|
+
if (fence) {
|
|
24132
|
+
if (this.inFence) {
|
|
24133
|
+
this.inFence = false;
|
|
24134
|
+
return [{ style: "code", text: "└─" }];
|
|
24135
|
+
}
|
|
24136
|
+
this.inFence = true;
|
|
24137
|
+
const language = fence[1]?.trim();
|
|
24138
|
+
return [{ style: "code", text: language ? `┌─ ${language}` : "┌─" }];
|
|
24139
|
+
}
|
|
24140
|
+
if (this.inFence) return this.prefixed("code", "│ ", source, "│ ");
|
|
24141
|
+
if (source.trim() === "") return [{ style: "divider", text: "" }];
|
|
24142
|
+
const heading = source.match(/^\s{0,3}#{1,6}\s+(.+)$/);
|
|
24143
|
+
if (heading) return this.prefixed("heading", "◆ ", heading[1] ?? "");
|
|
24144
|
+
const quote = source.match(/^\s*>\s?(.*)$/);
|
|
24145
|
+
if (quote) return this.prefixed("quote", "│ ", quote[1] ?? "", "│ ");
|
|
24146
|
+
const list = source.match(/^(\s*)([-+*]|\d+\.)\s+(.+)$/);
|
|
24147
|
+
if (list) {
|
|
24148
|
+
const indent = list[1] ?? "";
|
|
24149
|
+
const marker = /^\d/.test(list[2] ?? "") ? list[2] : "•";
|
|
24150
|
+
const prefix = `${indent}${marker} `;
|
|
24151
|
+
return this.prefixed("text", prefix, list[3] ?? "", " ".repeat(displayWidth(prefix)));
|
|
24152
|
+
}
|
|
24153
|
+
return renderInlineMarkdown(source, this.width).map((line) => withPrefix("text", "", line));
|
|
24154
|
+
}
|
|
24155
|
+
}
|
|
24156
|
+
function createTerminalMarkdownRenderer(width = 100) {
|
|
24157
|
+
return new TerminalMarkdownRenderer(Math.max(8, width));
|
|
24158
|
+
}
|
|
24159
|
+
function terminalContentWidth(columns) {
|
|
24160
|
+
return Math.max(20, Math.min(100, (columns ?? 104) - 4));
|
|
24161
|
+
}
|
|
23857
24162
|
const TOOL_SUMMARY_MAX_LENGTH = 80;
|
|
24163
|
+
const TOOL_PROGRESS_VISIBLE_LINES = 3;
|
|
23858
24164
|
const TOOL_SUMMARY_KEYS = ["command", "file_path", "path", "pattern", "query", "url", "prompt"];
|
|
23859
24165
|
function truncate(value2, max) {
|
|
23860
24166
|
if (value2.length <= max) return value2;
|
|
@@ -23877,7 +24183,7 @@ function textLines(text2, style) {
|
|
|
23877
24183
|
if (text2.trim() === "") return [];
|
|
23878
24184
|
return text2.split("\n").map((line) => ({ style, text: line }));
|
|
23879
24185
|
}
|
|
23880
|
-
function renderSessionEvent(ev, envelopeRole) {
|
|
24186
|
+
function renderSessionEvent(ev, envelopeRole, progressByCall) {
|
|
23881
24187
|
switch (ev.t) {
|
|
23882
24188
|
case "text":
|
|
23883
24189
|
if (envelopeRole === "user") return textLines(ev.text, "user").map((line) => ({ ...line, text: `› ${line.text}` }));
|
|
@@ -23885,16 +24191,35 @@ function renderSessionEvent(ev, envelopeRole) {
|
|
|
23885
24191
|
case "service":
|
|
23886
24192
|
return textLines(ev.text, "status");
|
|
23887
24193
|
case "tool-call-start": {
|
|
24194
|
+
progressByCall.set(ev.call, { shown: 0, hidden: 0, lastHidden: null });
|
|
23888
24195
|
const summary = summarizeToolArgs(ev.args) ?? (ev.description?.trim() || null);
|
|
23889
24196
|
return [{ style: "tool", text: summary ? `⏺ ${ev.name}(${summary})` : `⏺ ${ev.name}` }];
|
|
23890
24197
|
}
|
|
23891
|
-
case "tool-call-progress":
|
|
23892
|
-
|
|
23893
|
-
|
|
23894
|
-
|
|
23895
|
-
|
|
23896
|
-
|
|
23897
|
-
|
|
24198
|
+
case "tool-call-progress": {
|
|
24199
|
+
const state = progressByCall.get(ev.call) ?? { shown: 0, hidden: 0, lastHidden: null };
|
|
24200
|
+
progressByCall.set(ev.call, state);
|
|
24201
|
+
const lines = [];
|
|
24202
|
+
for (const line of ev.lines) {
|
|
24203
|
+
if (ev.stream === "stderr") lines.push({ style: "error", text: ` │ ${line}` });
|
|
24204
|
+
else if (state.shown < TOOL_PROGRESS_VISIBLE_LINES) {
|
|
24205
|
+
state.shown += 1;
|
|
24206
|
+
lines.push({ style: "progress", text: ` │ ${line}` });
|
|
24207
|
+
} else {
|
|
24208
|
+
state.hidden += 1;
|
|
24209
|
+
state.lastHidden = line;
|
|
24210
|
+
}
|
|
24211
|
+
}
|
|
24212
|
+
return lines;
|
|
24213
|
+
}
|
|
24214
|
+
case "tool-call-end": {
|
|
24215
|
+
const state = progressByCall.get(ev.call);
|
|
24216
|
+
progressByCall.delete(ev.call);
|
|
24217
|
+
if (!state?.hidden || !state.lastHidden) return [];
|
|
24218
|
+
return [{
|
|
24219
|
+
style: "progress",
|
|
24220
|
+
text: ` └ ${state.hidden} more lines hidden · last: ${truncate(state.lastHidden, TOOL_SUMMARY_MAX_LENGTH)}`
|
|
24221
|
+
}];
|
|
24222
|
+
}
|
|
23898
24223
|
case "file":
|
|
23899
24224
|
return [{ style: "status", text: `📎 ${ev.name} (${formatBytes(ev.size)})` }];
|
|
23900
24225
|
case "turn-start":
|
|
@@ -23907,14 +24232,34 @@ function renderSessionEvent(ev, envelopeRole) {
|
|
|
23907
24232
|
return [];
|
|
23908
24233
|
}
|
|
23909
24234
|
}
|
|
23910
|
-
|
|
23911
|
-
|
|
23912
|
-
|
|
24235
|
+
class SessionEventRenderer {
|
|
24236
|
+
markdown;
|
|
24237
|
+
progressByCall = /* @__PURE__ */ new Map();
|
|
24238
|
+
constructor(width) {
|
|
24239
|
+
this.markdown = createTerminalMarkdownRenderer(width);
|
|
23913
24240
|
}
|
|
23914
|
-
|
|
23915
|
-
return
|
|
24241
|
+
flush() {
|
|
24242
|
+
return this.markdown.flush();
|
|
23916
24243
|
}
|
|
23917
|
-
|
|
24244
|
+
render(content) {
|
|
24245
|
+
if (content.role === "user") {
|
|
24246
|
+
return [...this.flush(), ...textLines(content.content.text, "user").map((line) => ({ ...line, text: `› ${line.text}` }))];
|
|
24247
|
+
}
|
|
24248
|
+
if (content.role !== "session") return [];
|
|
24249
|
+
const { ev, role } = content.content;
|
|
24250
|
+
if (ev.t === "text" && role === "agent" && !ev.thinking) {
|
|
24251
|
+
if (!this.markdown.hasPending && ev.text.trim() === "") return [];
|
|
24252
|
+
return this.markdown.push(ev.text);
|
|
24253
|
+
}
|
|
24254
|
+
const prefix = this.flush();
|
|
24255
|
+
if (ev.t === "turn-start") this.progressByCall.clear();
|
|
24256
|
+
const rendered = renderSessionEvent(ev, role, this.progressByCall);
|
|
24257
|
+
if (ev.t === "turn-end") this.progressByCall.clear();
|
|
24258
|
+
return [...prefix, ...rendered];
|
|
24259
|
+
}
|
|
24260
|
+
}
|
|
24261
|
+
function createSessionEventRenderer(width = 100) {
|
|
24262
|
+
return new SessionEventRenderer(width);
|
|
23918
24263
|
}
|
|
23919
24264
|
function record(value2) {
|
|
23920
24265
|
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
|
|
@@ -23952,10 +24297,10 @@ function decodeContent(transport, ciphertext) {
|
|
|
23952
24297
|
return null;
|
|
23953
24298
|
}
|
|
23954
24299
|
}
|
|
23955
|
-
function printContent(context, content) {
|
|
23956
|
-
context.out.lines(
|
|
24300
|
+
function printContent(context, content, renderer = createSessionEventRenderer()) {
|
|
24301
|
+
context.out.lines(renderer.render(content));
|
|
23957
24302
|
}
|
|
23958
|
-
async function prerollHistory(context, transport, sessionId, since) {
|
|
24303
|
+
async function prerollHistory(context, transport, sessionId, since, onContent, renderer = createSessionEventRenderer()) {
|
|
23959
24304
|
if (since <= 0) return 0;
|
|
23960
24305
|
const messages = await context.rest.recentMessages(sessionId, since);
|
|
23961
24306
|
let highestSeq = 0;
|
|
@@ -23964,28 +24309,31 @@ async function prerollHistory(context, transport, sessionId, since) {
|
|
|
23964
24309
|
const ciphertext = unwrapEncryptedField(message.content);
|
|
23965
24310
|
if (!ciphertext) continue;
|
|
23966
24311
|
const content = decodeContent(transport, ciphertext);
|
|
23967
|
-
if (content)
|
|
24312
|
+
if (!content) continue;
|
|
24313
|
+
printContent(context, content, renderer);
|
|
24314
|
+
onContent?.(content);
|
|
23968
24315
|
}
|
|
23969
24316
|
return highestSeq;
|
|
23970
24317
|
}
|
|
23971
24318
|
function followSession(input) {
|
|
23972
24319
|
let highestSeq = input.seenThroughSeq;
|
|
24320
|
+
const renderer = input.renderer ?? createSessionEventRenderer();
|
|
23973
24321
|
return input.transport.onUpdate((update) => {
|
|
23974
24322
|
input.onUpdate?.(update);
|
|
23975
24323
|
const message = readNewMessage(update, input.sessionId);
|
|
23976
|
-
if (!message) return;
|
|
23977
|
-
if (message.localId && input.ownLocalIds?.has(message.localId)) return;
|
|
24324
|
+
if (!message || message.localId && input.ownLocalIds?.has(message.localId)) return;
|
|
23978
24325
|
if (message.seq !== 0 && message.seq <= highestSeq) return;
|
|
23979
24326
|
highestSeq = Math.max(highestSeq, message.seq);
|
|
23980
24327
|
const content = decodeContent(input.transport, message.ciphertext);
|
|
23981
24328
|
if (!content) return;
|
|
23982
|
-
input.onContent ? input.onContent(content) : printContent(input.context, content);
|
|
24329
|
+
input.onContent ? input.onContent(content) : printContent(input.context, content, renderer);
|
|
23983
24330
|
});
|
|
23984
24331
|
}
|
|
23985
24332
|
async function runAttach(context, options) {
|
|
23986
24333
|
await withTransport(context, async (transport) => {
|
|
23987
|
-
const
|
|
23988
|
-
|
|
24334
|
+
const renderer = createSessionEventRenderer(terminalContentWidth(process.stdout.columns));
|
|
24335
|
+
const seenThroughSeq = await prerollHistory(context, transport, options.sessionId, options.since, void 0, renderer);
|
|
24336
|
+
followSession({ context, transport, sessionId: options.sessionId, seenThroughSeq, renderer });
|
|
23989
24337
|
context.out.raw(`Attached to ${options.sessionId}. Ctrl+C to detach — the session keeps running.`);
|
|
23990
24338
|
await new Promise((resolve) => {
|
|
23991
24339
|
process.once("SIGINT", () => {
|
|
@@ -23998,17 +24346,20 @@ async function runAttach(context, options) {
|
|
|
23998
24346
|
function createChatState() {
|
|
23999
24347
|
return { phase: "idle", pendingPermission: null };
|
|
24000
24348
|
}
|
|
24001
|
-
function
|
|
24002
|
-
if (content.role !== "session") return
|
|
24003
|
-
const {
|
|
24004
|
-
|
|
24349
|
+
function turnBoundary(content) {
|
|
24350
|
+
if (content.role !== "session") return null;
|
|
24351
|
+
const { role, ev } = content.content;
|
|
24352
|
+
if (role === "agent" && ev.t === "turn-start") return "start";
|
|
24353
|
+
return ev.t === "turn-end" || ev.t === "stop" ? "end" : null;
|
|
24005
24354
|
}
|
|
24006
24355
|
function reduceChatState(state, event) {
|
|
24007
24356
|
switch (event.type) {
|
|
24008
24357
|
case "message-sent":
|
|
24009
24358
|
return { phase: "responding", pendingPermission: null };
|
|
24010
24359
|
case "content": {
|
|
24011
|
-
|
|
24360
|
+
const boundary = turnBoundary(event.content);
|
|
24361
|
+
if (boundary === "start") return { phase: "responding", pendingPermission: null };
|
|
24362
|
+
if (boundary !== "end") return state;
|
|
24012
24363
|
if (state.phase === "idle") return state;
|
|
24013
24364
|
return { phase: "idle", pendingPermission: null };
|
|
24014
24365
|
}
|
|
@@ -24031,6 +24382,27 @@ function interruptMenuChoice(key) {
|
|
|
24031
24382
|
if (first === "s") return "stop";
|
|
24032
24383
|
return "watch";
|
|
24033
24384
|
}
|
|
24385
|
+
function chatPresentation(state) {
|
|
24386
|
+
if (state.phase === "idle") {
|
|
24387
|
+
return {
|
|
24388
|
+
inputMode: "prompt",
|
|
24389
|
+
status: "● Ready — you can type",
|
|
24390
|
+
prompt: "› "
|
|
24391
|
+
};
|
|
24392
|
+
}
|
|
24393
|
+
if (state.phase === "awaiting-permission") {
|
|
24394
|
+
return {
|
|
24395
|
+
inputMode: "permission",
|
|
24396
|
+
status: "◆ Action required — permission response only",
|
|
24397
|
+
prompt: null
|
|
24398
|
+
};
|
|
24399
|
+
}
|
|
24400
|
+
return {
|
|
24401
|
+
inputMode: "locked",
|
|
24402
|
+
status: "● Agent is responding — input locked · Ctrl+C for options",
|
|
24403
|
+
prompt: null
|
|
24404
|
+
};
|
|
24405
|
+
}
|
|
24034
24406
|
function describePermissionRequest(request) {
|
|
24035
24407
|
const summary = summarizeToolArgs(request.arguments ?? {});
|
|
24036
24408
|
if (summary) return `${request.toolName}(${summary})`;
|
|
@@ -24061,6 +24433,106 @@ function resumeHistoryCount(input) {
|
|
|
24061
24433
|
if (!input.resuming) return 0;
|
|
24062
24434
|
return input.since ?? DEFAULT_RESUME_HISTORY;
|
|
24063
24435
|
}
|
|
24436
|
+
class ChatInputController {
|
|
24437
|
+
constructor(readline, input) {
|
|
24438
|
+
this.readline = readline;
|
|
24439
|
+
this.input = input;
|
|
24440
|
+
this.readline.once("close", this.handleClose);
|
|
24441
|
+
}
|
|
24442
|
+
closed = false;
|
|
24443
|
+
locked = false;
|
|
24444
|
+
rawBeforeLock = false;
|
|
24445
|
+
onInterrupt = null;
|
|
24446
|
+
suspendedDataListeners = [];
|
|
24447
|
+
keyListener = null;
|
|
24448
|
+
resolveKey = null;
|
|
24449
|
+
questionAbort = null;
|
|
24450
|
+
handleClose = () => {
|
|
24451
|
+
this.closed = true;
|
|
24452
|
+
this.cancelQuestion();
|
|
24453
|
+
this.unlock();
|
|
24454
|
+
};
|
|
24455
|
+
handleLockedData = (chunk) => {
|
|
24456
|
+
const value2 = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
24457
|
+
if (value2.includes("")) this.onInterrupt?.();
|
|
24458
|
+
};
|
|
24459
|
+
lock(onInterrupt) {
|
|
24460
|
+
this.onInterrupt = onInterrupt;
|
|
24461
|
+
if (this.locked || this.closed) return;
|
|
24462
|
+
this.cancelQuestion();
|
|
24463
|
+
this.locked = true;
|
|
24464
|
+
this.rawBeforeLock = this.input.isRaw === true;
|
|
24465
|
+
this.readline.pause();
|
|
24466
|
+
this.suspendedDataListeners = this.input.listeners("data");
|
|
24467
|
+
for (const listener of this.suspendedDataListeners) this.input.off("data", listener);
|
|
24468
|
+
if (this.input.isTTY) this.input.setRawMode?.(true);
|
|
24469
|
+
this.input.resume();
|
|
24470
|
+
this.input.on("data", this.handleLockedData);
|
|
24471
|
+
}
|
|
24472
|
+
unlock() {
|
|
24473
|
+
if (!this.locked) return;
|
|
24474
|
+
this.locked = false;
|
|
24475
|
+
this.input.off("data", this.handleLockedData);
|
|
24476
|
+
if (this.keyListener) {
|
|
24477
|
+
this.input.off("data", this.keyListener);
|
|
24478
|
+
this.keyListener = null;
|
|
24479
|
+
const resolve = this.resolveKey;
|
|
24480
|
+
this.resolveKey = null;
|
|
24481
|
+
resolve?.("");
|
|
24482
|
+
}
|
|
24483
|
+
if (this.input.isTTY) this.input.setRawMode?.(this.rawBeforeLock);
|
|
24484
|
+
for (const listener of this.suspendedDataListeners) this.input.on("data", listener);
|
|
24485
|
+
this.suspendedDataListeners = [];
|
|
24486
|
+
this.readline.resume();
|
|
24487
|
+
}
|
|
24488
|
+
ask(prompt) {
|
|
24489
|
+
this.unlock();
|
|
24490
|
+
if (this.closed) return Promise.resolve(null);
|
|
24491
|
+
const abort = new AbortController();
|
|
24492
|
+
this.questionAbort = abort;
|
|
24493
|
+
return new Promise((resolve) => {
|
|
24494
|
+
let settled = false;
|
|
24495
|
+
const finish = (answer) => {
|
|
24496
|
+
if (settled) return;
|
|
24497
|
+
settled = true;
|
|
24498
|
+
this.readline.off("close", onClose);
|
|
24499
|
+
abort.signal.removeEventListener("abort", onAbort);
|
|
24500
|
+
if (this.questionAbort === abort) this.questionAbort = null;
|
|
24501
|
+
resolve(answer);
|
|
24502
|
+
};
|
|
24503
|
+
const onClose = () => finish(null);
|
|
24504
|
+
const onAbort = () => finish(null);
|
|
24505
|
+
this.readline.once("close", onClose);
|
|
24506
|
+
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
24507
|
+
this.readline.question(prompt, { signal: abort.signal }, (answer) => finish(answer));
|
|
24508
|
+
});
|
|
24509
|
+
}
|
|
24510
|
+
cancelQuestion() {
|
|
24511
|
+
this.questionAbort?.abort();
|
|
24512
|
+
}
|
|
24513
|
+
readKey() {
|
|
24514
|
+
if (!this.locked || this.closed) return Promise.resolve("");
|
|
24515
|
+
this.input.off("data", this.handleLockedData);
|
|
24516
|
+
return new Promise((resolve) => {
|
|
24517
|
+
this.resolveKey = resolve;
|
|
24518
|
+
this.keyListener = (chunk) => {
|
|
24519
|
+
if (!this.keyListener) return;
|
|
24520
|
+
this.input.off("data", this.keyListener);
|
|
24521
|
+
this.keyListener = null;
|
|
24522
|
+
this.resolveKey = null;
|
|
24523
|
+
if (this.locked) this.input.on("data", this.handleLockedData);
|
|
24524
|
+
resolve(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
24525
|
+
};
|
|
24526
|
+
this.input.on("data", this.keyListener);
|
|
24527
|
+
});
|
|
24528
|
+
}
|
|
24529
|
+
close() {
|
|
24530
|
+
this.cancelQuestion();
|
|
24531
|
+
this.unlock();
|
|
24532
|
+
this.closed = true;
|
|
24533
|
+
this.readline.off("close", this.handleClose);
|
|
24534
|
+
}
|
|
24535
|
+
}
|
|
24064
24536
|
function readString$1(source, key) {
|
|
24065
24537
|
const value2 = source?.[key];
|
|
24066
24538
|
return typeof value2 === "string" && value2 ? value2 : void 0;
|
|
@@ -24303,6 +24775,1166 @@ async function runProjectConversations(context, query, all) {
|
|
|
24303
24775
|
! Some conversations could not be resolved${payload.warnings.length ? `: ${payload.warnings.join(", ")}` : "."}`);
|
|
24304
24776
|
}
|
|
24305
24777
|
}
|
|
24778
|
+
const claudeEffortOptions = [
|
|
24779
|
+
{ key: "low", label: "Low" },
|
|
24780
|
+
{ key: "medium", label: "Medium" },
|
|
24781
|
+
{ key: "high", label: "High" },
|
|
24782
|
+
{ key: "max", label: "Max" }
|
|
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
|
+
};
|
|
24888
|
+
}
|
|
24889
|
+
const WORKTREE_NAME_ADJECTIVES = [
|
|
24890
|
+
"bright",
|
|
24891
|
+
"calm",
|
|
24892
|
+
"eager",
|
|
24893
|
+
"brave",
|
|
24894
|
+
"swift",
|
|
24895
|
+
"quiet",
|
|
24896
|
+
"clever",
|
|
24897
|
+
"lucky",
|
|
24898
|
+
"gentle",
|
|
24899
|
+
"bold",
|
|
24900
|
+
"fresh",
|
|
24901
|
+
"keen",
|
|
24902
|
+
"merry",
|
|
24903
|
+
"noble",
|
|
24904
|
+
"proud",
|
|
24905
|
+
"witty"
|
|
24906
|
+
];
|
|
24907
|
+
const WORKTREE_NAME_ANIMALS = [
|
|
24908
|
+
"fox",
|
|
24909
|
+
"otter",
|
|
24910
|
+
"hawk",
|
|
24911
|
+
"lynx",
|
|
24912
|
+
"wolf",
|
|
24913
|
+
"crane",
|
|
24914
|
+
"koala",
|
|
24915
|
+
"panda",
|
|
24916
|
+
"tiger",
|
|
24917
|
+
"whale",
|
|
24918
|
+
"finch",
|
|
24919
|
+
"gecko",
|
|
24920
|
+
"moose",
|
|
24921
|
+
"raven",
|
|
24922
|
+
"seal",
|
|
24923
|
+
"newt"
|
|
24924
|
+
];
|
|
24925
|
+
function sanitizeWorktreeName(raw) {
|
|
24926
|
+
return raw.trim().toLowerCase().replace(/[^a-z0-9/_-]+/g, "-").replace(/\/{2,}/g, "/").replace(/-{2,}/g, "-").replace(/^[-/]+|[-/]+$/g, "").replace(/\.+/g, ".").slice(0, 60);
|
|
24927
|
+
}
|
|
24928
|
+
function generateWorktreeName(random = Math.random) {
|
|
24929
|
+
const pick2 = (items) => items[Math.floor(random() * items.length) % items.length];
|
|
24930
|
+
const suffix = Math.floor(random() * 36 ** 4).toString(36).padStart(4, "0").slice(-4);
|
|
24931
|
+
return `${pick2(WORKTREE_NAME_ADJECTIVES)}-${pick2(WORKTREE_NAME_ANIMALS)}-${suffix}`;
|
|
24932
|
+
}
|
|
24933
|
+
function resolveWorktreeName(input) {
|
|
24934
|
+
const sanitized = input.requested ? sanitizeWorktreeName(input.requested) : "";
|
|
24935
|
+
if (sanitized) return sanitized;
|
|
24936
|
+
return generateWorktreeName(input.random);
|
|
24937
|
+
}
|
|
24938
|
+
const MANAGED_WORKTREE_SEGMENT = "/.aplus/worktrees/";
|
|
24939
|
+
const MAX_MANAGED_WORKTREE_DEPTH = 2;
|
|
24940
|
+
function isWorktreePathWithinProjectRoot(projectRoot, worktreePath) {
|
|
24941
|
+
const root = projectRoot.trim().replace(/\/+$/, "");
|
|
24942
|
+
const path = worktreePath.trim().replace(/\/+$/, "");
|
|
24943
|
+
if (!root || !path) return false;
|
|
24944
|
+
if (!path.startsWith(`${root}${MANAGED_WORKTREE_SEGMENT}`)) return false;
|
|
24945
|
+
const segments = path.slice(root.length + MANAGED_WORKTREE_SEGMENT.length).split("/");
|
|
24946
|
+
if (segments.length < 1 || segments.length > MAX_MANAGED_WORKTREE_DEPTH) return false;
|
|
24947
|
+
return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
24948
|
+
}
|
|
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
|
+
async function createWorktreeOnMachine(input) {
|
|
25007
|
+
const record2 = await input.client.createProjectWorktree(input.projectId, {
|
|
25008
|
+
name: input.name,
|
|
25009
|
+
baseRef: input.baseRef,
|
|
25010
|
+
machineId: input.machineId,
|
|
25011
|
+
...input.confirmRepoRoot ? { confirmRepoRoot: input.confirmRepoRoot } : {}
|
|
25012
|
+
});
|
|
25013
|
+
const targetMismatch = worktreeTargetMismatch(record2, {
|
|
25014
|
+
projectId: input.projectId,
|
|
25015
|
+
machineId: input.machineId
|
|
25016
|
+
});
|
|
25017
|
+
const allowedRoot = input.confirmRepoRoot ?? input.repoCwd;
|
|
25018
|
+
const pathMatches = isWorktreePathWithinProjectRoot(allowedRoot, record2.path);
|
|
25019
|
+
if (!targetMismatch && pathMatches) return record2;
|
|
25020
|
+
await input.client.deleteProjectWorktree(input.projectId, record2.id, true).catch(() => void 0);
|
|
25021
|
+
throw new Error(!pathMatches && input.pathMismatchMessage ? input.pathMismatchMessage(record2.path) : input.targetMismatchMessage);
|
|
25022
|
+
}
|
|
25023
|
+
const WORKTREE_SUBDIR_CONFIRM_CODE = "worktree-subdir-confirm-required";
|
|
25024
|
+
function trimmedString(value2) {
|
|
25025
|
+
if (typeof value2 !== "string") return null;
|
|
25026
|
+
const normalized = value2.trim();
|
|
25027
|
+
return normalized ? normalized : null;
|
|
25028
|
+
}
|
|
25029
|
+
function withoutTrailingSlash(path) {
|
|
25030
|
+
return path.replace(/\/+$/, "");
|
|
25031
|
+
}
|
|
25032
|
+
function isStrictAncestorDir(parent, child) {
|
|
25033
|
+
const base = withoutTrailingSlash(parent);
|
|
25034
|
+
const target = withoutTrailingSlash(child);
|
|
25035
|
+
if (!base || !target || base === target) return false;
|
|
25036
|
+
return target.startsWith(`${base}/`);
|
|
25037
|
+
}
|
|
25038
|
+
function isSafeRelativeDir(dir) {
|
|
25039
|
+
if (dir.startsWith("/") || dir.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(dir)) return false;
|
|
25040
|
+
return !dir.split(/[\\/]+/).some((segment) => segment === "..");
|
|
25041
|
+
}
|
|
25042
|
+
function parseWorktreeSubdirConfirmRequest(status, body, projectRoot) {
|
|
25043
|
+
if (status !== 409) return null;
|
|
25044
|
+
let parsed;
|
|
25045
|
+
try {
|
|
25046
|
+
parsed = JSON.parse(body);
|
|
25047
|
+
} catch {
|
|
25048
|
+
return null;
|
|
25049
|
+
}
|
|
25050
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
25051
|
+
const value2 = parsed;
|
|
25052
|
+
if (value2.code !== WORKTREE_SUBDIR_CONFIRM_CODE) return null;
|
|
25053
|
+
const repoRoot = trimmedString(value2.repoRoot);
|
|
25054
|
+
const workspaceDir = trimmedString(value2.workspaceDir);
|
|
25055
|
+
const repoRelativeDir = trimmedString(value2.repoRelativeDir);
|
|
25056
|
+
const root = trimmedString(projectRoot);
|
|
25057
|
+
if (!repoRoot || !workspaceDir || !repoRelativeDir || !root) return null;
|
|
25058
|
+
if (!isStrictAncestorDir(repoRoot, root)) return null;
|
|
25059
|
+
if (!isSafeRelativeDir(repoRelativeDir)) return null;
|
|
25060
|
+
return { repoRoot, workspaceDir, repoRelativeDir };
|
|
25061
|
+
}
|
|
25062
|
+
async function createCliProjectWorktree(input, deps) {
|
|
25063
|
+
const createInput = {
|
|
25064
|
+
client: {
|
|
25065
|
+
createProjectWorktree: deps.create,
|
|
25066
|
+
deleteProjectWorktree: deps.remove
|
|
25067
|
+
},
|
|
25068
|
+
projectId: input.projectId,
|
|
25069
|
+
machineId: input.machineId,
|
|
25070
|
+
repoCwd: input.projectRoot,
|
|
25071
|
+
name: input.name,
|
|
25072
|
+
baseRef: input.baseRef,
|
|
25073
|
+
targetMismatchMessage: "The worktree does not belong to the selected project and machine.",
|
|
25074
|
+
pathMismatchMessage: () => "The daemon returned a worktree outside the safe project boundary."
|
|
25075
|
+
};
|
|
25076
|
+
try {
|
|
25077
|
+
return await createWorktreeOnMachine(createInput);
|
|
25078
|
+
} catch (error) {
|
|
25079
|
+
if (!(error instanceof CliHttpError)) throw error;
|
|
25080
|
+
const request = parseWorktreeSubdirConfirmRequest(error.status, error.body, input.projectRoot);
|
|
25081
|
+
if (!request) throw error;
|
|
25082
|
+
if (!deps.confirmSubdir || !await deps.confirmSubdir(request)) {
|
|
25083
|
+
throw new Error("Worktree creation was cancelled because the project is a repository subdirectory.");
|
|
25084
|
+
}
|
|
25085
|
+
return createWorktreeOnMachine({ ...createInput, confirmRepoRoot: request.repoRoot });
|
|
25086
|
+
}
|
|
25087
|
+
}
|
|
25088
|
+
async function sha256HexFromBase64(b64Content) {
|
|
25089
|
+
if (typeof crypto === "undefined" || !crypto.subtle || typeof crypto.subtle.digest !== "function") {
|
|
25090
|
+
throw new Error(translateCurrent("remoteFile.sha256Failed"));
|
|
25091
|
+
}
|
|
25092
|
+
const bytes = new Uint8Array(Buffer$1.from(b64Content, "base64"));
|
|
25093
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
25094
|
+
const view = new Uint8Array(digest);
|
|
25095
|
+
let hex = "";
|
|
25096
|
+
for (let i = 0; i < view.length; i++) hex += view[i].toString(16).padStart(2, "0");
|
|
25097
|
+
return hex;
|
|
25098
|
+
}
|
|
25099
|
+
const MEGABYTE = 1024 * 1024;
|
|
25100
|
+
const MAX_IMAGE_ATTACHMENT_BYTES = 25 * MEGABYTE;
|
|
25101
|
+
const MAX_OTHER_ATTACHMENT_BYTES = 100 * MEGABYTE;
|
|
25102
|
+
function attachmentReferenceLabel(file) {
|
|
25103
|
+
return translateCurrent(file.type.startsWith("image/") ? "upload.kindImage" : "upload.kindFile");
|
|
25104
|
+
}
|
|
25105
|
+
function arrayBufferToBase64(buffer) {
|
|
25106
|
+
return encodeBase64(new Uint8Array(buffer));
|
|
25107
|
+
}
|
|
25108
|
+
function extensionFor(file) {
|
|
25109
|
+
const fromName = file.name.match(/\.([A-Za-z0-9]{1,12})$/)?.[1];
|
|
25110
|
+
if (fromName) return fromName.toLowerCase();
|
|
25111
|
+
const fromType = file.type.match(/^image\/([A-Za-z0-9.+-]+)$/)?.[1];
|
|
25112
|
+
if (!fromType) return "bin";
|
|
25113
|
+
return fromType.replace(/[^A-Za-z0-9]/g, "").toLowerCase() || "bin";
|
|
25114
|
+
}
|
|
25115
|
+
function safeFileName(file) {
|
|
25116
|
+
const name = file.name.split(/[\\/]/).pop()?.trim() ?? "";
|
|
25117
|
+
const safe = name.replace(/[^A-Za-z0-9._ -]/g, "_").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
|
|
25118
|
+
if (safe) return safe.slice(0, 120);
|
|
25119
|
+
return `attachment.${extensionFor(file)}`;
|
|
25120
|
+
}
|
|
25121
|
+
function chatUploadRelPath(file, id) {
|
|
25122
|
+
if (file.type.startsWith("image/")) return `.aplus/uploads/${id}.${extensionFor(file)}`;
|
|
25123
|
+
return `.aplus/uploads/${id}-${safeFileName(file)}`;
|
|
25124
|
+
}
|
|
25125
|
+
async function uploadChatFileToMachineCore(file, workspaceRoot, deps) {
|
|
25126
|
+
const root = workspaceRoot.trim().replace(/\/+$/, "");
|
|
25127
|
+
if (!root) throw new Error(translateCurrent("upload.noWorkspaceRoot"));
|
|
25128
|
+
const id = (deps.now?.() ?? Date.now()).toString(36) + (deps.random?.() ?? Math.random()).toString(36).slice(2, 8);
|
|
25129
|
+
const absPath = `${root}/${chatUploadRelPath(file, id)}`;
|
|
25130
|
+
const contentBase64 = arrayBufferToBase64(await file.arrayBuffer());
|
|
25131
|
+
let expectedHash = null;
|
|
25132
|
+
const probe = await deps.readFile(absPath);
|
|
25133
|
+
if (probe && probe.success && typeof probe.content === "string") {
|
|
25134
|
+
expectedHash = await sha256HexFromBase64(probe.content);
|
|
25135
|
+
}
|
|
25136
|
+
const res = await deps.writeFile({ path: absPath, content: contentBase64, expectedHash });
|
|
25137
|
+
if (!res || res.success === false) {
|
|
25138
|
+
throw new Error(res?.error || translateCurrent("upload.failed"));
|
|
25139
|
+
}
|
|
25140
|
+
return absPath;
|
|
25141
|
+
}
|
|
25142
|
+
async function preparePersonalChatMessageWithAttachments(input, deps) {
|
|
25143
|
+
const text2 = input.text.trim();
|
|
25144
|
+
if (input.files.length === 0) return text2;
|
|
25145
|
+
const references = [];
|
|
25146
|
+
for (const file of input.files) {
|
|
25147
|
+
const path = await deps.uploadFile(file);
|
|
25148
|
+
references.push(`${attachmentReferenceLabel(file)}: ${path}`);
|
|
25149
|
+
}
|
|
25150
|
+
const fileReferences = references.join("\n");
|
|
25151
|
+
return text2 ? `${text2}
|
|
25152
|
+
|
|
25153
|
+
${fileReferences}` : `${translateCurrent("composer.attachPromptPrefix")}
|
|
25154
|
+
${fileReferences}`;
|
|
25155
|
+
}
|
|
25156
|
+
const MIME_BY_EXTENSION = {
|
|
25157
|
+
".gif": "image/gif",
|
|
25158
|
+
".jpeg": "image/jpeg",
|
|
25159
|
+
".jpg": "image/jpeg",
|
|
25160
|
+
".png": "image/png",
|
|
25161
|
+
".webp": "image/webp",
|
|
25162
|
+
".pdf": "application/pdf",
|
|
25163
|
+
".json": "application/json",
|
|
25164
|
+
".md": "text/markdown",
|
|
25165
|
+
".txt": "text/plain"
|
|
25166
|
+
};
|
|
25167
|
+
function mimeTypeForPath(path) {
|
|
25168
|
+
return MIME_BY_EXTENSION[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
25169
|
+
}
|
|
25170
|
+
async function loadCliAttachment(path, deps = { stat, readFile }) {
|
|
25171
|
+
const info = await deps.stat(path);
|
|
25172
|
+
const mimeType = mimeTypeForPath(path);
|
|
25173
|
+
const limit = mimeType.startsWith("image/") ? MAX_IMAGE_ATTACHMENT_BYTES : MAX_OTHER_ATTACHMENT_BYTES;
|
|
25174
|
+
if (info.size > limit) {
|
|
25175
|
+
throw new Error(`${basename(path)} is too large (maximum ${Math.round(limit / 1024 / 1024)} MB).`);
|
|
25176
|
+
}
|
|
25177
|
+
const bytes = await deps.readFile(path);
|
|
25178
|
+
return new File([new Uint8Array(bytes)], basename(path), { type: mimeType });
|
|
25179
|
+
}
|
|
25180
|
+
function uploadCliAttachment(transport, machineId, workspaceRoot, file, deterministic = {}) {
|
|
25181
|
+
return uploadChatFileToMachineCore(file, workspaceRoot, {
|
|
25182
|
+
readFile: (path) => transport.rpc(machineId, "readFile", { path }),
|
|
25183
|
+
writeFile: (params) => transport.rpc(machineId, "writeFile", params),
|
|
25184
|
+
...deterministic
|
|
25185
|
+
});
|
|
25186
|
+
}
|
|
25187
|
+
function prepareCliMessageWithAttachments(text2, files, uploadFile) {
|
|
25188
|
+
return preparePersonalChatMessageWithAttachments({ text: text2, files }, { uploadFile });
|
|
25189
|
+
}
|
|
25190
|
+
function choiceIndex(answer, length, currentIndex) {
|
|
25191
|
+
const trimmed = answer?.trim() ?? "";
|
|
25192
|
+
if (!trimmed) return Math.max(0, currentIndex);
|
|
25193
|
+
const number = Number.parseInt(trimmed, 10);
|
|
25194
|
+
return Number.isInteger(number) && number >= 1 && number <= length ? number - 1 : Math.max(0, currentIndex);
|
|
25195
|
+
}
|
|
25196
|
+
function choiceQuestion(label, choices, currentIndex) {
|
|
25197
|
+
const rendered = choices.map((choice, index) => `${index + 1}) ${choice.label}`).join(" ");
|
|
25198
|
+
return `${label} [${rendered}] (${currentIndex + 1}): `;
|
|
25199
|
+
}
|
|
25200
|
+
function yes(answer, fallback) {
|
|
25201
|
+
const normalized = answer?.trim().toLowerCase() ?? "";
|
|
25202
|
+
if (!normalized) return fallback;
|
|
25203
|
+
return normalized === "y" || normalized === "yes";
|
|
25204
|
+
}
|
|
25205
|
+
async function promptNewConversationOptions(initial, ask) {
|
|
25206
|
+
let options = initial;
|
|
25207
|
+
const agents = options.agent === "gemini" ? [{ id: "gemini", label: "Gemini", provider: "Google" }, ...newSessionAgents] : newSessionAgents;
|
|
25208
|
+
const agentIndex = Math.max(0, agents.findIndex((entry) => entry.id === options.agent));
|
|
25209
|
+
const selectedAgent = agents[choiceIndex(
|
|
25210
|
+
await ask(choiceQuestion("Agent", agents, agentIndex)),
|
|
25211
|
+
agents.length,
|
|
25212
|
+
agentIndex
|
|
25213
|
+
)]?.id;
|
|
25214
|
+
options = normalizeNewConversationOptions(options, { agent: selectedAgent });
|
|
25215
|
+
const models = modelChoices(options.agent);
|
|
25216
|
+
const modelIndex = Math.max(0, models.findIndex((entry) => entry.key === options.model));
|
|
25217
|
+
const model = models[choiceIndex(
|
|
25218
|
+
await ask(choiceQuestion("Model", models, modelIndex)),
|
|
25219
|
+
models.length,
|
|
25220
|
+
modelIndex
|
|
25221
|
+
)]?.key ?? "default";
|
|
25222
|
+
options = normalizeNewConversationOptions(options, { model });
|
|
25223
|
+
const efforts = effortChoices(options.agent, options.model);
|
|
25224
|
+
if (efforts.length > 0) {
|
|
25225
|
+
const effortIndex = Math.max(0, efforts.findIndex((entry) => entry.key === options.effort));
|
|
25226
|
+
const effort = efforts[choiceIndex(
|
|
25227
|
+
await ask(choiceQuestion("Effort", efforts, effortIndex)),
|
|
25228
|
+
efforts.length,
|
|
25229
|
+
effortIndex
|
|
25230
|
+
)]?.key ?? null;
|
|
25231
|
+
options = normalizeNewConversationOptions(options, { effort });
|
|
25232
|
+
}
|
|
25233
|
+
options = normalizeNewConversationOptions(options, {
|
|
25234
|
+
useWorktree: yes(await ask(`Create isolated worktree? ${options.useWorktree ? "[Y/n]" : "[y/N]"}: `), options.useWorktree)
|
|
25235
|
+
});
|
|
25236
|
+
if (options.files.length === 0) {
|
|
25237
|
+
const files = (await ask("Attach local files (comma-separated, blank for none): "))?.split(",").map((entry) => entry.trim()).filter(Boolean) ?? [];
|
|
25238
|
+
options = normalizeNewConversationOptions(options, { files });
|
|
25239
|
+
}
|
|
25240
|
+
if (!options.voice && !options.voiceFile) {
|
|
25241
|
+
options = normalizeNewConversationOptions(options, {
|
|
25242
|
+
voice: yes(await ask("Record the first prompt by voice? [y/N]: "), false)
|
|
25243
|
+
});
|
|
25244
|
+
}
|
|
25245
|
+
return options;
|
|
25246
|
+
}
|
|
25247
|
+
const MIN_AUDIO_BYTES = 1600;
|
|
25248
|
+
function resolveTranscriptionOutcome(text2) {
|
|
25249
|
+
const cleaned = text2.trim();
|
|
25250
|
+
if (!cleaned) return { errorKind: "no-speech" };
|
|
25251
|
+
return { transcript: cleaned };
|
|
25252
|
+
}
|
|
25253
|
+
function recorderSpec(platform2, available) {
|
|
25254
|
+
if (platform2 === "linux" && available.has("arecord")) {
|
|
25255
|
+
return { command: "arecord", args: (path) => ["-q", "-f", "S16_LE", "-r", "16000", "-c", "1", path] };
|
|
25256
|
+
}
|
|
25257
|
+
if (available.has("rec")) {
|
|
25258
|
+
return { command: "rec", args: (path) => ["-q", "-c", "1", "-r", "16000", path] };
|
|
25259
|
+
}
|
|
25260
|
+
if (available.has("ffmpeg")) {
|
|
25261
|
+
if (platform2 === "darwin") {
|
|
25262
|
+
return {
|
|
25263
|
+
command: "ffmpeg",
|
|
25264
|
+
args: (path) => ["-nostdin", "-loglevel", "error", "-f", "avfoundation", "-i", ":0", "-ac", "1", "-ar", "16000", "-y", path]
|
|
25265
|
+
};
|
|
25266
|
+
}
|
|
25267
|
+
return {
|
|
25268
|
+
command: "ffmpeg",
|
|
25269
|
+
args: (path) => ["-nostdin", "-loglevel", "error", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", "-y", path]
|
|
25270
|
+
};
|
|
25271
|
+
}
|
|
25272
|
+
return null;
|
|
25273
|
+
}
|
|
25274
|
+
async function installedCommands(env) {
|
|
25275
|
+
const result = /* @__PURE__ */ new Set();
|
|
25276
|
+
const directories = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
25277
|
+
for (const command of ["arecord", "rec", "ffmpeg"]) {
|
|
25278
|
+
for (const directory of directories) {
|
|
25279
|
+
try {
|
|
25280
|
+
await access(join(directory, command), constants$1.X_OK);
|
|
25281
|
+
result.add(command);
|
|
25282
|
+
break;
|
|
25283
|
+
} catch {
|
|
25284
|
+
}
|
|
25285
|
+
}
|
|
25286
|
+
}
|
|
25287
|
+
return result;
|
|
25288
|
+
}
|
|
25289
|
+
function waitForSpawn(child) {
|
|
25290
|
+
return new Promise((resolve, reject) => {
|
|
25291
|
+
const onSpawn = () => {
|
|
25292
|
+
cleanup();
|
|
25293
|
+
resolve();
|
|
25294
|
+
};
|
|
25295
|
+
const onError = (error) => {
|
|
25296
|
+
cleanup();
|
|
25297
|
+
reject(error);
|
|
25298
|
+
};
|
|
25299
|
+
const cleanup = () => {
|
|
25300
|
+
child.off("spawn", onSpawn);
|
|
25301
|
+
child.off("error", onError);
|
|
25302
|
+
};
|
|
25303
|
+
child.once("spawn", onSpawn);
|
|
25304
|
+
child.once("error", onError);
|
|
25305
|
+
});
|
|
25306
|
+
}
|
|
25307
|
+
function waitForClose(child) {
|
|
25308
|
+
return new Promise((resolve) => {
|
|
25309
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
25310
|
+
resolve();
|
|
25311
|
+
return;
|
|
25312
|
+
}
|
|
25313
|
+
const timer = setTimeout(() => {
|
|
25314
|
+
child.kill("SIGTERM");
|
|
25315
|
+
resolve();
|
|
25316
|
+
}, 5e3);
|
|
25317
|
+
child.once("close", () => {
|
|
25318
|
+
clearTimeout(timer);
|
|
25319
|
+
resolve();
|
|
25320
|
+
});
|
|
25321
|
+
});
|
|
25322
|
+
}
|
|
25323
|
+
async function recordCliVoice(waitForStop, input = {}) {
|
|
25324
|
+
const platform2 = input.platform ?? process.platform;
|
|
25325
|
+
const spec = recorderSpec(platform2, await installedCommands(input.env ?? process.env));
|
|
25326
|
+
if (!spec) {
|
|
25327
|
+
throw new Error("No supported microphone recorder was found. Install ffmpeg/SoX/arecord or use `--voice-file <audio>` instead.");
|
|
25328
|
+
}
|
|
25329
|
+
const directory = await mkdtemp(join(tmpdir(), "saycode-voice-"));
|
|
25330
|
+
const path = join(directory, "prompt.wav");
|
|
25331
|
+
const cleanup = () => rm(directory, { recursive: true, force: true });
|
|
25332
|
+
try {
|
|
25333
|
+
const child = spawn(spec.command, spec.args(path), { stdio: ["ignore", "ignore", "pipe"] });
|
|
25334
|
+
await waitForSpawn(child);
|
|
25335
|
+
await waitForStop();
|
|
25336
|
+
child.kill("SIGINT");
|
|
25337
|
+
await waitForClose(child);
|
|
25338
|
+
const info = await stat(path);
|
|
25339
|
+
if (info.size < MIN_AUDIO_BYTES) throw new Error("No usable speech was recorded.");
|
|
25340
|
+
return { path, cleanup };
|
|
25341
|
+
} catch (error) {
|
|
25342
|
+
await cleanup();
|
|
25343
|
+
throw error;
|
|
25344
|
+
}
|
|
25345
|
+
}
|
|
25346
|
+
function audioMimeTypeForPath(path) {
|
|
25347
|
+
switch (extname(path).toLowerCase()) {
|
|
25348
|
+
case ".m4a":
|
|
25349
|
+
case ".mp4":
|
|
25350
|
+
return "audio/mp4";
|
|
25351
|
+
case ".ogg":
|
|
25352
|
+
return "audio/ogg";
|
|
25353
|
+
case ".webm":
|
|
25354
|
+
return "audio/webm";
|
|
25355
|
+
case ".mp3":
|
|
25356
|
+
return "audio/mpeg";
|
|
25357
|
+
default:
|
|
25358
|
+
return "audio/wav";
|
|
25359
|
+
}
|
|
25360
|
+
}
|
|
25361
|
+
async function transcribeCliVoiceFile(rest, path, read = readFile) {
|
|
25362
|
+
const bytes = await read(path);
|
|
25363
|
+
const mimeType = audioMimeTypeForPath(path);
|
|
25364
|
+
const result = await rest.transcribeAudio(new Blob([new Uint8Array(bytes)], { type: mimeType }), {
|
|
25365
|
+
fileName: basename(path),
|
|
25366
|
+
mimeType
|
|
25367
|
+
});
|
|
25368
|
+
const outcome = resolveTranscriptionOutcome(result.text);
|
|
25369
|
+
if ("errorKind" in outcome) throw new Error("No speech was recognized in that recording.");
|
|
25370
|
+
return outcome.transcript;
|
|
25371
|
+
}
|
|
25372
|
+
function configurableAgent(agent) {
|
|
25373
|
+
if (agent === "gemini") return agent;
|
|
25374
|
+
return newSessionAgents.some((entry) => entry.id === agent) ? agent : null;
|
|
25375
|
+
}
|
|
25376
|
+
function hasNewConversationSettings(request) {
|
|
25377
|
+
return request.model !== void 0 || request.effort !== void 0 || request.useWorktree === true || request.worktreeName !== void 0 || request.worktreeBaseRef !== void 0 || request.files !== void 0 || request.voice === true || request.voiceFile !== void 0;
|
|
25378
|
+
}
|
|
25379
|
+
function validateExplicitSelection(request, agent) {
|
|
25380
|
+
if (request.model !== void 0 && !modelChoices(agent).some((option) => option.key === request.model)) {
|
|
25381
|
+
throw new CliError(`Unknown ${agent} model "${request.model}".`);
|
|
25382
|
+
}
|
|
25383
|
+
if (request.effort !== void 0) {
|
|
25384
|
+
const model = request.model ?? DEFAULT_CLI_NEW_CONVERSATION_OPTIONS.model;
|
|
25385
|
+
if (!effortChoices(agent, model).some((option) => option.key === request.effort)) {
|
|
25386
|
+
throw new CliError(`Effort "${request.effort}" is not available for ${agent} model "${model}".`);
|
|
25387
|
+
}
|
|
25388
|
+
}
|
|
25389
|
+
}
|
|
25390
|
+
async function resolveCliNewConversationOptions(request, ask) {
|
|
25391
|
+
const agent = configurableAgent(request.agent);
|
|
25392
|
+
if (!agent) {
|
|
25393
|
+
if (hasNewConversationSettings(request)) {
|
|
25394
|
+
throw new CliError(`New-conversation options are not available for agent "${request.agent}".`);
|
|
25395
|
+
}
|
|
25396
|
+
return null;
|
|
25397
|
+
}
|
|
25398
|
+
validateExplicitSelection(request, agent);
|
|
25399
|
+
let resolved = normalizeNewConversationOptions(DEFAULT_CLI_NEW_CONVERSATION_OPTIONS, {
|
|
25400
|
+
agent,
|
|
25401
|
+
...request.model !== void 0 ? { model: request.model } : {},
|
|
25402
|
+
...request.effort !== void 0 ? { effort: request.effort } : {},
|
|
25403
|
+
...request.useWorktree ? { useWorktree: true } : {},
|
|
25404
|
+
...request.worktreeName !== void 0 ? { worktreeName: request.worktreeName } : {},
|
|
25405
|
+
...request.worktreeBaseRef !== void 0 ? { worktreeBaseRef: request.worktreeBaseRef } : {},
|
|
25406
|
+
...request.files ? { files: request.files } : {},
|
|
25407
|
+
...request.voice ? { voice: true } : {},
|
|
25408
|
+
...request.voiceFile !== void 0 ? { voiceFile: request.voiceFile } : {}
|
|
25409
|
+
});
|
|
25410
|
+
if (ask) resolved = await promptNewConversationOptions(resolved, ask);
|
|
25411
|
+
return resolved;
|
|
25412
|
+
}
|
|
25413
|
+
async function captureCliVoicePrompt(context, voiceFile, waitForStop) {
|
|
25414
|
+
if (!context.hasApiAccess) {
|
|
25415
|
+
throw new CliError("Voice transcription needs a Saycode account login. Run `saycode login`, or type the prompt instead.");
|
|
25416
|
+
}
|
|
25417
|
+
if (voiceFile) return transcribeCliVoiceFile(context.rest, voiceFile);
|
|
25418
|
+
context.out.raw("Recording from the microphone…");
|
|
25419
|
+
const recording = await recordCliVoice(waitForStop);
|
|
25420
|
+
try {
|
|
25421
|
+
return await transcribeCliVoiceFile(context.rest, recording.path);
|
|
25422
|
+
} finally {
|
|
25423
|
+
await recording.cleanup();
|
|
25424
|
+
}
|
|
25425
|
+
}
|
|
25426
|
+
const USAGE_SETTINGS_NAVIGATION_DESTINATIONS = [
|
|
25427
|
+
"settings_profile",
|
|
25428
|
+
"settings_system_prompt",
|
|
25429
|
+
"settings_personal_memory",
|
|
25430
|
+
"settings_notifications",
|
|
25431
|
+
"settings_visual_effects",
|
|
25432
|
+
"settings_language",
|
|
25433
|
+
"settings_shortcuts",
|
|
25434
|
+
"settings_machines",
|
|
25435
|
+
"settings_mobile",
|
|
25436
|
+
"settings_env",
|
|
25437
|
+
"settings_security",
|
|
25438
|
+
"settings_memory",
|
|
25439
|
+
"settings_conversation_search",
|
|
25440
|
+
"settings_connectors",
|
|
25441
|
+
"settings_about"
|
|
25442
|
+
];
|
|
25443
|
+
const USAGE_ANALYTICS_EVENT_NAMES = [
|
|
25444
|
+
"app_started",
|
|
25445
|
+
"login_success",
|
|
25446
|
+
"logout",
|
|
25447
|
+
"session_created",
|
|
25448
|
+
"message_sent",
|
|
25449
|
+
"command_completed",
|
|
25450
|
+
"navigation_selected",
|
|
25451
|
+
"secondary_action_selected",
|
|
25452
|
+
"first_response_received",
|
|
25453
|
+
"preview_opened",
|
|
25454
|
+
"preview_start_failed",
|
|
25455
|
+
"terminal_opened",
|
|
25456
|
+
"worktree_created",
|
|
25457
|
+
"browser_tab_opened",
|
|
25458
|
+
"update_installed",
|
|
25459
|
+
"client_error_occurred",
|
|
25460
|
+
// Meta event: emitted by the queue itself when the cap forced drops (plan
|
|
25461
|
+
// "리스크와 대응"), so silent loss is visible in the dashboard.
|
|
25462
|
+
"usage_events_dropped"
|
|
25463
|
+
];
|
|
25464
|
+
const USAGE_ANALYTICS_COMMAND_ACTIONS = [
|
|
25465
|
+
"help",
|
|
25466
|
+
"version",
|
|
25467
|
+
"login",
|
|
25468
|
+
"logout",
|
|
25469
|
+
"workspaces",
|
|
25470
|
+
"browse",
|
|
25471
|
+
"machines",
|
|
25472
|
+
"projects",
|
|
25473
|
+
"ls",
|
|
25474
|
+
"attach",
|
|
25475
|
+
"chat",
|
|
25476
|
+
"watch",
|
|
25477
|
+
"show_status",
|
|
25478
|
+
"open_chat",
|
|
25479
|
+
"send_selection",
|
|
25480
|
+
"create_project",
|
|
25481
|
+
"mount_project"
|
|
25482
|
+
];
|
|
25483
|
+
const USAGE_NAVIGATION_SURFACES = ["sidebar_primary", "sidebar_more", "workspace_switcher", "settings"];
|
|
25484
|
+
const USAGE_NAVIGATION_DESTINATIONS = [
|
|
25485
|
+
"projects",
|
|
25486
|
+
"agent_board",
|
|
25487
|
+
"personal_chats",
|
|
25488
|
+
"all_conversations",
|
|
25489
|
+
"machines",
|
|
25490
|
+
"organization_home",
|
|
25491
|
+
"settings",
|
|
25492
|
+
"organization_admin",
|
|
25493
|
+
"system_admin",
|
|
25494
|
+
...USAGE_SETTINGS_NAVIGATION_DESTINATIONS
|
|
25495
|
+
];
|
|
25496
|
+
const USAGE_SECONDARY_ACTION_SURFACES = ["chat_header", "composer"];
|
|
25497
|
+
const USAGE_SECONDARY_ACTIONS = [
|
|
25498
|
+
"terminal",
|
|
25499
|
+
"preview",
|
|
25500
|
+
"new_chat",
|
|
25501
|
+
"more",
|
|
25502
|
+
"copy",
|
|
25503
|
+
"complete",
|
|
25504
|
+
"archive",
|
|
25505
|
+
"project_settings",
|
|
25506
|
+
"deploy",
|
|
25507
|
+
"memory",
|
|
25508
|
+
"machine_usage",
|
|
25509
|
+
"workspace_layout",
|
|
25510
|
+
"tool_menu",
|
|
25511
|
+
"model",
|
|
25512
|
+
"voice",
|
|
25513
|
+
"attachment",
|
|
25514
|
+
"work_completion"
|
|
25515
|
+
];
|
|
25516
|
+
const DEMO_ANALYTICS_USER_ID = "demo-user";
|
|
25517
|
+
function isDemoAnalyticsUserId(userId) {
|
|
25518
|
+
return userId === DEMO_ANALYTICS_USER_ID;
|
|
25519
|
+
}
|
|
25520
|
+
const EVENT_PROPERTY_ALLOWLIST = {
|
|
25521
|
+
app_started: ["mode"],
|
|
25522
|
+
login_success: ["mode"],
|
|
25523
|
+
logout: ["mode"],
|
|
25524
|
+
session_created: ["mode", "worktree", "new_project"],
|
|
25525
|
+
message_sent: ["mode", "length_bucket"],
|
|
25526
|
+
command_completed: ["action"],
|
|
25527
|
+
navigation_selected: ["mode", "surface", "destination"],
|
|
25528
|
+
secondary_action_selected: ["mode", "surface", "action"],
|
|
25529
|
+
first_response_received: ["mode", "duration_bucket"],
|
|
25530
|
+
preview_opened: ["mode"],
|
|
25531
|
+
preview_start_failed: ["mode", "failure_code"],
|
|
25532
|
+
terminal_opened: ["mode"],
|
|
25533
|
+
worktree_created: ["mode"],
|
|
25534
|
+
browser_tab_opened: ["mode"],
|
|
25535
|
+
update_installed: ["mode", "from_version", "to_version"],
|
|
25536
|
+
client_error_occurred: ["mode", "error_kind"],
|
|
25537
|
+
usage_events_dropped: ["mode", "dropped_count"]
|
|
25538
|
+
};
|
|
25539
|
+
const EVENT_PROPERTY_VALUE_ALLOWLIST = {
|
|
25540
|
+
navigation_selected: {
|
|
25541
|
+
surface: USAGE_NAVIGATION_SURFACES,
|
|
25542
|
+
destination: USAGE_NAVIGATION_DESTINATIONS
|
|
25543
|
+
},
|
|
25544
|
+
secondary_action_selected: {
|
|
25545
|
+
surface: USAGE_SECONDARY_ACTION_SURFACES,
|
|
25546
|
+
action: USAGE_SECONDARY_ACTIONS
|
|
25547
|
+
},
|
|
25548
|
+
command_completed: {
|
|
25549
|
+
action: USAGE_ANALYTICS_COMMAND_ACTIONS
|
|
25550
|
+
}
|
|
25551
|
+
};
|
|
25552
|
+
const MAX_STRING_PROPERTY_LENGTH = 200;
|
|
25553
|
+
function isUsageEventName(value2) {
|
|
25554
|
+
return typeof value2 === "string" && USAGE_ANALYTICS_EVENT_NAMES.includes(value2);
|
|
25555
|
+
}
|
|
25556
|
+
function sanitizeUsageEvent(name, props) {
|
|
25557
|
+
if (!isUsageEventName(name)) return null;
|
|
25558
|
+
const allowlist = EVENT_PROPERTY_ALLOWLIST[name];
|
|
25559
|
+
const properties = {};
|
|
25560
|
+
if (props && typeof props === "object" && !Array.isArray(props)) {
|
|
25561
|
+
for (const [key, value2] of Object.entries(props)) {
|
|
25562
|
+
if (!allowlist.includes(key)) continue;
|
|
25563
|
+
if (typeof value2 === "string") {
|
|
25564
|
+
const valueAllowlist = EVENT_PROPERTY_VALUE_ALLOWLIST[name]?.[key];
|
|
25565
|
+
if (valueAllowlist && !valueAllowlist.includes(value2)) continue;
|
|
25566
|
+
properties[key] = value2.slice(0, MAX_STRING_PROPERTY_LENGTH);
|
|
25567
|
+
} else if (typeof value2 === "number" && Number.isFinite(value2)) {
|
|
25568
|
+
properties[key] = value2;
|
|
25569
|
+
} else if (typeof value2 === "boolean") {
|
|
25570
|
+
properties[key] = value2;
|
|
25571
|
+
}
|
|
25572
|
+
}
|
|
25573
|
+
}
|
|
25574
|
+
const requiredEnumProperties = EVENT_PROPERTY_VALUE_ALLOWLIST[name];
|
|
25575
|
+
if (requiredEnumProperties && Object.keys(requiredEnumProperties).some((key) => properties[key] === void 0)) return null;
|
|
25576
|
+
return { event: name, properties };
|
|
25577
|
+
}
|
|
25578
|
+
function buildUsageCommonProperties(input) {
|
|
25579
|
+
return { app: input.service, app_version: input.appVersion, os: input.platform };
|
|
25580
|
+
}
|
|
25581
|
+
const USAGE_ANALYTICS_QUEUE_FILE = "queue.jsonl";
|
|
25582
|
+
const USAGE_ANALYTICS_QUEUE_MAX_EVENTS = 5e3;
|
|
25583
|
+
function isQueuedUsageEvent(value2) {
|
|
25584
|
+
if (!value2 || typeof value2 !== "object") return false;
|
|
25585
|
+
const record2 = value2;
|
|
25586
|
+
return typeof record2.uuid === "string" && record2.uuid.length > 0 && typeof record2.event === "string" && record2.event.length > 0 && typeof record2.distinctId === "string" && typeof record2.timestamp === "string" && !!record2.properties && typeof record2.properties === "object";
|
|
25587
|
+
}
|
|
25588
|
+
function createUsageAnalyticsQueue(options) {
|
|
25589
|
+
const maxEvents = options.maxEvents ?? USAGE_ANALYTICS_QUEUE_MAX_EVENTS;
|
|
25590
|
+
const path = join(options.dir, USAGE_ANALYTICS_QUEUE_FILE);
|
|
25591
|
+
let droppedCount = 0;
|
|
25592
|
+
let knownCount = null;
|
|
25593
|
+
let chain = Promise.resolve();
|
|
25594
|
+
const enqueue = (task) => {
|
|
25595
|
+
const result = chain.then(task);
|
|
25596
|
+
chain = result.then(() => void 0, () => void 0);
|
|
25597
|
+
return result;
|
|
25598
|
+
};
|
|
25599
|
+
const readAll = async () => {
|
|
25600
|
+
let raw;
|
|
25601
|
+
try {
|
|
25602
|
+
raw = await readFile(path, "utf8");
|
|
25603
|
+
} catch {
|
|
25604
|
+
return [];
|
|
25605
|
+
}
|
|
25606
|
+
const records = [];
|
|
25607
|
+
for (const line of raw.split("\n")) {
|
|
25608
|
+
if (!line.trim()) continue;
|
|
25609
|
+
try {
|
|
25610
|
+
const parsed = JSON.parse(line);
|
|
25611
|
+
if (isQueuedUsageEvent(parsed)) records.push(parsed);
|
|
25612
|
+
} catch {
|
|
25613
|
+
}
|
|
25614
|
+
}
|
|
25615
|
+
return records;
|
|
25616
|
+
};
|
|
25617
|
+
const rewrite = async (records) => {
|
|
25618
|
+
if (records.length === 0) {
|
|
25619
|
+
await rm(path, { force: true });
|
|
25620
|
+
return;
|
|
25621
|
+
}
|
|
25622
|
+
await writeFile(path, records.map((r) => JSON.stringify(r)).join("\n") + "\n", { mode: 384 });
|
|
25623
|
+
};
|
|
25624
|
+
return {
|
|
25625
|
+
append(record2) {
|
|
25626
|
+
return enqueue(async () => {
|
|
25627
|
+
try {
|
|
25628
|
+
await mkdir(options.dir, { recursive: true, mode: 448 });
|
|
25629
|
+
await appendFile(path, JSON.stringify(record2) + "\n", { mode: 384 });
|
|
25630
|
+
if (knownCount === null) knownCount = (await readAll()).length;
|
|
25631
|
+
else knownCount += 1;
|
|
25632
|
+
if (knownCount > maxEvents) {
|
|
25633
|
+
const all = await readAll();
|
|
25634
|
+
if (all.length > maxEvents) {
|
|
25635
|
+
droppedCount += all.length - maxEvents;
|
|
25636
|
+
await rewrite(all.slice(all.length - maxEvents));
|
|
25637
|
+
}
|
|
25638
|
+
knownCount = Math.min(all.length, maxEvents);
|
|
25639
|
+
}
|
|
25640
|
+
} catch {
|
|
25641
|
+
}
|
|
25642
|
+
});
|
|
25643
|
+
},
|
|
25644
|
+
readBatch(limit) {
|
|
25645
|
+
return enqueue(async () => (await readAll()).slice(0, limit));
|
|
25646
|
+
},
|
|
25647
|
+
removeSent(uuids) {
|
|
25648
|
+
return enqueue(async () => {
|
|
25649
|
+
try {
|
|
25650
|
+
const sent = new Set(uuids);
|
|
25651
|
+
const kept = (await readAll()).filter((record2) => !sent.has(record2.uuid));
|
|
25652
|
+
await rewrite(kept);
|
|
25653
|
+
knownCount = kept.length;
|
|
25654
|
+
} catch {
|
|
25655
|
+
knownCount = null;
|
|
25656
|
+
}
|
|
25657
|
+
});
|
|
25658
|
+
},
|
|
25659
|
+
clear() {
|
|
25660
|
+
return enqueue(async () => {
|
|
25661
|
+
droppedCount = 0;
|
|
25662
|
+
knownCount = 0;
|
|
25663
|
+
try {
|
|
25664
|
+
await rm(path, { force: true });
|
|
25665
|
+
} catch {
|
|
25666
|
+
knownCount = null;
|
|
25667
|
+
}
|
|
25668
|
+
});
|
|
25669
|
+
},
|
|
25670
|
+
takeDroppedCount() {
|
|
25671
|
+
const count = droppedCount;
|
|
25672
|
+
droppedCount = 0;
|
|
25673
|
+
return count;
|
|
25674
|
+
}
|
|
25675
|
+
};
|
|
25676
|
+
}
|
|
25677
|
+
const USAGE_ANALYTICS_BATCH_SIZE = 100;
|
|
25678
|
+
const USAGE_ANALYTICS_REQUEST_TIMEOUT_MS = 1e4;
|
|
25679
|
+
function createUsageAnalyticsSender(options) {
|
|
25680
|
+
const { queue, config, commonProperties } = options;
|
|
25681
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
25682
|
+
const batchSize = options.batchSize ?? USAGE_ANALYTICS_BATCH_SIZE;
|
|
25683
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? USAGE_ANALYTICS_REQUEST_TIMEOUT_MS;
|
|
25684
|
+
return {
|
|
25685
|
+
async flush() {
|
|
25686
|
+
if (!config || !config.token) return "disabled";
|
|
25687
|
+
const batch = await queue.readBatch(batchSize);
|
|
25688
|
+
if (batch.length === 0) return "empty";
|
|
25689
|
+
const controller = new AbortController();
|
|
25690
|
+
const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
25691
|
+
timeout.unref?.();
|
|
25692
|
+
try {
|
|
25693
|
+
const response = await fetchImpl(`${config.host.replace(/\/$/, "")}/batch/`, {
|
|
25694
|
+
method: "POST",
|
|
25695
|
+
signal: controller.signal,
|
|
25696
|
+
headers: { "Content-Type": "application/json" },
|
|
25697
|
+
body: JSON.stringify({
|
|
25698
|
+
api_key: config.token,
|
|
25699
|
+
batch: batch.map((record2) => ({
|
|
25700
|
+
event: record2.event,
|
|
25701
|
+
distinct_id: record2.distinctId,
|
|
25702
|
+
uuid: record2.uuid,
|
|
25703
|
+
timestamp: record2.timestamp,
|
|
25704
|
+
properties: {
|
|
25705
|
+
...record2.properties,
|
|
25706
|
+
...commonProperties,
|
|
25707
|
+
// web-ui(person_profiles:'identified_only') 파리티: 익명 이벤트는
|
|
25708
|
+
// person 을 만들지 않는다 — 같은 프로젝트 공유 시 person 폭증 방지.
|
|
25709
|
+
...record2.identified === false ? { $process_person_profile: false } : {}
|
|
25710
|
+
}
|
|
25711
|
+
}))
|
|
25712
|
+
})
|
|
25713
|
+
});
|
|
25714
|
+
if (!response.ok) return "failed";
|
|
25715
|
+
} catch {
|
|
25716
|
+
return "failed";
|
|
25717
|
+
} finally {
|
|
25718
|
+
clearTimeout(timeout);
|
|
25719
|
+
}
|
|
25720
|
+
await queue.removeSent(batch.map((record2) => record2.uuid));
|
|
25721
|
+
return "sent";
|
|
25722
|
+
}
|
|
25723
|
+
};
|
|
25724
|
+
}
|
|
25725
|
+
const USAGE_ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
|
|
25726
|
+
function createUsageAnalyticsCoordinator(options) {
|
|
25727
|
+
const now = options.now ?? (() => Date.now());
|
|
25728
|
+
const queue = createUsageAnalyticsQueue({ dir: options.dir, maxEvents: options.queueMaxEvents });
|
|
25729
|
+
const sender2 = createUsageAnalyticsSender({
|
|
25730
|
+
queue,
|
|
25731
|
+
config: options.senderConfig,
|
|
25732
|
+
commonProperties: options.commonProperties,
|
|
25733
|
+
fetchImpl: options.fetchImpl
|
|
25734
|
+
});
|
|
25735
|
+
let timer = null;
|
|
25736
|
+
let forcedDisabled = false;
|
|
25737
|
+
let operationChain = Promise.resolve();
|
|
25738
|
+
const enqueueOperation = (operation) => {
|
|
25739
|
+
const result = operationChain.then(operation);
|
|
25740
|
+
operationChain = result.then(() => void 0, () => void 0);
|
|
25741
|
+
return result;
|
|
25742
|
+
};
|
|
25743
|
+
const append = async (name, props) => {
|
|
25744
|
+
const sanitized = sanitizeUsageEvent(name, props);
|
|
25745
|
+
if (!sanitized) return;
|
|
25746
|
+
try {
|
|
25747
|
+
const distinctId = await options.getDistinctId();
|
|
25748
|
+
const record2 = {
|
|
25749
|
+
uuid: randomUUID(),
|
|
25750
|
+
event: sanitized.event,
|
|
25751
|
+
properties: sanitized.properties,
|
|
25752
|
+
distinctId,
|
|
25753
|
+
timestamp: new Date(now()).toISOString()
|
|
25754
|
+
};
|
|
25755
|
+
if (options.getInstallId) record2.identified = distinctId !== await options.getInstallId();
|
|
25756
|
+
await queue.append(record2);
|
|
25757
|
+
} catch {
|
|
25758
|
+
}
|
|
25759
|
+
};
|
|
25760
|
+
const coordinator = {
|
|
25761
|
+
async record(name, props) {
|
|
25762
|
+
await enqueueOperation(async () => {
|
|
25763
|
+
if (forcedDisabled) return;
|
|
25764
|
+
try {
|
|
25765
|
+
if (!await options.isEnabled()) return;
|
|
25766
|
+
} catch {
|
|
25767
|
+
return;
|
|
25768
|
+
}
|
|
25769
|
+
await append(name, props);
|
|
25770
|
+
});
|
|
25771
|
+
},
|
|
25772
|
+
async recordAlias(input) {
|
|
25773
|
+
await enqueueOperation(async () => {
|
|
25774
|
+
if (forcedDisabled) return;
|
|
25775
|
+
try {
|
|
25776
|
+
if (!await options.isEnabled()) return;
|
|
25777
|
+
if (!input.userId || !input.installId || input.userId === input.installId) return;
|
|
25778
|
+
if (isDemoAnalyticsUserId(input.userId)) return;
|
|
25779
|
+
await queue.append({
|
|
25780
|
+
uuid: randomUUID(),
|
|
25781
|
+
event: "$create_alias",
|
|
25782
|
+
properties: { distinct_id: input.userId, alias: input.installId },
|
|
25783
|
+
distinctId: input.userId,
|
|
25784
|
+
identified: true,
|
|
25785
|
+
timestamp: new Date(now()).toISOString()
|
|
25786
|
+
});
|
|
25787
|
+
} catch {
|
|
25788
|
+
}
|
|
25789
|
+
});
|
|
25790
|
+
},
|
|
25791
|
+
async flushNow() {
|
|
25792
|
+
await enqueueOperation(async () => {
|
|
25793
|
+
if (forcedDisabled) return;
|
|
25794
|
+
const dropped = queue.takeDroppedCount();
|
|
25795
|
+
if (dropped > 0) await append("usage_events_dropped", { dropped_count: dropped });
|
|
25796
|
+
await sender2.flush();
|
|
25797
|
+
});
|
|
25798
|
+
},
|
|
25799
|
+
async setEnabled(enabled) {
|
|
25800
|
+
forcedDisabled = !enabled;
|
|
25801
|
+
await enqueueOperation(async () => {
|
|
25802
|
+
if (!enabled) await queue.clear();
|
|
25803
|
+
});
|
|
25804
|
+
},
|
|
25805
|
+
start() {
|
|
25806
|
+
if (timer) return;
|
|
25807
|
+
timer = setInterval(() => {
|
|
25808
|
+
void coordinator.flushNow();
|
|
25809
|
+
}, options.flushIntervalMs ?? USAGE_ANALYTICS_FLUSH_INTERVAL_MS);
|
|
25810
|
+
timer.unref?.();
|
|
25811
|
+
},
|
|
25812
|
+
dispose() {
|
|
25813
|
+
if (timer) clearInterval(timer);
|
|
25814
|
+
timer = null;
|
|
25815
|
+
},
|
|
25816
|
+
peekQueue(limit) {
|
|
25817
|
+
return queue.readBatch(limit);
|
|
25818
|
+
}
|
|
25819
|
+
};
|
|
25820
|
+
return coordinator;
|
|
25821
|
+
}
|
|
25822
|
+
const USAGE_ANALYTICS_IDENTITY_FILE = "identity.json";
|
|
25823
|
+
function createUsageAnalyticsIdentity(options) {
|
|
25824
|
+
const path = join(options.dir, USAGE_ANALYTICS_IDENTITY_FILE);
|
|
25825
|
+
let pending = null;
|
|
25826
|
+
const resolveInstallId = async () => {
|
|
25827
|
+
try {
|
|
25828
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
25829
|
+
if (typeof parsed.installId === "string" && parsed.installId.length > 0) return parsed.installId;
|
|
25830
|
+
} catch {
|
|
25831
|
+
}
|
|
25832
|
+
const installId = randomUUID();
|
|
25833
|
+
try {
|
|
25834
|
+
await mkdir(options.dir, { recursive: true, mode: 448 });
|
|
25835
|
+
await writeFile(path, JSON.stringify({ installId }), { mode: 384 });
|
|
25836
|
+
} catch {
|
|
25837
|
+
}
|
|
25838
|
+
return installId;
|
|
25839
|
+
};
|
|
25840
|
+
const getInstallId = () => {
|
|
25841
|
+
if (!pending) {
|
|
25842
|
+
pending = resolveInstallId();
|
|
25843
|
+
void pending.catch(() => {
|
|
25844
|
+
pending = null;
|
|
25845
|
+
});
|
|
25846
|
+
}
|
|
25847
|
+
return pending;
|
|
25848
|
+
};
|
|
25849
|
+
return {
|
|
25850
|
+
getInstallId,
|
|
25851
|
+
async getDistinctId() {
|
|
25852
|
+
try {
|
|
25853
|
+
const userId = await options.loadUserId();
|
|
25854
|
+
if (userId && !isDemoAnalyticsUserId(userId)) return userId;
|
|
25855
|
+
} catch {
|
|
25856
|
+
}
|
|
25857
|
+
return getInstallId();
|
|
25858
|
+
}
|
|
25859
|
+
};
|
|
25860
|
+
}
|
|
25861
|
+
async function flushUsageAnalyticsWithin(flush, timeoutMs) {
|
|
25862
|
+
let timer;
|
|
25863
|
+
await Promise.race([
|
|
25864
|
+
flush().catch(() => {
|
|
25865
|
+
}),
|
|
25866
|
+
new Promise((resolve) => {
|
|
25867
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
25868
|
+
})
|
|
25869
|
+
]);
|
|
25870
|
+
if (timer) clearTimeout(timer);
|
|
25871
|
+
}
|
|
25872
|
+
function createNodeUsageAnalytics(options) {
|
|
25873
|
+
const identity = createUsageAnalyticsIdentity({ dir: options.dir, loadUserId: async () => null });
|
|
25874
|
+
const coordinator = createUsageAnalyticsCoordinator({
|
|
25875
|
+
dir: options.dir,
|
|
25876
|
+
senderConfig: options.senderConfig,
|
|
25877
|
+
commonProperties: buildUsageCommonProperties({
|
|
25878
|
+
service: options.service,
|
|
25879
|
+
appVersion: options.appVersion,
|
|
25880
|
+
platform: options.platform
|
|
25881
|
+
}),
|
|
25882
|
+
getDistinctId: () => identity.getInstallId(),
|
|
25883
|
+
getInstallId: () => identity.getInstallId(),
|
|
25884
|
+
isEnabled: options.isEnabled,
|
|
25885
|
+
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
25886
|
+
});
|
|
25887
|
+
return {
|
|
25888
|
+
record: (name, props = {}) => coordinator.record(name, props),
|
|
25889
|
+
async start() {
|
|
25890
|
+
coordinator.start();
|
|
25891
|
+
await coordinator.record("app_started", {});
|
|
25892
|
+
},
|
|
25893
|
+
async flush() {
|
|
25894
|
+
coordinator.dispose();
|
|
25895
|
+
await coordinator.flushNow();
|
|
25896
|
+
},
|
|
25897
|
+
setEnabled: (enabled) => coordinator.setEnabled(enabled),
|
|
25898
|
+
peekQueue: (limit) => coordinator.peekQueue(limit)
|
|
25899
|
+
};
|
|
25900
|
+
}
|
|
25901
|
+
function resolvePublicPostHogConfig(input) {
|
|
25902
|
+
const token = input.token?.trim() ?? "";
|
|
25903
|
+
if (!token.startsWith("phc_")) return null;
|
|
25904
|
+
return { token, host: input.host?.trim() || "https://us.i.posthog.com" };
|
|
25905
|
+
}
|
|
25906
|
+
let analytics = null;
|
|
25907
|
+
const CLI_USAGE_ANALYTICS_FLUSH_TIMEOUT_MS = 1500;
|
|
25908
|
+
async function initializeCliUsageAnalytics(options = {}) {
|
|
25909
|
+
const homeDir = options.homeDir ?? homedir();
|
|
25910
|
+
const env = options.env ?? process.env;
|
|
25911
|
+
const builtHost = "https://us.i.posthog.com";
|
|
25912
|
+
analytics = createNodeUsageAnalytics({
|
|
25913
|
+
dir: join(homeDir, ".saycode", "usage-analytics"),
|
|
25914
|
+
service: "saycode_cli",
|
|
25915
|
+
appVersion: manifest.version,
|
|
25916
|
+
platform: platform(),
|
|
25917
|
+
senderConfig: resolvePublicPostHogConfig({
|
|
25918
|
+
token: env.APLUS_POSTHOG_TOKEN,
|
|
25919
|
+
host: builtHost
|
|
25920
|
+
}),
|
|
25921
|
+
isEnabled: async () => readConfig(homeDir).usageAnalyticsEnabled !== false
|
|
25922
|
+
});
|
|
25923
|
+
if (readConfig(homeDir).usageAnalyticsEnabled === false) await analytics.setEnabled(false).catch(() => {
|
|
25924
|
+
});
|
|
25925
|
+
await analytics.start().catch(() => {
|
|
25926
|
+
});
|
|
25927
|
+
}
|
|
25928
|
+
async function recordCliUsage(name, props = {}) {
|
|
25929
|
+
await analytics?.record(name, props).catch(() => {
|
|
25930
|
+
});
|
|
25931
|
+
}
|
|
25932
|
+
async function flushCliUsageAnalytics() {
|
|
25933
|
+
const current = analytics;
|
|
25934
|
+
analytics = null;
|
|
25935
|
+
if (!current) return;
|
|
25936
|
+
await flushUsageAnalyticsWithin(() => current.flush(), CLI_USAGE_ANALYTICS_FLUSH_TIMEOUT_MS);
|
|
25937
|
+
}
|
|
24306
25938
|
const DETACH_COMMAND = "/detach";
|
|
24307
25939
|
async function spawnSession(transport, machineId, directory, agent) {
|
|
24308
25940
|
const result = await transport.rpc(machineId, "spawn-happy-session", { directory, agent }, 6e4);
|
|
@@ -24312,31 +25944,15 @@ async function spawnSession(transport, machineId, directory, agent) {
|
|
|
24312
25944
|
}
|
|
24313
25945
|
return result.sessionId;
|
|
24314
25946
|
}
|
|
24315
|
-
function
|
|
24316
|
-
|
|
24317
|
-
|
|
24318
|
-
|
|
24319
|
-
|
|
24320
|
-
|
|
24321
|
-
|
|
24322
|
-
|
|
24323
|
-
|
|
24324
|
-
});
|
|
24325
|
-
}
|
|
24326
|
-
function readSingleKey(rl, input) {
|
|
24327
|
-
rl.pause();
|
|
24328
|
-
const wasRaw = input.isRaw === true;
|
|
24329
|
-
if (input.isTTY) input.setRawMode(true);
|
|
24330
|
-
input.resume();
|
|
24331
|
-
return new Promise((resolve) => {
|
|
24332
|
-
const onData = (chunk) => {
|
|
24333
|
-
input.off("data", onData);
|
|
24334
|
-
if (input.isTTY) input.setRawMode(wasRaw);
|
|
24335
|
-
rl.resume();
|
|
24336
|
-
resolve(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
24337
|
-
};
|
|
24338
|
-
input.on("data", onData);
|
|
24339
|
-
});
|
|
25947
|
+
async function askTemporaryLine(question) {
|
|
25948
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
25949
|
+
const input = new ChatInputController(rl, process.stdin);
|
|
25950
|
+
try {
|
|
25951
|
+
return await input.ask(question);
|
|
25952
|
+
} finally {
|
|
25953
|
+
input.close();
|
|
25954
|
+
rl.close();
|
|
25955
|
+
}
|
|
24340
25956
|
}
|
|
24341
25957
|
async function replyToPermission(transport, sessionId, request, answer) {
|
|
24342
25958
|
const approved = answer !== "deny";
|
|
@@ -24347,12 +25963,8 @@ async function replyToPermission(transport, sessionId, request, answer) {
|
|
|
24347
25963
|
...answer === "approve-always" ? { mode: "bypassPermissions" } : {}
|
|
24348
25964
|
});
|
|
24349
25965
|
}
|
|
24350
|
-
async function sendPrompt(context, sessionId, text2) {
|
|
24351
|
-
const content = {
|
|
24352
|
-
role: "user",
|
|
24353
|
-
content: { type: "text", text: text2 },
|
|
24354
|
-
meta: { permissionMode: "default" }
|
|
24355
|
-
};
|
|
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" } };
|
|
24356
25968
|
const key = new Uint8Array(Buffer.from(context.syncCredential.secret, "base64"));
|
|
24357
25969
|
const localId = `saycode-cli-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
24358
25970
|
await context.rest.sendMessage(sessionId, localId, encodeMessageContent(content, key));
|
|
@@ -24401,7 +26013,7 @@ async function resolveSpawnTarget(context, options) {
|
|
|
24401
26013
|
if (!project.workspaceDir) {
|
|
24402
26014
|
throw new CliError(`Project "${project.name}" has no working directory recorded. Use -m and -d instead.`);
|
|
24403
26015
|
}
|
|
24404
|
-
return { machineId: project.machineId, directory: project.workspaceDir };
|
|
26016
|
+
return { machineId: project.machineId, directory: project.workspaceDir, project };
|
|
24405
26017
|
}
|
|
24406
26018
|
if (!options.machine || !options.directory) {
|
|
24407
26019
|
throw new CliError("Starting a session needs either -p <project> or both -m <machine> and -d <directory>.");
|
|
@@ -24410,124 +26022,241 @@ async function resolveSpawnTarget(context, options) {
|
|
|
24410
26022
|
}
|
|
24411
26023
|
async function runChat(context, options, providedTransport) {
|
|
24412
26024
|
const resuming = !!options.sessionId;
|
|
24413
|
-
|
|
24414
|
-
|
|
24415
|
-
|
|
24416
|
-
|
|
24417
|
-
|
|
24418
|
-
|
|
24419
|
-
|
|
24420
|
-
|
|
24421
|
-
|
|
24422
|
-
|
|
24423
|
-
|
|
24424
|
-
|
|
24425
|
-
|
|
24426
|
-
|
|
24427
|
-
);
|
|
24428
|
-
|
|
24429
|
-
|
|
24430
|
-
|
|
24431
|
-
|
|
24432
|
-
|
|
24433
|
-
|
|
24434
|
-
|
|
24435
|
-
|
|
24436
|
-
|
|
24437
|
-
|
|
24438
|
-
|
|
24439
|
-
|
|
24440
|
-
|
|
24441
|
-
|
|
26025
|
+
let optionPrompter = null;
|
|
26026
|
+
let optionInput = null;
|
|
26027
|
+
if (!resuming && options.promptForOptions) {
|
|
26028
|
+
optionPrompter = createInterface({ input: process.stdin, output: process.stdout });
|
|
26029
|
+
optionInput = new ChatInputController(optionPrompter, process.stdin);
|
|
26030
|
+
}
|
|
26031
|
+
const newConversationOptions = resuming ? null : await resolveCliNewConversationOptions(
|
|
26032
|
+
options,
|
|
26033
|
+
optionInput ? (question) => optionInput.ask(question) : void 0
|
|
26034
|
+
).finally(() => {
|
|
26035
|
+
optionInput?.close();
|
|
26036
|
+
optionPrompter?.close();
|
|
26037
|
+
});
|
|
26038
|
+
if (newConversationOptions?.voice && process.stdin.isTTY !== true) {
|
|
26039
|
+
throw new CliError("Live voice capture needs a TTY. Use `--voice-file <audio>` for piped or redirected input.");
|
|
26040
|
+
}
|
|
26041
|
+
let target = resuming ? null : await resolveSpawnTarget(context, options);
|
|
26042
|
+
let createdWorktree = null;
|
|
26043
|
+
if (target && newConversationOptions?.useWorktree) {
|
|
26044
|
+
if (!target.project) throw new CliError("An isolated worktree needs a project target. Use `saycode chat -p <project>`.");
|
|
26045
|
+
if (!context.hasApiAccess) throw new CliError("Creating a worktree needs a Saycode account login. Run `saycode login`.");
|
|
26046
|
+
createdWorktree = await createCliProjectWorktree({
|
|
26047
|
+
projectId: target.project.id,
|
|
26048
|
+
machineId: target.machineId,
|
|
26049
|
+
projectRoot: target.directory,
|
|
26050
|
+
name: resolveWorktreeName({ requested: newConversationOptions.worktreeName }),
|
|
26051
|
+
baseRef: newConversationOptions.worktreeBaseRef
|
|
26052
|
+
}, {
|
|
26053
|
+
create: (projectId, input) => context.rest.createProjectWorktree(projectId, input),
|
|
26054
|
+
remove: (projectId, worktreeId, force) => context.rest.deleteProjectWorktree(projectId, worktreeId, force),
|
|
26055
|
+
confirmSubdir: async (request) => {
|
|
26056
|
+
if (process.stdin.isTTY !== true) return false;
|
|
26057
|
+
const answer = await askTemporaryLine(`Project is ${request.repoRelativeDir} inside ${request.repoRoot}. Create the worktree for the whole repository? [y/N]: `);
|
|
26058
|
+
return answer?.trim().toLowerCase() === "y" || answer?.trim().toLowerCase() === "yes";
|
|
26059
|
+
}
|
|
26060
|
+
});
|
|
26061
|
+
target = { ...target, directory: createdWorktree.path };
|
|
26062
|
+
}
|
|
26063
|
+
let sessionSpawned = false;
|
|
26064
|
+
try {
|
|
26065
|
+
return await withTransport(context, async (transport) => {
|
|
26066
|
+
const sessionId = options.sessionId ?? await spawnSession(
|
|
26067
|
+
transport,
|
|
26068
|
+
target.machineId,
|
|
26069
|
+
target.directory,
|
|
26070
|
+
newConversationOptions?.agent ?? options.agent
|
|
26071
|
+
);
|
|
26072
|
+
sessionSpawned = true;
|
|
26073
|
+
if (!resuming) await recordCliUsage("session_created");
|
|
26074
|
+
if (!resuming && target?.project) {
|
|
24442
26075
|
try {
|
|
24443
|
-
|
|
24444
|
-
} catch {
|
|
24445
|
-
|
|
24446
|
-
}
|
|
24447
|
-
for (const request of pendingPermissionRequestsFromState(decoded)) {
|
|
24448
|
-
if (answered.has(request.requestId)) continue;
|
|
24449
|
-
chat.apply({ type: "permission-requested", request });
|
|
26076
|
+
await context.rest.assignSessionToProject(target.project.id, sessionId, createdWorktree?.id);
|
|
26077
|
+
} catch (error) {
|
|
26078
|
+
context.out.raw(`! Session started, but project assignment failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
24450
26079
|
}
|
|
24451
26080
|
}
|
|
24452
|
-
|
|
24453
|
-
|
|
24454
|
-
|
|
24455
|
-
|
|
24456
|
-
|
|
24457
|
-
|
|
24458
|
-
|
|
24459
|
-
|
|
24460
|
-
|
|
24461
|
-
|
|
24462
|
-
|
|
24463
|
-
|
|
24464
|
-
|
|
26081
|
+
context.out.raw(`Session ${sessionId}`);
|
|
26082
|
+
context.out.raw("During a response, typing is locked. Ctrl+C opens response options.");
|
|
26083
|
+
context.out.raw("At the prompt: /voice to dictate · /detach to leave with the agent still running · /exit to leave.\n");
|
|
26084
|
+
const chat = new ChatSession();
|
|
26085
|
+
const renderer = createSessionEventRenderer(terminalContentWidth(process.stdout.columns));
|
|
26086
|
+
const answered = /* @__PURE__ */ new Set();
|
|
26087
|
+
const ownLocalIds = /* @__PURE__ */ new Set();
|
|
26088
|
+
let pendingFilePaths = [...newConversationOptions?.files ?? []];
|
|
26089
|
+
let pendingVoice = newConversationOptions?.voiceFile ?? (newConversationOptions?.voice ? true : null);
|
|
26090
|
+
const seenThroughSeq = await prerollHistory(
|
|
26091
|
+
context,
|
|
26092
|
+
transport,
|
|
26093
|
+
sessionId,
|
|
26094
|
+
resumeHistoryCount({ resuming, ...options.since !== void 0 ? { since: options.since } : {} }),
|
|
26095
|
+
(content) => chat.apply({ type: "content", content }),
|
|
26096
|
+
renderer
|
|
26097
|
+
);
|
|
26098
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
26099
|
+
const input = new ChatInputController(rl, process.stdin);
|
|
26100
|
+
let menuOpen = false;
|
|
26101
|
+
const onSigint = () => {
|
|
26102
|
+
if (interruptDecision(chat.current) === "exit-cli") {
|
|
26103
|
+
context.out.raw(`
|
|
24465
26104
|
Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
24466
|
-
|
|
24467
|
-
|
|
24468
|
-
}
|
|
24469
|
-
if (menuOpen) return;
|
|
24470
|
-
menuOpen = true;
|
|
24471
|
-
context.out.raw("\n^C [Enter] keep watching · [d] detach, agent keeps working · [s] stop the response");
|
|
24472
|
-
void readSingleKey(rl, process.stdin).then((key) => {
|
|
24473
|
-
menuOpen = false;
|
|
24474
|
-
const choice = interruptMenuChoice(key);
|
|
24475
|
-
if (choice === "detach") {
|
|
24476
|
-
chat.detach();
|
|
24477
|
-
return;
|
|
26105
|
+
rl.close();
|
|
26106
|
+
process.exit(0);
|
|
24478
26107
|
}
|
|
24479
|
-
if (
|
|
24480
|
-
|
|
24481
|
-
|
|
24482
|
-
|
|
26108
|
+
if (menuOpen) return;
|
|
26109
|
+
menuOpen = true;
|
|
26110
|
+
context.out.raw("\n^C [Enter] keep watching · [d] detach, agent keeps working · [s] stop the response");
|
|
26111
|
+
void input.readKey().then((key) => {
|
|
26112
|
+
menuOpen = false;
|
|
26113
|
+
const choice = interruptMenuChoice(key);
|
|
26114
|
+
if (choice === "detach") {
|
|
26115
|
+
chat.detach();
|
|
26116
|
+
return;
|
|
26117
|
+
}
|
|
26118
|
+
if (choice === "stop") {
|
|
26119
|
+
context.out.raw("Stopping the response…");
|
|
26120
|
+
void transport.rpc(sessionId, "abort", {}).catch(() => void 0);
|
|
26121
|
+
return;
|
|
26122
|
+
}
|
|
26123
|
+
context.out.raw("…still working.");
|
|
26124
|
+
});
|
|
26125
|
+
};
|
|
26126
|
+
const stopFollowing = followSession({
|
|
26127
|
+
context,
|
|
26128
|
+
transport,
|
|
26129
|
+
sessionId,
|
|
26130
|
+
seenThroughSeq,
|
|
26131
|
+
ownLocalIds,
|
|
26132
|
+
renderer,
|
|
26133
|
+
onContent: (content) => {
|
|
26134
|
+
printContent(context, content, renderer);
|
|
26135
|
+
chat.apply({ type: "content", content });
|
|
26136
|
+
if (chat.current.phase === "responding") input.lock(onSigint);
|
|
26137
|
+
},
|
|
26138
|
+
onUpdate: (update) => {
|
|
26139
|
+
const cipher = readSessionAgentState(update, sessionId);
|
|
26140
|
+
if (!cipher) return;
|
|
26141
|
+
let decoded;
|
|
26142
|
+
try {
|
|
26143
|
+
decoded = transport.decodePayload(cipher);
|
|
26144
|
+
} catch {
|
|
26145
|
+
return;
|
|
26146
|
+
}
|
|
26147
|
+
for (const request of pendingPermissionRequestsFromState(decoded)) {
|
|
26148
|
+
if (answered.has(request.requestId)) continue;
|
|
26149
|
+
chat.apply({ type: "permission-requested", request });
|
|
26150
|
+
input.cancelQuestion();
|
|
26151
|
+
}
|
|
24483
26152
|
}
|
|
24484
|
-
context.out.raw("…still working.");
|
|
24485
26153
|
});
|
|
24486
|
-
|
|
24487
|
-
|
|
24488
|
-
|
|
24489
|
-
try {
|
|
24490
|
-
for (; ; ) {
|
|
24491
|
-
const line = await ask(rl, "› ", stdinState);
|
|
24492
|
-
if (line === null) break;
|
|
24493
|
-
const prompt = line.trim();
|
|
24494
|
-
if (prompt === "/exit" || prompt === "/quit") break;
|
|
24495
|
-
if (prompt === DETACH_COMMAND) {
|
|
24496
|
-
outcome = "detached";
|
|
24497
|
-
break;
|
|
24498
|
-
}
|
|
24499
|
-
if (prompt === "") continue;
|
|
24500
|
-
ownLocalIds.add(await sendPrompt(context, sessionId, prompt));
|
|
24501
|
-
chat.apply({ type: "message-sent" });
|
|
24502
|
-
context.out.line({ style: "thinking", text: "⏳ working — Ctrl+C for options" });
|
|
26154
|
+
process.on("SIGINT", onSigint);
|
|
26155
|
+
let outcome = "exited";
|
|
26156
|
+
try {
|
|
24503
26157
|
for (; ; ) {
|
|
24504
|
-
|
|
24505
|
-
|
|
24506
|
-
|
|
24507
|
-
|
|
24508
|
-
|
|
24509
|
-
|
|
24510
|
-
|
|
26158
|
+
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
|
+
for (; ; ) {
|
|
26164
|
+
const state = await chat.waitForAttention();
|
|
26165
|
+
if (chat.isDetached) break;
|
|
26166
|
+
if (state.phase !== "awaiting-permission" || !state.pendingPermission) break;
|
|
26167
|
+
const request = state.pendingPermission;
|
|
26168
|
+
context.out.line({ style: "tool", text: chatPresentation(state).status });
|
|
26169
|
+
let answer = parsePermissionAnswer(await input.ask(`${permissionPromptText(request)} `) ?? "n");
|
|
26170
|
+
while (answer === "unknown") {
|
|
26171
|
+
answer = parsePermissionAnswer(await input.ask("Please answer y, n, or a. ") ?? "n");
|
|
26172
|
+
}
|
|
26173
|
+
answered.add(request.requestId);
|
|
26174
|
+
await replyToPermission(transport, sessionId, request, answer);
|
|
26175
|
+
chat.apply({ type: "permission-answered" });
|
|
26176
|
+
context.out.line({ style: "thinking", text: chatPresentation(chat.current).status });
|
|
26177
|
+
input.lock(onSigint);
|
|
26178
|
+
}
|
|
26179
|
+
if (chat.isDetached) {
|
|
26180
|
+
outcome = "detached";
|
|
26181
|
+
break;
|
|
26182
|
+
}
|
|
26183
|
+
continue;
|
|
24511
26184
|
}
|
|
24512
|
-
|
|
24513
|
-
|
|
24514
|
-
|
|
24515
|
-
|
|
24516
|
-
|
|
24517
|
-
|
|
24518
|
-
|
|
26185
|
+
const ready = chatPresentation(chat.current);
|
|
26186
|
+
context.out.line({ style: "status", text: ready.status });
|
|
26187
|
+
let prompt = "";
|
|
26188
|
+
if (pendingVoice) {
|
|
26189
|
+
const voiceFile = typeof pendingVoice === "string" ? pendingVoice : null;
|
|
26190
|
+
pendingVoice = null;
|
|
26191
|
+
try {
|
|
26192
|
+
prompt = await captureCliVoicePrompt(context, voiceFile, async () => {
|
|
26193
|
+
await input.ask("Press Enter to stop recording. ");
|
|
26194
|
+
});
|
|
26195
|
+
context.out.raw(`Transcript: ${prompt}`);
|
|
26196
|
+
} catch (error) {
|
|
26197
|
+
context.out.raw(`Voice input failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
26198
|
+
continue;
|
|
26199
|
+
}
|
|
26200
|
+
} else {
|
|
26201
|
+
const line = await input.ask(ready.prompt);
|
|
26202
|
+
if (line === null) {
|
|
26203
|
+
if (chat.current.phase !== "idle") continue;
|
|
26204
|
+
break;
|
|
26205
|
+
}
|
|
26206
|
+
prompt = line.trim();
|
|
26207
|
+
}
|
|
26208
|
+
if (prompt === "/exit" || prompt === "/quit") break;
|
|
26209
|
+
if (prompt === DETACH_COMMAND) {
|
|
26210
|
+
outcome = "detached";
|
|
26211
|
+
break;
|
|
26212
|
+
}
|
|
26213
|
+
if (prompt === "/voice" || prompt.startsWith("/voice ")) {
|
|
26214
|
+
const voiceFile = prompt.slice("/voice".length).trim() || null;
|
|
26215
|
+
try {
|
|
26216
|
+
prompt = await captureCliVoicePrompt(context, voiceFile, async () => {
|
|
26217
|
+
await input.ask("Press Enter to stop recording. ");
|
|
26218
|
+
});
|
|
26219
|
+
context.out.raw(`Transcript: ${prompt}`);
|
|
26220
|
+
} catch (error) {
|
|
26221
|
+
context.out.raw(`Voice input failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
26222
|
+
continue;
|
|
26223
|
+
}
|
|
26224
|
+
}
|
|
26225
|
+
if (pendingFilePaths.length > 0) {
|
|
26226
|
+
try {
|
|
26227
|
+
const files = [];
|
|
26228
|
+
for (const path of pendingFilePaths) files.push(await loadCliAttachment(path));
|
|
26229
|
+
prompt = await prepareCliMessageWithAttachments(prompt, files, (file) => uploadCliAttachment(transport, target.machineId, target.directory, file));
|
|
26230
|
+
pendingFilePaths = [];
|
|
26231
|
+
} catch (error) {
|
|
26232
|
+
pendingFilePaths = [];
|
|
26233
|
+
context.out.raw(`Attachment failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
26234
|
+
continue;
|
|
26235
|
+
}
|
|
26236
|
+
}
|
|
26237
|
+
if (prompt === "") continue;
|
|
26238
|
+
chat.apply({ type: "message-sent" });
|
|
26239
|
+
input.lock(onSigint);
|
|
26240
|
+
ownLocalIds.add(await sendPrompt(context, sessionId, prompt, newConversationOptions));
|
|
26241
|
+
await recordCliUsage("message_sent");
|
|
24519
26242
|
}
|
|
26243
|
+
} finally {
|
|
26244
|
+
stopFollowing();
|
|
26245
|
+
process.off("SIGINT", onSigint);
|
|
26246
|
+
input.close();
|
|
26247
|
+
rl.close();
|
|
24520
26248
|
}
|
|
24521
|
-
|
|
24522
|
-
stopFollowing();
|
|
24523
|
-
process.off("SIGINT", onSigint);
|
|
24524
|
-
rl.close();
|
|
24525
|
-
}
|
|
24526
|
-
context.out.raw(outcome === "detached" ? `
|
|
26249
|
+
context.out.raw(outcome === "detached" ? `
|
|
24527
26250
|
Detached — the session keeps running. Reattach with \`saycode chat -s ${sessionId}\`.` : `
|
|
24528
26251
|
Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
24529
|
-
|
|
24530
|
-
|
|
26252
|
+
return outcome;
|
|
26253
|
+
}, providedTransport);
|
|
26254
|
+
} catch (error) {
|
|
26255
|
+
if (createdWorktree && !sessionSpawned && target?.project) {
|
|
26256
|
+
await context.rest.deleteProjectWorktree(target.project.id, createdWorktree.id, true).catch(() => void 0);
|
|
26257
|
+
}
|
|
26258
|
+
throw error;
|
|
26259
|
+
}
|
|
24531
26260
|
}
|
|
24532
26261
|
const ESC = "\x1B";
|
|
24533
26262
|
const CTRL_C = "";
|
|
@@ -24999,7 +26728,7 @@ async function runBrowse(context) {
|
|
|
24999
26728
|
try {
|
|
25000
26729
|
if (isNewConversation(chosen.id)) {
|
|
25001
26730
|
const projectId = selectedId(state, "project");
|
|
25002
|
-
await runChat(context, { project: projectId, agent: "claude" }, transports2.get(context.syncCredential));
|
|
26731
|
+
await runChat(context, { project: projectId, agent: "claude", promptForOptions: true }, transports2.get(context.syncCredential));
|
|
25003
26732
|
} else {
|
|
25004
26733
|
await runChat(context, { sessionId: chosen.id, agent: "claude" }, transports2.get(context.syncCredential));
|
|
25005
26734
|
}
|
|
@@ -25060,14 +26789,17 @@ async function contextFor(workspace) {
|
|
|
25060
26789
|
return context;
|
|
25061
26790
|
}
|
|
25062
26791
|
async function main(argv) {
|
|
26792
|
+
await initializeCliUsageAnalytics();
|
|
25063
26793
|
const parsed = parseArgs(argv, process.stdout.isTTY === true && process.stdin.isTTY === true);
|
|
25064
26794
|
const out = createWriter();
|
|
25065
26795
|
switch (parsed.command) {
|
|
25066
26796
|
case "help":
|
|
25067
26797
|
out.raw(HELP_TEXT);
|
|
26798
|
+
await recordCliUsage("command_completed", { action: "help" });
|
|
25068
26799
|
return 0;
|
|
25069
26800
|
case "version":
|
|
25070
26801
|
out.raw(manifest.version);
|
|
26802
|
+
await recordCliUsage("command_completed", { action: "version" });
|
|
25071
26803
|
return 0;
|
|
25072
26804
|
case "error":
|
|
25073
26805
|
process.stderr.write(`saycode: ${parsed.message}
|
|
@@ -25077,36 +26809,47 @@ Run \`saycode --help\` for usage.
|
|
|
25077
26809
|
return 2;
|
|
25078
26810
|
case "login":
|
|
25079
26811
|
await runLogin(out);
|
|
26812
|
+
await recordCliUsage("login_success");
|
|
26813
|
+
await recordCliUsage("command_completed", { action: "login" });
|
|
25080
26814
|
return 0;
|
|
25081
26815
|
case "logout":
|
|
25082
26816
|
runLogout(out);
|
|
26817
|
+
await recordCliUsage("logout");
|
|
26818
|
+
await recordCliUsage("command_completed", { action: "logout" });
|
|
25083
26819
|
return 0;
|
|
25084
26820
|
case "workspaces":
|
|
25085
26821
|
await runWorkspaces(loadContext(), { ...parsed.use ? { use: parsed.use } : {} });
|
|
26822
|
+
await recordCliUsage("command_completed", { action: "workspaces" });
|
|
25086
26823
|
return 0;
|
|
25087
26824
|
case "browse":
|
|
25088
26825
|
await runBrowse(await contextFor(parsed.workspace));
|
|
26826
|
+
await recordCliUsage("command_completed", { action: "browse" });
|
|
25089
26827
|
return 0;
|
|
25090
26828
|
case "machines":
|
|
25091
26829
|
await runMachines(await contextFor(parsed.workspace));
|
|
26830
|
+
await recordCliUsage("command_completed", { action: "machines" });
|
|
25092
26831
|
return 0;
|
|
25093
26832
|
case "projects":
|
|
25094
26833
|
await runProjects(await contextFor(parsed.workspace), {
|
|
25095
26834
|
...parsed.machine ? { machine: parsed.machine } : {}
|
|
25096
26835
|
});
|
|
26836
|
+
await recordCliUsage("command_completed", { action: "projects" });
|
|
25097
26837
|
return 0;
|
|
25098
26838
|
case "ls":
|
|
25099
26839
|
if (parsed.project) {
|
|
25100
26840
|
await runProjectConversations(await contextFor(parsed.workspace), parsed.project, parsed.all);
|
|
26841
|
+
await recordCliUsage("command_completed", { action: "ls" });
|
|
25101
26842
|
return 0;
|
|
25102
26843
|
}
|
|
25103
26844
|
await runLs(await contextFor(parsed.workspace), {
|
|
25104
26845
|
...parsed.machine ? { machine: parsed.machine } : {},
|
|
25105
26846
|
all: parsed.all
|
|
25106
26847
|
});
|
|
26848
|
+
await recordCliUsage("command_completed", { action: "ls" });
|
|
25107
26849
|
return 0;
|
|
25108
26850
|
case "attach":
|
|
25109
26851
|
await runAttach(await contextFor(parsed.workspace), { sessionId: parsed.sessionId, since: parsed.since });
|
|
26852
|
+
await recordCliUsage("command_completed", { action: "attach" });
|
|
25110
26853
|
return 0;
|
|
25111
26854
|
case "chat":
|
|
25112
26855
|
await runChat(await contextFor(parsed.workspace), {
|
|
@@ -25115,21 +26858,37 @@ Run \`saycode --help\` for usage.
|
|
|
25115
26858
|
...parsed.directory ? { directory: parsed.directory } : {},
|
|
25116
26859
|
...parsed.project ? { project: parsed.project } : {},
|
|
25117
26860
|
...parsed.since !== void 0 ? { since: parsed.since } : {},
|
|
26861
|
+
...parsed.model !== void 0 ? { model: parsed.model } : {},
|
|
26862
|
+
...parsed.effort !== void 0 ? { effort: parsed.effort } : {},
|
|
26863
|
+
...parsed.useWorktree ? { useWorktree: true } : {},
|
|
26864
|
+
...parsed.worktreeName !== void 0 ? { worktreeName: parsed.worktreeName } : {},
|
|
26865
|
+
...parsed.worktreeBaseRef !== void 0 ? { worktreeBaseRef: parsed.worktreeBaseRef } : {},
|
|
26866
|
+
...parsed.files ? { files: parsed.files } : {},
|
|
26867
|
+
...parsed.voice ? { voice: true } : {},
|
|
26868
|
+
...parsed.voiceFile !== void 0 ? { voiceFile: parsed.voiceFile } : {},
|
|
26869
|
+
...parsed.promptForOptions ? { promptForOptions: true } : {},
|
|
25118
26870
|
agent: parsed.agent
|
|
25119
26871
|
});
|
|
26872
|
+
await recordCliUsage("command_completed", { action: "chat" });
|
|
25120
26873
|
return 0;
|
|
25121
26874
|
case "watch":
|
|
25122
26875
|
await runWatch(await contextFor(parsed.workspace), { intervalSeconds: parsed.intervalSeconds });
|
|
26876
|
+
await recordCliUsage("command_completed", { action: "watch" });
|
|
25123
26877
|
return 0;
|
|
25124
26878
|
}
|
|
25125
26879
|
}
|
|
25126
|
-
main(process.argv.slice(2)).then(
|
|
26880
|
+
main(process.argv.slice(2)).then(async (code) => {
|
|
26881
|
+
await flushCliUsageAnalytics();
|
|
26882
|
+
process.exit(code);
|
|
26883
|
+
}).catch(async (error) => {
|
|
25127
26884
|
if (error instanceof CliError) {
|
|
25128
26885
|
process.stderr.write(`${error.message}
|
|
25129
26886
|
`);
|
|
26887
|
+
await flushCliUsageAnalytics();
|
|
25130
26888
|
process.exit(2);
|
|
25131
26889
|
}
|
|
25132
26890
|
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
25133
26891
|
`);
|
|
26892
|
+
await flushCliUsageAnalytics();
|
|
25134
26893
|
process.exit(1);
|
|
25135
26894
|
});
|