@mariozechner/pi-coding-agent 0.8.4 → 0.9.0

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
@@ -18,16 +18,6 @@ const __filename = fileURLToPath(import.meta.url);
18
18
  const __dirname = dirname(__filename);
19
19
  const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
20
20
  const VERSION = packageJson.version;
21
- const envApiKeyMap = {
22
- google: ["GEMINI_API_KEY"],
23
- openai: ["OPENAI_API_KEY"],
24
- anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
25
- xai: ["XAI_API_KEY"],
26
- groq: ["GROQ_API_KEY"],
27
- cerebras: ["CEREBRAS_API_KEY"],
28
- openrouter: ["OPENROUTER_API_KEY"],
29
- zai: ["ZAI_API_KEY"],
30
- };
31
21
  const defaultModelPerProvider = {
32
22
  anthropic: "claude-sonnet-4-5",
33
23
  openai: "gpt-5.1-codex",
@@ -80,6 +70,18 @@ function parseArgs(args) {
80
70
  else if (arg === "--models" && i + 1 < args.length) {
81
71
  result.models = args[++i].split(",").map((s) => s.trim());
82
72
  }
73
+ else if (arg === "--thinking" && i + 1 < args.length) {
74
+ const level = args[++i];
75
+ if (level === "off" || level === "minimal" || level === "low" || level === "medium" || level === "high") {
76
+ result.thinking = level;
77
+ }
78
+ else {
79
+ console.error(chalk.yellow(`Warning: Invalid thinking level "${level}". Valid values: off, minimal, low, medium, high`));
80
+ }
81
+ }
82
+ else if (arg === "--print" || arg === "-p") {
83
+ result.print = true;
84
+ }
83
85
  else if (!arg.startsWith("-")) {
84
86
  result.messages.push(arg);
85
87
  }
@@ -98,21 +100,26 @@ ${chalk.bold("Options:")}
98
100
  --api-key <key> API key (defaults to env vars)
99
101
  --system-prompt <text> System prompt (default: coding assistant prompt)
100
102
  --mode <mode> Output mode: text (default), json, or rpc
103
+ --print, -p Non-interactive mode: process prompt and exit
101
104
  --continue, -c Continue previous session
102
105
  --resume, -r Select a session to resume
103
106
  --session <path> Use specific session file
104
107
  --no-session Don't save session (ephemeral)
105
108
  --models <patterns> Comma-separated model patterns for quick cycling with Ctrl+P
109
+ --thinking <level> Set thinking level: off, minimal, low, medium, high
106
110
  --help, -h Show this help
107
111
 
108
112
  ${chalk.bold("Examples:")}
109
- # Interactive mode (no messages = interactive TUI)
113
+ # Interactive mode
110
114
  pi
111
115
 
112
- # Single message
116
+ # Interactive mode with initial prompt
113
117
  pi "List all .ts files in src/"
114
118
 
115
- # Multiple messages
119
+ # Non-interactive mode (process and exit)
120
+ pi -p "List all .ts files in src/"
121
+
122
+ # Multiple messages (interactive)
116
123
  pi "Read package.json" "What dependencies do we have?"
117
124
 
118
125
  # Continue previous session
@@ -124,6 +131,12 @@ ${chalk.bold("Examples:")}
124
131
  # Limit model cycling to specific models
125
132
  pi --models claude-sonnet,claude-haiku,gpt-4o
126
133
 
134
+ # Cycle models with fixed thinking levels
135
+ pi --models sonnet:high,haiku:low
136
+
137
+ # Start with a specific thinking level
138
+ pi --thinking high "Solve this complex problem"
139
+
127
140
  ${chalk.bold("Environment Variables:")}
128
141
  ANTHROPIC_API_KEY - Anthropic Claude API key
129
142
  ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)
@@ -307,7 +320,8 @@ async function checkForNewVersion(currentVersion) {
307
320
  }
308
321
  }
