@oh-my-pi/pi-coding-agent 3.33.0 → 3.35.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 (72) hide show
  1. package/CHANGELOG.md +57 -8
  2. package/docs/custom-tools.md +1 -1
  3. package/docs/extensions.md +4 -4
  4. package/docs/hooks.md +2 -2
  5. package/docs/sdk.md +4 -8
  6. package/examples/custom-tools/README.md +2 -2
  7. package/examples/extensions/README.md +1 -1
  8. package/examples/extensions/todo.ts +1 -1
  9. package/examples/hooks/custom-compaction.ts +4 -2
  10. package/examples/hooks/handoff.ts +1 -1
  11. package/examples/hooks/qna.ts +1 -1
  12. package/examples/sdk/02-custom-model.ts +1 -1
  13. package/examples/sdk/README.md +1 -1
  14. package/package.json +5 -5
  15. package/src/capability/ssh.ts +42 -0
  16. package/src/cli/file-processor.ts +1 -1
  17. package/src/cli/list-models.ts +1 -1
  18. package/src/core/agent-session.ts +214 -31
  19. package/src/core/auth-storage.ts +1 -1
  20. package/src/core/compaction/branch-summarization.ts +2 -2
  21. package/src/core/compaction/compaction.ts +2 -2
  22. package/src/core/compaction/utils.ts +1 -1
  23. package/src/core/custom-tools/types.ts +1 -1
  24. package/src/core/extensions/runner.ts +1 -1
  25. package/src/core/extensions/types.ts +1 -1
  26. package/src/core/extensions/wrapper.ts +1 -1
  27. package/src/core/hooks/runner.ts +2 -2
  28. package/src/core/hooks/types.ts +1 -1
  29. package/src/core/index.ts +11 -0
  30. package/src/core/messages.ts +1 -1
  31. package/src/core/model-registry.ts +1 -1
  32. package/src/core/model-resolver.ts +7 -6
  33. package/src/core/sdk.ts +33 -4
  34. package/src/core/session-manager.ts +16 -1
  35. package/src/core/settings-manager.ts +20 -6
  36. package/src/core/ssh/connection-manager.ts +466 -0
  37. package/src/core/ssh/ssh-executor.ts +190 -0
  38. package/src/core/ssh/sshfs-mount.ts +162 -0
  39. package/src/core/ssh-executor.ts +5 -0
  40. package/src/core/system-prompt.ts +424 -1
  41. package/src/core/title-generator.ts +2 -2
  42. package/src/core/tools/edit.ts +1 -0
  43. package/src/core/tools/grep.ts +1 -1
  44. package/src/core/tools/index.test.ts +1 -0
  45. package/src/core/tools/index.ts +5 -0
  46. package/src/core/tools/output.ts +1 -1
  47. package/src/core/tools/read.ts +24 -11
  48. package/src/core/tools/renderers.ts +3 -0
  49. package/src/core/tools/ssh.ts +302 -0
  50. package/src/core/tools/task/index.ts +11 -2
  51. package/src/core/tools/task/model-resolver.ts +5 -4
  52. package/src/core/tools/task/types.ts +1 -1
  53. package/src/core/tools/task/worker.ts +1 -1
  54. package/src/core/voice.ts +1 -1
  55. package/src/discovery/index.ts +3 -0
  56. package/src/discovery/ssh.ts +162 -0
  57. package/src/main.ts +4 -1
  58. package/src/modes/interactive/components/assistant-message.ts +1 -1
  59. package/src/modes/interactive/components/custom-message.ts +1 -1
  60. package/src/modes/interactive/components/footer.ts +1 -1
  61. package/src/modes/interactive/components/hook-message.ts +1 -1
  62. package/src/modes/interactive/components/model-selector.ts +1 -1
  63. package/src/modes/interactive/components/oauth-selector.ts +1 -1
  64. package/src/modes/interactive/components/status-line.ts +1 -1
  65. package/src/modes/interactive/components/tool-execution.ts +15 -12
  66. package/src/modes/interactive/interactive-mode.ts +43 -9
  67. package/src/modes/print-mode.ts +1 -1
  68. package/src/modes/rpc/rpc-client.ts +1 -1
  69. package/src/modes/rpc/rpc-types.ts +1 -1
  70. package/src/prompts/system-prompt.md +4 -0
  71. package/src/prompts/tools/ssh.md +74 -0
  72. package/src/utils/image-resize.ts +1 -1
