@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
@@ -329,6 +329,7 @@ export class AgentSession {
329
329
  private _streamingEditAbortTriggered = false;
330
330
  private _streamingEditCheckedLineCounts = new Map<string, number>();
331
331
  private _streamingEditFileCache = new Map<string, string>();
332
+ private _promptInFlight = false;
332
333
 
333
334
  constructor(config: AgentSessionConfig) {
334
335
  this.agent = config.agent;
@@ -693,7 +694,11 @@ export class AgentSession {
693
694
  .map(line => line.slice(1));
694
695
  if (removedLines.length > 0) {
695
696
  const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
696
- const cachedContent = this._streamingEditFileCache.get(resolvedPath);
697
+ let cachedContent = this._streamingEditFileCache.get(resolvedPath);
698
+ if (cachedContent === undefined) {
699
+ this._ensureFileCache(resolvedPath);
700
+ cachedContent = this._streamingEditFileCache.get(resolvedPath);
701
+ }
697
702
  if (cachedContent !== undefined) {
698
703
  const missing = removedLines.find(line => !cachedContent.includes(normalizeToLF(line)));
699
704
  if (missing) {
@@ -898,7 +903,7 @@ export class AgentSession {
898
903
 
899
904
  /** Whether agent is currently streaming a response */
900
905
  get isStreaming(): boolean {
901
- return this.agent.state.isStreaming;
906
+ return this.agent.state.isStreaming || this._promptInFlight;
902
907
  }
903
908
 
904
909
  /** Current retry attempt (0 if not retrying) */
@@ -1014,6 +1019,23 @@ export class AgentSession {
1014
1019
  this._planReferenceSent = true;
1015
1020
  }
1016
1021
 
1022
+ /**
1023
+ * Inject the plan mode context message into the conversation history.
1024
+ */
1025
+ async sendPlanModeContext(options?: { deliverAs?: "steer" | "followUp" | "nextTurn" }): Promise<void> {
1026
+ const message = await this._buildPlanModeMessage();
1027
+ if (!message) return;
1028
+ await this.sendCustomMessage(
1029
+ {
1030
+ customType: message.customType,
1031
+ content: message.content,
1032
+ display: message.display,
1033
+ details: message.details,
1034
+ },
1035
+ options ? { deliverAs: options.deliverAs } : undefined,
1036
+ );
1037
+ }
1038
+
1017
1039
  resolveRoleModel(role: ModelRole): Model | undefined {
1018
1040
  return this._resolveRoleModel(role, this._modelRegistry.getAvailable(), this.model);
1019
1041
  }
@@ -1229,93 +1251,100 @@ export class AgentSession {
1229
1251
  expandedText: string,
1230
1252
  options?: Pick<PromptOptions, "toolChoice" | "images">,
1231
1253
  ): Promise<void> {
1232
- // Flush any pending bash messages before the new prompt
1233
- this._flushPendingBashMessages();
1234
- this._flushPendingPythonMessages();
1254
+ this._promptInFlight = true;
1255
+ try {
1256
+ // Flush any pending bash messages before the new prompt
1257
+ this._flushPendingBashMessages();
1258
+ this._flushPendingPythonMessages();
1235
1259
 
1236
- // Reset todo reminder count on new user prompt
1237
- this._todoReminderCount = 0;
1260
+ // Reset todo reminder count on new user prompt
1261
+ this._todoReminderCount = 0;
1238
1262
 
1239
- // Validate model
1240
- if (!this.model) {
1241
- throw new Error(
1242
- "No model selected.\n\n" +
1243
- `Use /login, set an API key environment variable, or create ${getAgentDbPath()}\n\n` +
1244
- "Then use /model to select a model.",
1245
- );
1246
- }
1263
+ // Validate model
1264
+ if (!this.model) {
1265
+ throw new Error(
1266
+ "No model selected.\n\n" +
1267
+ `Use /login, set an API key environment variable, or create ${getAgentDbPath()}\n\n` +
1268
+ "Then use /model to select a model.",
1269
+ );
1270
+ }
1247
1271
 
1248
- // Validate API key
1249
- const apiKey = await this._modelRegistry.getApiKey(this.model, this.sessionId);
1250
- if (!apiKey) {
1251
- throw new Error(
1252
- `No API key found for ${this.model.provider}.\n\n` +
1253
- `Use /login, set an API key environment variable, or create ${getAgentDbPath()}`,
1254
- );
1255
- }
1272
+ // Validate API key
1273
+ const apiKey = await this._modelRegistry.getApiKey(this.model, this.sessionId);
1274
+ if (!apiKey) {
1275
+ throw new Error(
1276
+ `No API key found for ${this.model.provider}.\n\n` +
1277
+ `Use /login, set an API key environment variable, or create ${getAgentDbPath()}`,
1278
+ );
1279
+ }
1256
1280
 
1257
- // Check if we need to compact before sending (catches aborted responses)
1258
- const lastAssistant = this._findLastAssistantMessage();
1259
- if (lastAssistant) {
1260
- await this._checkCompaction(lastAssistant, false);
1261
- }
1281
+ // Check if we need to compact before sending (catches aborted responses)
1282
+ const lastAssistant = this._findLastAssistantMessage();
1283
+ if (lastAssistant) {
1284
+ await this._checkCompaction(lastAssistant, false);
1285
+ }
1262
1286
 
1263
- // Build messages array (custom messages if any, then user message)
1264
- const messages: AgentMessage[] = [];
1265
- const planReferenceMessage = await this._buildPlanReferenceMessage?.();
1266
- if (planReferenceMessage) {
1267
- messages.push(planReferenceMessage);
1268
- }
1269
- const planModeMessage = await this._buildPlanModeMessage();
1270
- if (planModeMessage) {
1271
- messages.push(planModeMessage);
1272
- }
1287
+ // Build messages array (custom messages if any, then user message)
1288
+ const messages: AgentMessage[] = [];
1289
+ const planReferenceMessage = await this._buildPlanReferenceMessage?.();
1290
+ if (planReferenceMessage) {
1291
+ messages.push(planReferenceMessage);
1292
+ }
1293
+ const planModeMessage = await this._buildPlanModeMessage();
1294
+ if (planModeMessage) {
1295
+ messages.push(planModeMessage);
1296
+ }
1273
1297
 
1274
- messages.push(message);
1298
+ messages.push(message);
1275
1299
 
1276
- // Inject any pending "nextTurn" messages as context alongside the user message
1277
- for (const msg of this._pendingNextTurnMessages) {
1278
- messages.push(msg);
1279
- }
1280
- this._pendingNextTurnMessages = [];
1300
+ // Inject any pending "nextTurn" messages as context alongside the user message
1301
+ for (const msg of this._pendingNextTurnMessages) {
1302
+ messages.push(msg);
1303
+ }
1304
+ this._pendingNextTurnMessages = [];
1281
1305
 
1282
- // Auto-read @filepath mentions
1283
- const fileMentions = extractFileMentions(expandedText);
1284
- if (fileMentions.length > 0) {
1285
- const fileMentionMessages = await generateFileMentionMessages(fileMentions, this.sessionManager.getCwd());
1286
- messages.push(...fileMentionMessages);
1287
- }
1306
+ // Auto-read @filepath mentions
1307
+ const fileMentions = extractFileMentions(expandedText);
1308
+ if (fileMentions.length > 0) {
1309
+ const fileMentionMessages = await generateFileMentionMessages(fileMentions, this.sessionManager.getCwd(), {
1310
+ autoResizeImages: this.settings.get("images.autoResize"),
1311
+ });
1312
+ messages.push(...fileMentionMessages);
1313
+ }
1288
1314
 
1289
- // Emit before_agent_start extension event
1290
- if (this._extensionRunner) {
1291
- const result = await this._extensionRunner.emitBeforeAgentStart(
1292
- expandedText,
1293
- options?.images,
1294
- this._baseSystemPrompt,
1295
- );
1296
- if (result?.messages) {
1297
- for (const msg of result.messages) {
1298
- messages.push({
1299
- role: "custom",
1300
- customType: msg.customType,
1301
- content: msg.content,
1302
- display: msg.display,
1303
- details: msg.details,
1304
- timestamp: Date.now(),
1305
- });
1315
+ // Emit before_agent_start extension event
1316
+ if (this._extensionRunner) {
1317
+ const result = await this._extensionRunner.emitBeforeAgentStart(
1318
+ expandedText,
1319
+ options?.images,
1320
+ this._baseSystemPrompt,
1321
+ );
1322
+ if (result?.messages) {
1323
+ for (const msg of result.messages) {
1324
+ messages.push({
1325
+ role: "custom",
1326
+ customType: msg.customType,
1327
+ content: msg.content,
1328
+ display: msg.display,
1329
+ details: msg.details,
1330
+ timestamp: Date.now(),
1331
+ });
1332
+ }
1306
1333
  }
1307
- }
1308
1334
 
1309
- if (result?.systemPrompt !== undefined) {
1310
- this.agent.setSystemPrompt(result.systemPrompt);
1311
- } else {
1312
- this.agent.setSystemPrompt(this._baseSystemPrompt);
1335
+ if (result?.systemPrompt !== undefined) {
1336
+ this.agent.setSystemPrompt(result.systemPrompt);
1337
+ } else {
1338
+ this.agent.setSystemPrompt(this._baseSystemPrompt);
1339
+ }
1313
1340
  }
1314
- }
1315
1341
 
1316
- const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
1317
- await this.agent.prompt(messages, agentPromptOptions);
1318
- await this.waitForRetry();
1342
+ const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
1343
+ await this.agent.prompt(messages, agentPromptOptions);
1344
+ await this.waitForRetry();
1345
+ } finally {
1346
+ this._promptInFlight = false;
1347
+ }
1319
1348
  }
1320
1349
 
1321
1350
  /**
@@ -1811,7 +1840,7 @@ export class AgentSession {
1811
1840
  this.settings.setModelRole(role, `${model.provider}/${model.id}`);
1812
1841
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
1813
1842
 
1814
- // Re-clamp thinking level for new model's capabilities
1843
+ // Re-clamp thinking level for new model's capabilities without persisting settings
1815
1844
  this.setThinkingLevel(this.thinkingLevel);
1816
1845
  }
1817
1846
 
@@ -1830,7 +1859,7 @@ export class AgentSession {
1830
1859
  this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
1831
1860
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
1832
1861
 
1833
- // Re-clamp thinking level for new model's capabilities
1862
+ // Re-clamp thinking level for new model's capabilities without persisting settings
1834
1863
  this.setThinkingLevel(this.thinkingLevel);
1835
1864
  }
1836
1865
 
@@ -1956,7 +1985,7 @@ export class AgentSession {
1956
1985
  this.settings.setModelRole("default", `${nextModel.provider}/${nextModel.id}`);
1957
1986
  this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
1958
1987
 
1959
- // Re-clamp thinking level for new model's capabilities
1988
+ // Re-clamp thinking level for new model's capabilities without persisting settings
1960
1989
  this.setThinkingLevel(this.thinkingLevel);
1961
1990
 
1962
1991
  return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
@@ -1978,12 +2007,12 @@ export class AgentSession {
1978
2007
  * Clamps to model capabilities based on available thinking levels.
1979
2008
  * Saves to session, with optional persistence to settings.
1980
2009
  */
1981
- setThinkingLevel(level: ThinkingLevel, options?: { persist?: boolean }): void {
2010
+ setThinkingLevel(level: ThinkingLevel, persist: boolean = false): void {
1982
2011
  const availableLevels = this.getAvailableThinkingLevels();
1983
2012
  const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);
1984
2013
  this.agent.setThinkingLevel(effectiveLevel);
1985
2014
  this.sessionManager.appendThinkingLevelChange(effectiveLevel);
1986
- if (options?.persist !== false) {
2015
+ if (persist) {
1987
2016
  this.settings.set("defaultThinkingLevel", effectiveLevel);
1988
2017
  }
1989
2018
  }
@@ -2000,7 +2029,7 @@ export class AgentSession {
2000
2029
  const nextIndex = (currentIndex + 1) % levels.length;
2001
2030
  const nextLevel = levels[nextIndex];
2002
2031
 
2003
- this.setThinkingLevel(nextLevel, { persist: false });
2032
+ this.setThinkingLevel(nextLevel);
2004
2033
  return nextLevel;
2005
2034
  }
2006
2035
 
@@ -3941,7 +3970,12 @@ Be thorough - include exact file paths, function names, error messages, and tech
3941
3970
  lines.push("## File Mention\n");
3942
3971
  for (const file of fileMsg.files) {
3943
3972
  lines.push(`<file path="${file.path}">`);
3944
- lines.push(file.content);
3973
+ if (file.content) {
3974
+ lines.push(file.content);
3975
+ }
3976
+ if (file.image) {
3977
+ lines.push("[Image attached]");
3978
+ }
3945
3979
  lines.push("</file>\n");
3946
3980
  }
3947
3981
  lines.push("\n");
@@ -2,8 +2,6 @@
2
2
  * Credential storage for API keys and OAuth tokens.
3
3
  * Handles loading, saving, and refreshing credentials from agent.db.
4
4
  */
5
- import { Buffer } from "node:buffer";
6
- import * as path from "node:path";
7
5
  import {
8
6
  antigravityUsageProvider,
9
7
  claudeUsageProvider,
@@ -35,9 +33,8 @@ import {
35
33
  zaiUsageProvider,
36
34
  } from "@oh-my-pi/pi-ai";
37
35
  import { logger } from "@oh-my-pi/pi-utils";
38
- import { getAgentDbPath, getAuthPath } from "../config";
36
+ import { getAgentDbPath } from "../config";
39
37
  import { AgentStorage } from "./agent-storage";
40
- import { migrateJsonStorage } from "./storage-migration";
41
38
 
42
39
  export type ApiKeyCredential = {
43
40
  type: "api_key";
@@ -68,7 +65,6 @@ export interface SerializedAuthStorage {
68
65
  }>
69
66
  >;
70
67
  runtimeOverrides?: Record<string, string>;
71
- authPath?: string;
72
68
  dbPath?: string;
73
69
  }
74
70
 
@@ -141,16 +137,13 @@ class AuthStorageUsageCache implements UsageCache {
141
137
 
142
138
  /**
143
139
  * Credential storage backed by agent.db.
144
- * Reads from SQLite and migrates legacy auth.json paths.
140
+ * Reads from SQLite (agent.db).
145
141
  */
146
142
  export class AuthStorage {
147
143
  private static readonly defaultBackoffMs = 60_000; // Default backoff when no reset time available
148
144
 
149
145
  /** Provider -> credentials cache, populated from agent.db on reload(). */
150
146
  private data: Map<string, StoredCredential[]> = new Map();
151
- private storage: AgentStorage;
152
- /** Resolved path to agent.db (derived from authPath or used directly if .db). */
153
- private dbPath: string;
154
147
  private runtimeOverrides: Map<string, string> = new Map();
155
148
  /** Tracks next credential index per provider:type key for round-robin distribution (non-session use). */
156
149
  private providerRoundRobinIndex: Map<string, number> = new Map();
@@ -166,13 +159,9 @@ export class AuthStorage {
166
159
  private fallbackResolver?: (provider: string) => string | undefined;
167
160
 
168
161
  private constructor(
169
- private authPath: string,
170
- private fallbackPaths: string[] = [],
171
- storage: AgentStorage,
162
+ private storage: AgentStorage,
172
163
  options: AuthStorageOptions = {},
173
164
  ) {
174
- this.dbPath = AuthStorage.resolveDbPath(authPath);
175
- this.storage = storage;
176
165
  this.usageProviderResolver = options.usageProviderResolver ?? resolveDefaultUsageProvider;
177
166
  this.usageCache = options.usageCache ?? new AuthStorageUsageCache(this.storage);
178
167
  this.usageFetch = options.usageFetch ?? fetch;
@@ -187,17 +176,11 @@ export class AuthStorage {
187
176
 
188
177
  /**
189
178
  * Create an AuthStorage instance.
190
- * @param authPath - Legacy auth.json path used for migration and locating agent.db
191
- * @param fallbackPaths - Additional auth.json paths to migrate (legacy support)
179
+ * @param dbPath - Path to agent.db
192
180
  */
193
- static async create(
194
- authPath: string,
195
- fallbackPaths: string[] = [],
196
- options: AuthStorageOptions = {},
197
- ): Promise<AuthStorage> {
198
- const dbPath = AuthStorage.resolveDbPath(authPath);
181
+ static async create(dbPath: string, options: AuthStorageOptions = {}): Promise<AuthStorage> {
199
182
  const storage = await AgentStorage.open(dbPath);
200
- return new AuthStorage(authPath, fallbackPaths, storage, options);
183
+ return new AuthStorage(storage, options);
201
184
  }
202
185
 
203
186
  /**
@@ -205,14 +188,10 @@ export class AuthStorage {
205
188
  * Used by subagent workers to bypass discovery and use parent's credentials.
206
189
  */
207
190
  static async fromSerialized(data: SerializedAuthStorage, options: AuthStorageOptions = {}): Promise<AuthStorage> {
208
- const authPath = data.authPath ?? data.dbPath ?? getAuthPath();
209
- const dbPath = data.dbPath ?? AuthStorage.resolveDbPath(authPath);
191
+ const dbPath = data.dbPath ?? getAgentDbPath();
210
192
  const storage = await AgentStorage.open(dbPath);
211
193
 
212
194
  const instance = Object.create(AuthStorage.prototype) as AuthStorage;
213
- instance.authPath = authPath;
214
- instance.fallbackPaths = [];
215
- instance.dbPath = dbPath;
216
195
  instance.storage = storage;
217
196
  instance.data = new Map();
218
197
  instance.runtimeOverrides = new Map();
@@ -271,23 +250,9 @@ export class AuthStorage {
271
250
  return {
272
251
  credentials,
273
252
  runtimeOverrides: Object.keys(runtimeOverrides).length > 0 ? runtimeOverrides : undefined,
274
- authPath: this.authPath,
275
- dbPath: this.dbPath,
276
253
  };
277
254
  }
278
255
 
279
- /**
280
- * Converts legacy auth.json path to agent.db path, or returns .db path as-is.
281
- * @param authPath - Path to auth.json or agent.db
282
- * @returns Resolved path to agent.db
283
- */
284
- private static resolveDbPath(authPath: string): string {
285
- if (authPath.endsWith(".db")) {
286
- return authPath;
287
- }
288
- return getAgentDbPath(path.dirname(authPath));
289
- }
290
-
291
256
  /**
292
257
  * Set a runtime API key override (not persisted to disk).
293
258
  * Used for CLI --api-key flag.
@@ -313,16 +278,9 @@ export class AuthStorage {
313
278
 
314
279
  /**
315
280
  * Reload credentials from agent.db.
316
- * Migrates legacy auth.json/settings.json on first load.
281
+ * Reloads credentials from the database.
317
282
  */
318
283
  async reload(): Promise<void> {
319
- const agentDir = path.dirname(this.dbPath);
320
- await migrateJsonStorage({
321
- agentDir,
322
- settingsPath: path.join(agentDir, "settings.json"),
323
- authPaths: [this.authPath, ...this.fallbackPaths],
324
- });
325
-
326
284
  const records = this.storage.listAuthCredentials();
327
285
  const grouped = new Map<string, StoredCredential[]>();
328
286
  for (const record of records) {
@@ -388,9 +346,10 @@ export class AuthStorage {
388
346
  const parts = token.split(".");
389
347
  if (parts.length !== 3) return undefined;
390
348
  const payloadRaw = parts[1];
349
+ const decoder = new TextDecoder("utf-8");
391
350
  try {
392
351
  const payload = JSON.parse(
393
- Buffer.from(payloadRaw.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"),
352
+ decoder.decode(Uint8Array.fromBase64(payloadRaw, { alphabet: "base64url" })),
394
353
  ) as Record<string, unknown>;
395
354
  if (!payload || typeof payload !== "object") return undefined;
396
355
  const identifiers: string[] = [];
@@ -113,7 +113,8 @@ export interface FileMentionMessage {
113
113
  files: Array<{
114
114
  path: string;
115
115
  content: string;
116
- lineCount: number;
116
+ lineCount?: number;
117
+ image?: ImageContent;
117
118
  }>;
118
119
  timestamp: number;
119
120
  }
@@ -274,10 +275,23 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
274
275
  timestamp: m.timestamp,
275
276
  };
276
277
  case "fileMention": {
277
- const fileContents = m.files.map(f => `<file path="${f.path}">\n${f.content}\n</file>`).join("\n\n");
278
+ const fileContents = m.files
279
+ .map(file => {
280
+ const inner = file.content ? `\n${file.content}\n` : "\n";
281
+ return `<file path="${file.path}">${inner}</file>`;
282
+ })
283
+ .join("\n\n");
284
+ const content: (TextContent | ImageContent)[] = [
285
+ { type: "text" as const, text: `<system-reminder>\n${fileContents}\n</system-reminder>` },
286
+ ];
287
+ for (const file of m.files) {
288
+ if (file.image) {
289
+ content.push(file.image);
290
+ }
291
+ }
278
292
  return {
279
293
  role: "user",
280
- content: [{ type: "text" as const, text: `<system-reminder>\n${fileContents}\n</system-reminder>` }],
294
+ content,
281
295
  timestamp: m.timestamp,
282
296
  };
283
297
  }
@@ -279,26 +279,30 @@ function migrateToCurrentVersion(entries: FileEntry[]): boolean {
279
279
  return true;
280
280
  }
281
281
 
282
- function parseJsonlEntries<T>(content: string): T[] {
283
- if (!content.trim()) return [];
284
- const entries: T[] = [];
285
- let buffer = content;
282
+ function parseJsonlEntries<T>(buffer: string): T[] {
283
+ let entries: T[] | undefined;
284
+
286
285
  while (buffer.length > 0) {
287
- const result = Bun.JSONL.parseChunk(buffer);
288
- if (result.values.length > 0) {
289
- entries.push(...(result.values as T[]));
286
+ const { values, error, read, done } = Bun.JSONL.parseChunk(buffer);
287
+ if (values.length > 0) {
288
+ const ext = values as T[];
289
+ if (!entries) {
290
+ entries = ext;
291
+ } else {
292
+ entries.push(...ext);
293
+ }
290
294
  }
291
- if (result.error) {
292
- const nextNewline = buffer.indexOf("\n", result.read);
295
+ if (error) {
296
+ const nextNewline = buffer.indexOf("\n", read);
293
297
  if (nextNewline === -1) break;
294
- buffer = buffer.slice(nextNewline + 1);
298
+ buffer = buffer.substring(nextNewline + 1);
295
299
  continue;
296
300
  }
297
- if (result.read === 0) break;
298
- buffer = buffer.slice(result.read);
299
- if (result.done) break;
301
+ if (read === 0) break;
302
+ buffer = buffer.substring(read);
303
+ if (done) break;
300
304
  }
301
- return entries;
305
+ return entries ?? [];
302
306
  }
303
307
 
304
308
  /** Exported for testing */
@@ -506,17 +510,16 @@ function sanitizeSessionName(value: string | undefined): string | undefined {
506
510
  }
507
511
 
508
512
  class RecentSessionInfo {
509
- readonly path: string;
510
- readonly mtime: number;
511
-
512
513
  #fullName: string | undefined;
513
514
  #name: string | undefined;
514
515
  #timeAgo: string | undefined;
515
516
 
516
- constructor(path: string, mtime: number, header: Record<string, unknown>, firstPrompt?: string) {
517
- this.path = path;
518
- this.mtime = mtime;
519
-
517
+ constructor(
518
+ readonly path: string,
519
+ readonly mtime: number,
520
+ header: Record<string, unknown>,
521
+ firstPrompt?: string,
522
+ ) {
520
523
  // Extract title from session header, falling back to first user prompt, then id
521
524
  const trystr = (v: unknown) => (typeof v === "string" ? v : undefined);
522
525
  this.#fullName =
@@ -975,9 +978,6 @@ export class SessionManager {
975
978
  private sessionId: string = "";
976
979
  private sessionName: string | undefined;
977
980
  private sessionFile: string | undefined;
978
- private sessionDir: string;
979
- private cwd: string;
980
- private persist: boolean;
981
981
  private flushed: boolean = false;
982
982
  private fileEntries: FileEntry[] = [];
983
983
  private byId: Map<string, SessionEntry> = new Map();
@@ -989,13 +989,13 @@ export class SessionManager {
989
989
  private persistChain: Promise<void> = Promise.resolve();
990
990
  private persistError: Error | undefined;
991
991
  private persistErrorReported = false;
992
- private storage: SessionStorage;
993
992
 
994
- private constructor(cwd: string, sessionDir: string, persist: boolean, storage: SessionStorage) {
995
- this.cwd = cwd;
996
- this.sessionDir = sessionDir;
997
- this.persist = persist;
998
- this.storage = storage;
993
+ private constructor(
994
+ private readonly cwd: string,
995
+ private readonly sessionDir: string,
996
+ private readonly persist: boolean,
997
+ private readonly storage: SessionStorage,
998
+ ) {
999
999
  if (persist && sessionDir) {
1000
1000
  this.storage.ensureDirSync(sessionDir);
1001
1001
  }
@@ -140,7 +140,7 @@ export class OutputSink {
140
140
  await this.push(dec.decode());
141
141
  };
142
142
 
143
- return new WritableStream<Uint8Array | string>({
143
+ return new WritableStream({
144
144
  write: async chunk => {
145
145
  if (typeof chunk === "string") {
146
146
  await this.push(chunk);
@@ -79,6 +79,7 @@ export async function executeSSH(
79
79
  using child = ptree.spawn(["ssh", ...(await buildRemoteCommand(host, resolvedCommand))], {
80
80
  signal: options?.signal,
81
81
  timeout: options?.timeout,
82
+ stderr: "full",
82
83
  });
83
84
 
84
85
  const sink = new OutputSink({
@@ -87,9 +88,11 @@ export async function executeSSH(
87
88
  artifactId: options?.artifactId,
88
89
  });
89
90
 
90
- await Promise.allSettled([child.stdout.pipeTo(sink.createInput()), child.stderr.pipeTo(sink.createInput())]).catch(
91
- () => {},
92
- );
91
+ const streams = [child.stdout.pipeTo(sink.createInput())];
92
+ if (child.stderr) {
93
+ streams.push(child.stderr.pipeTo(sink.createInput()));
94
+ }
95
+ await Promise.allSettled(streams).catch(() => {});
93
96
 
94
97
  try {
95
98
  return {
@@ -48,6 +48,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
48
48
  description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
49
49
  spawns: "*",
50
50
  model: "default",
51
+ thinkingLevel: "medium",
51
52
  },
52
53
  template: taskMd,
53
54
  },
@@ -57,6 +58,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
57
58
  name: "quick_task",
58
59
  description: "Low-reasoning agent for strictly mechanical updates or data collection only",
59
60
  model: "pi/smol",
61
+ thinkingLevel: "minimal",
60
62
  },
61
63
  template: taskMd,
62
64
  },
@@ -66,7 +66,7 @@ export async function discoverAgents(cwd: string): Promise<DiscoveryResult> {
66
66
  }));
67
67
 
68
68
  // Get project directories by walking up from cwd (priority order)
69
- const projectDirs = (await findAllNearestProjectConfigDirs("agents", resolvedCwd))
69
+ const projectDirs = findAllNearestProjectConfigDirs("agents", resolvedCwd)
70
70
  .filter(entry => agentSources.includes(entry.source))
71
71
  .map(entry => ({
72
72
  ...entry,