309
322
  /**
310
- * Resolve model patterns to actual Model objects
323
+ * Resolve model patterns to actual Model objects with optional thinking levels
324
+ * Format: "pattern:level" where :level is optional
311
325
  * For each pattern, finds all matching models and picks the best version:
312
326
  * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929)
313
327
  * 2. If no alias, pick the latest dated version
@@ -320,10 +334,47 @@ async function resolveModelScope(patterns) {
320
334
  }
321
335
  const scopedModels = [];
322
336
  for (const pattern of patterns) {
323
- // Find all models matching this pattern (case-insensitive partial match)
324
- const matches = availableModels.filter((m) => m.id.toLowerCase().includes(pattern.toLowerCase()) || m.name?.toLowerCase().includes(pattern.toLowerCase()));
337
+ // Parse pattern:level format
338
+ const parts = pattern.split(":");
339
+ const modelPattern = parts[0];
340
+ let thinkingLevel = "off";
341
+ if (parts.length > 1) {
342
+ const level = parts[1];
343
+ if (level === "off" || level === "minimal" || level === "low" || level === "medium" || level === "high") {
344
+ thinkingLevel = level;
345
+ }
346
+ else {
347
+ console.warn(chalk.yellow(`Warning: Invalid thinking level "${level}" in pattern "${pattern}". Using "off" instead.`));
348
+ }
349
+ }
350
+ // Check for provider/modelId format (provider is everything before the first /)
351
+ const slashIndex = modelPattern.indexOf("/");
352
+ if (slashIndex !== -1) {
353
+ const provider = modelPattern.substring(0, slashIndex);
354
+ const modelId = modelPattern.substring(slashIndex + 1);
355
+ const providerMatch = availableModels.find((m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase());
356
+ if (providerMatch) {
357
+ if (!scopedModels.find((sm) => sm.model.id === providerMatch.id && sm.model.provider === providerMatch.provider)) {
358
+ scopedModels.push({ model: providerMatch, thinkingLevel });
359
+ }
360
+ continue;
361
+ }
362
+ // No exact provider/model match - fall through to other matching
363
+ }
364
+ // Check for exact ID match (case-insensitive)
365
+ const exactMatch = availableModels.find((m) => m.id.toLowerCase() === modelPattern.toLowerCase());
366
+ if (exactMatch) {
367
+ // Exact match found - use it directly
368
+ if (!scopedModels.find((sm) => sm.model.id === exactMatch.id && sm.model.provider === exactMatch.provider)) {
369
+ scopedModels.push({ model: exactMatch, thinkingLevel });
370
+ }
371
+ continue;
372
+ }
373
+ // No exact match - fall back to partial matching
374
+ const matches = availableModels.filter((m) => m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
375
+ m.name?.toLowerCase().includes(modelPattern.toLowerCase()));
325
376
  if (matches.length === 0) {
326
- console.warn(chalk.yellow(`Warning: No models match pattern "${pattern}"`));
377
+ console.warn(chalk.yellow(`Warning: No models match pattern "${modelPattern}"`));
327
378
  continue;
328
379
  }
329
380
  // Helper to check if a model ID looks like an alias (no date suffix)
@@ -351,8 +402,8 @@ async function resolveModelScope(patterns) {
351
402
  bestMatch = datedVersions[0];
352
403
  }
353
404
  // Avoid duplicates
354
- if (!scopedModels.find((m) => m.id === bestMatch.id && m.provider === bestMatch.provider)) {
355
- scopedModels.push(bestMatch);
405
+ if (!scopedModels.find((sm) => sm.model.id === bestMatch.id && sm.model.provider === bestMatch.provider)) {
406
+ scopedModels.push({ model: bestMatch, thinkingLevel });
356
407
  }
357
408
  }
358
409
  return scopedModels;
@@ -379,25 +430,26 @@ async function selectSession(sessionManager) {
379
430
  ui.start();
380
431
  });
381
432
  }
382
- async function runInteractiveMode(agent, sessionManager, settingsManager, version, changelogMarkdown = null, modelFallbackMessage = null, newVersion = null, scopedModels = []) {
433
+ async function runInteractiveMode(agent, sessionManager, settingsManager, version, changelogMarkdown = null, modelFallbackMessage = null, newVersion = null, scopedModels = [], initialMessages = []) {
383
434
  const renderer = new TuiRenderer(agent, sessionManager, settingsManager, version, changelogMarkdown, newVersion, scopedModels);
384
- // Initialize TUI
435
+ // Initialize TUI (subscribes to agent events internally)
385
436
  await renderer.init();
386
- // Set interrupt callback
387
- renderer.setInterruptCallback(() => {
388
- agent.abort();
389
- });
390
437
  // Render any existing messages (from --continue mode)
391
438
  renderer.renderInitialMessages(agent.state);
392
439
  // Show model fallback warning at the end of the chat if applicable
393
440
  if (modelFallbackMessage) {
394
441
  renderer.showWarning(modelFallbackMessage);
395
442
  }
396
- // Subscribe to agent events
397
- agent.subscribe(async (event) => {
398
- // Pass all events to the renderer
399
- await renderer.handleEvent(event, agent.state);
400
- });
443
+ // Process initial messages if provided (from CLI args)
444
+ for (const message of initialMessages) {
445
+ try {
446
+ await agent.prompt(message);
447
+ }
448
+ catch (error) {
449
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
450
+ renderer.showError(errorMessage);
451
+ }
452
+ }
401
453
  // Interactive loop
402
454
  while (true) {
403
455
  const userInput = await renderer.getUserInput();
@@ -407,7 +459,8 @@ async function runInteractiveMode(agent, sessionManager, settingsManager, versio
407
459
  }
408
460
  catch (error) {
409
461
  // Display error in the TUI by adding an error message to the chat
410
- renderer.showError(error.message || "Unknown error occurred");
462
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
463
+ renderer.showError(errorMessage);
411
464
  }
412
465
  }
413
466
  }
@@ -491,13 +544,20 @@ export async function main(args) {
491
544
  // Set the selected session as the active session
492
545
  sessionManager.setSessionFile(selectedSession);
493
546
  }
547
+ // Resolve model scope early if provided (needed for initial model selection)
548
+ let scopedModels = [];
549
+ if (parsed.models && parsed.models.length > 0) {
550
+ scopedModels = await resolveModelScope(parsed.models);
551
+ }
494
552
  // Determine initial model using priority system:
495
553
  // 1. CLI args (--provider and --model)
496
- // 2. Restored from session (if --continue or --resume)
497
- // 3. Saved default from settings.json
498
- // 4. First available model with valid API key
499
- // 5. null (allowed in interactive mode)
554
+ // 2. First model from --models scope
555
+ // 3. Restored from session (if --continue or --resume)
556
+ // 4. Saved default from settings.json
557
+ // 5. First available model with valid API key
558
+ // 6. null (allowed in interactive mode)
500
559
  let initialModel = null;
560
+ let initialThinking = "off";
501
561
  if (parsed.provider && parsed.model) {
502
562
  // 1. CLI args take priority
503
563
  const { model, error } = findModel(parsed.provider, parsed.model);
@@ -511,8 +571,13 @@ export async function main(args) {
511
571
  }
512
572
  initialModel = model;
513
573
  }
574
+ else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
575
+ // 2. Use first model from --models scope (skip if continuing/resuming session)
576
+ initialModel = scopedModels[0].model;
577
+ initialThinking = scopedModels[0].thinkingLevel;
578
+ }
514
579
  else if (parsed.continue || parsed.resume) {
515
- // 2. Restore from session (will be handled below after loading session)
580
+ // 3. Restore from session (will be handled below after loading session)
516
581
  // Leave initialModel as null for now
517
582
  }
518
583
  if (!initialModel) {
@@ -526,6 +591,11 @@ export async function main(args) {
526
591
  process.exit(1);
527
592
  }
528
593
  initialModel = model;
594
+ // Also load saved thinking level if we're using saved model
595
+ const savedThinking = settingsManager.getDefaultThinkingLevel();
596
+ if (savedThinking) {
597
+ initialThinking = savedThinking;
598
+ }
529
599
  }
530
600
  }
531
601
  if (!initialModel) {
@@ -553,7 +623,9 @@ export async function main(args) {
553
623
  }
554
624
  }
555
625
  // Determine mode early to know if we should print messages and fail early
556
- const isInteractive = parsed.messages.length === 0 && parsed.mode === undefined;
626
+ // Interactive mode: no --print flag and no --mode flag
627
+ // Having initial messages doesn't make it non-interactive anymore
628
+ const isInteractive = !parsed.print && parsed.mode === undefined;
557
629
  const mode = parsed.mode || "text";
558
630
  const shouldPrintMessages = isInteractive || mode === "text";
559
631
  // Non-interactive mode: fail early if no model available
@@ -576,10 +648,6 @@ export async function main(args) {
576
648
  // Load previous messages if continuing or resuming
577
649
  // This may update initialModel if restoring from session
578
650
  if (parsed.continue || parsed.resume) {
579
- const messages = sessionManager.loadMessages();
580
- if (messages.length > 0 && shouldPrintMessages) {
581
- console.log(chalk.dim(`Loaded ${messages.length} messages from previous session`));
582
- }
583
651
  // Load and restore model (overrides initialModel if found and has API key)
584
652
  const savedModel = sessionManager.loadModel();
585
653
  if (savedModel) {
@@ -644,12 +712,16 @@ export async function main(args) {
644
712
  }
645
713
  }
646
714
  }
715
+ // CLI --thinking flag takes highest priority
716
+ if (parsed.thinking) {
717
+ initialThinking = parsed.thinking;
718
+ }
647
719
  // Create agent (initialModel can be null in interactive mode)
648
720
  const agent = new Agent({
649
721
  initialState: {
650
722
  systemPrompt,
651
723
  model: initialModel, // Can be null
652
- thinkingLevel: "off",
724
+ thinkingLevel: initialThinking,
653
725
  tools: codingTools,
654
726
  },
655
727
  queueMode: settingsManager.getQueueMode(),
@@ -673,6 +745,10 @@ export async function main(args) {
673
745
  },
674
746
  }),
675
747
  });
748
+ // If initial thinking was requested but model doesn't support it, silently reset to off
749
+ if (initialThinking !== "off" && initialModel && !initialModel.reasoning) {
750
+ agent.setThinkingLevel("off");
751
+ }
676
752
  // Track if we had to fall back from saved model (to show in chat later)
677
753
  let modelFallbackMessage = null;
678
754
  // Load previous messages if continuing or resuming
@@ -706,8 +782,6 @@ export async function main(args) {
706
782
  }
707
783
  }
708
784
  }
709
- // Note: Session will be started lazily after first user+assistant message exchange
710
- // (unless continuing/resuming, in which case it's already initialized)
711
785
  // Log loaded context files (they're already in the system prompt)
712
786
  if (shouldPrintMessages && !parsed.continue && !parsed.resume) {
713
787
  const contextFiles = loadProjectContextFiles();
@@ -718,17 +792,6 @@ export async function main(args) {
718
792
  }
719
793
  }
720
794
  }
721
- // Subscribe to agent events to save messages
722
- agent.subscribe((event) => {
723
- // Save messages on completion
724
- if (event.type === "message_end") {
725
- sessionManager.saveMessage(event.message);
726
- // Check if we should initialize session now (after first user+assistant exchange)
727
- if (sessionManager.shouldInitializeSession(agent.state.messages)) {
728
- sessionManager.startSession(agent.state);
729
- }
730
- }
731
- });
732
795
  // Route to appropriate mode
733
796
  if (mode === "rpc") {
734
797
  // RPC mode - headless operation
@@ -762,8 +825,6 @@ export async function main(args) {
762
825
  }
763
826
  else {
764
827
  // Parse current and last versions
765
- const currentParts = VERSION.split(".").map(Number);
766
- const current = { major: currentParts[0] || 0, minor: currentParts[1] || 0, patch: currentParts[2] || 0 };
767
828
  const changelogPath = getChangelogPath();
768
829
  const entries = parseChangelog(changelogPath);
769
830
  const newEntries = getNewEntries(entries, lastVersion);
@@ -773,19 +834,21 @@ export async function main(args) {
773
834
  }
774
835
  }
775
836
  }
776
- // Resolve model scope if provided
777
- let scopedModels = [];
778
- if (parsed.models && parsed.models.length > 0) {
779
- scopedModels = await resolveModelScope(parsed.models);
780
- if (scopedModels.length > 0) {
781
- console.log(chalk.dim(`Model scope: ${scopedModels.map((m) => m.id).join(", ")} ${chalk.gray("(Ctrl+P to cycle)")}`));
782
- }
783
- }
784
- // No messages and not RPC - use TUI
785
- await runInteractiveMode(agent, sessionManager, settingsManager, VERSION, changelogMarkdown, modelFallbackMessage, newVersion, scopedModels);
837
+ // Show model scope if provided
838
+ if (scopedModels.length > 0) {
839
+ const modelList = scopedModels
840
+ .map((sm) => {
841
+ const thinkingStr = sm.thinkingLevel !== "off" ? `:${sm.thinkingLevel}` : "";
842
+ return `${sm.model.id}${thinkingStr}`;
843
+ })
844
+ .join(", ");
845
+ console.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`));
846
+ }
847
+ // Interactive mode - use TUI (may have initial messages from CLI args)
848
+ await runInteractiveMode(agent, sessionManager, settingsManager, VERSION, changelogMarkdown, modelFallbackMessage, newVersion, scopedModels, parsed.messages);
786
849
  }
787
850
  else {
788
- // CLI mode with messages
851
+ // Non-interactive mode (--print flag or --mode flag)
789
852
  await runSingleShotMode(agent, sessionManager, parsed.messages, mode);
790
853
  }
791
854
  }