@cryer/star-cli 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -11,32 +11,81 @@ import {
11
11
  formatTokens,
12
12
  imageMimeType,
13
13
  listSessionEntries,
14
- loadEnvFile,
15
14
  loadSessionSnapshots,
16
15
  readImageInput,
17
16
  resolveMentions,
18
- resolveSessionId
19
- } from "./chunk-IYWYSFSF.js";
17
+ resolveSessionId,
18
+ summarizeArgs
19
+ } from "./chunk-PDFC7QVW.js";
20
20
  import {
21
- AgentLoop,
22
21
  ConfigSchema,
22
+ HookConfigSchema,
23
+ ModelConfigSchema,
24
+ PermissionsConfigSchema,
25
+ ProviderConfigSchema,
23
26
  contextWindowTokens,
24
- createDefaultRegistry,
25
- createModel,
27
+ reconcileStarMessages,
28
+ toCoreMessages
29
+ } from "./chunk-IJGUW4PM.js";
30
+ import {
31
+ resolveModelConfig,
32
+ resolveStartupModel
33
+ } from "./chunk-4IRY44TG.js";
34
+ import {
26
35
  defaultAgentTasks,
27
36
  defaultTaskManager,
28
37
  globalConfigPath,
29
38
  hydrateSnapshots,
39
+ loadEnvFile,
30
40
  loadTodos,
31
41
  projectConfigPath,
32
- reasoningEffortMetadata,
33
- reconcileToolCalls,
34
- resolveModelConfig
35
- } from "./chunk-OHLA2DP7.js";
42
+ registerSensitivePatterns,
43
+ setTodoSession,
44
+ setWebFetchAllowPrivateHosts
45
+ } from "./chunk-A5PXPWHB.js";
36
46
 
37
47
  // src/main.tsx
38
48
  import { Command } from "commander";
39
49
 
50
+ // src/cli/print-human.ts
51
+ function printEventLine(event) {
52
+ switch (event.type) {
53
+ case "text-delta":
54
+ return { stream: "stdout", text: event.text };
55
+ case "tool-call":
56
+ return { stream: "stderr", text: `
57
+ [tool] ${event.name} ${summarizeArgs(event.args)}
58
+ ` };
59
+ case "tool-result": {
60
+ const preview = event.content.length > 500 ? `${event.content.slice(0, 500)}... (truncated)` : event.content;
61
+ return {
62
+ stream: "stderr",
63
+ text: `[result] ${event.isError ? "ERROR: " : ""}${preview}
64
+ `
65
+ };
66
+ }
67
+ case "retry": {
68
+ const wait = event.delayMs !== void 0 && event.delayMs >= 1e3 ? ` in ${Math.round(event.delayMs / 1e3)}s` : "";
69
+ return {
70
+ stream: "stderr",
71
+ text: `
72
+ [retry ${event.attempt}/${event.maxAttempts}${wait}] ${event.reason}
73
+ `
74
+ };
75
+ }
76
+ case "notice":
77
+ return { stream: "stderr", text: `
78
+ [notice] ${event.message}
79
+ ` };
80
+ case "error":
81
+ return { stream: "stderr", text: `
82
+ [error] ${formatStreamError(event.error)}
83
+ ` };
84
+ default:
85
+ return null;
86
+ }
87
+ }
88
+
40
89
  // src/cli/print-json.ts
