@buzzni/saycode-cli 0.1.2 → 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 +16 -6
- package/index.mjs +1582 -160
- 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);
|
|
@@ -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
|
}
|
|
@@ -24650,6 +24775,1166 @@ async function runProjectConversations(context, query, all) {
|
|
|
24650
24775
|
! Some conversations could not be resolved${payload.warnings.length ? `: ${payload.warnings.join(", ")}` : "."}`);
|
|
24651
24776
|
}
|
|
24652
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
|
+
}
|
|
24653
25938
|
const DETACH_COMMAND = "/detach";
|
|
24654
25939
|
async function spawnSession(transport, machineId, directory, agent) {
|
|
24655
25940
|
const result = await transport.rpc(machineId, "spawn-happy-session", { directory, agent }, 6e4);
|
|
@@ -24659,6 +25944,16 @@ async function spawnSession(transport, machineId, directory, agent) {
|
|
|
24659
25944
|
}
|
|
24660
25945
|
return result.sessionId;
|
|
24661
25946
|
}
|
|
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
|
+
}
|
|
25956
|
+
}
|
|
24662
25957
|
async function replyToPermission(transport, sessionId, request, answer) {
|
|
24663
25958
|
const approved = answer !== "deny";
|
|
24664
25959
|
await transport.rpc(sessionId, "permission", {
|
|
@@ -24668,12 +25963,8 @@ async function replyToPermission(transport, sessionId, request, answer) {
|
|
|
24668
25963
|
...answer === "approve-always" ? { mode: "bypassPermissions" } : {}
|
|
24669
25964
|
});
|
|
24670
25965
|
}
|
|
24671
|
-
async function sendPrompt(context, sessionId, text2) {
|
|
24672
|
-
const content = {
|
|
24673
|
-
role: "user",
|
|
24674
|
-
content: { type: "text", text: text2 },
|
|
24675
|
-
meta: { permissionMode: "default" }
|
|
24676
|
-
};
|
|
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" } };
|
|
24677
25968
|
const key = new Uint8Array(Buffer.from(context.syncCredential.secret, "base64"));
|
|
24678
25969
|
const localId = `saycode-cli-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
24679
25970
|
await context.rest.sendMessage(sessionId, localId, encodeMessageContent(content, key));
|
|
@@ -24722,7 +26013,7 @@ async function resolveSpawnTarget(context, options) {
|
|
|
24722
26013
|
if (!project.workspaceDir) {
|
|
24723
26014
|
throw new CliError(`Project "${project.name}" has no working directory recorded. Use -m and -d instead.`);
|
|
24724
26015
|
}
|
|
24725
|
-
return { machineId: project.machineId, directory: project.workspaceDir };
|
|
26016
|
+
return { machineId: project.machineId, directory: project.workspaceDir, project };
|
|
24726
26017
|
}
|
|
24727
26018
|
if (!options.machine || !options.directory) {
|
|
24728
26019
|
throw new CliError("Starting a session needs either -p <project> or both -m <machine> and -d <directory>.");
|
|
@@ -24731,140 +26022,241 @@ async function resolveSpawnTarget(context, options) {
|
|
|
24731
26022
|
}
|
|
24732
26023
|
async function runChat(context, options, providedTransport) {
|
|
24733
26024
|
const resuming = !!options.sessionId;
|
|
24734
|
-
|
|
24735
|
-
|
|
24736
|
-
|
|
24737
|
-
|
|
24738
|
-
|
|
24739
|
-
|
|
24740
|
-
|
|
24741
|
-
|
|
24742
|
-
|
|
24743
|
-
|
|
24744
|
-
|
|
24745
|
-
|
|
24746
|
-
|
|
24747
|
-
|
|
24748
|
-
|
|
24749
|
-
|
|
24750
|
-
|
|
24751
|
-
|
|
24752
|
-
|
|
24753
|
-
|
|
24754
|
-
|
|
24755
|
-
|
|
24756
|
-
|
|
24757
|
-
|
|
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) {
|
|
26075
|
+
try {
|
|
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)}`);
|
|
26079
|
+
}
|
|
26080
|
+
}
|
|
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(`
|
|
24758
26104
|
Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
24759
|
-
|
|
24760
|
-
|
|
24761
|
-
}
|
|
24762
|
-
if (menuOpen) return;
|
|
24763
|
-
menuOpen = true;
|
|
24764
|
-
context.out.raw("\n^C [Enter] keep watching · [d] detach, agent keeps working · [s] stop the response");
|
|
24765
|
-
void input.readKey().then((key) => {
|
|
24766
|
-
menuOpen = false;
|
|
24767
|
-
const choice = interruptMenuChoice(key);
|
|
24768
|
-
if (choice === "detach") {
|
|
24769
|
-
chat.detach();
|
|
24770
|
-
return;
|
|
26105
|
+
rl.close();
|
|
26106
|
+
process.exit(0);
|
|
24771
26107
|
}
|
|
24772
|
-
if (
|
|
24773
|
-
|
|
24774
|
-
|
|
24775
|
-
|
|
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
|
+
}
|
|
24776
26152
|
}
|
|
24777
|
-
context.out.raw("…still working.");
|
|
24778
26153
|
});
|
|
24779
|
-
|
|
24780
|
-
|
|
24781
|
-
|
|
24782
|
-
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
|
|
24796
|
-
|
|
24797
|
-
|
|
24798
|
-
|
|
24799
|
-
|
|
24800
|
-
|
|
24801
|
-
|
|
24802
|
-
|
|
24803
|
-
|
|
24804
|
-
|
|
24805
|
-
|
|
24806
|
-
|
|
24807
|
-
|
|
24808
|
-
|
|
24809
|
-
let outcome = "exited";
|
|
24810
|
-
try {
|
|
24811
|
-
for (; ; ) {
|
|
24812
|
-
if (chat.current.phase !== "idle") {
|
|
24813
|
-
if (chat.current.phase === "responding") {
|
|
24814
|
-
context.out.line({ style: "thinking", text: chatPresentation(chat.current).status });
|
|
24815
|
-
input.lock(onSigint);
|
|
26154
|
+
process.on("SIGINT", onSigint);
|
|
26155
|
+
let outcome = "exited";
|
|
26156
|
+
try {
|
|
26157
|
+
for (; ; ) {
|
|
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;
|
|
24816
26184
|
}
|
|
24817
|
-
|
|
24818
|
-
|
|
24819
|
-
|
|
24820
|
-
|
|
24821
|
-
const
|
|
24822
|
-
|
|
24823
|
-
|
|
24824
|
-
|
|
24825
|
-
|
|
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;
|
|
24826
26199
|
}
|
|
24827
|
-
|
|
24828
|
-
|
|
24829
|
-
|
|
24830
|
-
|
|
24831
|
-
|
|
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();
|
|
24832
26207
|
}
|
|
24833
|
-
if (
|
|
26208
|
+
if (prompt === "/exit" || prompt === "/quit") break;
|
|
26209
|
+
if (prompt === DETACH_COMMAND) {
|
|
24834
26210
|
outcome = "detached";
|
|
24835
26211
|
break;
|
|
24836
26212
|
}
|
|
24837
|
-
|
|
24838
|
-
|
|
24839
|
-
|
|
24840
|
-
|
|
24841
|
-
|
|
24842
|
-
|
|
24843
|
-
|
|
24844
|
-
|
|
24845
|
-
|
|
24846
|
-
|
|
24847
|
-
|
|
24848
|
-
|
|
24849
|
-
|
|
24850
|
-
|
|
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");
|
|
24851
26242
|
}
|
|
24852
|
-
|
|
24853
|
-
|
|
24854
|
-
|
|
24855
|
-
|
|
26243
|
+
} finally {
|
|
26244
|
+
stopFollowing();
|
|
26245
|
+
process.off("SIGINT", onSigint);
|
|
26246
|
+
input.close();
|
|
26247
|
+
rl.close();
|
|
24856
26248
|
}
|
|
24857
|
-
|
|
24858
|
-
stopFollowing();
|
|
24859
|
-
process.off("SIGINT", onSigint);
|
|
24860
|
-
input.close();
|
|
24861
|
-
rl.close();
|
|
24862
|
-
}
|
|
24863
|
-
context.out.raw(outcome === "detached" ? `
|
|
26249
|
+
context.out.raw(outcome === "detached" ? `
|
|
24864
26250
|
Detached — the session keeps running. Reattach with \`saycode chat -s ${sessionId}\`.` : `
|
|
24865
26251
|
Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
24866
|
-
|
|
24867
|
-
|
|
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
|
+
}
|
|
24868
26260
|
}
|
|
24869
26261
|
const ESC = "\x1B";
|
|
24870
26262
|
const CTRL_C = "";
|
|
@@ -25336,7 +26728,7 @@ async function runBrowse(context) {
|
|
|
25336
26728
|
try {
|
|
25337
26729
|
if (isNewConversation(chosen.id)) {
|
|
25338
26730
|
const projectId = selectedId(state, "project");
|
|
25339
|
-
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));
|
|
25340
26732
|
} else {
|
|
25341
26733
|
await runChat(context, { sessionId: chosen.id, agent: "claude" }, transports2.get(context.syncCredential));
|
|
25342
26734
|
}
|
|
@@ -25397,14 +26789,17 @@ async function contextFor(workspace) {
|
|
|
25397
26789
|
return context;
|
|
25398
26790
|
}
|
|
25399
26791
|
async function main(argv) {
|
|
26792
|
+
await initializeCliUsageAnalytics();
|
|
25400
26793
|
const parsed = parseArgs(argv, process.stdout.isTTY === true && process.stdin.isTTY === true);
|
|
25401
26794
|
const out = createWriter();
|
|
25402
26795
|
switch (parsed.command) {
|
|
25403
26796
|
case "help":
|
|
25404
26797
|
out.raw(HELP_TEXT);
|
|
26798
|
+
await recordCliUsage("command_completed", { action: "help" });
|
|
25405
26799
|
return 0;
|
|
25406
26800
|
case "version":
|
|
25407
26801
|
out.raw(manifest.version);
|
|
26802
|
+
await recordCliUsage("command_completed", { action: "version" });
|
|
25408
26803
|
return 0;
|
|
25409
26804
|
case "error":
|
|
25410
26805
|
process.stderr.write(`saycode: ${parsed.message}
|
|
@@ -25414,36 +26809,47 @@ Run \`saycode --help\` for usage.
|
|
|
25414
26809
|
return 2;
|
|
25415
26810
|
case "login":
|
|
25416
26811
|
await runLogin(out);
|
|
26812
|
+
await recordCliUsage("login_success");
|
|
26813
|
+
await recordCliUsage("command_completed", { action: "login" });
|
|
25417
26814
|
return 0;
|
|
25418
26815
|
case "logout":
|
|
25419
26816
|
runLogout(out);
|
|
26817
|
+
await recordCliUsage("logout");
|
|
26818
|
+
await recordCliUsage("command_completed", { action: "logout" });
|
|
25420
26819
|
return 0;
|
|
25421
26820
|
case "workspaces":
|
|
25422
26821
|
await runWorkspaces(loadContext(), { ...parsed.use ? { use: parsed.use } : {} });
|
|
26822
|
+
await recordCliUsage("command_completed", { action: "workspaces" });
|
|
25423
26823
|
return 0;
|
|
25424
26824
|
case "browse":
|
|
25425
26825
|
await runBrowse(await contextFor(parsed.workspace));
|
|
26826
|
+
await recordCliUsage("command_completed", { action: "browse" });
|
|
25426
26827
|
return 0;
|
|
25427
26828
|
case "machines":
|
|
25428
26829
|
await runMachines(await contextFor(parsed.workspace));
|
|
26830
|
+
await recordCliUsage("command_completed", { action: "machines" });
|
|
25429
26831
|
return 0;
|
|
25430
26832
|
case "projects":
|
|
25431
26833
|
await runProjects(await contextFor(parsed.workspace), {
|
|
25432
26834
|
...parsed.machine ? { machine: parsed.machine } : {}
|
|
25433
26835
|
});
|
|
26836
|
+
await recordCliUsage("command_completed", { action: "projects" });
|
|
25434
26837
|
return 0;
|
|
25435
26838
|
case "ls":
|
|
25436
26839
|
if (parsed.project) {
|
|
25437
26840
|
await runProjectConversations(await contextFor(parsed.workspace), parsed.project, parsed.all);
|
|
26841
|
+
await recordCliUsage("command_completed", { action: "ls" });
|
|
25438
26842
|
return 0;
|
|
25439
26843
|
}
|
|
25440
26844
|
await runLs(await contextFor(parsed.workspace), {
|
|
25441
26845
|
...parsed.machine ? { machine: parsed.machine } : {},
|
|
25442
26846
|
all: parsed.all
|
|
25443
26847
|
});
|
|
26848
|
+
await recordCliUsage("command_completed", { action: "ls" });
|
|
25444
26849
|
return 0;
|
|
25445
26850
|
case "attach":
|
|
25446
26851
|
await runAttach(await contextFor(parsed.workspace), { sessionId: parsed.sessionId, since: parsed.since });
|
|
26852
|
+
await recordCliUsage("command_completed", { action: "attach" });
|
|
25447
26853
|
return 0;
|
|
25448
26854
|
case "chat":
|
|
25449
26855
|
await runChat(await contextFor(parsed.workspace), {
|
|
@@ -25452,21 +26858,37 @@ Run \`saycode --help\` for usage.
|
|
|
25452
26858
|
...parsed.directory ? { directory: parsed.directory } : {},
|
|
25453
26859
|
...parsed.project ? { project: parsed.project } : {},
|
|
25454
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 } : {},
|
|
25455
26870
|
agent: parsed.agent
|
|
25456
26871
|
});
|
|
26872
|
+
await recordCliUsage("command_completed", { action: "chat" });
|
|
25457
26873
|
return 0;
|
|
25458
26874
|
case "watch":
|
|
25459
26875
|
await runWatch(await contextFor(parsed.workspace), { intervalSeconds: parsed.intervalSeconds });
|
|
26876
|
+
await recordCliUsage("command_completed", { action: "watch" });
|
|
25460
26877
|
return 0;
|
|
25461
26878
|
}
|
|
25462
26879
|
}
|
|
25463
|
-
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) => {
|
|
25464
26884
|
if (error instanceof CliError) {
|
|
25465
26885
|
process.stderr.write(`${error.message}
|
|
25466
26886
|
`);
|
|
26887
|
+
await flushCliUsageAnalytics();
|
|
25467
26888
|
process.exit(2);
|
|
25468
26889
|
}
|
|
25469
26890
|
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
25470
26891
|
`);
|
|
26892
|
+
await flushCliUsageAnalytics();
|
|
25471
26893
|
process.exit(1);
|
|
25472
26894
|
});
|