@@ -13,9 +13,9 @@
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
15
 
16
- import type { AssistantMessage, ImageContent, Message, Model, TextContent, Usage } from "@mariozechner/pi-ai";
17
- import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
18
16
  import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
17
+ import type { AssistantMessage, ImageContent, Message, Model, TextContent, Usage } from "@oh-my-pi/pi-ai";
18
+ import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
19
19
  import type { Rule } from "../capability/rule";
20
20
  import { getAuthPath } from "../config";
21
21
  import { theme } from "../modes/interactive/theme/theme";
@@ -45,6 +45,7 @@ import type {
45
45
  } from "./extensions";
46
46
  import { extractFileMentions, generateFileMentionMessages } from "./file-mentions";
47
47
  import type { HookCommandContext } from "./hooks/types";
48
+ import { logger } from "./logger";
48
49
  import type { BashExecutionMessage, CustomMessage } from "./messages";
49
50
  import type { ModelRegistry } from "./model-registry";
50
51
  import { parseModelString } from "./model-resolver";
@@ -52,6 +53,8 @@ import { expandPromptTemplate, type PromptTemplate, parseCommandArgs } from "./p
52
53
  import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
53
54
  import type { SettingsManager, SkillsSettings } from "./settings-manager";
54
55
  import { expandSlashCommand, type FileSlashCommand } from "./slash-commands";
56
+ import { closeAllConnections } from "./ssh/connection-manager";
57
+ import { unmountAll } from "./ssh/sshfs-mount";
55
58
  import type { TtsrManager } from "./ttsr";
56
59
 
57
60
  /** Session-specific events that extend the core AgentEvent */
@@ -167,6 +170,15 @@ const noOpUIContext: ExtensionUIContext = {
167
170
  },
168
171
  };
169
172
 
173
+ async function cleanupSshResources(): Promise<void> {
174
+ const results = await Promise.allSettled([closeAllConnections(), unmountAll()]);
175
+ for (const result of results) {
176
+ if (result.status === "rejected") {
177
+ logger.warn("SSH cleanup failed", { error: String(result.reason) });
178
+ }
179
+ }
180
+ }
181
+
170
182
  // ============================================================================
171
183
  // AgentSession Class
172
184
  // ============================================================================
@@ -447,7 +459,10 @@ export class AgentSession {
447
459
  const content = message.content;
448
460
  if (typeof content === "string") return content;
449
461
  const textBlocks = content.filter((c) => c.type === "text");
450
- return textBlocks.map((c) => (c as TextContent).text).join("");
462
+ const text = textBlocks.map((c) => (c as TextContent).text).join("");
463
+ if (text.length > 0) return text;
464
+ const hasImages = content.some((c) => c.type === "image");
465
+ return hasImages ? "[Image]" : "";
451
466
  }
452
467
 
453
468
  /** Find the last assistant message in agent state (including aborted ones) */
@@ -534,6 +549,7 @@ export class AgentSession {
534
549
  */
535
550
  async dispose(): Promise<void> {
536
551
  await this.sessionManager.flush();
552
+ await cleanupSshResources();
537
553
  this._disconnectFromAgent();
538
554
  this._eventListeners = [];
539
555
  }
@@ -709,9 +725,9 @@ export class AgentSession {
709
725
  );
710
726
  }
711
727
  if (options.streamingBehavior === "followUp") {
712
- await this._queueFollowUp(expandedText);
728
+ await this._queueFollowUp(expandedText, options?.images);
713
729
  } else {
714
- await this._queueSteer(expandedText);
730
+ await this._queueSteer(expandedText, options?.images);
715
731
  }
716
732
  return;
717
733
  }
