@oh-my-pi/pi-coding-agent 11.0.3 → 11.2.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.
Files changed (143) hide show
  1. package/CHANGELOG.md +199 -49
  2. package/README.md +1 -1
  3. package/docs/config-usage.md +3 -4
  4. package/docs/sdk.md +6 -5
  5. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  6. package/examples/sdk/README.md +1 -1
  7. package/package.json +19 -11
  8. package/src/cli/args.ts +11 -94
  9. package/src/cli/config-cli.ts +1 -1
  10. package/src/cli/file-processor.ts +3 -3
  11. package/src/cli/oclif-help.ts +26 -0
  12. package/src/cli/web-search-cli.ts +148 -0
  13. package/src/cli.ts +8 -2
  14. package/src/commands/commit.ts +36 -0
  15. package/src/commands/config.ts +51 -0
  16. package/src/commands/grep.ts +41 -0
  17. package/src/commands/index/index.ts +136 -0
  18. package/src/commands/jupyter.ts +32 -0
  19. package/src/commands/plugin.ts +70 -0
  20. package/src/commands/setup.ts +39 -0
  21. package/src/commands/shell.ts +29 -0
  22. package/src/commands/stats.ts +29 -0
  23. package/src/commands/update.ts +21 -0
  24. package/src/commands/web-search.ts +50 -0
  25. package/src/commit/agentic/index.ts +3 -2
  26. package/src/commit/agentic/tools/analyze-file.ts +1 -3
  27. package/src/commit/git/errors.ts +4 -6
  28. package/src/commit/pipeline.ts +3 -2
  29. package/src/config/keybindings.ts +1 -3
  30. package/src/config/model-registry.ts +89 -162
  31. package/src/config/settings-schema.ts +10 -0
  32. package/src/config.ts +202 -132
  33. package/src/exa/mcp-client.ts +8 -41
  34. package/src/export/html/index.ts +1 -1
  35. package/src/extensibility/extensions/loader.ts +7 -10
  36. package/src/extensibility/extensions/runner.ts +5 -15
  37. package/src/extensibility/extensions/types.ts +1 -1
  38. package/src/extensibility/hooks/runner.ts +6 -9
  39. package/src/index.ts +0 -1
  40. package/src/ipy/kernel.ts +10 -22
  41. package/src/lsp/clients/biome-client.ts +4 -7
  42. package/src/lsp/clients/lsp-linter-client.ts +4 -6
  43. package/src/lsp/index.ts +5 -4
  44. package/src/lsp/utils.ts +18 -0
  45. package/src/main.ts +86 -181
  46. package/src/mcp/json-rpc.ts +2 -2
  47. package/src/mcp/transports/http.ts +12 -49
  48. package/src/modes/components/armin.ts +1 -3
  49. package/src/modes/components/assistant-message.ts +4 -4
  50. package/src/modes/components/bash-execution.ts +5 -3
  51. package/src/modes/components/branch-summary-message.ts +1 -3
  52. package/src/modes/components/compaction-summary-message.ts +1 -3
  53. package/src/modes/components/custom-message.ts +4 -5
  54. package/src/modes/components/extensions/extension-dashboard.ts +10 -16
  55. package/src/modes/components/extensions/extension-list.ts +5 -5
  56. package/src/modes/components/footer.ts +1 -4
  57. package/src/modes/components/hook-editor.ts +7 -32
  58. package/src/modes/components/hook-message.ts +4 -5
  59. package/src/modes/components/model-selector.ts +2 -2
  60. package/src/modes/components/plugin-settings.ts +16 -20
  61. package/src/modes/components/python-execution.ts +5 -5
  62. package/src/modes/components/session-selector.ts +6 -7
  63. package/src/modes/components/settings-defs.ts +49 -40
  64. package/src/modes/components/settings-selector.ts +8 -17
  65. package/src/modes/components/skill-message.ts +1 -3
  66. package/src/modes/components/status-line-segment-editor.ts +1 -3
  67. package/src/modes/components/status-line.ts +1 -3
  68. package/src/modes/components/todo-reminder.ts +5 -7
  69. package/src/modes/components/tree-selector.ts +10 -12
  70. package/src/modes/components/ttsr-notification.ts +1 -3
  71. package/src/modes/components/user-message-selector.ts +2 -4
  72. package/src/modes/components/welcome.ts +6 -18
  73. package/src/modes/controllers/event-controller.ts +1 -0
  74. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  75. package/src/modes/controllers/input-controller.ts +7 -34
  76. package/src/modes/controllers/selector-controller.ts +3 -3
  77. package/src/modes/interactive-mode.ts +27 -1
  78. package/src/modes/rpc/rpc-client.ts +2 -5
  79. package/src/modes/rpc/rpc-mode.ts +2 -2
  80. package/src/modes/theme/theme.ts +2 -6
  81. package/src/modes/types.ts +1 -0
  82. package/src/modes/utils/ui-helpers.ts +6 -1
  83. package/src/patch/index.ts +1 -4
  84. package/src/prompts/agents/explore.md +1 -0
  85. package/src/prompts/agents/frontmatter.md +2 -1
  86. package/src/prompts/agents/init.md +1 -0
  87. package/src/prompts/agents/plan.md +1 -0
  88. package/src/prompts/agents/reviewer.md +1 -0
  89. package/src/prompts/system/subagent-submit-reminder.md +2 -0
  90. package/src/prompts/system/subagent-system-prompt.md +2 -0
  91. package/src/prompts/system/subagent-user-prompt.md +8 -0
  92. package/src/prompts/system/system-prompt.md +5 -3
  93. package/src/prompts/system/web-search.md +6 -4
  94. package/src/prompts/tools/task.md +216 -163
  95. package/src/sdk.ts +11 -110
  96. package/src/session/agent-session.ts +117 -83
  97. package/src/session/auth-storage.ts +10 -51
  98. package/src/session/messages.ts +17 -3
  99. package/src/session/session-manager.ts +30 -30
  100. package/src/session/streaming-output.ts +1 -1
  101. package/src/ssh/ssh-executor.ts +6 -3
  102. package/src/task/agents.ts +2 -0
  103. package/src/task/discovery.ts +1 -1
  104. package/src/task/executor.ts +5 -10
  105. package/src/task/index.ts +43 -23
  106. package/src/task/render.ts +67 -64
  107. package/src/task/template.ts +17 -34
  108. package/src/task/types.ts +49 -22
  109. package/src/tools/ask.ts +1 -3
  110. package/src/tools/bash.ts +1 -4
  111. package/src/tools/browser.ts +5 -7
  112. package/src/tools/exit-plan-mode.ts +1 -4
  113. package/src/tools/fetch.ts +1 -3
  114. package/src/tools/find.ts +4 -3
  115. package/src/tools/gemini-image.ts +24 -55
  116. package/src/tools/grep.ts +4 -4
  117. package/src/tools/index.ts +12 -14
  118. package/src/tools/notebook.ts +1 -5
  119. package/src/tools/python.ts +4 -3
  120. package/src/tools/read.ts +2 -4
  121. package/src/tools/render-utils.ts +23 -0
  122. package/src/tools/ssh.ts +8 -12
  123. package/src/tools/todo-write.ts +1 -4
  124. package/src/tools/tool-errors.ts +1 -4
  125. package/src/tools/write.ts +1 -3
  126. package/src/utils/external-editor.ts +59 -0
  127. package/src/utils/file-mentions.ts +39 -1
  128. package/src/utils/image-convert.ts +1 -1
  129. package/src/utils/image-resize.ts +4 -4
  130. package/src/web/search/auth.ts +3 -33
  131. package/src/web/search/index.ts +73 -139
  132. package/src/web/search/provider.ts +58 -0
  133. package/src/web/search/providers/anthropic.ts +53 -14
  134. package/src/web/search/providers/base.ts +22 -0
  135. package/src/web/search/providers/codex.ts +38 -16
  136. package/src/web/search/providers/exa.ts +30 -6
  137. package/src/web/search/providers/gemini.ts +56 -20
  138. package/src/web/search/providers/jina.ts +28 -5
  139. package/src/web/search/providers/perplexity.ts +103 -36
  140. package/src/web/search/render.ts +84 -74
  141. package/src/web/search/types.ts +285 -59
  142. package/src/migrations.ts +0 -175
  143. package/src/session/storage-migration.ts +0 -173
