@oh-my-pi/pi-coding-agent 15.5.13 → 15.6.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 (192) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/initial-message.d.ts +1 -1
  4. package/dist/types/cli/tiny-models-cli.d.ts +9 -0
  5. package/dist/types/commands/tiny-models.d.ts +22 -0
  6. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  7. package/dist/types/commit/analysis/summary.d.ts +1 -1
  8. package/dist/types/commit/changelog/generate.d.ts +1 -1
  9. package/dist/types/commit/changelog/index.d.ts +2 -2
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  12. package/dist/types/config/model-id-affixes.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +1 -1
  14. package/dist/types/config/models-config-schema.d.ts +2 -0
  15. package/dist/types/config/settings-schema.d.ts +233 -17
  16. package/dist/types/discovery/helpers.d.ts +1 -1
  17. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  18. package/dist/types/eval/__tests__/llm-bridge.test.d.ts +1 -0
  19. package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
  20. package/dist/types/eval/llm-bridge.d.ts +25 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +15 -0
  23. package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
  24. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
  25. package/dist/types/internal-urls/local-protocol.d.ts +2 -1
  26. package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
  27. package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
  28. package/dist/types/internal-urls/router.d.ts +8 -1
  29. package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
  30. package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
  31. package/dist/types/internal-urls/types.d.ts +26 -0
  32. package/dist/types/memory-backend/index.d.ts +1 -0
  33. package/dist/types/memory-backend/resolve.d.ts +2 -1
  34. package/dist/types/memory-backend/types.d.ts +7 -1
  35. package/dist/types/mnemosyne/backend.d.ts +4 -0
  36. package/dist/types/mnemosyne/config.d.ts +29 -0
  37. package/dist/types/mnemosyne/index.d.ts +3 -0
  38. package/dist/types/mnemosyne/state.d.ts +72 -0
  39. package/dist/types/modes/components/custom-editor.d.ts +2 -3
  40. package/dist/types/modes/components/hook-selector.d.ts +27 -0
  41. package/dist/types/modes/components/index.d.ts +1 -0
  42. package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
  43. package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
  44. package/dist/types/modes/components/welcome.d.ts +1 -0
  45. package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
  46. package/dist/types/modes/gradient-highlight.d.ts +23 -0
  47. package/dist/types/modes/interactive-mode.d.ts +4 -2
  48. package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
  49. package/dist/types/modes/orchestrate.d.ts +10 -0
  50. package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
  51. package/dist/types/modes/theme/theme.d.ts +2 -1
  52. package/dist/types/modes/ultrathink.d.ts +3 -3
  53. package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
  54. package/dist/types/sdk.d.ts +3 -0
  55. package/dist/types/session/agent-session.d.ts +35 -0
  56. package/dist/types/system-prompt.d.ts +2 -0
  57. package/dist/types/task/executor.d.ts +2 -0
  58. package/dist/types/task/render.d.ts +5 -1
  59. package/dist/types/tiny/models.d.ts +185 -0
  60. package/dist/types/tiny/text.d.ts +4 -0
  61. package/dist/types/tiny/title-client.d.ts +24 -0
  62. package/dist/types/tiny/title-protocol.d.ts +74 -0
  63. package/dist/types/tiny/worker.d.ts +2 -0
  64. package/dist/types/tools/bash.d.ts +3 -1
  65. package/dist/types/tools/index.d.ts +7 -4
  66. package/dist/types/tools/memory-edit.d.ts +40 -0
  67. package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
  68. package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
  69. package/dist/types/tools/memory-render.d.ts +60 -0
  70. package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
  71. package/dist/types/tools/todo-write.d.ts +8 -0
  72. package/dist/types/tools/tool-result.d.ts +2 -0
  73. package/dist/types/utils/title-generator.d.ts +3 -0
  74. package/package.json +18 -14
  75. package/scripts/build-binary.ts +1 -0
  76. package/src/cli/tiny-models-cli.ts +127 -0
  77. package/src/cli-commands.ts +1 -0
  78. package/src/cli.ts +8 -8
  79. package/src/commands/tiny-models.ts +36 -0
  80. package/src/config/model-equivalence.ts +43 -2
  81. package/src/config/model-id-affixes.ts +64 -0
  82. package/src/config/model-registry.ts +166 -8
  83. package/src/config/models-config-schema.ts +1 -1
  84. package/src/config/settings-schema.ts +206 -14
  85. package/src/edit/hashline/diff.ts +5 -7
  86. package/src/eval/__tests__/llm-bridge.test.ts +297 -0
  87. package/src/eval/__tests__/shared-executors.test.ts +36 -0
  88. package/src/eval/js/shared/local-module-loader.ts +13 -1
  89. package/src/eval/js/shared/prelude.txt +8 -0
  90. package/src/eval/js/shared/rewrite-imports.ts +31 -26
  91. package/src/eval/js/tool-bridge.ts +4 -0
  92. package/src/eval/llm-bridge.ts +181 -0
  93. package/src/eval/py/prelude.py +52 -31
  94. package/src/export/html/template.generated.ts +1 -1
  95. package/src/export/html/template.js +0 -13
  96. package/src/extensibility/plugins/legacy-pi-compat.ts +60 -23
  97. package/src/internal-urls/agent-protocol.ts +18 -1
  98. package/src/internal-urls/artifact-protocol.ts +19 -1
  99. package/src/internal-urls/docs-index.generated.ts +5 -4
  100. package/src/internal-urls/local-protocol.ts +14 -1
  101. package/src/internal-urls/memory-protocol.ts +6 -1
  102. package/src/internal-urls/omp-protocol.ts +5 -1
  103. package/src/internal-urls/router.ts +20 -1
  104. package/src/internal-urls/rule-protocol.ts +8 -1
  105. package/src/internal-urls/skill-protocol.ts +8 -1
  106. package/src/internal-urls/types.ts +27 -0
  107. package/src/lsp/render.ts +1 -1
  108. package/src/main.ts +4 -0
  109. package/src/mcp/oauth-flow.ts +2 -2
  110. package/src/memory-backend/index.ts +1 -0
  111. package/src/memory-backend/resolve.ts +4 -1
  112. package/src/memory-backend/types.ts +8 -1
  113. package/src/mnemosyne/backend.ts +374 -0
  114. package/src/mnemosyne/config.ts +160 -0
  115. package/src/mnemosyne/index.ts +3 -0
  116. package/src/mnemosyne/state.ts +548 -0
  117. package/src/modes/acp/acp-agent.ts +11 -6
  118. package/src/modes/components/agent-dashboard.ts +4 -4
  119. package/src/modes/components/custom-editor.ts +3 -2
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/extensions/extension-list.ts +3 -2
  122. package/src/modes/components/footer.ts +5 -6
  123. package/src/modes/components/history-search.ts +3 -3
  124. package/src/modes/components/hook-selector.ts +94 -8
  125. package/src/modes/components/index.ts +1 -0
  126. package/src/modes/components/mcp-add-wizard.ts +3 -3
  127. package/src/modes/components/model-selector.ts +124 -26
  128. package/src/modes/components/oauth-selector.ts +3 -3
  129. package/src/modes/components/session-observer-overlay.ts +19 -13
  130. package/src/modes/components/session-selector.ts +3 -3
  131. package/src/modes/components/settings-defs.ts +7 -0
  132. package/src/modes/components/status-line/context-thresholds.ts +11 -0
  133. package/src/modes/components/status-line/presets.ts +1 -0
  134. package/src/modes/components/status-line/segments.ts +25 -2
  135. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  136. package/src/modes/components/tips.txt +12 -0
  137. package/src/modes/components/tool-execution.ts +67 -3
  138. package/src/modes/components/tree-selector.ts +3 -3
  139. package/src/modes/components/user-message-selector.ts +3 -3
  140. package/src/modes/components/welcome.ts +55 -1
  141. package/src/modes/controllers/command-controller.ts +16 -1
  142. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  143. package/src/modes/controllers/input-controller.ts +57 -0
  144. package/src/modes/gradient-highlight.ts +70 -0
  145. package/src/modes/interactive-mode.ts +80 -196
  146. package/src/modes/internal-url-autocomplete.ts +143 -0
  147. package/src/modes/orchestrate.ts +36 -0
  148. package/src/modes/prompt-action-autocomplete.ts +12 -0
  149. package/src/modes/theme/theme.ts +7 -0
  150. package/src/modes/ultrathink.ts +9 -53
  151. package/src/modes/utils/keybinding-matchers.ts +11 -0
  152. package/src/prompts/system/memory-consolidation-system.md +8 -0
  153. package/src/prompts/system/memory-extraction-system.md +26 -0
  154. package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +5 -16
  155. package/src/prompts/system/system-prompt.md +2 -0
  156. package/src/prompts/system/tiny-title-system.md +8 -0
  157. package/src/prompts/tools/eval.md +2 -0
  158. package/src/prompts/tools/memory-edit.md +8 -0
  159. package/src/prompts/tools/task.md +4 -7
  160. package/src/sdk.ts +8 -6
  161. package/src/session/agent-session.ts +147 -44
  162. package/src/session/session-manager.ts +47 -0
  163. package/src/slash-commands/builtin-registry.ts +10 -1
  164. package/src/system-prompt.ts +4 -0
  165. package/src/task/commands.ts +1 -5
  166. package/src/task/executor.ts +8 -0
  167. package/src/task/index.ts +2 -0
  168. package/src/task/render.ts +69 -26
  169. package/src/tiny/models.ts +217 -0
  170. package/src/tiny/text.ts +19 -0
  171. package/src/tiny/title-client.ts +340 -0
  172. package/src/tiny/title-protocol.ts +51 -0
  173. package/src/tiny/worker.ts +523 -0
  174. package/src/tools/bash.ts +58 -16
  175. package/src/tools/browser/tab-worker.ts +1 -1
  176. package/src/tools/eval.ts +24 -48
  177. package/src/tools/index.ts +17 -15
  178. package/src/tools/memory-edit.ts +59 -0
  179. package/src/tools/memory-recall.ts +100 -0
  180. package/src/tools/memory-reflect.ts +88 -0
  181. package/src/tools/memory-render.ts +185 -0
  182. package/src/tools/memory-retain.ts +91 -0
  183. package/src/tools/renderers.ts +4 -2
  184. package/src/tools/todo-write.ts +128 -29
  185. package/src/tools/tool-result.ts +8 -0
  186. package/src/utils/title-generator.ts +115 -13
  187. package/dist/types/tools/calculator.d.ts +0 -77
  188. package/src/prompts/tools/calculator.md +0 -10
  189. package/src/tools/calculator.ts +0 -541
  190. package/src/tools/hindsight-recall.ts +0 -69
  191. package/src/tools/hindsight-reflect.ts +0 -58
  192. package/src/tools/hindsight-retain.ts +0 -57
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Inline TUI renderers for the long-term memory tools (`retain`, `recall`,
3
+ * `reflect`).
4
+ *
5
+ * These keep the transcript terse — one status line plus, for `retain`, one
6
+ * `Remember: …` line per stored item — instead of the generic JSON arg tree,
7
+ * which exploded multi-line memory blobs into an unreadable wall.
8
+ */
9
+ import type { Component } from "@oh-my-pi/pi-tui";
10
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
+ import type { Theme } from "../modes/theme/theme";
12
+ interface RetainRenderArgs {
13
+ items?: Array<{
14
+ content?: string;
15
+ context?: string;
16
+ }>;
17
+ }
18
+ interface QueryRenderArgs {
19
+ query?: string;
20
+ }
21
+ export declare const retainToolRenderer: {
22
+ inline: boolean;
23
+ mergeCallAndResult: boolean;
24
+ renderCall(args: RetainRenderArgs, options: RenderResultOptions, theme: Theme): Component;
25
+ renderResult(result: {
26
+ content: Array<{
27
+ type: string;
28
+ text?: string;
29
+ }>;
30
+ details?: {
31
+ count?: number;
32
+ };
33
+ isError?: boolean;
34
+ }, options: RenderResultOptions, theme: Theme, args?: RetainRenderArgs): Component;
35
+ };
36
+ export declare const recallToolRenderer: {
37
+ inline: boolean;
38
+ mergeCallAndResult: boolean;
39
+ renderCall(args: QueryRenderArgs, _options: RenderResultOptions, theme: Theme): Component;
40
+ renderResult(result: {
41
+ content: Array<{
42
+ type: string;
43
+ text?: string;
44
+ }>;
45
+ isError?: boolean;
46
+ }, options: RenderResultOptions, theme: Theme, args?: QueryRenderArgs): Component;
47
+ };
48
+ export declare const reflectToolRenderer: {
49
+ inline: boolean;
50
+ mergeCallAndResult: boolean;
51
+ renderCall(args: QueryRenderArgs, _options: RenderResultOptions, theme: Theme): Component;
52
+ renderResult(result: {
53
+ content: Array<{
54
+ type: string;
55
+ text?: string;
56
+ }>;
57
+ isError?: boolean;
58
+ }, options: RenderResultOptions, theme: Theme, args?: QueryRenderArgs): Component;
59
+ };
60
+ export {};
@@ -1,14 +1,14 @@
1
1
  import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
