@buzzni/saycode-cli 0.1.1 → 0.1.3

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