package/src/lsp/index.ts CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  formatLocation,
51
51
  formatSymbolInformation,
52
52
  formatWorkspaceEdit,
53
+ sortDiagnostics,
53
54
  symbolKindToIcon,
54
55
  } from "./utils";
55
56
 
@@ -458,6 +459,7 @@ async function getDiagnosticsForFile(
458
459
  }
459
460
  }
460
461
 
462
+ sortDiagnostics(uniqueDiagnostics);
461
463
  const formatted = uniqueDiagnostics.map(d => formatDiagnostic(d, relPath));
462
464
  const limited = limitDiagnosticMessages(formatted);
463
465
  const summary = formatDiagnosticsSummary(uniqueDiagnostics);
@@ -858,10 +860,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
858
860
  public readonly mergeCallAndResult = true;
859
861
  public readonly inline = true;
860
862
 
861
- private readonly session: ToolSession;
862
-
863
- constructor(session: ToolSession) {
864
- this.session = session;
863
+ constructor(private readonly session: ToolSession) {
865
864
  this.description = renderPromptTemplate(lspDescription);
866
865
  }
867
866
 
@@ -966,6 +965,8 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
966
965
  }
967
966
  }
968
967
 
968
+ sortDiagnostics(uniqueDiagnostics);
969
+
969
970
  if (!detailed && targets.length === 1) {
970
971
  if (uniqueDiagnostics.length === 0) {
971
972
  return {
package/src/lsp/utils.ts CHANGED
@@ -205,6 +205,24 @@ export function severityToString(severity?: DiagnosticSeverity): string {
205
205
  return SEVERITY_NAMES[severity ?? 1] ?? "unknown";
206
206
  }
207
207
 
208
+ /**
209
+ * Sort diagnostics by severity, then by location and message.
210
+ */
211
+ export function sortDiagnostics(diagnostics: Diagnostic[]): Diagnostic[] {
212
+ return diagnostics.sort((a, b) => {
213
+ const aSeverity = a.severity ?? 1;
214
+ const bSeverity = b.severity ?? 1;
215
+ if (aSeverity !== bSeverity) return aSeverity - bSeverity;
216
+ const aLine = a.range.start.line;
217
+ const bLine = b.range.start.line;
218
+ if (aLine !== bLine) return aLine - bLine;
219
+ const aCol = a.range.start.character;
220
+ const bCol = b.range.start.character;
221
+ if (aCol !== bCol) return aCol - bCol;
222
+ return a.message.localeCompare(b.message);
223
+ });
224
+ }
225
+
208
226
  /**
209
227
  * Get icon for diagnostic severity.
210
228
  */
package/src/main.ts CHANGED
@@ -8,34 +8,24 @@ import * as fs from "node:fs/promises";
8
8
  import * as os from "node:os";
9
9
  import * as path from "node:path";
10
10
  import { createInterface } from "node:readline/promises";
11
+ import { run } from "@oclif/core";
11
12
  import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
12
13
  import { $env, postmortem } from "@oh-my-pi/pi-utils";
13
14
  import chalk from "chalk";
14
- import { type Args, parseArgs, printHelp } from "./cli/args";
15
- import { parseConfigArgs, printConfigHelp, runConfigCommand } from "./cli/config-cli";
15
+ import type { Args } from "./cli/args";
16
16
  import { processFileArguments } from "./cli/file-processor";
17
- import { parseGrepArgs, printGrepHelp, runGrepCommand } from "./cli/grep-cli";
18
- import { parseJupyterArgs, printJupyterHelp, runJupyterCommand } from "./cli/jupyter-cli";
19
17
  import { listModels } from "./cli/list-models";
20
- import { parsePluginArgs, printPluginHelp, runPluginCommand } from "./cli/plugin-cli";
21
18
  import { selectSession } from "./cli/session-picker";
22
- import { parseSetupArgs, printSetupHelp, runSetupCommand } from "./cli/setup-cli";
23
- import { parseShellArgs, printShellHelp, runShellCommand } from "./cli/shell-cli";
24
- import { parseStatsArgs, printStatsHelp, runStatsCommand } from "./cli/stats-cli";
25
- import { parseUpdateArgs, printUpdateHelp, runUpdateCommand } from "./cli/update-cli";
26
- import { runCommitCommand } from "./commit";
27
- import { parseCommitArgs, printCommitHelp } from "./commit/cli";
28
- import { findConfigFile, getModelsPath, VERSION } from "./config";
29
- import type { ModelRegistry } from "./config/model-registry";
19
+ import { findConfigFile, VERSION } from "./config";
20
+ import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
30
21
  import { parseModelPattern, parseModelString, resolveModelScope, type ScopedModel } from "./config/model-resolver";
31
22
  import { Settings, settings } from "./config/settings";
32
23
  import { initializeWithSettings } from "./discovery";
33
24
  import { exportFromFile } from "./export/html";
34
25
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
35
- import { runMigrations, showDeprecationWarnings } from "./migrations";
36
26
  import { InteractiveMode, runPrintMode, runRpcMode } from "./modes";
37
27
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
38
- import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage, discoverModels } from "./sdk";
28
+ import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage } from "./sdk";
39
29
  import type { AgentSession } from "./session/agent-session";
40
30
  import { type SessionInfo, SessionManager } from "./session/session-manager";
41
31
  import { resolvePromptInput } from "./system-prompt";
@@ -82,13 +72,16 @@ async function readPipedInput(): Promise<string | undefined> {
82
72
  }
83
73
  }