41
90
  function eventToJsonLine(event) {
42
91
  switch (event.type) {
@@ -80,11 +129,35 @@ var UsageTracker = class {
80
129
  }
81
130
  };
82
131
 
132
+ // src/cli/startup.ts
133
+ function imageRequiresPrintError(print, images) {
134
+ if (print !== void 0) return null;
135
+ return images.length > 0 ? "--image requires -p/--print" : null;
136
+ }
137
+ function interactiveRequiresTtyError(streams) {
138
+ return streams.stdinTTY && streams.stdoutTTY ? null : "interactive mode requires a TTY; use -p/--print";
139
+ }
140
+ function installStdoutEpipeGuard(stdout = process.stdout) {
141
+ stdout.on("error", (error) => {
142
+ if (error?.code === "EPIPE") process.exit(0);
143
+ else throw error;
144
+ });
145
+ }
146
+ async function reportResumeSessions(cwd) {
147
+ const entries = await listSessionEntries(cwd);
148
+ if (entries.length === 0) {
149
+ console.error("No sessions found for this directory.");
150
+ return 1;
151
+ }
152
+ console.log(formatSessionEntries(entries));
153
+ return 0;
154
+ }
155
+
83
156
  // src/config/loader.ts
84
157
  import fs from "fs";
85
158
  import { parse } from "smol-toml";
86
159
  var PartialConfigSchema = ConfigSchema.partial();
87
- function parseTomlFile(content, filePath) {
160
+ function parseTomlFile(content, filePath, scope) {
88
161
  let raw;
89
162
  try {
90
163
  raw = parse(content);
@@ -94,8 +167,14 @@ function parseTomlFile(content, filePath) {
94
167
  }
95
168
  const result = PartialConfigSchema.safeParse(raw);
96
169
  if (!result.success) {
97
- throw new Error(`Invalid config in ${filePath}: ${result.error.message}`);
170
+ const details = result.error.issues.map((issue) => {
171
+ const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
172
+ return ` ${where}: ${issue.message}`;
173
+ }).join("\n");
174
+ throw new Error(`Invalid config in ${filePath}:
175
+ ${details}`);
98
176
  }
177
+ warnUnknownKeys(raw, filePath, scope);
99
178
  return result.data;
100
179
  }
101
180
  var PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
@@ -121,6 +200,73 @@ var PROJECT_STREAM_RANGES = {
121
200
  streamMaxRetries: { min: 0, max: 10 }
122
201
  };
123
202
  var PROJECT_PROTECTIVE_MIN_KEYS = /* @__PURE__ */ new Set(["maxSteps", "doomLoopThreshold"]);
203
+ var TOP_LEVEL_KEYS = new Set(Object.keys(ConfigSchema.shape));
204
+ var SECTION_KEYS = {
205
+ permissions: { keys: new Set(Object.keys(PermissionsConfigSchema.shape)), array: false },
206
+ providers: { keys: new Set(Object.keys(ProviderConfigSchema.shape)), array: true },
207
+ models: { keys: new Set(Object.keys(ModelConfigSchema.shape)), array: true },
208
+ hooks: { keys: new Set(Object.keys(HookConfigSchema.shape)), array: true }
209
+ };
210
+ function editDistance(a, b) {
211
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
212
+ for (let i = 1; i <= a.length; i++) {
213
+ const current = [i];
214
+ for (let j = 1; j <= b.length; j++) {
215
+ current[j] = Math.min(
216
+ (previous[j] ?? 0) + 1,
217
+ (current[j - 1] ?? 0) + 1,
218
+ (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1)
219
+ );
220
+ }
221
+ previous = current;
222
+ }
223
+ return previous[b.length] ?? 0;
224
+ }
225
+ function suggestKey(key, known) {
226
+ const needle = key.toLowerCase();
227
+ let best;
228
+ let bestDistance = Number.POSITIVE_INFINITY;
229
+ for (const candidate of known) {
230
+ const distance = editDistance(needle, candidate.toLowerCase());
231
+ if (distance <= Math.max(1, Math.floor(candidate.length / 4)) && distance < bestDistance) {
232
+ best = candidate;
233
+ bestDistance = distance;
234
+ }
235
+ }
236
+ return best;
237
+ }
238
+ function warnUnknownKey(keyPath, filePath, known) {
239
+ const leaf = keyPath.slice(keyPath.lastIndexOf(".") + 1);
240
+ const suggestion = suggestKey(leaf, known);
241
+ const hint = suggestion ? `possible typo of "${suggestion}"? the key is ignored` : "possible typo? the key is ignored";
242
+ process.stderr.write(`[star] unknown config key "${keyPath}" in ${filePath}: ${hint}
243
+ `);
244
+ }
245
+ function warnUnknownKeys(raw, filePath, scope) {
246
+ if (typeof raw !== "object" || raw === null) return;
247
+ const table = raw;
248
+ for (const key of Object.keys(table)) {
249
+ if (!TOP_LEVEL_KEYS.has(key)) warnUnknownKey(key, filePath, TOP_LEVEL_KEYS);
250
+ }
251
+ for (const [section, { keys, array }] of Object.entries(SECTION_KEYS)) {
252
+ if (scope === "project" && !PROJECT_ALLOWED_KEYS.has(section)) continue;
253
+ const value = table[section];
254
+ if (array) {
255
+ if (!Array.isArray(value)) continue;
256
+ value.forEach((entry, index) => {
257
+ if (typeof entry !== "object" || entry === null) return;
258
+ for (const key of Object.keys(entry)) {
259
+ if (!keys.has(key)) warnUnknownKey(`${section}[${index}].${key}`, filePath, keys);
260
+ }
261
+ });
262
+ continue;
263
+ }
264
+ if (typeof value !== "object" || value === null) continue;
265
+ for (const key of Object.keys(value)) {
266
+ if (!keys.has(key)) warnUnknownKey(`${section}.${key}`, filePath, keys);
267
+ }
268
+ }
269
+ }
124
270
  function warnProject(filePath, message) {
125
271
  process.stderr.write(`[star] ${message} in ${filePath}
126
272
  `);
@@ -181,19 +327,56 @@ function sanitizeProjectConfig(project, filePath) {
181
327
  }
182
328
  return sanitized;
183
329
  }
184
- function mergeProjectConfig(global, project) {
330
+ function mergeModelsByName(globalModels, projectModels) {
331
+ const projectByName = new Map(projectModels.map((model) => [model.name, model]));
332
+ const merged = globalModels.map((model) => projectByName.get(model.name) ?? model);
333
+ const globalNames = new Set(globalModels.map((model) => model.name));
334
+ for (const model of projectModels) {
335
+ if (!globalNames.has(model.name)) merged.push(model);
336
+ }
337
+ return merged;
338
+ }
339
+ function protectiveMin(a, b) {
340
+ const result = Math.min(
341
+ a === 0 ? Number.POSITIVE_INFINITY : a,
342
+ b === 0 ? Number.POSITIVE_INFINITY : b
343
+ );
344
+ return result === Number.POSITIVE_INFINITY ? 0 : result;
345
+ }
346
+ function mergeProjectConfig(global, project, projectPath) {
185
347
  const merged = { ...global, ...project };
348
+ if (project.models !== void 0) {
349
+ merged.models = mergeModelsByName(global.models ?? [], project.models);
350
+ warnProject(projectPath, `project config overrides ${project.models.length} model(s)`);
351
+ }
186
352
  if (project.sessionBudgetUsd !== void 0 && global.sessionBudgetUsd !== void 0) {
187
353
  merged.sessionBudgetUsd = Math.min(global.sessionBudgetUsd, project.sessionBudgetUsd);
188
354
  }
189
355
  if (project.maxSteps !== void 0) {
190
- merged.maxSteps = Math.min(global.maxSteps ?? 100, project.maxSteps);
356
+ merged.maxSteps = protectiveMin(global.maxSteps ?? 100, project.maxSteps);
191
357
  }
192
358
  if (project.doomLoopThreshold !== void 0) {
193
- merged.doomLoopThreshold = Math.min(global.doomLoopThreshold ?? 3, project.doomLoopThreshold);
359
+ merged.doomLoopThreshold = protectiveMin(
360
+ global.doomLoopThreshold ?? 3,
361
+ project.doomLoopThreshold
362
+ );
194
363
  }
195
364
  return merged;
196
365
  }
366
+ function warnPlaintextApiKey(config) {
367
+ const offenders = [];
368
+ for (const provider of config.providers ?? []) {
369
+ if (provider.apiKey !== void 0) offenders.push(`provider "${provider.name}"`);
370
+ }
371
+ for (const model of config.models ?? []) {
372
+ if ("apiKey" in model && model.apiKey !== void 0) offenders.push(`model "${model.name}"`);
373
+ }
374
+ if (offenders.length === 0) return;
375
+ process.stderr.write(
376
+ `[star] plaintext apiKey in config (${offenders.join(", ")}) is deprecated: store keys in ~/.star-cli/.env and reference them with apiKeyEnv instead
377
+ `
378
+ );
379
+ }
197
380
  function applyOverrides(config, overrides) {
198
381
  if (!overrides) return config;
199
382
  const merged = { ...config };
@@ -201,22 +384,29 @@ function applyOverrides(config, overrides) {
201
384
  if (overrides.permissionMode !== void 0) merged.permissionMode = overrides.permissionMode;
202
385
  return merged;
203
386
  }
204
- function readTomlFileSync(filePath) {
387
+ function readTomlFileSync(filePath, scope) {
205
388
  if (!fs.existsSync(filePath)) return {};
206
389
  const content = fs.readFileSync(filePath, "utf8");
207
- return parseTomlFile(content, filePath);
390
+ return parseTomlFile(content, filePath, scope);
208
391
  }
209
392
  function loadConfigSync(cwd, overrides) {
210
393
  loadEnvFile();
211
- const global = readTomlFileSync(globalConfigPath());
394
+ const global = readTomlFileSync(globalConfigPath(), "global");
212
395
  const projectPath = projectConfigPath(cwd);
213
- const project = sanitizeProjectConfig(readTomlFileSync(projectPath), projectPath);
214
- const merged = applyOverrides(mergeProjectConfig(global, project), overrides);
215
- return ConfigSchema.parse(merged);
396
+ const project = sanitizeProjectConfig(readTomlFileSync(projectPath, "project"), projectPath);
397
+ const merged = applyOverrides(mergeProjectConfig(global, project, projectPath), overrides);
398
+ warnPlaintextApiKey(merged);
399
+ return wireRuntimeSettings(ConfigSchema.parse(merged));
400
+ }
401
+ function wireRuntimeSettings(config) {
402
+ registerSensitivePatterns(config.permissions.sensitive);
403
+ setWebFetchAllowPrivateHosts(config.webFetchAllowPrivateHosts);
404
+ return config;
216
405
  }
217
406
 
218
407
  // src/main.tsx
219
408
  async function createLoop(config, modelName, cwd, sessionStore) {
409
+ const [{ AgentLoop }, { createModel, reasoningEffortMetadata }, { createDefaultRegistry }] = await Promise.all([import("./loop-TW2FHL7J.js"), import("./provider-EWMUPBLH.js"), import("./tools-7EBGCYYE.js")]);
220
410
  const model = createModel(config, modelName);
221
411
  const registry = createDefaultRegistry();
222
412
  const modelConfig = resolveModelConfig(config, modelName);
@@ -227,11 +417,13 @@ async function createLoop(config, modelName, cwd, sessionStore) {
227
417
  cwd,
228
418
  system: SYSTEM_PROMPT,
229
419
  sessionStore,
420
+ modelName,
230
421
  contextMaxTokens: contextWindowTokens(config, modelName),
231
422
  providerMetadata: reasoningEffortMetadata(config, modelName),
232
423
  temperature: modelConfig.temperature,
233
424
  streamIdleTimeoutSec: modelConfig.streamIdleTimeoutSec,
234
- streamFirstChunkTimeoutSec: modelConfig.streamFirstChunkTimeoutSec
425
+ streamFirstChunkTimeoutSec: modelConfig.streamFirstChunkTimeoutSec,
426
+ vision: modelConfig.vision
235
427
  });
236
428
  }
237
429
  async function printMode(loop, prompt, cwd, json, imagePaths, sessionStore) {
@@ -281,26 +473,13 @@ async function printMode(loop, prompt, cwd, json, imagePaths, sessionStore) {
281
473
  const line = eventToJsonLine(event);
282
474
  if (line) process.stdout.write(`${line}
283
475
  `);
476
+ } else {
477
+ const line = printEventLine(event);
478
+ if (line) {
479
+ (line.stream === "stdout" ? process.stdout : process.stderr).write(line.text);
480
+ }
284
481
  }
285
482
  switch (event.type) {
286
- case "text-delta":
287
- if (!json) process.stdout.write(event.text);
288
- break;
289
- case "reasoning":
290
- break;
291
- case "tool-call":
292
- if (!json) process.stderr.write(`
293
- [tool] ${event.name} ${JSON.stringify(event.args)}
294
- `);
295
- break;
296
- case "tool-result": {
297
- if (!json) {
298
- const preview = event.content.length > 500 ? `${event.content.slice(0, 500)}... (truncated)` : event.content;
299
- process.stderr.write(`[result] ${event.isError ? "ERROR: " : ""}${preview}
300
- `);
301
- }
302
- break;
303
- }
304
483
  case "finish":
305
484
  usage.add(event.usage);
306
485
  if (event.usage) {
@@ -308,26 +487,7 @@ async function printMode(loop, prompt, cwd, json, imagePaths, sessionStore) {
308
487
  });
309
488
  }
310
489
  break;
311
- case "retry": {
312
- if (!json) {
313
- const wait = event.delayMs !== void 0 && event.delayMs >= 1e3 ? ` in ${Math.round(event.delayMs / 1e3)}s` : "";
314
- process.stderr.write(
315
- `
316
- [retry ${event.attempt}/${event.maxAttempts}${wait}] ${event.reason}
317
- `
318
- );
319
- }
320
- break;
321
- }
322
- case "notice":
323
- if (!json) process.stderr.write(`
324
- [notice] ${event.message}
325
- `);
326
- break;
327
490
  case "error":
328
- if (!json) process.stderr.write(`
329
- [error] ${formatStreamError(event.error)}
330
- `);
331
491
  exitCode = 1;
332
492
  break;
333
493
  }
@@ -377,6 +537,7 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
377
537
  "--clear-sessions [scope]",
378
538
  "delete stored sessions for the current directory ('all' deletes every session) and exit"
379
539
  ).action(async (opts) => {
540
+ installStdoutEpipeGuard();
380
541
  const cwd = process.cwd();
381
542
  if (opts.json && opts.print === void 0) {
382
543
  console.error(
@@ -384,18 +545,17 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
384
545
  );
385
546
  process.exit(1);
386
547
  }
548
+ const imageError = imageRequiresPrintError(opts.print, opts.image ?? []);
549
+ if (imageError) {
550
+ console.error(imageError);
551
+ process.exit(1);
552
+ }
387
553
  if (opts.resume !== void 0 && opts.continue) {
388
554
  console.error("Options --resume and --continue are mutually exclusive: pick one.");
389
555
  process.exit(1);
390
556
  }
391
557
  if (opts.resume === true) {
392
- const entries = await listSessionEntries(cwd);
393
- if (entries.length === 0) {
394
- console.error("No sessions found for this directory.");
395
- } else {
396
- console.log(formatSessionEntries(entries));
397
- }
398
- process.exit(0);
558
+ process.exit(await reportResumeSessions(cwd));
399
559
  }
400
560
  if (opts.clearSessions !== void 0) {
401
561
  const scope = opts.clearSessions;
@@ -417,13 +577,6 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
417
577
  console.error(error instanceof Error ? error.message : String(error));
418
578
  process.exit(1);
419
579
  }
420
- const modelName = opts.model ?? config.defaultModel;
421
- if (!modelName) {
422
- console.error(
423
- "No model configured. Add one to ~/.star-cli/config.toml or pass --model.\nSee README.md for configuration examples."
424
- );
425
- process.exit(1);
426
- }
427
580
  let sessionStore = null;
428
581
  let resumed = null;
429
582
  let resumeId = null;
@@ -447,14 +600,31 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
447
600
  console.error(`Session not found: ${resumeId}`);
448
601
  process.exit(1);
449
602
  }
450
- const [meta, loaded] = await Promise.all([sessionStore.meta(), sessionStore.messages()]);
451
- const messages = reconcileToolCalls(loaded);
603
+ const [meta, loaded] = await Promise.all([sessionStore.meta(), sessionStore.starMessages()]);
604
+ const messages = reconcileStarMessages(loaded);
452
605
  if (messages.length !== loaded.length) {
453
606
  await sessionStore.replaceMessages(messages);
454
607
  }
455
608
  resumed = { meta, messages };
456
- } else if (!opts.print) {
609
+ setTodoSession(resumeId);
610
+ }
611
+ const { name: modelName, notice: modelNotice } = resolveStartupModel(
612
+ config,
613
+ opts.model ? void 0 : resumed?.meta.model
614
+ );
615
+ if (modelNotice) {
616
+ process.stderr.write(`${modelNotice}
617
+ `);
618
+ }
619
+ if (!modelName) {
620
+ console.error(
621
+ "No model configured. Add one to ~/.star-cli/config.toml or pass --model.\nSee README.md for configuration examples."
622
+ );
623
+ process.exit(1);
624
+ }
625
+ if (!resumeId && !opts.print) {
457
626
  sessionStore = await SessionStore.create(cwd, modelName);
627
+ setTodoSession(sessionStore.id);
458
628
  }
459
629
  let loop;
460
630
  try {
@@ -465,7 +635,7 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
465
635
  }
466
636
  if (resumed) {
467
637
  await loop.loadMessages(resumed.messages);
468
- await loadTodos(cwd);
638
+ await loadTodos(cwd, resumeId ?? void 0);
469
639
  if (sessionStore) {
470
640
  hydrateSnapshots(await loadSessionSnapshots(sessionStore.dir));
471
641
  }
@@ -481,14 +651,22 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
481
651
  );
482
652
  return;
483
653
  }
484
- const { renderRepl } = await import("./repl-YA23CK5R.js");
654
+ const ttyError = interactiveRequiresTtyError({
655
+ stdinTTY: Boolean(process.stdin.isTTY),
656
+ stdoutTTY: Boolean(process.stdout.isTTY)
657
+ });
658
+ if (ttyError) {
659
+ console.error(ttyError);
660
+ process.exit(1);
661
+ }
662
+ const { renderRepl } = await import("./repl-GBVIW5QR.js");
485
663
  renderRepl(loop, {
486
664
  model: modelName,
487
665
  permissionMode: config.permissionMode,
488
666
  config,
489
667
  cwd,
490
668
  sessionStore,
491
- initialMessages: resumed?.messages,
669
+ initialMessages: resumed ? toCoreMessages(resumed.messages) : void 0,
492
670
  initialUsage: resumed?.meta.usage
493
671
  });
494
672
  });