2
  import * as z from "zod/v4";
3
3
  import type { ToolSession } from ".";
4
- declare const hindsightRetainSchema: z.ZodObject<{
4
+ declare const memoryRetainSchema: z.ZodObject<{
5
5
  items: z.ZodArray<z.ZodObject<{
6
6
  content: z.ZodString;
7
7
  context: z.ZodOptional<z.ZodString>;
8
8
  }, z.core.$strip>>;
9
9
  }, z.core.$strip>;
10
- export type HindsightRetainParams = z.infer<typeof hindsightRetainSchema>;
11
- export declare class HindsightRetainTool implements AgentTool<typeof hindsightRetainSchema> {
10
+ export type MemoryRetainParams = z.infer<typeof memoryRetainSchema>;
11
+ export declare class MemoryRetainTool implements AgentTool<typeof memoryRetainSchema> {
12
12
  private readonly session;
13
13
  readonly name = "retain";
14
14
  readonly approval: "read";
@@ -22,9 +22,9 @@ export declare class HindsightRetainTool implements AgentTool<typeof hindsightRe
22
22
  }, z.core.$strip>;
23
23
  readonly strict = true;
24
24
  readonly loadMode = "discoverable";
25
- readonly summary = "Store important facts in hindsight memory";
25
+ readonly summary = "Store important facts in long-term memory";
26
26
  constructor(session: ToolSession);
27
- static createIf(session: ToolSession): HindsightRetainTool | null;
28
- execute(_id: string, params: HindsightRetainParams): Promise<AgentToolResult>;
27
+ static createIf(session: ToolSession): MemoryRetainTool | null;
28
+ execute(_id: string, params: MemoryRetainParams): Promise<AgentToolResult>;
29
29
  }
30
30
  export {};
@@ -21,9 +21,14 @@ export interface TodoPhase {
21
21
  name: string;
22
22
  tasks: TodoItem[];
23
23
  }
24
+ export interface TodoCompletionTransition {
25
+ phase: string;
26
+ content: string;
27
+ }
24
28
  export interface TodoWriteToolDetails {
25
29
  phases: TodoPhase[];
26
30
  storage: "session" | "memory";
31
+ completedTasks?: TodoCompletionTransition[];
27
32
  }
28
33
  declare const todoWriteSchema: z.ZodObject<{
29
34
  ops: z.ZodArray<z.ZodObject<{
@@ -137,6 +142,9 @@ type TodoWriteRenderArgs = {
137
142
  export declare function phaseRomanNumeral(oneBasedIndex: number): string;
138
143
  /** Display-only phase header: `I. Foundation`. State and prompts never see this. */
139
144
  export declare function formatPhaseDisplayName(name: string, oneBasedIndex: number): string;
145
+ export declare const TODO_WRITE_STRIKE_HOLD_FRAMES = 2;
146
+ export declare const TODO_WRITE_STRIKE_REVEAL_FRAMES = 12;
147
+ export declare const TODO_WRITE_STRIKE_TOTAL_FRAMES: number;
140
148
  export declare const todoWriteToolRenderer: {
141
149
  renderCall(args: TodoWriteRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component;
142
150
  renderResult(result: {
@@ -24,6 +24,8 @@ export declare class ToolResultBuilder<TDetails extends DetailsWithMeta> {
24
24
  sourcePath(value: string): this;
25
25
  sourceInternal(value: string): this;
26
26
  diagnostics(summary: string, messages: string[]): this;
27
+ /** Flag the result as a non-throwing failure (agent-loop surfaces it as a tool error). */
28
+ error(value?: boolean): this;
27
29
  done(): AgentToolResult<TDetails>;
28
30
  }
29
31
  export declare function toolResult<TDetails extends DetailsWithMeta>(details?: TDetails): ToolResultBuilder<TDetails>;
@@ -1,6 +1,8 @@
1
1
  import { type Api, type Model } from "@oh-my-pi/pi-ai";
2
2
  import type { ModelRegistry } from "../config/model-registry";
3
3
  import type { Settings } from "../config/settings";
4
+ export declare const TITLE_LOCAL_FALLBACK_DELAY_MS = 10000;
5
+ export declare function raceFirstNonNull<T>(primary: Promise<T | null>, startFallback: () => Promise<T | null>, delayMs?: number, onPrimaryWinAfterFallback?: () => void): Promise<T | null>;
4
6
  /**
5
7
  * Generate a title for a session based on the first user message.
6
8
  *
@@ -15,6 +17,7 @@ import type { Settings } from "../config/settings";
15
17
  * reflects the credential actually selected for this request.
16
18
  */
17
19
  export declare function generateSessionTitle(firstMessage: string, registry: ModelRegistry, settings: Settings, sessionId?: string, currentModel?: Model<Api>, metadataResolver?: (provider: string) => Record<string, unknown> | undefined): Promise<string | null>;
20
+ export declare function generateTitleOnline(firstMessage: string, registry: ModelRegistry, settings: Settings, sessionId?: string, currentModel?: Model<Api>, metadataResolver?: (provider: string) => Record<string, unknown> | undefined, signal?: AbortSignal): Promise<string | null>;
18
21
  export declare function formatSessionTerminalTitle(sessionName: string | undefined, cwd?: string): string;
19
22
  /**
20
23
  * Set the terminal title using OSC 0 (sets both tab and window title). Unsupported terminals ignore it.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.5.13",
4
+ "version": "15.6.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -44,31 +44,35 @@
44
44
  "generate-template": "bun scripts/generate-template.ts"
45
45
  },
46
46
  "dependencies": {
47
- "@agentclientprotocol/sdk": "0.21.0",
48
- "@babel/parser": "^7.29.3",
47
+ "@agentclientprotocol/sdk": "0.22.1",
48
+ "@babel/parser": "^7.29.7",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "15.5.13",
51
- "@oh-my-pi/omp-stats": "15.5.13",
52
- "@oh-my-pi/pi-agent-core": "15.5.13",
53
- "@oh-my-pi/pi-ai": "15.5.13",
54
- "@oh-my-pi/pi-natives": "15.5.13",
55
- "@oh-my-pi/pi-tui": "15.5.13",
56
- "@oh-my-pi/pi-utils": "15.5.13",
57
- "@puppeteer/browsers": "^2.13.0",
50
+ "@oh-my-pi/hashline": "15.6.0",
51
+ "@oh-my-pi/omp-stats": "15.6.0",
52
+ "@oh-my-pi/pi-agent-core": "15.6.0",
53
+ "@oh-my-pi/pi-ai": "15.6.0",
54
+ "@oh-my-pi/pi-mnemosyne": "15.6.0",
55
+ "@oh-my-pi/pi-natives": "15.6.0",
56
+ "@oh-my-pi/pi-tui": "15.6.0",
57
+ "@oh-my-pi/pi-utils": "15.6.0",
58
+ "@puppeteer/browsers": "^3.0.4",
58
59
  "@types/turndown": "5.0.6",
59
60
  "@xterm/headless": "^6.0.0",
60
61
  "chalk": "^5.6.2",
61
62
  "diff": "^9.0.0",
62
- "fflate": "0.8.2",
63
+ "fflate": "0.8.3",
63
64
  "handlebars": "^4.7.9",
64
65
  "linkedom": "^0.18.12",
65
- "lru-cache": "11.3.6",
66
+ "lru-cache": "11.5.1",
66
67
  "markit-ai": "0.5.3",
67
- "puppeteer-core": "^24.42.0",
68
+ "puppeteer-core": "^25.1.0",
68
69
  "turndown": "7.2.4",
69
70
  "turndown-plugin-gfm": "1.0.2",
70
71
  "zod": "4.4.3"
71
72
  },
73
+ "optionalDependencies": {
74
+ "@huggingface/transformers": "^4.2.0"
75
+ },
72
76
  "devDependencies": {
73
77
  "@types/bun": "^1.3.14"
74
78
  },
@@ -56,6 +56,7 @@ async function main(): Promise<void> {
56
56
  "../stats/src/sync-worker.ts",
57
57
  "./src/tools/browser/tab-worker-entry.ts",
58
58
  "./src/eval/js/worker-entry.ts",
59
+ "./src/tiny/worker.ts",
59
60
  // Legacy pi-* extension compat entrypoints served by
60
61
  // `legacy-pi-compat.ts`. These are reached via computed bunfs paths
61
62
  // (which `--compile`'s static analyzer cannot trace), so each must be
@@ -0,0 +1,127 @@
1
+ import { formatBytes } from "@oh-my-pi/pi-utils";
2
+ import chalk from "chalk";
3
+ import {
4
+ DEFAULT_TINY_TITLE_LOCAL_MODEL_KEY,
5
+ getTinyLocalModelSpec,
6
+ isTinyLocalModelKey,
7
+ TINY_LOCAL_MODELS,
8
+ type TinyLocalModelKey,
9
+ } from "../tiny/models";
10
+ import { shutdownTinyTitleClient, tinyTitleClient } from "../tiny/title-client";
11
+ import type { TinyTitleProgressEvent } from "../tiny/title-protocol";
12
+
13
+ export type TinyModelsAction = "download" | "list";
14
+
15
+ export interface TinyModelsCommandArgs {
16
+ action: TinyModelsAction;
17
+ model?: string;
18
+ flags: {
19
+ json?: boolean;
20
+ };
21
+ }
22
+
23
+ interface ProgressReporter {
24
+ onProgress(event: TinyTitleProgressEvent): void;
25
+ finish(ok: boolean): void;
26
+ }
27
+
28
+ interface DownloadResult {
29
+ model: TinyLocalModelKey;
30
+ ok: boolean;
31
+ }
32
+
33
+ function writeLine(text = ""): void {
34
+ process.stdout.write(`${text}\n`);
35
+ }
36
+
37
+ function resolveModels(model: string | undefined): TinyLocalModelKey[] {
38
+ if (!model) return [DEFAULT_TINY_TITLE_LOCAL_MODEL_KEY];
39
+ if (model === "all") return TINY_LOCAL_MODELS.map(spec => spec.key);
40
+ if (!isTinyLocalModelKey(model)) {
41
+ const values = TINY_LOCAL_MODELS.map(spec => spec.key).join(", ");
42
+ throw new Error(`Unknown tiny local model: ${model}. Expected one of: ${values}, all`);
43
+ }
44
+ return [model];
45
+ }
46
+
47
+ function listModels(json: boolean | undefined): void {
48
+ if (json) {
49
+ writeLine(JSON.stringify({ models: TINY_LOCAL_MODELS }));
50
+ return;
51
+ }
52
+ writeLine(chalk.bold("Tiny local models"));
53
+ for (const spec of TINY_LOCAL_MODELS) {
54
+ const defaultMark = spec.key === DEFAULT_TINY_TITLE_LOCAL_MODEL_KEY ? chalk.cyan(" default") : "";
55
+ writeLine(`${chalk.cyan(spec.key)}${defaultMark}`);
56
+ writeLine(` ${spec.label} — ${spec.description}`);
57
+ }
58
+ }
59
+
60
+ function makeProgressReporter(modelKey: TinyLocalModelKey, json: boolean | undefined): ProgressReporter {
61
+ if (json || !process.stdout.isTTY) {
62
+ return { onProgress: () => undefined, finish: () => undefined };
63
+ }
64
+ const label = getTinyLocalModelSpec(modelKey)?.label ?? modelKey;
65
+ let lastWidth = 0;
66
+ let lastProgress = -1;
67
+ const render = (event: TinyTitleProgressEvent): void => {
68
+ const progress = event.progress ?? lastProgress;
69
+ if (progress >= 0 && progress < lastProgress + 1 && event.status !== "ready") return;
70
+ if (progress >= 0) lastProgress = progress;
71
+ const ratio = progress >= 0 ? Math.max(0, Math.min(1, progress / 100)) : 0;
72
+ const barWidth = 30;
73
+ const filled = Math.round(ratio * barWidth);
74
+ const bar = `${"█".repeat(filled)}${"░".repeat(barWidth - filled)}`;
75
+ const pct = progress >= 0 ? `${Math.floor(progress).toString().padStart(3, " ")}%` : " --%";
76
+ const bytes = event.loaded && event.total ? ` ${formatBytes(event.loaded)}/${formatBytes(event.total)}` : "";
77
+ const file = event.file ? ` ${event.file.split("/").at(-1) ?? event.file}` : "";
78
+ const statusLabel = event.status === "ready" ? "Ready" : "Downloading";
79
+ const line = `${chalk.cyan(statusLabel)} ${label} [${bar}] ${pct}${bytes}${file}`;
80
+ process.stdout.write(`\r${line.padEnd(lastWidth)}`);
81
+ lastWidth = line.length;
82
+ };
83
+ return {
84
+ onProgress(event) {
85
+ if (event.modelKey !== modelKey) return;
86
+ render(event);
87
+ },
88
+ finish(ok) {
89
+ const suffix = ok ? chalk.green("done") : chalk.red("failed");
90
+ process.stdout.write(`\r${`${label}: ${suffix}`.padEnd(lastWidth)}\n`);
91
+ },
92
+ };
93
+ }
94
+
95
+ async function downloadOne(modelKey: TinyLocalModelKey, json: boolean | undefined): Promise<DownloadResult> {
96
+ const label = getTinyLocalModelSpec(modelKey)?.label ?? modelKey;
97
+ if (!json && !process.stdout.isTTY) writeLine(`Downloading ${label} (${modelKey})...`);
98
+ const progress = makeProgressReporter(modelKey, json);
99
+ const ok = await tinyTitleClient.downloadModel(modelKey, { onProgress: progress.onProgress });
100
+ progress.finish(ok);
101
+ if (!json && !process.stdout.isTTY) writeLine(ok ? `Downloaded ${label}.` : `Failed to download ${label}.`);
102
+ return { model: modelKey, ok };
103
+ }
104
+
105
+ export async function runTinyModelsCommand(command: TinyModelsCommandArgs): Promise<void> {
106
+ if (command.action === "list") {
107
+ listModels(command.flags.json);
108
+ return;
109
+ }
110
+
111
+ const models = resolveModels(command.model);
112
+ const results: DownloadResult[] = [];
113
+ try {
114
+ for (const model of models) {
115
+ results.push(await downloadOne(model, command.flags.json));
116
+ }
117
+ } finally {
118
+ await shutdownTinyTitleClient();
119
+ }
120
+
121
+ if (command.flags.json) {
122
+ writeLine(JSON.stringify({ results }));
123
+ }
124
+ if (results.some(result => !result.ok)) {
125
+ throw new Error("One or more tiny title models failed to download");
126
+ }
127
+ }
@@ -28,6 +28,7 @@ export const commands: CommandEntry[] = [
28
28
  { name: "ssh", load: () => import("./commands/ssh").then(m => m.default) },
29
29
  { name: "stats", load: () => import("./commands/stats").then(m => m.default) },
30
30
  { name: "update", load: () => import("./commands/update").then(m => m.default) },
31
+ { name: "tiny-models", load: () => import("./commands/tiny-models").then(m => m.default) },
31
32
  { name: "worktree", load: () => import("./commands/worktree").then(m => m.default), aliases: ["wt"] },
32
33
  { name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
33
34
  ];
package/src/cli.ts CHANGED
@@ -32,20 +32,20 @@ async function showHelp(config: CliConfig): Promise<void> {
32
32
  }
33
33
  }
34
34
  /**
35
- * Smoke-test entry. Spawns the stats sync worker, pings it, exits.
35
+ * Smoke-test entry. Spawns bundled workers, pings them, exits.
36
36
  *
37
37
  * Purpose: catch the silent worker-load regressions that hit compiled
38
- * binaries (issues #1011 and #1027). Neither `--version` nor
39
- * `stats --summary` actually spawns a Worker on a fresh install the
40
- * sync path early-returns when no session files exist. This probe is the
41
- * minimal end-to-end test that proves `new Worker(...)` resolves and the
42
- * bundled worker module evaluates successfully. Wired into
43
- * `scripts/install-tests/run-ci.sh` so binary / source-link / tarball
44
- * installs all exercise it on every CI run.
38
+ * binaries (issues #1011 and #1027). Version/help paths do not spawn worker
39
+ * modules on a fresh install, so this probe is the minimal end-to-end test
40
+ * that proves `new Worker(...)` resolves and bundled worker modules evaluate.
41
+ * Wired into `scripts/install-tests/run-ci.sh` so binary / source-link /
42
+ * tarball installs all exercise it on every CI run.
45
43
  */
46
44
  async function runSmokeTest(): Promise<void> {
47
45
  const { smokeTestSyncWorker } = await import("@oh-my-pi/omp-stats");
46
+ const { smokeTestTinyTitleWorker } = await import("./tiny/title-client");
48
47
  await smokeTestSyncWorker();
48
+ await smokeTestTinyTitleWorker();
49
49
  process.stdout.write("smoke-test: ok\n");
50
50
  }
51
51
 
@@ -0,0 +1,36 @@
1
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
2
+ import { runTinyModelsCommand, type TinyModelsAction, type TinyModelsCommandArgs } from "../cli/tiny-models-cli";
3
+
4
+ const ACTIONS: TinyModelsAction[] = ["download", "list"];
5
+
6
+ export default class TinyModels extends Command {
7
+ static description = "Download tiny local models (session titles + memory)";
8
+
9
+ static args = {
10
+ action: Args.string({
11
+ description: "Action to perform",
12
+ required: false,
13
+ options: ACTIONS,
14
+ }),
15
+ model: Args.string({
16
+ description: "Model key, or all",
17
+ required: false,
18
+ }),
19
+ };
20
+
21
+ static flags = {
22
+ json: Flags.boolean({ description: "Output JSON" }),
23
+ };
24
+
25
+ async run(): Promise<void> {
26
+ const { args, flags } = await this.parse(TinyModels);
27
+ const command: TinyModelsCommandArgs = {
28
+ action: (args.action ?? "download") as TinyModelsAction,
29
+ model: args.model,
30
+ flags: {
31
+ json: flags.json,
32
+ },
33
+ };
34
+ await runTinyModelsCommand(command);
35
+ }
36
+ }
@@ -1,4 +1,9 @@
1
1
  import { type Api, getBundledModels, getBundledProviders, type Model } from "@oh-my-pi/pi-ai";
2
+ import {
3
+ getBracketStrippedModelIdCandidates,
4
+ getLongestModelLikeIdSegment,
5
+ getModelLikeIdSegments,
6
+ } from "./model-id-affixes";
2
7
 
3
8
  export type CanonicalModelSource = "override" | "bundled" | "heuristic" | "fallback";
4
9
 
@@ -29,6 +34,7 @@ export interface CanonicalModelIndex {
29
34
  interface CanonicalReferenceData {
30
35
  references: Map<string, Model<Api>>;
31
36
  officialIds: Set<string>;
37
+ suffixAliases: Map<string, string>;
32
38
  }
33
39
 
34
40
  interface CompiledEquivalenceConfig {
@@ -70,6 +76,26 @@ function shouldReplaceReference(existing: Model<Api> | undefined, candidate: Mod
70
76
  return existing.provider !== "openai" && candidate.provider === "openai";
71
77
  }
72
78
 
79
+ function buildCanonicalSuffixAliasMap(references: ReadonlyMap<string, Model<Api>>): Map<string, string> {
80
+ const aliases = new Map<string, string>();
81
+ for (const reference of references.values()) {
82
+ const slashIndex = reference.id.lastIndexOf("/");
83
+ if (slashIndex === -1) {
84
+ continue;
85
+ }
86
+ const suffix = reference.id.slice(slashIndex + 1);
87
+ const alias = getLongestModelLikeIdSegment(suffix);
88
+ if (!alias) {
89
+ continue;
90
+ }
91
+ const existing = aliases.get(alias);
92
+ if (!existing || compareCandidatePreference(reference.id, existing) < 0) {
93
+ aliases.set(alias, reference.id);
94
+ }
95
+ }
96
+ return new Map([...aliases.entries()].map(([alias, referenceId]) => [normalizeCanonicalIdKey(alias), referenceId]));
97
+ }
98
+
73
99
  function createCanonicalReferenceData(): CanonicalReferenceData {
74
100
  if (referenceDataCache) {
75
101
  return referenceDataCache;
@@ -85,9 +111,11 @@ function createCanonicalReferenceData(): CanonicalReferenceData {
85
111
  }
86
112
  }
87
113
  const officialIds = new Set(references.keys());
114
+ const suffixAliases = buildCanonicalSuffixAliasMap(references);
88
115
  referenceDataCache = {
89
116
  references: Object.freeze(references) as Map<string, Model<Api>>,
90
117
  officialIds: Object.freeze(officialIds) as Set<string>,
118
+ suffixAliases: Object.freeze(suffixAliases) as Map<string, string>,
91
119
  };
92
120
  return referenceDataCache;
93
121
  }
@@ -590,6 +618,13 @@ function expandHeavyCanonicalCandidates(normalized: string, queue: string[]): vo
590
618
  queue.push(wrapperCandidate);
591
619
  }
592
620
 
621
+ for (const strippedAffixCandidate of getBracketStrippedModelIdCandidates(normalized)) {
622
+ queue.push(strippedAffixCandidate);
623
+ }
624
+ for (const segment of getModelLikeIdSegments(normalized)) {
625
+ queue.push(segment);
626
+ }
627
+
593
628
  const strippedSyntheticPrefix = stripSyntheticPrefix(normalized);
594
629
  if (strippedSyntheticPrefix) {
595
630
  queue.push(strippedSyntheticPrefix);
@@ -718,9 +753,15 @@ function resolveCanonicalIdForModel(
718
753
  }
719
754
 
720
755
  const heuristicCandidates = getHeuristicCanonicalCandidates(model.id, referenceData.officialIds);
721
- const officialMatches = heuristicCandidates.filter(candidate => referenceData.officialIds.has(candidate));
756
+ const officialMatches = new Set(heuristicCandidates.filter(candidate => referenceData.officialIds.has(candidate)));
757
+ for (const candidate of heuristicCandidates) {
758
+ const aliased = referenceData.suffixAliases.get(normalizeCanonicalIdKey(candidate));
759
+ if (aliased) {
760
+ officialMatches.add(aliased);
761
+ }
762
+ }
722
763
  const preferredFallback = getPreferredFallbackCanonicalCandidate(model.id, heuristicCandidates);
723
- const match = selectBestOfficialCandidate(officialMatches);
764
+ const match = selectBestOfficialCandidate([...officialMatches]);
724
765
  if (match) {
725
766
  if (
726
767
  preferredFallback &&
@@ -0,0 +1,64 @@
1
+ const LEADING_BRACKETED_AFFIX_PATTERN = /^(?:\s*(?:\[|【)[^\]】]+(?:\]|】)\s*)+/u;
2
+ const TRAILING_BRACKETED_AFFIX_PATTERN = /(?:\s*(?:\[|【)[^\]】]+(?:\]|】)\s*)+$/u;
3
+ const MODEL_ID_SEGMENT_PATTERN = /[a-z0-9.:-]+/g;
4
+ const MODEL_FAMILY_PREFIX_PATTERN =
5
+ /^(claude|gemini|gpt|grok|glm|qwen|deepseek|kimi|mimo|doubao|ernie|gpt-oss|gemma|minimax|step|command|jamba|llama|o[1345])/i;
6
+
7
+ function hasDigit(value: string): boolean {
8
+ return /\d/.test(value);
9
+ }
10
+
11
+ function compareSegmentPreference(left: string, right: string): number {
12
+ if (left.length !== right.length) {
13
+ return right.length - left.length;
14
+ }
15
+ return left.localeCompare(right);
16
+ }
17
+
18
+ export function getModelLikeIdSegments(modelId: string): string[] {
19
+ const normalized = normalizeModelIdWhitespace(modelId).toLowerCase();
20
+ if (!normalized) return [];
21
+ const segments = (normalized.match(MODEL_ID_SEGMENT_PATTERN) ?? []).filter(
22
+ segment => MODEL_FAMILY_PREFIX_PATTERN.test(segment) && hasDigit(segment),
23
+ );
24
+ const unique = [...new Set(segments)];
25
+ unique.sort(compareSegmentPreference);
26
+ return unique;
27
+ }
28
+
29
+ export function getLongestModelLikeIdSegment(modelId: string): string | undefined {
30
+ return getModelLikeIdSegments(modelId)[0];
31
+ }
32
+
33
+ function normalizeModelIdWhitespace(value: string): string {
34
+ return value.trim().replace(/\s+/g, " ");
35
+ }
36
+
37
+ /**
38
+ * Strip reseller / wrapper tags that are injected as bracketed affixes around an
39
+ * upstream model id, e.g.
40
+ * "[Kiro] claude-opus-4-8" -> "claude-opus-4-8"
41
+ * "[gcli转] gemini-3.1-pro-preview [假流]" -> "gemini-3.1-pro-preview"
42
+ */
43
+ export function getBracketStrippedModelIdCandidates(modelId: string): string[] {
44
+ const normalized = normalizeModelIdWhitespace(modelId);
45
+ if (!normalized) return [];
46
+
47
+ const candidates = new Set<string>();
48
+ const withoutLeading = normalizeModelIdWhitespace(normalized.replace(LEADING_BRACKETED_AFFIX_PATTERN, ""));
49
+ const withoutTrailing = normalizeModelIdWhitespace(normalized.replace(TRAILING_BRACKETED_AFFIX_PATTERN, ""));
50
+ const withoutBoth = normalizeModelIdWhitespace(
51
+ normalized.replace(LEADING_BRACKETED_AFFIX_PATTERN, "").replace(TRAILING_BRACKETED_AFFIX_PATTERN, ""),
52
+ );
53
+
54
+ for (const candidate of [withoutBoth, withoutLeading, withoutTrailing]) {
55
+ if (candidate && candidate !== normalized) {
56
+ candidates.add(candidate);
57
+ }
58
+ }
59
+ return [...candidates];
60
+ }
61
+
62
+ export function stripBracketedModelIdAffixes(modelId: string): string | undefined {
63
+ return getBracketStrippedModelIdCandidates(modelId)[0];
64
+ }