84
74
 
75
+ export interface InteractiveModeNotify {
76
+ kind: "warn" | "error" | "info";
77
+ message: string;
78
+ }
79
+
85
80
  async function runInteractiveMode(
86
81
  session: AgentSession,
87
82
  version: string,
88
83
  changelogMarkdown: string | undefined,
89
- modelFallbackMessage: string | undefined,
90
- modelsJsonError: string | undefined,
91
- migratedProviders: string[],
84
+ notifs: (InteractiveModeNotify | null)[],
92
85
  versionCheckPromise: Promise<string | undefined>,
93
86
  initialMessages: string[],
94
87
  setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
@@ -111,16 +104,17 @@ async function runInteractiveMode(
111
104
 
112
105
  mode.renderInitialMessages();
113
106
 
114
- if (migratedProviders.length > 0) {
115
- mode.showWarning(`Migrated credentials to agent.db: ${migratedProviders.join(", ")}`);
116
- }
117
-
118
- if (modelsJsonError) {
119
- mode.showError(`models.json error: ${modelsJsonError}`);
120
- }
121
-
122
- if (modelFallbackMessage) {
123
- mode.showWarning(modelFallbackMessage);
107
+ for (const notify of notifs) {
108
+ if (!notify) {
109
+ continue;
110
+ }
111
+ if (notify.kind === "warn") {
112
+ mode.showWarning(notify.message);
113
+ } else if (notify.kind === "error") {
114
+ mode.showError(notify.message);
115
+ } else if (notify.kind === "info") {
116
+ mode.showStatus(notify.message);
117
+ }
124
118
  }
125
119
 
126
120
  if (initialMessage) {
@@ -477,7 +471,7 @@ async function buildSessionOptions(
477
471
  return options;
478
472
  }
479
473
 
480
- export async function main(args: string[]) {
474
+ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<void> {
481
475
  time("start");
482
476
  debugStartup("main:entry");
483
477
 
@@ -486,140 +480,34 @@ export async function main(args: string[]) {
486
480
  await initTheme();
487
481
  debugStartup("main:initTheme");
488
482
 
489
- // Handle plugin subcommand before regular parsing
490
- const pluginCmd = parsePluginArgs(args);
491
- if (pluginCmd) {
492
- if (args.includes("--help") || args.includes("-h")) {
493
- printPluginHelp();
494
- return;
495
- }
496
- await runPluginCommand(pluginCmd);
497
- return;
498
- }
499
-
500
- // Handle update subcommand
501
- const updateCmd = parseUpdateArgs(args);
502
- if (updateCmd) {
503
- if (args.includes("--help") || args.includes("-h")) {
504
- printUpdateHelp();
505
- return;
506
- }
507
- await runUpdateCommand(updateCmd);
508
- return;
509
- }
510
-
511
- // Handle config subcommand
512
- const configCmd = parseConfigArgs(args);
513
- if (configCmd) {
514
- if (args.includes("--help") || args.includes("-h")) {
515
- printConfigHelp();
516
- return;
517
- }
518
- await runConfigCommand(configCmd);
519
- return;
520
- }
521
-
522
- // Handle setup subcommand
523
- const setupCmd = parseSetupArgs(args);
524
- if (setupCmd) {
525
- if (args.includes("--help") || args.includes("-h")) {
526
- printSetupHelp();
527
- return;
528
- }
529
- await runSetupCommand(setupCmd);
530
- return;
531
- }
532
-
533
- // Handle jupyter subcommand
534
- const jupyterCmd = parseJupyterArgs(args);
535
- if (jupyterCmd) {
536
- if (args.includes("--help") || args.includes("-h")) {
537
- printJupyterHelp();
538
- return;
539
- }
540
- await runJupyterCommand(jupyterCmd);
541
- return;
542
- }
543
-
544
- // Handle stats subcommand
545
- const statsCmd = parseStatsArgs(args);
546
- if (statsCmd) {
547
- if (args.includes("--help") || args.includes("-h")) {
548
- printStatsHelp();
549
- return;
550
- }
551
- await runStatsCommand(statsCmd);
552
- return;
553
- }
554
-
555
- // Handle grep subcommand (for testing grep tool)
556
- const grepCmd = parseGrepArgs(args);
557
- if (grepCmd) {
558
- if (args.includes("--help") || args.includes("-h")) {
559
- printGrepHelp();
560
- return;
561
- }
562
- await runGrepCommand(grepCmd);
563
- return;
564
- }
565
-
566
- // Handle shell subcommand (for testing brush-core shell)
567
- const shellCmd = parseShellArgs(args);
568
- if (shellCmd) {
569
- if (args.includes("--help") || args.includes("-h")) {
570
- printShellHelp();
571
- return;
572
- }
573
- await runShellCommand(shellCmd);
574
- return;
575
- }
576
-
577
- // Handle commit subcommand
578
- const commitCmd = parseCommitArgs(args);
579
- if (commitCmd) {
580
- if (args.includes("--help") || args.includes("-h")) {
581
- printCommitHelp();
582
- return;
583
- }
584
- await runCommitCommand(commitCmd);
585
- process.exit(0);
586
- }
587
-
588
- const parsed = parseArgs(args);
483
+ const parsedArgs = parsed;
589
484
  debugStartup("main:parseArgs");
590
485
  time("parseArgs");
591
- await maybeAutoChdir(parsed);
486
+ await maybeAutoChdir(parsedArgs);
592
487
 
593
- // Run migrations (pass cwd for project-local migrations)
594
- const { migratedAuthProviders: migratedProviders, deprecationWarnings } = await runMigrations(process.cwd());
595
- debugStartup("main:runMigrations");
488
+ const notifs: (InteractiveModeNotify | null)[] = [];
596
489
 
597
490
  // Create AuthStorage and ModelRegistry upfront
598
491
  const authStorage = await discoverAuthStorage();
599
- const modelRegistry = discoverModels(authStorage);
492
+ const modelRegistry = new ModelRegistry(authStorage);
600
493
  debugStartup("main:discoverModels");
601
494
  time("discoverModels");
602
495
 
603
- if (parsed.version) {
496
+ if (parsedArgs.version) {
604
497
  writeStdout(VERSION);
605
498
  return;
606
499
  }
607
500
 
608
- if (parsed.help) {
609
- printHelp();
610
- return;
611
- }
612
-
613
- if (parsed.listModels !== undefined) {
614
- const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
501
+ if (parsedArgs.listModels !== undefined) {
502
+ const searchPattern = typeof parsedArgs.listModels === "string" ? parsedArgs.listModels : undefined;
615
503
  await listModels(modelRegistry, searchPattern);
616
504
  return;
617
505
  }
618
506
 
619
- if (parsed.export) {
507
+ if (parsedArgs.export) {
620
508
  try {
621
- const outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;
622
- const result = await exportFromFile(parsed.export, outputPath);
509
+ const outputPath = parsedArgs.messages.length > 0 ? parsedArgs.messages[0] : undefined;
510
+ const result = await exportFromFile(parsedArgs.export, outputPath);
623
511
  writeStdout(`Exported to: ${result}`);
624
512
  return;
625
513
  } catch (error: unknown) {
@@ -629,7 +517,7 @@ export async function main(args: string[]) {
629
517
  }
630
518
  }
631
519
 
632
- if (parsed.mode === "rpc" && parsed.fileArgs.length > 0) {
520
+ if (parsedArgs.mode === "rpc" && parsedArgs.fileArgs.length > 0) {
633
521
  writeStderr(chalk.red("Error: @file arguments are not supported in RPC mode"));
634
522
  process.exit(1);
635
523
  }
@@ -639,23 +527,23 @@ export async function main(args: string[]) {
639
527
  debugStartup("main:Settings.init");
640
528
  time("Settings.init");
641
529
  const pipedInput = await readPipedInput();
642
- let { initialMessage, initialImages } = await prepareInitialMessage(parsed, settings.get("images.autoResize"));
530
+ let { initialMessage, initialImages } = await prepareInitialMessage(parsedArgs, settings.get("images.autoResize"));
643
531
  if (pipedInput) {
644
532
  initialMessage = initialMessage ? `${initialMessage}\n${pipedInput}` : pipedInput;
645
533
  }
646
534
  time("prepareInitialMessage");
647
- const autoPrint = pipedInput !== undefined && !parsed.print && parsed.mode === undefined;
648
- const isInteractive = !parsed.print && !autoPrint && parsed.mode === undefined;
649
- const mode = parsed.mode || "text";
535
+ const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
536
+ const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
537
+ const mode = parsedArgs.mode || "text";
650
538
 
651
539
  // Initialize discovery system with settings for provider persistence
652
540
  initializeWithSettings(settings);
653
541
  time("initializeWithSettings");
654
542
 
655
543
  // Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
656
- const smolModel = parsed.smol ?? $env.PI_SMOL_MODEL;
657
- const slowModel = parsed.slow ?? $env.PI_SLOW_MODEL;
658
- const planModel = parsed.plan ?? $env.PI_PLAN_MODEL;
544
+ const smolModel = parsedArgs.smol ?? $env.PI_SMOL_MODEL;
545
+ const slowModel = parsedArgs.slow ?? $env.PI_SLOW_MODEL;
546
+ const planModel = parsedArgs.plan ?? $env.PI_PLAN_MODEL;
659
547
  if (smolModel || slowModel || planModel) {
660
548
  settings.overrideModelRoles({
661
549
  smol: smolModel,
@@ -668,13 +556,8 @@ export async function main(args: string[]) {
668
556
  debugStartup("main:initTheme2");
669
557
  time("initTheme");
670
558
 
671
- // Show deprecation warnings in interactive mode
672
- if (isInteractive && deprecationWarnings.length > 0) {
673
- await showDeprecationWarnings(deprecationWarnings);
674
- }
675
-
676
559
  let scopedModels: ScopedModel[] = [];
677
- const modelPatterns = parsed.models ?? settings.get("enabledModels");
560
+ const modelPatterns = parsedArgs.models ?? settings.get("enabledModels");
678
561
  const modelMatchPreferences = {
679
562
  usageOrder: settings.getStorage()?.getModelUsageOrder(),
680
563
  };
@@ -684,13 +567,13 @@ export async function main(args: string[]) {
684
567
  }
685
568
 
686
569
  // Create session manager based on CLI flags
687
- let sessionManager = await createSessionManager(parsed, cwd);
570
+ let sessionManager = await createSessionManager(parsedArgs, cwd);
688
571
  debugStartup("main:createSessionManager");
689
572
  time("createSessionManager");
690
573
 
691
574
  // Handle --resume: show session picker
692
- if (parsed.resume) {
693
- const sessions = await SessionManager.list(cwd, parsed.sessionDir);
575
+ if (parsedArgs.resume) {
576
+ const sessions = await SessionManager.list(cwd, parsedArgs.sessionDir);
694
577
  time("SessionManager.list");
695
578
  if (sessions.length === 0) {
696
579
  writeStdout(chalk.dim("No sessions found"));
@@ -705,19 +588,19 @@ export async function main(args: string[]) {
705
588
  sessionManager = await SessionManager.open(selectedPath);
706
589
  }
707
590
 
708
- const sessionOptions = await buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry);
591
+ const sessionOptions = await buildSessionOptions(parsedArgs, scopedModels, sessionManager, modelRegistry);
709
592
  debugStartup("main:buildSessionOptions");
710
593
  sessionOptions.authStorage = authStorage;
711
594
  sessionOptions.modelRegistry = modelRegistry;
712
595
  sessionOptions.hasUI = isInteractive;
713
596
 
714
597
  // Handle CLI --api-key as runtime override (not persisted)
715
- if (parsed.apiKey) {
598
+ if (parsedArgs.apiKey) {
716
599
  if (!sessionOptions.model) {
717
600
  writeStderr(chalk.red("--api-key requires a model to be specified via --provider/--model or -m/--models"));
718
601
  process.exit(1);
719
602
  }
720
- authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);
603
+ authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsedArgs.apiKey);
721
604
  }
722
605
 
723
606
  time("buildSessionOptions");
@@ -726,17 +609,36 @@ export async function main(args: string[]) {
726
609
  debugStartup("main:createAgentSession");
727
610
  time("createAgentSession");
728
611
 
612
+ if (modelFallbackMessage) {
613
+ notifs.push({ kind: "warn", message: modelFallbackMessage });
614
+ }
615
+
616
+ const modelRegistryError = modelRegistry.getError();
617
+ if (modelRegistryError) {
618
+ notifs.push({ kind: "error", message: modelRegistryError.message });
619
+ }
620
+
729
621
  // Re-parse CLI args with extension flags and apply values
730
622
  if (session.extensionRunner) {
731
623
  const extFlags = session.extensionRunner.getFlags();
732
624
  if (extFlags.size > 0) {
733
- const flagDefs = new Map<string, { type: "boolean" | "string" }>();
734
- for (const [name, flag] of extFlags) {
735
- flagDefs.set(name, { type: flag.type });
736
- }
737
- const reparsed = parseArgs(args, flagDefs);
738
- for (const [name, value] of reparsed.unknownFlags) {
739
- session.extensionRunner.setFlagValue(name, value);
625
+ for (let i = 0; i < rawArgs.length; i++) {
626
+ const arg = rawArgs[i];
627
+ if (!arg.startsWith("--")) {
628
+ continue;
629
+ }
630
+ const flagName = arg.slice(2);
631
+ const extFlag = extFlags.get(flagName);
632
+ if (!extFlag) {
633
+ continue;
634
+ }
635
+ if (extFlag.type === "boolean") {
636
+ session.extensionRunner.setFlagValue(flagName, true);
637
+ continue;
638
+ }
639
+ if (i + 1 < rawArgs.length) {
640
+ session.extensionRunner.setFlagValue(flagName, rawArgs[++i]);
641
+ }
740
642
  }
741
643
  }
742
644
  }
@@ -747,13 +649,13 @@ export async function main(args: string[]) {
747
649
  writeStderr(chalk.red("No models available."));
748
650
  writeStderr(chalk.yellow("\nSet an API key environment variable:"));
749
651
  writeStderr(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
750
- writeStderr(chalk.yellow(`\nOr create ${getModelsPath()}`));
652
+ writeStderr(chalk.yellow(`\nOr create ${ModelsConfigFile.path()}`));
751
653
  process.exit(1);
752
654
  }
753
655
 
754
656
  // Clamp thinking level to model capabilities (for CLI override case)
755
- if (session.model && parsed.thinking) {
756
- let effectiveThinking = parsed.thinking;
657
+ if (session.model && parsedArgs.thinking) {
658
+ let effectiveThinking = parsedArgs.thinking;
757
659
  if (!session.model.reasoning) {
758
660
  effectiveThinking = "off";
759
661
  } else if (effectiveThinking === "xhigh" && !supportsXhigh(session.model)) {
@@ -768,7 +670,7 @@ export async function main(args: string[]) {
768
670
  await runRpcMode(session);
769
671
  } else if (isInteractive) {
770
672
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
771
- const changelogMarkdown = await getChangelogForDisplay(parsed);
673
+ const changelogMarkdown = await getChangelogForDisplay(parsedArgs);
772
674
 
773
675
  const scopedModelsForDisplay = sessionOptions.scopedModels ?? scopedModels;
774
676
  if (scopedModelsForDisplay.length > 0) {
@@ -787,11 +689,9 @@ export async function main(args: string[]) {
787
689
  session,
788
690
  VERSION,
789
691
  changelogMarkdown,
790
- modelFallbackMessage,
791
- modelRegistry.getError(),
792
- migratedProviders,
692
+ notifs,
793
693
  versionCheckPromise,
794
- parsed.messages,
694
+ parsedArgs.messages,
795
695
  setToolUIContext,
796
696
  lspServers,
797
697
  mcpManager,
@@ -801,7 +701,7 @@ export async function main(args: string[]) {
801
701
  } else {
802
702
  await runPrintMode(session, {
803
703
  mode,
804
- messages: parsed.messages,
704
+ messages: parsedArgs.messages,
805
705
  initialMessage,
806
706
  initialImages,
807
707
  });
@@ -810,3 +710,8 @@ export async function main(args: string[]) {
810
710
  await postmortem.quit(0);
811
711
  }
812
712
  }
713
+
714
+ export async function main(args: string[]): Promise<void> {
715
+ const argv = args.length === 0 ? ["index"] : args;
716
+ await run(argv, import.meta.url);
717
+ }
@@ -13,8 +13,8 @@ export function parseSSE(text: string): unknown {
13
13
  if (line.startsWith("data: ")) {
14
14
  const data = line.slice(6).trim();
15
15
  if (data === "[DONE]") continue;
16
- const result = Bun.JSONL.parseChunk(`${data}\n`);
17
- if (result.values.length > 0) return result.values[0];
16
+ const result = JSON.parse(data) as unknown;
17
+ if (result) return result;
18
18
  }
19
19
  }
20
20
  // Fallback: try parsing entire response as JSON
@@ -4,7 +4,7 @@
4
4
  * Implements JSON-RPC 2.0 over HTTP POST with optional SSE streaming.
5
5
  * Based on MCP spec 2025-03-26.
6
6
  */
7
- import { readSseEvents } from "@oh-my-pi/pi-utils";
7
+ import { readSseJson } from "@oh-my-pi/pi-utils";
8
8
  import type {
9
9
  JsonRpcMessage,
10
10
  JsonRpcResponse,
@@ -86,26 +86,11 @@ export class HttpTransport implements MCPTransport {
86
86
  return;
87
87
  }
88
88
 
89
- let buffer = "";
90
89
  // Read SSE stream
91
- for await (const event of readSseEvents(response.body)) {
90
+ for await (const message of readSseJson<JsonRpcMessage>(response.body, this.sseConnection.signal)) {
92
91
  if (!this._connected) break;
93
- const data = event.data?.trim();
94
- if (!data || data === "[DONE]") continue;
95
- buffer += data;
96
- if (!data.endsWith("\n")) {
97
- buffer += "\n";
98
- }
99
- const result = Bun.JSONL.parseChunk(buffer);
100
- buffer = buffer.slice(result.read);
101
- if (result.error) {
102
- buffer = "";
103
- continue;
104
- }
105
- for (const message of result.values as JsonRpcMessage[]) {
106
- if ("method" in message && !("id" in message)) {
107
- this.onNotification?.(message.method, message.params);
108
- }
92
+ if ("method" in message && !("id" in message)) {
93
+ this.onNotification?.(message.method, message.params);
109
94
  }
110
95
  }
111
96
  } catch (error) {
@@ -182,40 +167,18 @@ export class HttpTransport implements MCPTransport {
182
167
  const timeout = this.config.timeout ?? 30000;
183
168
 
184
169
  const parse = async (): Promise<T> => {
185
- let buffer = "";
186
- for await (const event of readSseEvents(response.body!)) {
187
- const data = event.data?.trim();
188
- if (!data || data === "[DONE]") continue;
189
- buffer += data;
190
- if (!data.endsWith("\n")) {
191
- buffer += "\n";
192
- }
193
- const result = Bun.JSONL.parseChunk(buffer);
194
- buffer = buffer.slice(result.read);
195
- if (result.error) {
196
- buffer = "";
197
- continue;
198
- }
199
-
200
- for (const message of result.values as JsonRpcMessage[]) {
201
- if (
202
- "id" in message &&
203
- (message as JsonRpcResponse).id === expectedId &&
204
- ("result" in message || "error" in message)
205
- ) {
206
- const response = message as JsonRpcResponse;
207
- if (response.error) {
208
- throw new Error(`MCP error ${response.error.code}: ${response.error.message}`);
209
- }
210
- return response.result as T;
170
+ for await (const message of readSseJson<JsonRpcMessage>(response.body!)) {
171
+ if ("id" in message && message.id === expectedId && ("result" in message || "error" in message)) {
172
+ if (message.error) {
173
+ throw new Error(`MCP error ${message.error.code}: ${message.error.message}`);
211
174
  }
175
+ return message.result as T;
176
+ }
212
177
 
213
- if ("method" in message && !("id" in message)) {
214
- this.onNotification?.(message.method, message.params);
215
- }
178
+ if ("method" in message && !("id" in message)) {
179
+ this.onNotification?.(message.method, message.params);
216
180
  }
217
181
  }
218
-
219
182
  throw new Error(`No response received for request ID ${expectedId}`);
220
183
  };
221
184
 
@@ -57,7 +57,6 @@ function buildFinalGrid(): string[][] {
57
57
  }
58
58
 
59
59
  export class ArminComponent implements Component {
60
- private ui: TUI;
61
60
  private interval: ReturnType<typeof setInterval> | null = null;
62
61
  private effect: Effect;
63
62
  private finalGrid: string[][];
@@ -68,8 +67,7 @@ export class ArminComponent implements Component {
68
67
  private gridVersion = 0;
69
68
  private cachedVersion = -1;
70
69
 
71
- constructor(ui: TUI) {
72
- this.ui = ui;
70
+ constructor(private readonly ui: TUI) {
73
71
  this.effect = EFFECTS[Math.floor(Math.random() * EFFECTS.length)];
74
72
  this.finalGrid = buildFinalGrid();
75
73
  this.currentGrid = this.createEmptyGrid();
@@ -8,15 +8,15 @@ import { getMarkdownTheme, theme } from "../../modes/theme/theme";
8
8
  */
9
9
  export class AssistantMessageComponent extends Container {
10
10
  private contentContainer: Container;
11
- private hideThinkingBlock: boolean;
12
11
  private lastMessage?: AssistantMessage;
13
12
  private prerenderInFlight = false;
14
13
 
15
- constructor(message?: AssistantMessage, hideThinkingBlock = false) {
14
+ constructor(
15
+ message?: AssistantMessage,
16
+ private hideThinkingBlock = false,
17
+ ) {
16
18
  super();
17
19
 
18
- this.hideThinkingBlock = hideThinkingBlock;
19
-
20
20
  // Container for text/thinking content
21
21
  this.contentContainer = new Container();
22
22
  this.addChild(this.contentContainer);
@@ -12,7 +12,6 @@ import { truncateToVisualLines } from "./visual-truncate";
12
12
  const PREVIEW_LINES = 20;
13
13
 
14
14
  export class BashExecutionComponent extends Container {
15
- private command: string;
16
15
  private outputLines: string[] = [];
17
16
  private status: "running" | "complete" | "cancelled" | "error" = "running";
18
17
  private exitCode: number | undefined = undefined;
@@ -21,9 +20,12 @@ export class BashExecutionComponent extends Container {
21
20
  private expanded = false;
22
21
  private contentContainer: Container;
23
22
 
24
- constructor(command: string, ui: TUI, excludeFromContext = false) {
23
+ constructor(
24
+ private readonly command: string,
25
+ ui: TUI,
26
+ excludeFromContext = false,
27
+ ) {
25
28
  super();
26
- this.command = command;
27
29
 
28
30
  // Use dim border for excluded-from-context commands (!! prefix)
29
31
  const colorKey = excludeFromContext ? "dim" : "bashMode";
@@ -8,11 +8,9 @@ import type { BranchSummaryMessage } from "../../session/messages";
8
8
  */
9
9
  export class BranchSummaryMessageComponent extends Box {
10
10
  private expanded = false;
11
- private message: BranchSummaryMessage;
12
11
 
13
- constructor(message: BranchSummaryMessage) {
12
+ constructor(private readonly message: BranchSummaryMessage) {
14
13
  super(1, 1, t => theme.bg("customMessageBg", t));
15
- this.message = message;
16
14
  this.updateDisplay();
17
15
  }
18
16