@buzzni/saycode-cli 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +16 -6
  2. package/index.mjs +1582 -160
  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.2";
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);
@@ -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
  }
@@ -24650,6 +24775,1166 @@ async function runProjectConversations(context, query, all) {
24650
24775
  ! Some conversations could not be resolved${payload.warnings.length ? `: ${payload.warnings.join(", ")}` : "."}`);
24651
24776
  }
24652
24777
  }
24778
+ const claudeEffortOptions = [
24779
+ { key: "low", label: "Low" },
24780
+ { key: "medium", label: "Medium" },
24781
+ { key: "high", label: "High" },
24782
+ { key: "max", label: "Max" }
24783
+ ];
24784
+ const claudeFableEffortOptions = [
24785
+ { key: "low", label: "Low" },
24786
+ { key: "medium", label: "Medium" },
24787
+ { key: "high", label: "High" },
24788
+ { key: "xhigh", label: "Xhigh" },
24789
+ { key: "max", label: "Max" }
24790
+ ];
24791
+ const codexEffortOptions = [
24792
+ { key: "low", label: "Low" },
24793
+ { key: "medium", label: "Medium" },
24794
+ { key: "high", label: "High" },
24795
+ { key: "xhigh", label: "Xhigh" }
24796
+ ];
24797
+ const codexMaxEffortOptions = [
24798
+ ...codexEffortOptions,
24799
+ { key: "max", label: "Max" }
24800
+ ];
24801
+ const codexUltraEffortOptions = [
24802
+ ...codexMaxEffortOptions,
24803
+ { key: "ultra", label: "Ultra" }
24804
+ ];
24805
+ function getEffortOptionsForAgent(agent, model) {
24806
+ if (model === "default") return [];
24807
+ if (agent === "codex") {
24808
+ if (model === "gpt-5.6-sol" || model === "gpt-5.6-terra") return codexUltraEffortOptions;
24809
+ if (model === "gpt-5.6-luna") return codexMaxEffortOptions;
24810
+ return codexEffortOptions;
24811
+ }
24812
+ if (agent === "gemini" || agent === "opencode") return [];
24813
+ if (model === "claude-fable-5") return claudeFableEffortOptions;
24814
+ return claudeEffortOptions;
24815
+ }
24816
+ function defaultEffortForAgent(agent, model) {
24817
+ const options = getEffortOptionsForAgent(agent, model);
24818
+ if (options.length === 0) return null;
24819
+ if (agent === "codex") {
24820
+ if (model === "gpt-5.6-sol") return "low";
24821
+ if (model === "gpt-5.6-terra" || model === "gpt-5.6-luna") return "medium";
24822
+ }
24823
+ return options.some((option) => option.key === "high") ? "high" : options[options.length - 1].key;
24824
+ }
24825
+ const newSessionAgents = [
24826
+ { id: "claude", label: "Claude", provider: "Anthropic" },
24827
+ { id: "codex", label: "Codex", provider: "OpenAI" },
24828
+ { id: "opencode", label: "opencode", provider: "opencode" }
24829
+ ];
24830
+ const claudeModelOptions = [
24831
+ { key: "default", label: "Default" },
24832
+ { key: "claude-fable-5", label: "Fable 5" },
24833
+ { key: "claude-opus-5", label: "Opus 5" },
24834
+ { key: "claude-sonnet-5", label: "Sonnet 5" },
24835
+ { key: "claude-haiku-4-5", label: "Haiku 4.5" }
24836
+ ];
24837
+ const codexModelOptions = [
24838
+ { key: "default", label: "Default" },
24839
+ { key: "gpt-5.6-sol", label: "GPT-5.6 Sol" },
24840
+ { key: "gpt-5.6-terra", label: "GPT-5.6 Terra" },
24841
+ { key: "gpt-5.6-luna", label: "GPT-5.6 Luna" },
24842
+ { key: "gpt-5.5", label: "gpt-5.5" },
24843
+ { key: "gpt-5.4", label: "gpt-5.4" },
24844
+ { key: "gpt-5.3-codex", label: "gpt-5.3-codex" },
24845
+ { key: "gpt-5.2-codex", label: "gpt-5.2-codex" },
24846
+ { key: "gpt-5.1-codex-max", label: "gpt-5.1-codex-max" },
24847
+ { key: "gpt-5.2", label: "gpt-5.2" },
24848
+ { key: "gpt-5.1-codex-mini", label: "gpt-5.1-codex-mini" }
24849
+ ];
24850
+ const geminiModelOptions = [
24851
+ { key: "gemini-3.1-pro-preview", label: "gemini 3.1 pro" },
24852
+ { key: "gemini-3-flash-preview", label: "gemini 3 flash" },
24853
+ { key: "gemini-3.1-flash-lite-preview", label: "gemini 3.1 flash lite" },
24854
+ { key: "gemini-2.5-pro", label: "gemini 2.5 pro" },
24855
+ { key: "gemini-2.5-flash", label: "gemini 2.5 flash" },
24856
+ { key: "gemini-2.5-flash-lite", label: "gemini 2.5 flash lite" }
24857
+ ];
24858
+ const opencodeModelOptions = [
24859
+ { key: "default", label: "Default" }
24860
+ ];
24861
+ function getModelsForAgent(agent) {
24862
+ if (agent === "codex") return codexModelOptions;
24863
+ if (agent === "gemini") return geminiModelOptions;
24864
+ if (agent === "opencode") return opencodeModelOptions;
24865
+ return claudeModelOptions;
24866
+ }
24867
+ function normalizeModelForAgent(agent, model) {
24868
+ if (agent === "opencode") return model || "default";
24869
+ const models = getModelsForAgent(agent);
24870
+ return models.some((option) => option.key === model) ? model : models[0]?.key ?? "default";
24871
+ }
24872
+ function normalizeModelSelectionOptions(agent, current, patch) {
24873
+ const model = normalizeModelForAgent(agent, patch.model ?? current.model);
24874
+ const efforts = getEffortOptionsForAgent(agent, model);
24875
+ const effort = patch.model !== void 0 ? defaultEffortForAgent(agent, model) : patch.effort !== void 0 && efforts.some((option) => option.key === patch.effort) ? patch.effort : current.effort && efforts.some((option) => option.key === current.effort) ? current.effort : defaultEffortForAgent(agent, model);
24876
+ return { model, effort };
24877
+ }
24878
+ function messageModelOptionsFromSelection(agent, options, availableModels) {
24879
+ if (agent === "opencode") {
24880
+ const model = normalizeOpencodeModel(options.model, availableModels);
24881
+ return { model: model !== "default" ? model : void 0, effort: null };
24882
+ }
24883
+ const normalized = normalizeModelSelectionOptions(agent, options, {});
24884
+ return {
24885
+ model: normalized.model !== "default" ? normalized.model : void 0,
24886
+ effort: normalized.effort
24887
+ };
24888
+ }
24889
+ const WORKTREE_NAME_ADJECTIVES = [
24890
+ "bright",
24891
+ "calm",
24892
+ "eager",
24893
+ "brave",
24894
+ "swift",
24895
+ "quiet",
24896
+ "clever",
24897
+ "lucky",
24898
+ "gentle",
24899
+ "bold",
24900
+ "fresh",
24901
+ "keen",
24902
+ "merry",
24903
+ "noble",
24904
+ "proud",
24905
+ "witty"
24906
+ ];
24907
+ const WORKTREE_NAME_ANIMALS = [
24908
+ "fox",
24909
+ "otter",
24910
+ "hawk",
24911
+ "lynx",
24912
+ "wolf",
24913
+ "crane",
24914
+ "koala",
24915
+ "panda",
24916
+ "tiger",
24917
+ "whale",
24918
+ "finch",
24919
+ "gecko",
24920
+ "moose",
24921
+ "raven",
24922
+ "seal",
24923
+ "newt"
24924
+ ];
24925
+ function sanitizeWorktreeName(raw) {
24926
+ return raw.trim().toLowerCase().replace(/[^a-z0-9/_-]+/g, "-").replace(/\/{2,}/g, "/").replace(/-{2,}/g, "-").replace(/^[-/]+|[-/]+$/g, "").replace(/\.+/g, ".").slice(0, 60);
24927
+ }
24928
+ function generateWorktreeName(random = Math.random) {
24929
+ const pick2 = (items) => items[Math.floor(random() * items.length) % items.length];
24930
+ const suffix = Math.floor(random() * 36 ** 4).toString(36).padStart(4, "0").slice(-4);
24931
+ return `${pick2(WORKTREE_NAME_ADJECTIVES)}-${pick2(WORKTREE_NAME_ANIMALS)}-${suffix}`;
24932
+ }
24933
+ function resolveWorktreeName(input) {
24934
+ const sanitized = input.requested ? sanitizeWorktreeName(input.requested) : "";
24935
+ if (sanitized) return sanitized;
24936
+ return generateWorktreeName(input.random);
24937
+ }
24938
+ const MANAGED_WORKTREE_SEGMENT = "/.aplus/worktrees/";
24939
+ const MAX_MANAGED_WORKTREE_DEPTH = 2;
24940
+ function isWorktreePathWithinProjectRoot(projectRoot, worktreePath) {
24941
+ const root = projectRoot.trim().replace(/\/+$/, "");
24942
+ const path = worktreePath.trim().replace(/\/+$/, "");
24943
+ if (!root || !path) return false;
24944
+ if (!path.startsWith(`${root}${MANAGED_WORKTREE_SEGMENT}`)) return false;
24945
+ const segments = path.slice(root.length + MANAGED_WORKTREE_SEGMENT.length).split("/");
24946
+ if (segments.length < 1 || segments.length > MAX_MANAGED_WORKTREE_DEPTH) return false;
24947
+ return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
24948
+ }
24949
+ const DEFAULT_CLI_NEW_CONVERSATION_OPTIONS = {
24950
+ agent: "claude",
24951
+ model: "default",
24952
+ effort: null,
24953
+ useWorktree: false,
24954
+ worktreeName: "",
24955
+ worktreeBaseRef: "HEAD",
24956
+ files: [],
24957
+ voice: false,
24958
+ voiceFile: null
24959
+ };
24960
+ function normalizeNewConversationOptions(current, patch) {
24961
+ const agent = patch.agent ?? current.agent;
24962
+ const agentChanged = patch.agent !== void 0 && patch.agent !== current.agent;
24963
+ const currentSelection = agentChanged ? { model: "default", effort: null } : { model: current.model, effort: current.effort };
24964
+ const modelSelection = normalizeModelSelectionOptions(agent, currentSelection, {
24965
+ ...patch.model !== void 0 ? { model: patch.model } : {}
24966
+ });
24967
+ const efforts = getEffortOptionsForAgent(agent, modelSelection.model);
24968
+ const requestedEffort = patch.effort !== void 0 ? patch.effort : modelSelection.effort;
24969
+ const effort = requestedEffort && efforts.some((option) => option.key === requestedEffort) ? requestedEffort : defaultEffortForAgent(agent, modelSelection.model);
24970
+ return {
24971
+ agent,
24972
+ model: modelSelection.model,
24973
+ effort,
24974
+ useWorktree: patch.useWorktree ?? current.useWorktree,
24975
+ worktreeName: patch.worktreeName !== void 0 ? sanitizeWorktreeName(patch.worktreeName) : current.worktreeName,
24976
+ worktreeBaseRef: (patch.worktreeBaseRef ?? current.worktreeBaseRef).trim() || "HEAD",
24977
+ files: patch.files ? [...patch.files] : [...current.files],
24978
+ voice: patch.voice ?? current.voice,
24979
+ voiceFile: patch.voiceFile !== void 0 ? patch.voiceFile : current.voiceFile
24980
+ };
24981
+ }
24982
+ function buildCliUserMessage(text2, options) {
24983
+ const modelOptions = messageModelOptionsFromSelection(options.agent, options);
24984
+ return {
24985
+ role: "user",
24986
+ content: { type: "text", text: text2 },
24987
+ meta: {
24988
+ permissionMode: "default",
24989
+ ...modelOptions.model ? { model: modelOptions.model } : {},
24990
+ ...modelOptions.effort ? { effort: modelOptions.effort } : {}
24991
+ }
24992
+ };
24993
+ }
24994
+ function modelChoices(agent) {
24995
+ return getModelsForAgent(agent);
24996
+ }
24997
+ function effortChoices(agent, model) {
24998
+ return getEffortOptionsForAgent(agent, normalizeModelForAgent(agent, model));
24999
+ }
25000
+ function worktreeTargetMismatch(record2, expected) {
25001
+ if (record2.projectId !== expected.projectId) return "project";
25002
+ if (record2.machineId !== expected.machineId) return "machine";
25003
+ if (expected.path !== void 0 && record2.path !== expected.path) return "path";
25004
+ return null;
25005
+ }
25006
+ async function createWorktreeOnMachine(input) {
25007
+ const record2 = await input.client.createProjectWorktree(input.projectId, {
25008
+ name: input.name,
25009
+ baseRef: input.baseRef,
25010
+ machineId: input.machineId,
25011
+ ...input.confirmRepoRoot ? { confirmRepoRoot: input.confirmRepoRoot } : {}
25012
+ });
25013
+ const targetMismatch = worktreeTargetMismatch(record2, {
25014
+ projectId: input.projectId,
25015
+ machineId: input.machineId
25016
+ });
25017
+ const allowedRoot = input.confirmRepoRoot ?? input.repoCwd;
25018
+ const pathMatches = isWorktreePathWithinProjectRoot(allowedRoot, record2.path);
25019
+ if (!targetMismatch && pathMatches) return record2;
25020
+ await input.client.deleteProjectWorktree(input.projectId, record2.id, true).catch(() => void 0);
25021
+ throw new Error(!pathMatches && input.pathMismatchMessage ? input.pathMismatchMessage(record2.path) : input.targetMismatchMessage);
25022
+ }
25023
+ const WORKTREE_SUBDIR_CONFIRM_CODE = "worktree-subdir-confirm-required";
25024
+ function trimmedString(value2) {
25025
+ if (typeof value2 !== "string") return null;
25026
+ const normalized = value2.trim();
25027
+ return normalized ? normalized : null;
25028
+ }
25029
+ function withoutTrailingSlash(path) {
25030
+ return path.replace(/\/+$/, "");
25031
+ }
25032
+ function isStrictAncestorDir(parent, child) {
25033
+ const base = withoutTrailingSlash(parent);
25034
+ const target = withoutTrailingSlash(child);
25035
+ if (!base || !target || base === target) return false;
25036
+ return target.startsWith(`${base}/`);
25037
+ }
25038
+ function isSafeRelativeDir(dir) {
25039
+ if (dir.startsWith("/") || dir.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(dir)) return false;
25040
+ return !dir.split(/[\\/]+/).some((segment) => segment === "..");
25041
+ }
25042
+ function parseWorktreeSubdirConfirmRequest(status, body, projectRoot) {
25043
+ if (status !== 409) return null;
25044
+ let parsed;
25045
+ try {
25046
+ parsed = JSON.parse(body);
25047
+ } catch {
25048
+ return null;
25049
+ }
25050
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
25051
+ const value2 = parsed;
25052
+ if (value2.code !== WORKTREE_SUBDIR_CONFIRM_CODE) return null;
25053
+ const repoRoot = trimmedString(value2.repoRoot);
25054
+ const workspaceDir = trimmedString(value2.workspaceDir);
25055
+ const repoRelativeDir = trimmedString(value2.repoRelativeDir);
25056
+ const root = trimmedString(projectRoot);
25057
+ if (!repoRoot || !workspaceDir || !repoRelativeDir || !root) return null;
25058
+ if (!isStrictAncestorDir(repoRoot, root)) return null;
25059
+ if (!isSafeRelativeDir(repoRelativeDir)) return null;
25060
+ return { repoRoot, workspaceDir, repoRelativeDir };
25061
+ }
25062
+ async function createCliProjectWorktree(input, deps) {
25063
+ const createInput = {
25064
+ client: {
25065
+ createProjectWorktree: deps.create,
25066
+ deleteProjectWorktree: deps.remove
25067
+ },
25068
+ projectId: input.projectId,
25069
+ machineId: input.machineId,
25070
+ repoCwd: input.projectRoot,
25071
+ name: input.name,
25072
+ baseRef: input.baseRef,
25073
+ targetMismatchMessage: "The worktree does not belong to the selected project and machine.",
25074
+ pathMismatchMessage: () => "The daemon returned a worktree outside the safe project boundary."
25075
+ };
25076
+ try {
25077
+ return await createWorktreeOnMachine(createInput);
25078
+ } catch (error) {
25079
+ if (!(error instanceof CliHttpError)) throw error;
25080
+ const request = parseWorktreeSubdirConfirmRequest(error.status, error.body, input.projectRoot);
25081
+ if (!request) throw error;
25082
+ if (!deps.confirmSubdir || !await deps.confirmSubdir(request)) {
25083
+ throw new Error("Worktree creation was cancelled because the project is a repository subdirectory.");
25084
+ }
25085
+ return createWorktreeOnMachine({ ...createInput, confirmRepoRoot: request.repoRoot });
25086
+ }
25087
+ }
25088
+ async function sha256HexFromBase64(b64Content) {
25089
+ if (typeof crypto === "undefined" || !crypto.subtle || typeof crypto.subtle.digest !== "function") {
25090
+ throw new Error(translateCurrent("remoteFile.sha256Failed"));
25091
+ }
25092
+ const bytes = new Uint8Array(Buffer$1.from(b64Content, "base64"));
25093
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
25094
+ const view = new Uint8Array(digest);
25095
+ let hex = "";
25096
+ for (let i = 0; i < view.length; i++) hex += view[i].toString(16).padStart(2, "0");
25097
+ return hex;
25098
+ }
25099
+ const MEGABYTE = 1024 * 1024;
25100
+ const MAX_IMAGE_ATTACHMENT_BYTES = 25 * MEGABYTE;
25101
+ const MAX_OTHER_ATTACHMENT_BYTES = 100 * MEGABYTE;
25102
+ function attachmentReferenceLabel(file) {
25103
+ return translateCurrent(file.type.startsWith("image/") ? "upload.kindImage" : "upload.kindFile");
25104
+ }
25105
+ function arrayBufferToBase64(buffer) {
25106
+ return encodeBase64(new Uint8Array(buffer));
25107
+ }
25108
+ function extensionFor(file) {
25109
+ const fromName = file.name.match(/\.([A-Za-z0-9]{1,12})$/)?.[1];
25110
+ if (fromName) return fromName.toLowerCase();
25111
+ const fromType = file.type.match(/^image\/([A-Za-z0-9.+-]+)$/)?.[1];
25112
+ if (!fromType) return "bin";
25113
+ return fromType.replace(/[^A-Za-z0-9]/g, "").toLowerCase() || "bin";
25114
+ }
25115
+ function safeFileName(file) {
25116
+ const name = file.name.split(/[\\/]/).pop()?.trim() ?? "";
25117
+ const safe = name.replace(/[^A-Za-z0-9._ -]/g, "_").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
25118
+ if (safe) return safe.slice(0, 120);
25119
+ return `attachment.${extensionFor(file)}`;
25120
+ }
25121
+ function chatUploadRelPath(file, id) {
25122
+ if (file.type.startsWith("image/")) return `.aplus/uploads/${id}.${extensionFor(file)}`;
25123
+ return `.aplus/uploads/${id}-${safeFileName(file)}`;
25124
+ }
25125
+ async function uploadChatFileToMachineCore(file, workspaceRoot, deps) {
25126
+ const root = workspaceRoot.trim().replace(/\/+$/, "");
25127
+ if (!root) throw new Error(translateCurrent("upload.noWorkspaceRoot"));
25128
+ const id = (deps.now?.() ?? Date.now()).toString(36) + (deps.random?.() ?? Math.random()).toString(36).slice(2, 8);
25129
+ const absPath = `${root}/${chatUploadRelPath(file, id)}`;
25130
+ const contentBase64 = arrayBufferToBase64(await file.arrayBuffer());
25131
+ let expectedHash = null;
25132
+ const probe = await deps.readFile(absPath);
25133
+ if (probe && probe.success && typeof probe.content === "string") {
25134
+ expectedHash = await sha256HexFromBase64(probe.content);
25135
+ }
25136
+ const res = await deps.writeFile({ path: absPath, content: contentBase64, expectedHash });
25137
+ if (!res || res.success === false) {
25138
+ throw new Error(res?.error || translateCurrent("upload.failed"));
25139
+ }
25140
+ return absPath;
25141
+ }
25142
+ async function preparePersonalChatMessageWithAttachments(input, deps) {
25143
+ const text2 = input.text.trim();
25144
+ if (input.files.length === 0) return text2;
25145
+ const references = [];
25146
+ for (const file of input.files) {
25147
+ const path = await deps.uploadFile(file);
25148
+ references.push(`${attachmentReferenceLabel(file)}: ${path}`);
25149
+ }
25150
+ const fileReferences = references.join("\n");
25151
+ return text2 ? `${text2}
25152
+
25153
+ ${fileReferences}` : `${translateCurrent("composer.attachPromptPrefix")}
25154
+ ${fileReferences}`;
25155
+ }
25156
+ const MIME_BY_EXTENSION = {
25157
+ ".gif": "image/gif",
25158
+ ".jpeg": "image/jpeg",
25159
+ ".jpg": "image/jpeg",
25160
+ ".png": "image/png",
25161
+ ".webp": "image/webp",
25162
+ ".pdf": "application/pdf",
25163
+ ".json": "application/json",
25164
+ ".md": "text/markdown",
25165
+ ".txt": "text/plain"
25166
+ };
25167
+ function mimeTypeForPath(path) {
25168
+ return MIME_BY_EXTENSION[extname(path).toLowerCase()] ?? "application/octet-stream";
25169
+ }
25170
+ async function loadCliAttachment(path, deps = { stat, readFile }) {
25171
+ const info = await deps.stat(path);
25172
+ const mimeType = mimeTypeForPath(path);
25173
+ const limit = mimeType.startsWith("image/") ? MAX_IMAGE_ATTACHMENT_BYTES : MAX_OTHER_ATTACHMENT_BYTES;
25174
+ if (info.size > limit) {
25175
+ throw new Error(`${basename(path)} is too large (maximum ${Math.round(limit / 1024 / 1024)} MB).`);
25176
+ }
25177
+ const bytes = await deps.readFile(path);
25178
+ return new File([new Uint8Array(bytes)], basename(path), { type: mimeType });
25179
+ }
25180
+ function uploadCliAttachment(transport, machineId, workspaceRoot, file, deterministic = {}) {
25181
+ return uploadChatFileToMachineCore(file, workspaceRoot, {
25182
+ readFile: (path) => transport.rpc(machineId, "readFile", { path }),
25183
+ writeFile: (params) => transport.rpc(machineId, "writeFile", params),
25184
+ ...deterministic
25185
+ });
25186
+ }
25187
+ function prepareCliMessageWithAttachments(text2, files, uploadFile) {
25188
+ return preparePersonalChatMessageWithAttachments({ text: text2, files }, { uploadFile });
25189
+ }
25190
+ function choiceIndex(answer, length, currentIndex) {
25191
+ const trimmed = answer?.trim() ?? "";
25192
+ if (!trimmed) return Math.max(0, currentIndex);
25193
+ const number = Number.parseInt(trimmed, 10);
25194
+ return Number.isInteger(number) && number >= 1 && number <= length ? number - 1 : Math.max(0, currentIndex);
25195
+ }
25196
+ function choiceQuestion(label, choices, currentIndex) {
25197
+ const rendered = choices.map((choice, index) => `${index + 1}) ${choice.label}`).join(" ");
25198
+ return `${label} [${rendered}] (${currentIndex + 1}): `;
25199
+ }
25200
+ function yes(answer, fallback) {
25201
+ const normalized = answer?.trim().toLowerCase() ?? "";
25202
+ if (!normalized) return fallback;
25203
+ return normalized === "y" || normalized === "yes";
25204
+ }
25205
+ async function promptNewConversationOptions(initial, ask) {
25206
+ let options = initial;
25207
+ const agents = options.agent === "gemini" ? [{ id: "gemini", label: "Gemini", provider: "Google" }, ...newSessionAgents] : newSessionAgents;
25208
+ const agentIndex = Math.max(0, agents.findIndex((entry) => entry.id === options.agent));
25209
+ const selectedAgent = agents[choiceIndex(
25210
+ await ask(choiceQuestion("Agent", agents, agentIndex)),
25211
+ agents.length,
25212
+ agentIndex
25213
+ )]?.id;
25214
+ options = normalizeNewConversationOptions(options, { agent: selectedAgent });
25215
+ const models = modelChoices(options.agent);
25216
+ const modelIndex = Math.max(0, models.findIndex((entry) => entry.key === options.model));
25217
+ const model = models[choiceIndex(
25218
+ await ask(choiceQuestion("Model", models, modelIndex)),
25219
+ models.length,
25220
+ modelIndex
25221
+ )]?.key ?? "default";
25222
+ options = normalizeNewConversationOptions(options, { model });
25223
+ const efforts = effortChoices(options.agent, options.model);
25224
+ if (efforts.length > 0) {
25225
+ const effortIndex = Math.max(0, efforts.findIndex((entry) => entry.key === options.effort));
25226
+ const effort = efforts[choiceIndex(
25227
+ await ask(choiceQuestion("Effort", efforts, effortIndex)),
25228
+ efforts.length,
25229
+ effortIndex
25230
+ )]?.key ?? null;
25231
+ options = normalizeNewConversationOptions(options, { effort });
25232
+ }
25233
+ options = normalizeNewConversationOptions(options, {
25234
+ useWorktree: yes(await ask(`Create isolated worktree? ${options.useWorktree ? "[Y/n]" : "[y/N]"}: `), options.useWorktree)
25235
+ });
25236
+ if (options.files.length === 0) {
25237
+ const files = (await ask("Attach local files (comma-separated, blank for none): "))?.split(",").map((entry) => entry.trim()).filter(Boolean) ?? [];
25238
+ options = normalizeNewConversationOptions(options, { files });
25239
+ }
25240
+ if (!options.voice && !options.voiceFile) {
25241
+ options = normalizeNewConversationOptions(options, {
25242
+ voice: yes(await ask("Record the first prompt by voice? [y/N]: "), false)
25243
+ });
25244
+ }
25245
+ return options;
25246
+ }
25247
+ const MIN_AUDIO_BYTES = 1600;
25248
+ function resolveTranscriptionOutcome(text2) {
25249
+ const cleaned = text2.trim();
25250
+ if (!cleaned) return { errorKind: "no-speech" };
25251
+ return { transcript: cleaned };
25252
+ }
25253
+ function recorderSpec(platform2, available) {
25254
+ if (platform2 === "linux" && available.has("arecord")) {
25255
+ return { command: "arecord", args: (path) => ["-q", "-f", "S16_LE", "-r", "16000", "-c", "1", path] };
25256
+ }
25257
+ if (available.has("rec")) {
25258
+ return { command: "rec", args: (path) => ["-q", "-c", "1", "-r", "16000", path] };
25259
+ }
25260
+ if (available.has("ffmpeg")) {
25261
+ if (platform2 === "darwin") {
25262
+ return {
25263
+ command: "ffmpeg",
25264
+ args: (path) => ["-nostdin", "-loglevel", "error", "-f", "avfoundation", "-i", ":0", "-ac", "1", "-ar", "16000", "-y", path]
25265
+ };
25266
+ }
25267
+ return {
25268
+ command: "ffmpeg",
25269
+ args: (path) => ["-nostdin", "-loglevel", "error", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", "-y", path]
25270
+ };
25271
+ }
25272
+ return null;
25273
+ }
25274
+ async function installedCommands(env) {
25275
+ const result = /* @__PURE__ */ new Set();
25276
+ const directories = (env.PATH ?? "").split(delimiter).filter(Boolean);
25277
+ for (const command of ["arecord", "rec", "ffmpeg"]) {
25278
+ for (const directory of directories) {
25279
+ try {
25280
+ await access(join(directory, command), constants$1.X_OK);
25281
+ result.add(command);
25282
+ break;
25283
+ } catch {
25284
+ }
25285
+ }
25286
+ }
25287
+ return result;
25288
+ }
25289
+ function waitForSpawn(child) {
25290
+ return new Promise((resolve, reject) => {
25291
+ const onSpawn = () => {
25292
+ cleanup();
25293
+ resolve();
25294
+ };
25295
+ const onError = (error) => {
25296
+ cleanup();
25297
+ reject(error);
25298
+ };
25299
+ const cleanup = () => {
25300
+ child.off("spawn", onSpawn);
25301
+ child.off("error", onError);
25302
+ };
25303
+ child.once("spawn", onSpawn);
25304
+ child.once("error", onError);
25305
+ });
25306
+ }
25307
+ function waitForClose(child) {
25308
+ return new Promise((resolve) => {
25309
+ if (child.exitCode !== null || child.signalCode !== null) {
25310
+ resolve();
25311
+ return;
25312
+ }
25313
+ const timer = setTimeout(() => {
25314
+ child.kill("SIGTERM");
25315
+ resolve();
25316
+ }, 5e3);
25317
+ child.once("close", () => {
25318
+ clearTimeout(timer);
25319
+ resolve();
25320
+ });
25321
+ });
25322
+ }
25323
+ async function recordCliVoice(waitForStop, input = {}) {
25324
+ const platform2 = input.platform ?? process.platform;
25325
+ const spec = recorderSpec(platform2, await installedCommands(input.env ?? process.env));
25326
+ if (!spec) {
25327
+ throw new Error("No supported microphone recorder was found. Install ffmpeg/SoX/arecord or use `--voice-file <audio>` instead.");
25328
+ }
25329
+ const directory = await mkdtemp(join(tmpdir(), "saycode-voice-"));
25330
+ const path = join(directory, "prompt.wav");
25331
+ const cleanup = () => rm(directory, { recursive: true, force: true });
25332
+ try {
25333
+ const child = spawn(spec.command, spec.args(path), { stdio: ["ignore", "ignore", "pipe"] });
25334
+ await waitForSpawn(child);
25335
+ await waitForStop();
25336
+ child.kill("SIGINT");
25337
+ await waitForClose(child);
25338
+ const info = await stat(path);
25339
+ if (info.size < MIN_AUDIO_BYTES) throw new Error("No usable speech was recorded.");
25340
+ return { path, cleanup };
25341
+ } catch (error) {
25342
+ await cleanup();
25343
+ throw error;
25344
+ }
25345
+ }
25346
+ function audioMimeTypeForPath(path) {
25347
+ switch (extname(path).toLowerCase()) {
25348
+ case ".m4a":
25349
+ case ".mp4":
25350
+ return "audio/mp4";
25351
+ case ".ogg":
25352
+ return "audio/ogg";
25353
+ case ".webm":
25354
+ return "audio/webm";
25355
+ case ".mp3":
25356
+ return "audio/mpeg";
25357
+ default:
25358
+ return "audio/wav";
25359
+ }
25360
+ }
25361
+ async function transcribeCliVoiceFile(rest, path, read = readFile) {
25362
+ const bytes = await read(path);
25363
+ const mimeType = audioMimeTypeForPath(path);
25364
+ const result = await rest.transcribeAudio(new Blob([new Uint8Array(bytes)], { type: mimeType }), {
25365
+ fileName: basename(path),
25366
+ mimeType
25367
+ });
25368
+ const outcome = resolveTranscriptionOutcome(result.text);
25369
+ if ("errorKind" in outcome) throw new Error("No speech was recognized in that recording.");
25370
+ return outcome.transcript;
25371
+ }
25372
+ function configurableAgent(agent) {
25373
+ if (agent === "gemini") return agent;
25374
+ return newSessionAgents.some((entry) => entry.id === agent) ? agent : null;
25375
+ }
25376
+ function hasNewConversationSettings(request) {
25377
+ return request.model !== void 0 || request.effort !== void 0 || request.useWorktree === true || request.worktreeName !== void 0 || request.worktreeBaseRef !== void 0 || request.files !== void 0 || request.voice === true || request.voiceFile !== void 0;
25378
+ }
25379
+ function validateExplicitSelection(request, agent) {
25380
+ if (request.model !== void 0 && !modelChoices(agent).some((option) => option.key === request.model)) {
25381
+ throw new CliError(`Unknown ${agent} model "${request.model}".`);
25382
+ }
25383
+ if (request.effort !== void 0) {
25384
+ const model = request.model ?? DEFAULT_CLI_NEW_CONVERSATION_OPTIONS.model;
25385
+ if (!effortChoices(agent, model).some((option) => option.key === request.effort)) {
25386
+ throw new CliError(`Effort "${request.effort}" is not available for ${agent} model "${model}".`);
25387
+ }
25388
+ }
25389
+ }
25390
+ async function resolveCliNewConversationOptions(request, ask) {
25391
+ const agent = configurableAgent(request.agent);
25392
+ if (!agent) {
25393
+ if (hasNewConversationSettings(request)) {
25394
+ throw new CliError(`New-conversation options are not available for agent "${request.agent}".`);
25395
+ }
25396
+ return null;
25397
+ }
25398
+ validateExplicitSelection(request, agent);
25399
+ let resolved = normalizeNewConversationOptions(DEFAULT_CLI_NEW_CONVERSATION_OPTIONS, {
25400
+ agent,
25401
+ ...request.model !== void 0 ? { model: request.model } : {},
25402
+ ...request.effort !== void 0 ? { effort: request.effort } : {},
25403
+ ...request.useWorktree ? { useWorktree: true } : {},
25404
+ ...request.worktreeName !== void 0 ? { worktreeName: request.worktreeName } : {},
25405
+ ...request.worktreeBaseRef !== void 0 ? { worktreeBaseRef: request.worktreeBaseRef } : {},
25406
+ ...request.files ? { files: request.files } : {},
25407
+ ...request.voice ? { voice: true } : {},
25408
+ ...request.voiceFile !== void 0 ? { voiceFile: request.voiceFile } : {}
25409
+ });
25410
+ if (ask) resolved = await promptNewConversationOptions(resolved, ask);
25411
+ return resolved;
25412
+ }
25413
+ async function captureCliVoicePrompt(context, voiceFile, waitForStop) {
25414
+ if (!context.hasApiAccess) {
25415
+ throw new CliError("Voice transcription needs a Saycode account login. Run `saycode login`, or type the prompt instead.");
25416
+ }
25417
+ if (voiceFile) return transcribeCliVoiceFile(context.rest, voiceFile);
25418
+ context.out.raw("Recording from the microphone…");
25419
+ const recording = await recordCliVoice(waitForStop);
25420
+ try {
25421
+ return await transcribeCliVoiceFile(context.rest, recording.path);
25422
+ } finally {
25423
+ await recording.cleanup();
25424
+ }
25425
+ }
25426
+ const USAGE_SETTINGS_NAVIGATION_DESTINATIONS = [
25427
+ "settings_profile",
25428
+ "settings_system_prompt",
25429
+ "settings_personal_memory",
25430
+ "settings_notifications",
25431
+ "settings_visual_effects",
25432
+ "settings_language",
25433
+ "settings_shortcuts",
25434
+ "settings_machines",
25435
+ "settings_mobile",
25436
+ "settings_env",
25437
+ "settings_security",
25438
+ "settings_memory",
25439
+ "settings_conversation_search",
25440
+ "settings_connectors",
25441
+ "settings_about"
25442
+ ];
25443
+ const USAGE_ANALYTICS_EVENT_NAMES = [
25444
+ "app_started",
25445
+ "login_success",
25446
+ "logout",
25447
+ "session_created",
25448
+ "message_sent",
25449
+ "command_completed",
25450
+ "navigation_selected",
25451
+ "secondary_action_selected",
25452
+ "first_response_received",
25453
+ "preview_opened",
25454
+ "preview_start_failed",
25455
+ "terminal_opened",
25456
+ "worktree_created",
25457
+ "browser_tab_opened",
25458
+ "update_installed",
25459
+ "client_error_occurred",
25460
+ // Meta event: emitted by the queue itself when the cap forced drops (plan
25461
+ // "리스크와 대응"), so silent loss is visible in the dashboard.
25462
+ "usage_events_dropped"
25463
+ ];
25464
+ const USAGE_ANALYTICS_COMMAND_ACTIONS = [
25465
+ "help",
25466
+ "version",
25467
+ "login",
25468
+ "logout",
25469
+ "workspaces",
25470
+ "browse",
25471
+ "machines",
25472
+ "projects",
25473
+ "ls",
25474
+ "attach",
25475
+ "chat",
25476
+ "watch",
25477
+ "show_status",
25478
+ "open_chat",
25479
+ "send_selection",
25480
+ "create_project",
25481
+ "mount_project"
25482
+ ];
25483
+ const USAGE_NAVIGATION_SURFACES = ["sidebar_primary", "sidebar_more", "workspace_switcher", "settings"];
25484
+ const USAGE_NAVIGATION_DESTINATIONS = [
25485
+ "projects",
25486
+ "agent_board",
25487
+ "personal_chats",
25488
+ "all_conversations",
25489
+ "machines",
25490
+ "organization_home",
25491
+ "settings",
25492
+ "organization_admin",
25493
+ "system_admin",
25494
+ ...USAGE_SETTINGS_NAVIGATION_DESTINATIONS
25495
+ ];
25496
+ const USAGE_SECONDARY_ACTION_SURFACES = ["chat_header", "composer"];
25497
+ const USAGE_SECONDARY_ACTIONS = [
25498
+ "terminal",
25499
+ "preview",
25500
+ "new_chat",
25501
+ "more",
25502
+ "copy",
25503
+ "complete",
25504
+ "archive",
25505
+ "project_settings",
25506
+ "deploy",
25507
+ "memory",
25508
+ "machine_usage",
25509
+ "workspace_layout",
25510
+ "tool_menu",
25511
+ "model",
25512
+ "voice",
25513
+ "attachment",
25514
+ "work_completion"
25515
+ ];
25516
+ const DEMO_ANALYTICS_USER_ID = "demo-user";
25517
+ function isDemoAnalyticsUserId(userId) {
25518
+ return userId === DEMO_ANALYTICS_USER_ID;
25519
+ }
25520
+ const EVENT_PROPERTY_ALLOWLIST = {
25521
+ app_started: ["mode"],
25522
+ login_success: ["mode"],
25523
+ logout: ["mode"],
25524
+ session_created: ["mode", "worktree", "new_project"],
25525
+ message_sent: ["mode", "length_bucket"],
25526
+ command_completed: ["action"],
25527
+ navigation_selected: ["mode", "surface", "destination"],
25528
+ secondary_action_selected: ["mode", "surface", "action"],
25529
+ first_response_received: ["mode", "duration_bucket"],
25530
+ preview_opened: ["mode"],
25531
+ preview_start_failed: ["mode", "failure_code"],
25532
+ terminal_opened: ["mode"],
25533
+ worktree_created: ["mode"],
25534
+ browser_tab_opened: ["mode"],
25535
+ update_installed: ["mode", "from_version", "to_version"],
25536
+ client_error_occurred: ["mode", "error_kind"],
25537
+ usage_events_dropped: ["mode", "dropped_count"]
25538
+ };
25539
+ const EVENT_PROPERTY_VALUE_ALLOWLIST = {
25540
+ navigation_selected: {
25541
+ surface: USAGE_NAVIGATION_SURFACES,
25542
+ destination: USAGE_NAVIGATION_DESTINATIONS
25543
+ },
25544
+ secondary_action_selected: {
25545
+ surface: USAGE_SECONDARY_ACTION_SURFACES,
25546
+ action: USAGE_SECONDARY_ACTIONS
25547
+ },
25548
+ command_completed: {
25549
+ action: USAGE_ANALYTICS_COMMAND_ACTIONS
25550
+ }
25551
+ };
25552
+ const MAX_STRING_PROPERTY_LENGTH = 200;
25553
+ function isUsageEventName(value2) {
25554
+ return typeof value2 === "string" && USAGE_ANALYTICS_EVENT_NAMES.includes(value2);
25555
+ }
25556
+ function sanitizeUsageEvent(name, props) {
25557
+ if (!isUsageEventName(name)) return null;
25558
+ const allowlist = EVENT_PROPERTY_ALLOWLIST[name];
25559
+ const properties = {};
25560
+ if (props && typeof props === "object" && !Array.isArray(props)) {
25561
+ for (const [key, value2] of Object.entries(props)) {
25562
+ if (!allowlist.includes(key)) continue;
25563
+ if (typeof value2 === "string") {
25564
+ const valueAllowlist = EVENT_PROPERTY_VALUE_ALLOWLIST[name]?.[key];
25565
+ if (valueAllowlist && !valueAllowlist.includes(value2)) continue;
25566
+ properties[key] = value2.slice(0, MAX_STRING_PROPERTY_LENGTH);
25567
+ } else if (typeof value2 === "number" && Number.isFinite(value2)) {
25568
+ properties[key] = value2;
25569
+ } else if (typeof value2 === "boolean") {
25570
+ properties[key] = value2;
25571
+ }
25572
+ }
25573
+ }
25574
+ const requiredEnumProperties = EVENT_PROPERTY_VALUE_ALLOWLIST[name];
25575
+ if (requiredEnumProperties && Object.keys(requiredEnumProperties).some((key) => properties[key] === void 0)) return null;
25576
+ return { event: name, properties };
25577
+ }
25578
+ function buildUsageCommonProperties(input) {
25579
+ return { app: input.service, app_version: input.appVersion, os: input.platform };
25580
+ }
25581
+ const USAGE_ANALYTICS_QUEUE_FILE = "queue.jsonl";
25582
+ const USAGE_ANALYTICS_QUEUE_MAX_EVENTS = 5e3;
25583
+ function isQueuedUsageEvent(value2) {
25584
+ if (!value2 || typeof value2 !== "object") return false;
25585
+ const record2 = value2;
25586
+ return typeof record2.uuid === "string" && record2.uuid.length > 0 && typeof record2.event === "string" && record2.event.length > 0 && typeof record2.distinctId === "string" && typeof record2.timestamp === "string" && !!record2.properties && typeof record2.properties === "object";
25587
+ }
25588
+ function createUsageAnalyticsQueue(options) {
25589
+ const maxEvents = options.maxEvents ?? USAGE_ANALYTICS_QUEUE_MAX_EVENTS;
25590
+ const path = join(options.dir, USAGE_ANALYTICS_QUEUE_FILE);
25591
+ let droppedCount = 0;
25592
+ let knownCount = null;
25593
+ let chain = Promise.resolve();
25594
+ const enqueue = (task) => {
25595
+ const result = chain.then(task);
25596
+ chain = result.then(() => void 0, () => void 0);
25597
+ return result;
25598
+ };
25599
+ const readAll = async () => {
25600
+ let raw;
25601
+ try {
25602
+ raw = await readFile(path, "utf8");
25603
+ } catch {
25604
+ return [];
25605
+ }
25606
+ const records = [];
25607
+ for (const line of raw.split("\n")) {
25608
+ if (!line.trim()) continue;
25609
+ try {
25610
+ const parsed = JSON.parse(line);
25611
+ if (isQueuedUsageEvent(parsed)) records.push(parsed);
25612
+ } catch {
25613
+ }
25614
+ }
25615
+ return records;
25616
+ };
25617
+ const rewrite = async (records) => {
25618
+ if (records.length === 0) {
25619
+ await rm(path, { force: true });
25620
+ return;
25621
+ }
25622
+ await writeFile(path, records.map((r) => JSON.stringify(r)).join("\n") + "\n", { mode: 384 });
25623
+ };
25624
+ return {
25625
+ append(record2) {
25626
+ return enqueue(async () => {
25627
+ try {
25628
+ await mkdir(options.dir, { recursive: true, mode: 448 });
25629
+ await appendFile(path, JSON.stringify(record2) + "\n", { mode: 384 });
25630
+ if (knownCount === null) knownCount = (await readAll()).length;
25631
+ else knownCount += 1;
25632
+ if (knownCount > maxEvents) {
25633
+ const all = await readAll();
25634
+ if (all.length > maxEvents) {
25635
+ droppedCount += all.length - maxEvents;
25636
+ await rewrite(all.slice(all.length - maxEvents));
25637
+ }
25638
+ knownCount = Math.min(all.length, maxEvents);
25639
+ }
25640
+ } catch {
25641
+ }
25642
+ });
25643
+ },
25644
+ readBatch(limit) {
25645
+ return enqueue(async () => (await readAll()).slice(0, limit));
25646
+ },
25647
+ removeSent(uuids) {
25648
+ return enqueue(async () => {
25649
+ try {
25650
+ const sent = new Set(uuids);
25651
+ const kept = (await readAll()).filter((record2) => !sent.has(record2.uuid));
25652
+ await rewrite(kept);
25653
+ knownCount = kept.length;
25654
+ } catch {
25655
+ knownCount = null;
25656
+ }
25657
+ });
25658
+ },
25659
+ clear() {
25660
+ return enqueue(async () => {
25661
+ droppedCount = 0;
25662
+ knownCount = 0;
25663
+ try {
25664
+ await rm(path, { force: true });
25665
+ } catch {
25666
+ knownCount = null;
25667
+ }
25668
+ });
25669
+ },
25670
+ takeDroppedCount() {
25671
+ const count = droppedCount;
25672
+ droppedCount = 0;
25673
+ return count;
25674
+ }
25675
+ };
25676
+ }
25677
+ const USAGE_ANALYTICS_BATCH_SIZE = 100;
25678
+ const USAGE_ANALYTICS_REQUEST_TIMEOUT_MS = 1e4;
25679
+ function createUsageAnalyticsSender(options) {
25680
+ const { queue, config, commonProperties } = options;
25681
+ const fetchImpl = options.fetchImpl ?? fetch;
25682
+ const batchSize = options.batchSize ?? USAGE_ANALYTICS_BATCH_SIZE;
25683
+ const requestTimeoutMs = options.requestTimeoutMs ?? USAGE_ANALYTICS_REQUEST_TIMEOUT_MS;
25684
+ return {
25685
+ async flush() {
25686
+ if (!config || !config.token) return "disabled";
25687
+ const batch = await queue.readBatch(batchSize);
25688
+ if (batch.length === 0) return "empty";
25689
+ const controller = new AbortController();
25690
+ const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
25691
+ timeout.unref?.();
25692
+ try {
25693
+ const response = await fetchImpl(`${config.host.replace(/\/$/, "")}/batch/`, {
25694
+ method: "POST",
25695
+ signal: controller.signal,
25696
+ headers: { "Content-Type": "application/json" },
25697
+ body: JSON.stringify({
25698
+ api_key: config.token,
25699
+ batch: batch.map((record2) => ({
25700
+ event: record2.event,
25701
+ distinct_id: record2.distinctId,
25702
+ uuid: record2.uuid,
25703
+ timestamp: record2.timestamp,
25704
+ properties: {
25705
+ ...record2.properties,
25706
+ ...commonProperties,
25707
+ // web-ui(person_profiles:'identified_only') 파리티: 익명 이벤트는
25708
+ // person 을 만들지 않는다 — 같은 프로젝트 공유 시 person 폭증 방지.
25709
+ ...record2.identified === false ? { $process_person_profile: false } : {}
25710
+ }
25711
+ }))
25712
+ })
25713
+ });
25714
+ if (!response.ok) return "failed";
25715
+ } catch {
25716
+ return "failed";
25717
+ } finally {
25718
+ clearTimeout(timeout);
25719
+ }
25720
+ await queue.removeSent(batch.map((record2) => record2.uuid));
25721
+ return "sent";
25722
+ }
25723
+ };
25724
+ }
25725
+ const USAGE_ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
25726
+ function createUsageAnalyticsCoordinator(options) {
25727
+ const now = options.now ?? (() => Date.now());
25728
+ const queue = createUsageAnalyticsQueue({ dir: options.dir, maxEvents: options.queueMaxEvents });
25729
+ const sender2 = createUsageAnalyticsSender({
25730
+ queue,
25731
+ config: options.senderConfig,
25732
+ commonProperties: options.commonProperties,
25733
+ fetchImpl: options.fetchImpl
25734
+ });
25735
+ let timer = null;
25736
+ let forcedDisabled = false;
25737
+ let operationChain = Promise.resolve();
25738
+ const enqueueOperation = (operation) => {
25739
+ const result = operationChain.then(operation);
25740
+ operationChain = result.then(() => void 0, () => void 0);
25741
+ return result;
25742
+ };
25743
+ const append = async (name, props) => {
25744
+ const sanitized = sanitizeUsageEvent(name, props);
25745
+ if (!sanitized) return;
25746
+ try {
25747
+ const distinctId = await options.getDistinctId();
25748
+ const record2 = {
25749
+ uuid: randomUUID(),
25750
+ event: sanitized.event,
25751
+ properties: sanitized.properties,
25752
+ distinctId,
25753
+ timestamp: new Date(now()).toISOString()
25754
+ };
25755
+ if (options.getInstallId) record2.identified = distinctId !== await options.getInstallId();
25756
+ await queue.append(record2);
25757
+ } catch {
25758
+ }
25759
+ };
25760
+ const coordinator = {
25761
+ async record(name, props) {
25762
+ await enqueueOperation(async () => {
25763
+ if (forcedDisabled) return;
25764
+ try {
25765
+ if (!await options.isEnabled()) return;
25766
+ } catch {
25767
+ return;
25768
+ }
25769
+ await append(name, props);
25770
+ });
25771
+ },
25772
+ async recordAlias(input) {
25773
+ await enqueueOperation(async () => {
25774
+ if (forcedDisabled) return;
25775
+ try {
25776
+ if (!await options.isEnabled()) return;
25777
+ if (!input.userId || !input.installId || input.userId === input.installId) return;
25778
+ if (isDemoAnalyticsUserId(input.userId)) return;
25779
+ await queue.append({
25780
+ uuid: randomUUID(),
25781
+ event: "$create_alias",
25782
+ properties: { distinct_id: input.userId, alias: input.installId },
25783
+ distinctId: input.userId,
25784
+ identified: true,
25785
+ timestamp: new Date(now()).toISOString()
25786
+ });
25787
+ } catch {
25788
+ }
25789
+ });
25790
+ },
25791
+ async flushNow() {
25792
+ await enqueueOperation(async () => {
25793
+ if (forcedDisabled) return;
25794
+ const dropped = queue.takeDroppedCount();
25795
+ if (dropped > 0) await append("usage_events_dropped", { dropped_count: dropped });
25796
+ await sender2.flush();
25797
+ });
25798
+ },
25799
+ async setEnabled(enabled) {
25800
+ forcedDisabled = !enabled;
25801
+ await enqueueOperation(async () => {
25802
+ if (!enabled) await queue.clear();
25803
+ });
25804
+ },
25805
+ start() {
25806
+ if (timer) return;
25807
+ timer = setInterval(() => {
25808
+ void coordinator.flushNow();
25809
+ }, options.flushIntervalMs ?? USAGE_ANALYTICS_FLUSH_INTERVAL_MS);
25810
+ timer.unref?.();
25811
+ },
25812
+ dispose() {
25813
+ if (timer) clearInterval(timer);
25814
+ timer = null;
25815
+ },
25816
+ peekQueue(limit) {
25817
+ return queue.readBatch(limit);
25818
+ }
25819
+ };
25820
+ return coordinator;
25821
+ }
25822
+ const USAGE_ANALYTICS_IDENTITY_FILE = "identity.json";
25823
+ function createUsageAnalyticsIdentity(options) {
25824
+ const path = join(options.dir, USAGE_ANALYTICS_IDENTITY_FILE);
25825
+ let pending = null;
25826
+ const resolveInstallId = async () => {
25827
+ try {
25828
+ const parsed = JSON.parse(await readFile(path, "utf8"));
25829
+ if (typeof parsed.installId === "string" && parsed.installId.length > 0) return parsed.installId;
25830
+ } catch {
25831
+ }
25832
+ const installId = randomUUID();
25833
+ try {
25834
+ await mkdir(options.dir, { recursive: true, mode: 448 });
25835
+ await writeFile(path, JSON.stringify({ installId }), { mode: 384 });
25836
+ } catch {
25837
+ }
25838
+ return installId;
25839
+ };
25840
+ const getInstallId = () => {
25841
+ if (!pending) {
25842
+ pending = resolveInstallId();
25843
+ void pending.catch(() => {
25844
+ pending = null;
25845
+ });
25846
+ }
25847
+ return pending;
25848
+ };
25849
+ return {
25850
+ getInstallId,
25851
+ async getDistinctId() {
25852
+ try {
25853
+ const userId = await options.loadUserId();
25854
+ if (userId && !isDemoAnalyticsUserId(userId)) return userId;
25855
+ } catch {
25856
+ }
25857
+ return getInstallId();
25858
+ }
25859
+ };
25860
+ }
25861
+ async function flushUsageAnalyticsWithin(flush, timeoutMs) {
25862
+ let timer;
25863
+ await Promise.race([
25864
+ flush().catch(() => {
25865
+ }),
25866
+ new Promise((resolve) => {
25867
+ timer = setTimeout(resolve, timeoutMs);
25868
+ })
25869
+ ]);
25870
+ if (timer) clearTimeout(timer);
25871
+ }
25872
+ function createNodeUsageAnalytics(options) {
25873
+ const identity = createUsageAnalyticsIdentity({ dir: options.dir, loadUserId: async () => null });
25874
+ const coordinator = createUsageAnalyticsCoordinator({
25875
+ dir: options.dir,
25876
+ senderConfig: options.senderConfig,
25877
+ commonProperties: buildUsageCommonProperties({
25878
+ service: options.service,
25879
+ appVersion: options.appVersion,
25880
+ platform: options.platform
25881
+ }),
25882
+ getDistinctId: () => identity.getInstallId(),
25883
+ getInstallId: () => identity.getInstallId(),
25884
+ isEnabled: options.isEnabled,
25885
+ ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
25886
+ });
25887
+ return {
25888
+ record: (name, props = {}) => coordinator.record(name, props),
25889
+ async start() {
25890
+ coordinator.start();
25891
+ await coordinator.record("app_started", {});
25892
+ },
25893
+ async flush() {
25894
+ coordinator.dispose();
25895
+ await coordinator.flushNow();
25896
+ },
25897
+ setEnabled: (enabled) => coordinator.setEnabled(enabled),
25898
+ peekQueue: (limit) => coordinator.peekQueue(limit)
25899
+ };
25900
+ }
25901
+ function resolvePublicPostHogConfig(input) {
25902
+ const token = input.token?.trim() ?? "";
25903
+ if (!token.startsWith("phc_")) return null;
25904
+ return { token, host: input.host?.trim() || "https://us.i.posthog.com" };
25905
+ }
25906
+ let analytics = null;
25907
+ const CLI_USAGE_ANALYTICS_FLUSH_TIMEOUT_MS = 1500;
25908
+ async function initializeCliUsageAnalytics(options = {}) {
25909
+ const homeDir = options.homeDir ?? homedir();
25910
+ const env = options.env ?? process.env;
25911
+ const builtHost = "https://us.i.posthog.com";
25912
+ analytics = createNodeUsageAnalytics({
25913
+ dir: join(homeDir, ".saycode", "usage-analytics"),
25914
+ service: "saycode_cli",
25915
+ appVersion: manifest.version,
25916
+ platform: platform(),
25917
+ senderConfig: resolvePublicPostHogConfig({
25918
+ token: env.APLUS_POSTHOG_TOKEN,
25919
+ host: builtHost
25920
+ }),
25921
+ isEnabled: async () => readConfig(homeDir).usageAnalyticsEnabled !== false
25922
+ });
25923
+ if (readConfig(homeDir).usageAnalyticsEnabled === false) await analytics.setEnabled(false).catch(() => {
25924
+ });
25925
+ await analytics.start().catch(() => {
25926
+ });
25927
+ }
25928
+ async function recordCliUsage(name, props = {}) {
25929
+ await analytics?.record(name, props).catch(() => {
25930
+ });
25931
+ }
25932
+ async function flushCliUsageAnalytics() {
25933
+ const current = analytics;
25934
+ analytics = null;
25935
+ if (!current) return;
25936
+ await flushUsageAnalyticsWithin(() => current.flush(), CLI_USAGE_ANALYTICS_FLUSH_TIMEOUT_MS);
25937
+ }
24653
25938
  const DETACH_COMMAND = "/detach";
24654
25939
  async function spawnSession(transport, machineId, directory, agent) {
24655
25940
  const result = await transport.rpc(machineId, "spawn-happy-session", { directory, agent }, 6e4);
@@ -24659,6 +25944,16 @@ async function spawnSession(transport, machineId, directory, agent) {
24659
25944
  }
24660
25945
  return result.sessionId;
24661
25946
  }
25947
+ async function askTemporaryLine(question) {
25948
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
25949
+ const input = new ChatInputController(rl, process.stdin);
25950
+ try {
25951
+ return await input.ask(question);
25952
+ } finally {
25953
+ input.close();
25954
+ rl.close();
25955
+ }
25956
+ }
24662
25957
  async function replyToPermission(transport, sessionId, request, answer) {
24663
25958
  const approved = answer !== "deny";
24664
25959
  await transport.rpc(sessionId, "permission", {
@@ -24668,12 +25963,8 @@ async function replyToPermission(transport, sessionId, request, answer) {
24668
25963
  ...answer === "approve-always" ? { mode: "bypassPermissions" } : {}
24669
25964
  });
24670
25965
  }
24671
- async function sendPrompt(context, sessionId, text2) {
24672
- const content = {
24673
- role: "user",
24674
- content: { type: "text", text: text2 },
24675
- meta: { permissionMode: "default" }
24676
- };
25966
+ async function sendPrompt(context, sessionId, text2, options) {
25967
+ const content = options ? buildCliUserMessage(text2, options) : { role: "user", content: { type: "text", text: text2 }, meta: { permissionMode: "default" } };
24677
25968
  const key = new Uint8Array(Buffer.from(context.syncCredential.secret, "base64"));
24678
25969
  const localId = `saycode-cli-${process.pid}-${Math.random().toString(36).slice(2)}`;
24679
25970
  await context.rest.sendMessage(sessionId, localId, encodeMessageContent(content, key));
@@ -24722,7 +26013,7 @@ async function resolveSpawnTarget(context, options) {
24722
26013
  if (!project.workspaceDir) {
24723
26014
  throw new CliError(`Project "${project.name}" has no working directory recorded. Use -m and -d instead.`);
24724
26015
  }
24725
- return { machineId: project.machineId, directory: project.workspaceDir };
26016
+ return { machineId: project.machineId, directory: project.workspaceDir, project };
24726
26017
  }
24727
26018
  if (!options.machine || !options.directory) {
24728
26019
  throw new CliError("Starting a session needs either -p <project> or both -m <machine> and -d <directory>.");
@@ -24731,140 +26022,241 @@ async function resolveSpawnTarget(context, options) {
24731
26022
  }
24732
26023
  async function runChat(context, options, providedTransport) {
24733
26024
  const resuming = !!options.sessionId;
24734
- const target = resuming ? null : await resolveSpawnTarget(context, options);
24735
- return withTransport(context, async (transport) => {
24736
- const sessionId = options.sessionId ?? await spawnSession(transport, target.machineId, target.directory, options.agent);
24737
- context.out.raw(`Session ${sessionId}`);
24738
- context.out.raw("During a response, typing is locked. Ctrl+C opens response options.");
24739
- context.out.raw("At the prompt: /detach to leave with the agent still running · /exit to leave.\n");
24740
- const chat = new ChatSession();
24741
- const renderer = createSessionEventRenderer(terminalContentWidth(process.stdout.columns));
24742
- const answered = /* @__PURE__ */ new Set();
24743
- const ownLocalIds = /* @__PURE__ */ new Set();
24744
- const seenThroughSeq = await prerollHistory(
24745
- context,
24746
- transport,
24747
- sessionId,
24748
- resumeHistoryCount({ resuming, ...options.since !== void 0 ? { since: options.since } : {} }),
24749
- (content) => chat.apply({ type: "content", content }),
24750
- renderer
24751
- );
24752
- const rl = createInterface({ input: process.stdin, output: process.stdout });
24753
- const input = new ChatInputController(rl, process.stdin);
24754
- let menuOpen = false;
24755
- const onSigint = () => {
24756
- if (interruptDecision(chat.current) === "exit-cli") {
24757
- context.out.raw(`
26025
+ let optionPrompter = null;
26026
+ let optionInput = null;
26027
+ if (!resuming && options.promptForOptions) {
26028
+ optionPrompter = createInterface({ input: process.stdin, output: process.stdout });
26029
+ optionInput = new ChatInputController(optionPrompter, process.stdin);
26030
+ }
26031
+ const newConversationOptions = resuming ? null : await resolveCliNewConversationOptions(
26032
+ options,
26033
+ optionInput ? (question) => optionInput.ask(question) : void 0
26034
+ ).finally(() => {
26035
+ optionInput?.close();
26036
+ optionPrompter?.close();
26037
+ });
26038
+ if (newConversationOptions?.voice && process.stdin.isTTY !== true) {
26039
+ throw new CliError("Live voice capture needs a TTY. Use `--voice-file <audio>` for piped or redirected input.");
26040
+ }
26041
+ let target = resuming ? null : await resolveSpawnTarget(context, options);
26042
+ let createdWorktree = null;
26043
+ if (target && newConversationOptions?.useWorktree) {
26044
+ if (!target.project) throw new CliError("An isolated worktree needs a project target. Use `saycode chat -p <project>`.");
26045
+ if (!context.hasApiAccess) throw new CliError("Creating a worktree needs a Saycode account login. Run `saycode login`.");
26046
+ createdWorktree = await createCliProjectWorktree({
26047
+ projectId: target.project.id,
26048
+ machineId: target.machineId,
26049
+ projectRoot: target.directory,
26050
+ name: resolveWorktreeName({ requested: newConversationOptions.worktreeName }),
26051
+ baseRef: newConversationOptions.worktreeBaseRef
26052
+ }, {
26053
+ create: (projectId, input) => context.rest.createProjectWorktree(projectId, input),
26054
+ remove: (projectId, worktreeId, force) => context.rest.deleteProjectWorktree(projectId, worktreeId, force),
26055
+ confirmSubdir: async (request) => {
26056
+ if (process.stdin.isTTY !== true) return false;
26057
+ const answer = await askTemporaryLine(`Project is ${request.repoRelativeDir} inside ${request.repoRoot}. Create the worktree for the whole repository? [y/N]: `);
26058
+ return answer?.trim().toLowerCase() === "y" || answer?.trim().toLowerCase() === "yes";
26059
+ }
26060
+ });
26061
+ target = { ...target, directory: createdWorktree.path };
26062
+ }
26063
+ let sessionSpawned = false;
26064
+ try {
26065
+ return await withTransport(context, async (transport) => {
26066
+ const sessionId = options.sessionId ?? await spawnSession(
26067
+ transport,
26068
+ target.machineId,
26069
+ target.directory,
26070
+ newConversationOptions?.agent ?? options.agent
26071
+ );
26072
+ sessionSpawned = true;
26073
+ if (!resuming) await recordCliUsage("session_created");
26074
+ if (!resuming && target?.project) {
26075
+ try {
26076
+ await context.rest.assignSessionToProject(target.project.id, sessionId, createdWorktree?.id);
26077
+ } catch (error) {
26078
+ context.out.raw(`! Session started, but project assignment failed: ${error instanceof Error ? error.message : String(error)}`);
26079
+ }
26080
+ }
26081
+ context.out.raw(`Session ${sessionId}`);
26082
+ context.out.raw("During a response, typing is locked. Ctrl+C opens response options.");
26083
+ context.out.raw("At the prompt: /voice to dictate · /detach to leave with the agent still running · /exit to leave.\n");
26084
+ const chat = new ChatSession();
26085
+ const renderer = createSessionEventRenderer(terminalContentWidth(process.stdout.columns));
26086
+ const answered = /* @__PURE__ */ new Set();
26087
+ const ownLocalIds = /* @__PURE__ */ new Set();
26088
+ let pendingFilePaths = [...newConversationOptions?.files ?? []];
26089
+ let pendingVoice = newConversationOptions?.voiceFile ?? (newConversationOptions?.voice ? true : null);
26090
+ const seenThroughSeq = await prerollHistory(
26091
+ context,
26092
+ transport,
26093
+ sessionId,
26094
+ resumeHistoryCount({ resuming, ...options.since !== void 0 ? { since: options.since } : {} }),
26095
+ (content) => chat.apply({ type: "content", content }),
26096
+ renderer
26097
+ );
26098
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
26099
+ const input = new ChatInputController(rl, process.stdin);
26100
+ let menuOpen = false;
26101
+ const onSigint = () => {
26102
+ if (interruptDecision(chat.current) === "exit-cli") {
26103
+ context.out.raw(`
24758
26104
  Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
24759
- rl.close();
24760
- process.exit(0);
24761
- }
24762
- if (menuOpen) return;
24763
- menuOpen = true;
24764
- context.out.raw("\n^C [Enter] keep watching · [d] detach, agent keeps working · [s] stop the response");
24765
- void input.readKey().then((key) => {
24766
- menuOpen = false;
24767
- const choice = interruptMenuChoice(key);
24768
- if (choice === "detach") {
24769
- chat.detach();
24770
- return;
26105
+ rl.close();
26106
+ process.exit(0);
24771
26107
  }
24772
- if (choice === "stop") {
24773
- context.out.raw("Stopping the response…");
24774
- void transport.rpc(sessionId, "abort", {}).catch(() => void 0);
24775
- 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
+ }
24776
26152
  }
24777
- context.out.raw("…still working.");
24778
26153
  });
24779
- };
24780
- const stopFollowing = followSession({
24781
- context,
24782
- transport,
24783
- sessionId,
24784
- seenThroughSeq,
24785
- ownLocalIds,
24786
- renderer,
24787
- onContent: (content) => {
24788
- printContent(context, content, renderer);
24789
- chat.apply({ type: "content", content });
24790
- if (chat.current.phase === "responding") input.lock(onSigint);
24791
- },
24792
- onUpdate: (update) => {
24793
- const cipher = readSessionAgentState(update, sessionId);
24794
- if (!cipher) return;
24795
- let decoded;
24796
- try {
24797
- decoded = transport.decodePayload(cipher);
24798
- } catch {
24799
- return;
24800
- }
24801
- for (const request of pendingPermissionRequestsFromState(decoded)) {
24802
- if (answered.has(request.requestId)) continue;
24803
- chat.apply({ type: "permission-requested", request });
24804
- input.cancelQuestion();
24805
- }
24806
- }
24807
- });
24808
- process.on("SIGINT", onSigint);
24809
- let outcome = "exited";
24810
- try {
24811
- for (; ; ) {
24812
- if (chat.current.phase !== "idle") {
24813
- if (chat.current.phase === "responding") {
24814
- context.out.line({ style: "thinking", text: chatPresentation(chat.current).status });
24815
- input.lock(onSigint);
26154
+ process.on("SIGINT", onSigint);
26155
+ let outcome = "exited";
26156
+ try {
26157
+ for (; ; ) {
26158
+ if (chat.current.phase !== "idle") {
26159
+ if (chat.current.phase === "responding") {
26160
+ context.out.line({ style: "thinking", text: chatPresentation(chat.current).status });
26161
+ input.lock(onSigint);
26162
+ }
26163
+ for (; ; ) {
26164
+ const state = await chat.waitForAttention();
26165
+ if (chat.isDetached) break;
26166
+ if (state.phase !== "awaiting-permission" || !state.pendingPermission) break;
26167
+ const request = state.pendingPermission;
26168
+ context.out.line({ style: "tool", text: chatPresentation(state).status });
26169
+ let answer = parsePermissionAnswer(await input.ask(`${permissionPromptText(request)} `) ?? "n");
26170
+ while (answer === "unknown") {
26171
+ answer = parsePermissionAnswer(await input.ask("Please answer y, n, or a. ") ?? "n");
26172
+ }
26173
+ answered.add(request.requestId);
26174
+ await replyToPermission(transport, sessionId, request, answer);
26175
+ chat.apply({ type: "permission-answered" });
26176
+ context.out.line({ style: "thinking", text: chatPresentation(chat.current).status });
26177
+ input.lock(onSigint);
26178
+ }
26179
+ if (chat.isDetached) {
26180
+ outcome = "detached";
26181
+ break;
26182
+ }
26183
+ continue;
24816
26184
  }
24817
- for (; ; ) {
24818
- const state = await chat.waitForAttention();
24819
- if (chat.isDetached) break;
24820
- if (state.phase !== "awaiting-permission" || !state.pendingPermission) break;
24821
- const request = state.pendingPermission;
24822
- context.out.line({ style: "tool", text: chatPresentation(state).status });
24823
- let answer = parsePermissionAnswer(await input.ask(`${permissionPromptText(request)} `) ?? "n");
24824
- while (answer === "unknown") {
24825
- answer = parsePermissionAnswer(await input.ask("Please answer y, n, or a. ") ?? "n");
26185
+ const ready = chatPresentation(chat.current);
26186
+ context.out.line({ style: "status", text: ready.status });
26187
+ let prompt = "";
26188
+ if (pendingVoice) {
26189
+ const voiceFile = typeof pendingVoice === "string" ? pendingVoice : null;
26190
+ pendingVoice = null;
26191
+ try {
26192
+ prompt = await captureCliVoicePrompt(context, voiceFile, async () => {
26193
+ await input.ask("Press Enter to stop recording. ");
26194
+ });
26195
+ context.out.raw(`Transcript: ${prompt}`);
26196
+ } catch (error) {
26197
+ context.out.raw(`Voice input failed: ${error instanceof Error ? error.message : String(error)}`);
26198
+ continue;
24826
26199
  }
24827
- answered.add(request.requestId);
24828
- await replyToPermission(transport, sessionId, request, answer);
24829
- chat.apply({ type: "permission-answered" });
24830
- context.out.line({ style: "thinking", text: chatPresentation(chat.current).status });
24831
- input.lock(onSigint);
26200
+ } else {
26201
+ const line = await input.ask(ready.prompt);
26202
+ if (line === null) {
26203
+ if (chat.current.phase !== "idle") continue;
26204
+ break;
26205
+ }
26206
+ prompt = line.trim();
24832
26207
  }
24833
- if (chat.isDetached) {
26208
+ if (prompt === "/exit" || prompt === "/quit") break;
26209
+ if (prompt === DETACH_COMMAND) {
24834
26210
  outcome = "detached";
24835
26211
  break;
24836
26212
  }
24837
- continue;
24838
- }
24839
- const ready = chatPresentation(chat.current);
24840
- context.out.line({ style: "status", text: ready.status });
24841
- const line = await input.ask(ready.prompt);
24842
- if (line === null) {
24843
- if (chat.current.phase !== "idle") continue;
24844
- break;
24845
- }
24846
- const prompt = line.trim();
24847
- if (prompt === "/exit" || prompt === "/quit") break;
24848
- if (prompt === DETACH_COMMAND) {
24849
- outcome = "detached";
24850
- break;
26213
+ if (prompt === "/voice" || prompt.startsWith("/voice ")) {
26214
+ const voiceFile = prompt.slice("/voice".length).trim() || null;
26215
+ try {
26216
+ prompt = await captureCliVoicePrompt(context, voiceFile, async () => {
26217
+ await input.ask("Press Enter to stop recording. ");
26218
+ });
26219
+ context.out.raw(`Transcript: ${prompt}`);
26220
+ } catch (error) {
26221
+ context.out.raw(`Voice input failed: ${error instanceof Error ? error.message : String(error)}`);
26222
+ continue;
26223
+ }
26224
+ }
26225
+ if (pendingFilePaths.length > 0) {
26226
+ try {
26227
+ const files = [];
26228
+ for (const path of pendingFilePaths) files.push(await loadCliAttachment(path));
26229
+ prompt = await prepareCliMessageWithAttachments(prompt, files, (file) => uploadCliAttachment(transport, target.machineId, target.directory, file));
26230
+ pendingFilePaths = [];
26231
+ } catch (error) {
26232
+ pendingFilePaths = [];
26233
+ context.out.raw(`Attachment failed: ${error instanceof Error ? error.message : String(error)}`);
26234
+ continue;
26235
+ }
26236
+ }
26237
+ if (prompt === "") continue;
26238
+ chat.apply({ type: "message-sent" });
26239
+ input.lock(onSigint);
26240
+ ownLocalIds.add(await sendPrompt(context, sessionId, prompt, newConversationOptions));
26241
+ await recordCliUsage("message_sent");
24851
26242
  }
24852
- if (prompt === "") continue;
24853
- chat.apply({ type: "message-sent" });
24854
- input.lock(onSigint);
24855
- ownLocalIds.add(await sendPrompt(context, sessionId, prompt));
26243
+ } finally {
26244
+ stopFollowing();
26245
+ process.off("SIGINT", onSigint);
26246
+ input.close();
26247
+ rl.close();
24856
26248
  }
24857
- } finally {
24858
- stopFollowing();
24859
- process.off("SIGINT", onSigint);
24860
- input.close();
24861
- rl.close();
24862
- }
24863
- context.out.raw(outcome === "detached" ? `
26249
+ context.out.raw(outcome === "detached" ? `
24864
26250
  Detached — the session keeps running. Reattach with \`saycode chat -s ${sessionId}\`.` : `
24865
26251
  Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
24866
- return outcome;
24867
- }, 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
+ }
24868
26260
  }
24869
26261
  const ESC = "\x1B";
24870
26262
  const CTRL_C = "";
@@ -25336,7 +26728,7 @@ async function runBrowse(context) {
25336
26728
  try {
25337
26729
  if (isNewConversation(chosen.id)) {
25338
26730
  const projectId = selectedId(state, "project");
25339
- await runChat(context, { project: projectId, agent: "claude" }, transports2.get(context.syncCredential));
26731
+ await runChat(context, { project: projectId, agent: "claude", promptForOptions: true }, transports2.get(context.syncCredential));
25340
26732
  } else {
25341
26733
  await runChat(context, { sessionId: chosen.id, agent: "claude" }, transports2.get(context.syncCredential));
25342
26734
  }
@@ -25397,14 +26789,17 @@ async function contextFor(workspace) {
25397
26789
  return context;
25398
26790
  }
25399
26791
  async function main(argv) {
26792
+ await initializeCliUsageAnalytics();
25400
26793
  const parsed = parseArgs(argv, process.stdout.isTTY === true && process.stdin.isTTY === true);
25401
26794
  const out = createWriter();
25402
26795
  switch (parsed.command) {
25403
26796
  case "help":
25404
26797
  out.raw(HELP_TEXT);
26798
+ await recordCliUsage("command_completed", { action: "help" });
25405
26799
  return 0;
25406
26800
  case "version":
25407
26801
  out.raw(manifest.version);
26802
+ await recordCliUsage("command_completed", { action: "version" });
25408
26803
  return 0;
25409
26804
  case "error":
25410
26805
  process.stderr.write(`saycode: ${parsed.message}
@@ -25414,36 +26809,47 @@ Run \`saycode --help\` for usage.
25414
26809
  return 2;
25415
26810
  case "login":
25416
26811
  await runLogin(out);
26812
+ await recordCliUsage("login_success");
26813
+ await recordCliUsage("command_completed", { action: "login" });
25417
26814
  return 0;
25418
26815
  case "logout":
25419
26816
  runLogout(out);
26817
+ await recordCliUsage("logout");
26818
+ await recordCliUsage("command_completed", { action: "logout" });
25420
26819
  return 0;
25421
26820
  case "workspaces":
25422
26821
  await runWorkspaces(loadContext(), { ...parsed.use ? { use: parsed.use } : {} });
26822
+ await recordCliUsage("command_completed", { action: "workspaces" });
25423
26823
  return 0;
25424
26824
  case "browse":
25425
26825
  await runBrowse(await contextFor(parsed.workspace));
26826
+ await recordCliUsage("command_completed", { action: "browse" });
25426
26827
  return 0;
25427
26828
  case "machines":
25428
26829
  await runMachines(await contextFor(parsed.workspace));
26830
+ await recordCliUsage("command_completed", { action: "machines" });
25429
26831
  return 0;
25430
26832
  case "projects":
25431
26833
  await runProjects(await contextFor(parsed.workspace), {
25432
26834
  ...parsed.machine ? { machine: parsed.machine } : {}
25433
26835
  });
26836
+ await recordCliUsage("command_completed", { action: "projects" });
25434
26837
  return 0;
25435
26838
  case "ls":
25436
26839
  if (parsed.project) {
25437
26840
  await runProjectConversations(await contextFor(parsed.workspace), parsed.project, parsed.all);
26841
+ await recordCliUsage("command_completed", { action: "ls" });
25438
26842
  return 0;
25439
26843
  }
25440
26844
  await runLs(await contextFor(parsed.workspace), {
25441
26845
  ...parsed.machine ? { machine: parsed.machine } : {},
25442
26846
  all: parsed.all
25443
26847
  });
26848
+ await recordCliUsage("command_completed", { action: "ls" });
25444
26849
  return 0;
25445
26850
  case "attach":
25446
26851
  await runAttach(await contextFor(parsed.workspace), { sessionId: parsed.sessionId, since: parsed.since });
26852
+ await recordCliUsage("command_completed", { action: "attach" });
25447
26853
  return 0;
25448
26854
  case "chat":
25449
26855
  await runChat(await contextFor(parsed.workspace), {
@@ -25452,21 +26858,37 @@ Run \`saycode --help\` for usage.
25452
26858
  ...parsed.directory ? { directory: parsed.directory } : {},
25453
26859
  ...parsed.project ? { project: parsed.project } : {},
25454
26860
  ...parsed.since !== void 0 ? { since: parsed.since } : {},
26861
+ ...parsed.model !== void 0 ? { model: parsed.model } : {},
26862
+ ...parsed.effort !== void 0 ? { effort: parsed.effort } : {},
26863
+ ...parsed.useWorktree ? { useWorktree: true } : {},
26864
+ ...parsed.worktreeName !== void 0 ? { worktreeName: parsed.worktreeName } : {},
26865
+ ...parsed.worktreeBaseRef !== void 0 ? { worktreeBaseRef: parsed.worktreeBaseRef } : {},
26866
+ ...parsed.files ? { files: parsed.files } : {},
26867
+ ...parsed.voice ? { voice: true } : {},
26868
+ ...parsed.voiceFile !== void 0 ? { voiceFile: parsed.voiceFile } : {},
26869
+ ...parsed.promptForOptions ? { promptForOptions: true } : {},
25455
26870
  agent: parsed.agent
25456
26871
  });
26872
+ await recordCliUsage("command_completed", { action: "chat" });
25457
26873
  return 0;
25458
26874
  case "watch":
25459
26875
  await runWatch(await contextFor(parsed.workspace), { intervalSeconds: parsed.intervalSeconds });
26876
+ await recordCliUsage("command_completed", { action: "watch" });
25460
26877
  return 0;
25461
26878
  }
25462
26879
  }
25463
- main(process.argv.slice(2)).then((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) => {
25464
26884
  if (error instanceof CliError) {
25465
26885
  process.stderr.write(`${error.message}
25466
26886
  `);
26887
+ await flushCliUsageAnalytics();
25467
26888
  process.exit(2);
25468
26889
  }
25469
26890
  process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
25470
26891
  `);
26892
+ await flushCliUsageAnalytics();
25471
26893
  process.exit(1);
25472
26894
  });