@@ -940,11 +956,16 @@ export class AgentSession {
940
956
  /**
941
957
  * Internal: Queue a steering message (already expanded, no extension command check).
942
958
  */
943
- private async _queueSteer(text: string): Promise<void> {
944
- this._steeringMessages.push(text);
959
+ private async _queueSteer(text: string, images?: ImageContent[]): Promise<void> {
960
+ const displayText = text || (images && images.length > 0 ? "[Image]" : "");
961
+ this._steeringMessages.push(displayText);
962
+ const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
963
+ if (images && images.length > 0) {
964
+ content.push(...images);
965
+ }
945
966
  this.agent.steer({
946
967
  role: "user",
947
- content: [{ type: "text", text }],
968
+ content,
948
969
  timestamp: Date.now(),
949
970
  });
950
971
  }
@@ -952,11 +973,16 @@ export class AgentSession {
952
973
  /**
953
974
  * Internal: Queue a follow-up message (already expanded, no extension command check).
954
975
  */
955
- private async _queueFollowUp(text: string): Promise<void> {
956
- this._followUpMessages.push(text);
976
+ private async _queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
977
+ const displayText = text || (images && images.length > 0 ? "[Image]" : "");
978
+ this._followUpMessages.push(displayText);
979
+ const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
980
+ if (images && images.length > 0) {
981
+ content.push(...images);
982
+ }
957
983
  this.agent.followUp({
958
984
  role: "user",
959
- content: [{ type: "text", text }],
985
+ content,
960
986
  timestamp: Date.now(),
961
987
  });
962
988
  }
@@ -1162,7 +1188,7 @@ export class AgentSession {
1162
1188
 
1163
1189
  /**
1164
1190
  * Cycle through configured role models in a fixed order.
1165
- * Skips missing roles and deduplicates models.
1191
+ * Skips missing roles.
1166
1192
  * @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
1167
1193
  * @param options - Optional settings: `temporary` to not persist to settings
1168
1194
  */
@@ -1176,7 +1202,6 @@ export class AgentSession {
1176
1202
  const currentModel = this.model;
1177
1203
  if (!currentModel) return undefined;
1178
1204
  const roleModels: Array<{ role: string; model: Model<any> }> = [];
1179
- const seen = new Set<string>();
1180
1205
 
1181
1206
  for (const role of roleOrder) {
1182
1207
  const roleModelStr =
@@ -1195,15 +1220,15 @@ export class AgentSession {
1195
1220
  }
1196
1221
  if (!match) continue;
1197
1222
 
1198
- const key = `${match.provider}/${match.id}`;
1199
- if (seen.has(key)) continue;
1200
- seen.add(key);
1201
1223
  roleModels.push({ role, model: match });
1202
1224
  }
1203
1225
 
1204
1226
  if (roleModels.length <= 1) return undefined;
1205
1227
 
1206
- let currentIndex = roleModels.findIndex((entry) => modelsAreEqual(entry.model, currentModel));
1228
+ const lastRole = this.sessionManager.getLastModelChangeRole();
1229
+ let currentIndex = lastRole
1230
+ ? roleModels.findIndex((entry) => entry.role === lastRole)
1231
+ : roleModels.findIndex((entry) => modelsAreEqual(entry.model, currentModel));
1207
1232
  if (currentIndex === -1) currentIndex = 0;
1208
1233
 
1209
1234
  const nextIndex = (currentIndex + 1) % roleModels.length;
@@ -1545,6 +1570,60 @@ export class AgentSession {
1545
1570
  }
1546
1571
  }
1547
1572
 
1573
+ private _getModelKey(model: Model<any>): string {
1574
+ return `${model.provider}/${model.id}`;
1575
+ }
1576
+
1577
+ private _resolveRoleModel(
1578
+ role: string,
1579
+ availableModels: Model<any>[],
1580
+ currentModel: Model<any> | undefined,
1581
+ ): Model<any> | undefined {
1582
+ const roleModelStr =
1583
+ role === "default"
1584
+ ? (this.settingsManager.getModelRole("default") ??
1585
+ (currentModel ? `${currentModel.provider}/${currentModel.id}` : undefined))
1586
+ : this.settingsManager.getModelRole(role);
1587
+
1588
+ if (!roleModelStr) return undefined;
1589
+
1590
+ const parsed = parseModelString(roleModelStr);
1591
+ if (parsed) {
1592
+ return availableModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
1593
+ }
1594
+ const roleLower = roleModelStr.toLowerCase();
1595
+ return availableModels.find((m) => m.id.toLowerCase() === roleLower);
1596
+ }
1597
+
1598
+ private _getCompactionModelCandidates(availableModels: Model<any>[]): Model<any>[] {
1599
+ const candidates: Model<any>[] = [];
1600
+ const seen = new Set<string>();
1601
+
1602
+ const addCandidate = (model: Model<any> | undefined): void => {
1603
+ if (!model) return;
1604
+ const key = this._getModelKey(model);
1605
+ if (seen.has(key)) return;
1606
+ seen.add(key);
1607
+ candidates.push(model);
1608
+ };
1609
+
1610
+ const currentModel = this.model;
1611
+ addCandidate(this._resolveRoleModel("default", availableModels, currentModel));
1612
+ addCandidate(this._resolveRoleModel("slow", availableModels, currentModel));
1613
+ addCandidate(this._resolveRoleModel("small", availableModels, currentModel));
1614
+ addCandidate(this._resolveRoleModel("smol", availableModels, currentModel));
1615
+
1616
+ const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
1617
+ for (const model of sortedByContext) {
1618
+ if (!seen.has(this._getModelKey(model))) {
1619
+ addCandidate(model);
1620
+ break;
1621
+ }
1622
+ }
1623
+
1624
+ return candidates;
1625
+ }
1626
+
1548
1627
  /**
1549
1628
  * Internal: Run auto-compaction with events.
1550
1629
  */
@@ -1564,8 +1643,8 @@ export class AgentSession {
1564
1643
  return;
1565
1644
  }
1566
1645
 
1567
- const apiKey = await this._modelRegistry.getApiKey(this.model);
1568
- if (!apiKey) {
1646
+ const availableModels = this._modelRegistry.getAvailable();
1647
+ if (availableModels.length === 0) {
1569
1648
  this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
1570
1649
  return;
1571
1650
  }
@@ -1613,14 +1692,68 @@ export class AgentSession {
1613
1692
  tokensBefore = hookCompaction.tokensBefore;
1614
1693
  details = hookCompaction.details;
1615
1694
  } else {
1616
- // Generate compaction result
1617
- const compactResult = await compact(
1618
- preparation,
1619
- this.model,
1620
- apiKey,
1621
- undefined,
1622
- this._autoCompactionAbortController.signal,
1623
- );
1695
+ const candidates = this._getCompactionModelCandidates(availableModels);
1696
+ const retrySettings = this.settingsManager.getRetrySettings();
1697
+ let compactResult: CompactionResult | undefined;
1698
+ let lastError: unknown;
1699
+
1700
+ for (const candidate of candidates) {
1701
+ const apiKey = await this._modelRegistry.getApiKey(candidate);
1702
+ if (!apiKey) continue;
1703
+
1704
+ let attempt = 0;
1705
+ while (true) {
1706
+ try {
1707
+ compactResult = await compact(
1708
+ preparation,
1709
+ candidate,
1710
+ apiKey,
1711
+ undefined,
1712
+ this._autoCompactionAbortController.signal,
1713
+ );
1714
+ break;
1715
+ } catch (error) {
1716
+ if (this._autoCompactionAbortController.signal.aborted) {
1717
+ throw error;
1718
+ }
1719
+
1720
+ const message = error instanceof Error ? error.message : String(error);
1721
+ const retryAfterMs = this._parseRetryAfterMsFromError(message);
1722
+ const shouldRetry =
1723
+ retrySettings.enabled &&
1724
+ attempt < retrySettings.maxRetries &&
1725
+ (retryAfterMs !== undefined || this._isRetryableErrorMessage(message));
1726
+ if (!shouldRetry) {
1727
+ lastError = error;
1728
+ break;
1729
+ }
1730
+
1731
+ const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
1732
+ const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;
1733
+ attempt++;
1734
+ logger.warn("Auto-compaction failed, retrying", {
1735
+ attempt,
1736
+ maxRetries: retrySettings.maxRetries,
1737
+ delayMs,
1738
+ retryAfterMs,
1739
+ error: message,
1740
+ });
1741
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1742
+ }
1743
+ }
1744
+
1745
+ if (compactResult) {
1746
+ break;
1747
+ }
1748
+ }
1749
+
1750
+ if (!compactResult) {
1751
+ if (lastError) {
1752
+ throw lastError;
1753
+ }
1754
+ throw new Error("Compaction failed: no available model");
1755
+ }
1756
+
1624
1757
  summary = compactResult.summary;
1625
1758
  firstKeptEntryId = compactResult.firstKeptEntryId;
1626
1759
  tokensBefore = compactResult.tokensBefore;
@@ -1712,12 +1845,61 @@ export class AgentSession {
1712
1845
  if (isContextOverflow(message, contextWindow)) return false;
1713
1846
 
1714
1847
  const err = message.errorMessage;
1848
+ return this._isRetryableErrorMessage(err);
1849
+ }
1850
+
1851
+ private _isRetryableErrorMessage(errorMessage: string): boolean {
1715
1852
  // Match: overloaded_error, rate limit, 429, 500, 502, 503, 504, service unavailable, connection error
1716
1853
  return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error/i.test(
1717
- err,
1854
+ errorMessage,
1718
1855
  );
1719
1856
  }
1720
1857
 
1858
+ private _parseRetryAfterMsFromError(errorMessage: string): number | undefined {
1859
+ const now = Date.now();
1860
+ const retryAfterMsMatch = /retry-after-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
1861
+ if (retryAfterMsMatch) {
1862
+ return Math.max(0, Number(retryAfterMsMatch[1]));
1863
+ }
1864
+
1865
+ const retryAfterMatch = /retry-after\s*[:=]\s*([^\s,;]+)/i.exec(errorMessage);
1866
+ if (retryAfterMatch) {
1867
+ const value = retryAfterMatch[1];
1868
+ const seconds = Number(value);
1869
+ if (!Number.isNaN(seconds)) {
1870
+ return Math.max(0, seconds * 1000);
1871
+ }
1872
+ const dateMs = Date.parse(value);
1873
+ if (!Number.isNaN(dateMs)) {
1874
+ return Math.max(0, dateMs - now);
1875
+ }
1876
+ }
1877
+
1878
+ const resetMsMatch = /x-ratelimit-reset-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
1879
+ if (resetMsMatch) {
1880
+ const resetMs = Number(resetMsMatch[1]);
1881
+ if (!Number.isNaN(resetMs)) {
1882
+ if (resetMs > 1_000_000_000_000) {
1883
+ return Math.max(0, resetMs - now);
1884
+ }
1885
+ return Math.max(0, resetMs);
1886
+ }
1887
+ }
1888
+
1889
+ const resetMatch = /x-ratelimit-reset\s*[:=]\s*(\d+)/i.exec(errorMessage);
1890
+ if (resetMatch) {
1891
+ const resetSeconds = Number(resetMatch[1]);
1892
+ if (!Number.isNaN(resetSeconds)) {
1893
+ if (resetSeconds > 1_000_000_000) {
1894
+ return Math.max(0, resetSeconds * 1000 - now);
1895
+ }
1896
+ return Math.max(0, resetSeconds * 1000);
1897
+ }
1898
+ }
1899
+
1900
+ return undefined;
1901
+ }
1902
+
1721
1903
  /**
1722
1904
  * Handle retryable errors with exponential backoff.
1723
1905
  * @returns true if retry was initiated, false if max retries exceeded or disabled
@@ -2472,9 +2654,10 @@ export class AgentSession {
2472
2654
  * Emit a custom tool session event (backwards compatibility for older callers).
2473
2655
  */
2474
2656
  async emitCustomToolSessionEvent(reason: "start" | "switch" | "branch" | "tree" | "shutdown"): Promise<void> {
2475
- if (!this._extensionRunner) return;
2476
2657
  if (reason !== "shutdown") return;
2477
- if (!this._extensionRunner.hasHandlers("session_shutdown")) return;
2478
- await this._extensionRunner.emit({ type: "session_shutdown" });
2658
+ if (this._extensionRunner?.hasHandlers("session_shutdown")) {
2659
+ await this._extensionRunner.emit({ type: "session_shutdown" });
2660
+ }
2661
+ await cleanupSshResources();
2479
2662
  }
2480
2663
  }
@@ -15,7 +15,7 @@ import {
15
15
  loginOpenAICodex,
16
16
  type OAuthCredentials,
17
17
  type OAuthProvider,
18
- } from "@mariozechner/pi-ai";
18
+ } from "@oh-my-pi/pi-ai";
19
19
  import { logger } from "./logger";
20
20
 
21
21
  export type ApiKeyCredential = {
@@ -5,9 +5,9 @@
5
5
  * a summary of the branch being left so context isn't lost.
6
6
  */
7
7
 
8
- import type { Model } from "@mariozechner/pi-ai";
9
- import { completeSimple } from "@mariozechner/pi-ai";
10
8
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
9
+ import type { Model } from "@oh-my-pi/pi-ai";
10
+ import { completeSimple } from "@oh-my-pi/pi-ai";
11
11
  import branchSummaryPrompt from "../../prompts/branch-summary.md" with { type: "text" };
12
12
  import branchSummaryPreamble from "../../prompts/branch-summary-preamble.md" with { type: "text" };
13
13
  import {
@@ -5,9 +5,9 @@
5
5
  * and after compaction the session is reloaded.
6
6
  */
7
7
 
8
- import type { AssistantMessage, Model, Usage } from "@mariozechner/pi-ai";
9
- import { complete, completeSimple } from "@mariozechner/pi-ai";
10
8
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
9
+ import type { AssistantMessage, Model, Usage } from "@oh-my-pi/pi-ai";
10
+ import { complete, completeSimple } from "@oh-my-pi/pi-ai";
11
11
  import compactionSummaryPrompt from "../../prompts/compaction-summary.md" with { type: "text" };
12
12
  import compactionTurnPrefixPrompt from "../../prompts/compaction-turn-prefix.md" with { type: "text" };
13
13
  import compactionUpdateSummaryPrompt from "../../prompts/compaction-update-summary.md" with { type: "text" };
@@ -2,8 +2,8 @@
2
2
  * Shared utilities for compaction and branch summarization.
3
3
  */
4
4
 
5
- import type { Message } from "@mariozechner/pi-ai";
6
5
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
+ import type { Message } from "@oh-my-pi/pi-ai";
7
7
  import summarizationSystemPrompt from "../../prompts/summarization-system.md" with { type: "text" };
8
8
 
9
9
  // ============================================================================
@@ -5,8 +5,8 @@
5
5
  * They can provide custom rendering for tool calls and results in the TUI.
6
6
  */
7
7
 
8
- import type { Model } from "@mariozechner/pi-ai";
9
8
  import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
9
+ import type { Model } from "@oh-my-pi/pi-ai";
10
10
  import type { Component } from "@oh-my-pi/pi-tui";
11
11
  import type { Static, TSchema } from "@sinclair/typebox";
12
12
  import type { Theme } from "../../modes/interactive/theme/theme";
@@ -2,8 +2,8 @@
2
2
  * Extension runner - executes extensions and manages their lifecycle.
3
3
  */
4
4
 
5
- import type { ImageContent, Model } from "@mariozechner/pi-ai";
6
5
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
+ import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
7
7
  import type { KeyId } from "@oh-my-pi/pi-tui";
8
8
  import { theme } from "../../modes/interactive/theme/theme";
9
9
  import type { ModelRegistry } from "../model-registry";
@@ -8,8 +8,8 @@
8
8
  * - Interact with the user via UI primitives
9
9
  */
10
10
 
11
- import type { ImageContent, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
12
11
  import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
12
+ import type { ImageContent, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
13
13
  import type { Component, KeyId, TUI } from "@oh-my-pi/pi-tui";
14
14
  import type { Static, TSchema } from "@sinclair/typebox";
15
15
  import type { Theme } from "../../modes/interactive/theme/theme";
@@ -2,8 +2,8 @@
2
2
  * Tool wrappers for extensions.
3
3
  */
4
4
 
5
- import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
6
5
  import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
6
+ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
7
7
  import type { Theme } from "../../modes/interactive/theme/theme";
8
8
  import type { ExtensionRunner } from "./runner";
9
9
  import type { ExtensionContext, RegisteredTool, ToolCallEventResult, ToolResultEventResult } from "./types";
@@ -2,8 +2,8 @@
2
2
  * Hook runner - executes hooks and manages their lifecycle.
3
3
  */
4
4
 
5
- import type { Model } from "@mariozechner/pi-ai";
6
5
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
+ import type { Model } from "@oh-my-pi/pi-ai";
7
7
  import { theme } from "../../modes/interactive/theme/theme";
8
8
  import type { ModelRegistry } from "../model-registry";
9
9
  import type { SessionManager } from "../session-manager";
@@ -400,7 +400,7 @@ export class HookRunner {
400
400
  */
401
401
  async emitBeforeAgentStart(
402
402
  prompt: string,
403
- images?: import("@mariozechner/pi-ai").ImageContent[],
403
+ images?: import("@oh-my-pi/pi-ai").ImageContent[],
404
404
  ): Promise<BeforeAgentStartEventResult | undefined> {
405
405
  const ctx = this.createContext();
406
406
  let result: BeforeAgentStartEventResult | undefined;
@@ -5,8 +5,8 @@
5
5
  * and interact with the user via UI primitives.
6
6
  */
7
7
 
8
- import type { ImageContent, Message, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
9
8
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
9
+ import type { ImageContent, Message, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
10
10
  import type { Component, TUI } from "@oh-my-pi/pi-tui";
11
11
  import type { Theme } from "../../modes/interactive/theme/theme";
12
12
  import type { CompactionPreparation, CompactionResult } from "../compaction/index";
package/src/core/index.ts CHANGED
@@ -38,5 +38,16 @@ export {
38
38
  type MCPToolsLoadResult,
39
39
  type MCPTransport,
40
40
  } from "./mcp/index";
41
+ export {
42
+ buildRemoteCommand,
43
+ closeAllConnections,
44
+ closeConnection,
45
+ ensureConnection,
46
+ getControlDir,
47
+ getControlPathTemplate,
48
+ type SSHConnectionTarget,
49
+ } from "./ssh/connection-manager";
50
+ export { executeSSH, type SSHExecutorOptions, type SSHResult } from "./ssh/ssh-executor";
51
+ export { hasSshfs, isMounted, mountRemote, unmountAll, unmountRemote } from "./ssh/sshfs-mount";
41
52
 
42
53
  export * as utils from "./utils";
@@ -5,8 +5,8 @@
5
5
  * and provides a transformer to convert them to LLM-compatible messages.
6
6
  */
7
7
 
8
- import type { ImageContent, Message, TextContent } from "@mariozechner/pi-ai";
9
8
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
9
+ import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
10
10
 
11
11
  export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
12
12
 
@@ -11,7 +11,7 @@ import {
11
11
  type KnownProvider,
12
12
  type Model,
13
13
  normalizeDomain,
14
- } from "@mariozechner/pi-ai";
14
+ } from "@oh-my-pi/pi-ai";
15
15
  import { type Static, Type } from "@sinclair/typebox";
16
16
  import AjvModule from "ajv";
17
17
  import type { AuthStorage } from "./auth-storage";
@@ -2,8 +2,8 @@
2
2
  * Model resolution, scoping, and initial selection
3
3
  */
4
4
 
5
- import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@mariozechner/pi-ai";
6
5
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
6
+ import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
7
7
  import chalk from "chalk";
8
8
  import { minimatch } from "minimatch";
9
9
  import { isValidThinkingLevel } from "../cli/args";
@@ -25,6 +25,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
25
25
  cerebras: "zai-glm-4.6",
26
26
  zai: "glm-4.6",
27
27
  mistral: "devstral-medium-latest",
28
+ opencode: "claude-sonnet-4-5",
28
29
  };
29
30
 
30
31
  export interface ScopedModel {
@@ -79,7 +80,7 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
79
80
  const provider = modelPattern.substring(0, slashIndex);
80
81
  const modelId = modelPattern.substring(slashIndex + 1);
81
82
  const providerMatch = availableModels.find(
82
- (m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase()
83
+ (m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase(),
83
84
  );
84
85
  if (providerMatch) {
85
86
  return providerMatch;
@@ -97,7 +98,7 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
97
98
  const matches = availableModels.filter(
98
99
  (m) =>
99
100
  m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
100
- m.name?.toLowerCase().includes(modelPattern.toLowerCase())
101
+ m.name?.toLowerCase().includes(modelPattern.toLowerCase()),
101
102
  );
102
103
 
103
104
  if (matches.length === 0) {
@@ -351,7 +352,7 @@ export async function restoreModelFromSession(
351
352
  savedModelId: string,
352
353
  currentModel: Model<Api> | undefined,
353
354
  shouldPrintMessages: boolean,
354
- modelRegistry: ModelRegistry
355
+ modelRegistry: ModelRegistry,
355
356
  ): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
356
357
  const restoredModel = modelRegistry.find(savedProvider, savedModelId);
357
358
 
@@ -427,7 +428,7 @@ export async function restoreModelFromSession(
427
428
  */
428
429
  export async function findSmolModel(
429
430
  modelRegistry: ModelRegistry,
430
- savedModel?: string
431
+ savedModel?: string,
431
432
  ): Promise<Model<Api> | undefined> {
432
433
  const availableModels = modelRegistry.getAvailable();
433
434
  if (availableModels.length === 0) return undefined;
@@ -470,7 +471,7 @@ export async function findSmolModel(
470
471
  */
471
472
  export async function findSlowModel(
472
473
  modelRegistry: ModelRegistry,
473
- savedModel?: string
474
+ savedModel?: string,
474
475
  ): Promise<Model<Api> | undefined> {
475
476
  const availableModels = modelRegistry.getAvailable();
476
477
  if (availableModels.length === 0) return undefined;