@oh-my-pi/pi-coding-agent 15.5.15 → 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 (167) hide show
  1. package/CHANGELOG.md +46 -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/settings-schema.d.ts +232 -7
  14. package/dist/types/discovery/helpers.d.ts +1 -1
  15. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  16. package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
  17. package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
  18. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
  19. package/dist/types/internal-urls/local-protocol.d.ts +2 -1
  20. package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
  21. package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
  22. package/dist/types/internal-urls/router.d.ts +8 -1
  23. package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
  24. package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
  25. package/dist/types/internal-urls/types.d.ts +26 -0
  26. package/dist/types/memory-backend/index.d.ts +1 -0
  27. package/dist/types/memory-backend/resolve.d.ts +2 -1
  28. package/dist/types/memory-backend/types.d.ts +7 -1
  29. package/dist/types/mnemosyne/backend.d.ts +4 -0
  30. package/dist/types/mnemosyne/config.d.ts +29 -0
  31. package/dist/types/mnemosyne/index.d.ts +3 -0
  32. package/dist/types/mnemosyne/state.d.ts +72 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -3
  34. package/dist/types/modes/components/hook-selector.d.ts +27 -0
  35. package/dist/types/modes/components/index.d.ts +1 -0
  36. package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
  37. package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
  38. package/dist/types/modes/components/welcome.d.ts +1 -0
  39. package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
  40. package/dist/types/modes/gradient-highlight.d.ts +23 -0
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
  43. package/dist/types/modes/orchestrate.d.ts +10 -0
  44. package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
  45. package/dist/types/modes/ultrathink.d.ts +3 -3
  46. package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
  47. package/dist/types/sdk.d.ts +3 -0
  48. package/dist/types/session/agent-session.d.ts +33 -0
  49. package/dist/types/system-prompt.d.ts +2 -0
  50. package/dist/types/task/executor.d.ts +2 -0
  51. package/dist/types/task/render.d.ts +5 -1
  52. package/dist/types/tiny/models.d.ts +185 -0
  53. package/dist/types/tiny/text.d.ts +4 -0
  54. package/dist/types/tiny/title-client.d.ts +24 -0
  55. package/dist/types/tiny/title-protocol.d.ts +74 -0
  56. package/dist/types/tiny/worker.d.ts +2 -0
  57. package/dist/types/tools/bash.d.ts +3 -1
  58. package/dist/types/tools/index.d.ts +7 -3
  59. package/dist/types/tools/memory-edit.d.ts +40 -0
  60. package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
  61. package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
  62. package/dist/types/tools/memory-render.d.ts +60 -0
  63. package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
  64. package/dist/types/tools/todo-write.d.ts +8 -0
  65. package/dist/types/tools/tool-result.d.ts +2 -0
  66. package/dist/types/utils/title-generator.d.ts +3 -0
  67. package/package.json +18 -14
  68. package/scripts/build-binary.ts +1 -0
  69. package/src/cli/tiny-models-cli.ts +127 -0
  70. package/src/cli-commands.ts +1 -0
  71. package/src/cli.ts +8 -8
  72. package/src/commands/tiny-models.ts +36 -0
  73. package/src/config/model-equivalence.ts +43 -2
  74. package/src/config/model-id-affixes.ts +64 -0
  75. package/src/config/model-registry.ts +84 -10
  76. package/src/config/settings-schema.ts +205 -4
  77. package/src/edit/hashline/diff.ts +5 -7
  78. package/src/eval/__tests__/shared-executors.test.ts +36 -0
  79. package/src/eval/js/shared/local-module-loader.ts +13 -1
  80. package/src/eval/js/shared/rewrite-imports.ts +31 -26
  81. package/src/internal-urls/agent-protocol.ts +18 -1
  82. package/src/internal-urls/artifact-protocol.ts +19 -1
  83. package/src/internal-urls/docs-index.generated.ts +3 -1
  84. package/src/internal-urls/local-protocol.ts +14 -1
  85. package/src/internal-urls/memory-protocol.ts +6 -1
  86. package/src/internal-urls/omp-protocol.ts +5 -1
  87. package/src/internal-urls/router.ts +20 -1
  88. package/src/internal-urls/rule-protocol.ts +8 -1
  89. package/src/internal-urls/skill-protocol.ts +8 -1
  90. package/src/internal-urls/types.ts +27 -0
  91. package/src/lsp/render.ts +1 -1
  92. package/src/mcp/oauth-flow.ts +2 -2
  93. package/src/memory-backend/index.ts +1 -0
  94. package/src/memory-backend/resolve.ts +4 -1
  95. package/src/memory-backend/types.ts +8 -1
  96. package/src/mnemosyne/backend.ts +374 -0
  97. package/src/mnemosyne/config.ts +160 -0
  98. package/src/mnemosyne/index.ts +3 -0
  99. package/src/mnemosyne/state.ts +548 -0
  100. package/src/modes/acp/acp-agent.ts +11 -6
  101. package/src/modes/components/agent-dashboard.ts +4 -4
  102. package/src/modes/components/custom-editor.ts +3 -2
  103. package/src/modes/components/diff.ts +2 -2
  104. package/src/modes/components/extensions/extension-list.ts +3 -2
  105. package/src/modes/components/footer.ts +5 -6
  106. package/src/modes/components/history-search.ts +3 -3
  107. package/src/modes/components/hook-selector.ts +94 -8
  108. package/src/modes/components/index.ts +1 -0
  109. package/src/modes/components/mcp-add-wizard.ts +3 -3
  110. package/src/modes/components/model-selector.ts +5 -4
  111. package/src/modes/components/oauth-selector.ts +3 -3
  112. package/src/modes/components/session-observer-overlay.ts +19 -13
  113. package/src/modes/components/session-selector.ts +3 -3
  114. package/src/modes/components/settings-defs.ts +7 -0
  115. package/src/modes/components/status-line/context-thresholds.ts +11 -0
  116. package/src/modes/components/status-line/segments.ts +2 -2
  117. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  118. package/src/modes/components/tips.txt +12 -0
  119. package/src/modes/components/tool-execution.ts +67 -3
  120. package/src/modes/components/tree-selector.ts +3 -3
  121. package/src/modes/components/user-message-selector.ts +3 -3
  122. package/src/modes/components/welcome.ts +55 -1
  123. package/src/modes/controllers/command-controller.ts +16 -1
  124. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  125. package/src/modes/controllers/input-controller.ts +57 -0
  126. package/src/modes/gradient-highlight.ts +70 -0
  127. package/src/modes/interactive-mode.ts +58 -109
  128. package/src/modes/internal-url-autocomplete.ts +143 -0
  129. package/src/modes/orchestrate.ts +36 -0
  130. package/src/modes/prompt-action-autocomplete.ts +12 -0
  131. package/src/modes/ultrathink.ts +9 -53
  132. package/src/modes/utils/keybinding-matchers.ts +11 -0
  133. package/src/prompts/system/memory-consolidation-system.md +8 -0
  134. package/src/prompts/system/memory-extraction-system.md +26 -0
  135. package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +5 -16
  136. package/src/prompts/system/system-prompt.md +2 -0
  137. package/src/prompts/system/tiny-title-system.md +8 -0
  138. package/src/prompts/tools/memory-edit.md +8 -0
  139. package/src/prompts/tools/task.md +4 -7
  140. package/src/sdk.ts +8 -6
  141. package/src/session/agent-session.ts +128 -44
  142. package/src/slash-commands/builtin-registry.ts +10 -1
  143. package/src/system-prompt.ts +4 -0
  144. package/src/task/commands.ts +1 -5
  145. package/src/task/executor.ts +8 -0
  146. package/src/task/index.ts +2 -0
  147. package/src/task/render.ts +69 -26
  148. package/src/tiny/models.ts +217 -0
  149. package/src/tiny/text.ts +19 -0
  150. package/src/tiny/title-client.ts +340 -0
  151. package/src/tiny/title-protocol.ts +51 -0
  152. package/src/tiny/worker.ts +523 -0
  153. package/src/tools/bash.ts +58 -16
  154. package/src/tools/browser/tab-worker.ts +1 -1
  155. package/src/tools/index.ts +17 -11
  156. package/src/tools/memory-edit.ts +59 -0
  157. package/src/tools/memory-recall.ts +100 -0
  158. package/src/tools/memory-reflect.ts +88 -0
  159. package/src/tools/memory-render.ts +185 -0
  160. package/src/tools/memory-retain.ts +91 -0
  161. package/src/tools/renderers.ts +4 -0
  162. package/src/tools/todo-write.ts +128 -29
  163. package/src/tools/tool-result.ts +8 -0
  164. package/src/utils/title-generator.ts +115 -13
  165. package/src/tools/hindsight-recall.ts +0 -69
  166. package/src/tools/hindsight-reflect.ts +0 -58
  167. package/src/tools/hindsight-retain.ts +0 -57
@@ -42,6 +42,12 @@ import {
42
42
  formatCanonicalVariantSelector,
43
43
  type ModelEquivalenceConfig,
44
44
  } from "./model-equivalence";
45
+ import {
46
+ getBracketStrippedModelIdCandidates,
47
+ getLongestModelLikeIdSegment,
48
+ getModelLikeIdSegments,
49
+ stripBracketedModelIdAffixes,
50
+ } from "./model-id-affixes";
45
51
  import {
46
52
  type ModelOverride,
47
53
  type ModelsConfig,
@@ -650,20 +656,53 @@ function shouldReplaceCustomReference(existing: Model<Api> | undefined, candidat
650
656
  return existing.provider !== "openai" && candidate.provider === "openai";
651
657
  }
652
658
 
659
+ function normalizeCustomReferenceKey(value: string): string {
660
+ return value.trim().toLowerCase();
661
+ }
662
+
653
663
  function buildCustomReferenceMap(): Map<string, Model<Api>> {
654
664
  const references = new Map<string, Model<Api>>();
655
665
  for (const provider of getBundledProviders()) {
656
666
  for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
657
667
  const candidate = model as Model<Api>;
658
- if (shouldReplaceCustomReference(references.get(candidate.id), candidate)) {
659
- references.set(candidate.id, candidate);
668
+ const key = normalizeCustomReferenceKey(candidate.id);
669
+ if (shouldReplaceCustomReference(references.get(key), candidate)) {
670
+ references.set(key, candidate);
660
671
  }
661
672
  }
662
673
  }
663
674
  return references;
664
675
  }
665
676
 
677
+ function buildCustomReferenceSuffixAliasMap(exactReferences: ReadonlyMap<string, Model<Api>>): Map<string, Model<Api>> {
678
+ const aliases = new Map<string, Model<Api>>();
679
+ for (const reference of exactReferences.values()) {
680
+ const slashIndex = reference.id.lastIndexOf("/");
681
+ if (slashIndex === -1) {
682
+ continue;
683
+ }
684
+ const suffix = reference.id.slice(slashIndex + 1);
685
+ const alias = getLongestModelLikeIdSegment(suffix);
686
+ if (!alias) {
687
+ continue;
688
+ }
689
+ if (shouldReplaceCustomReference(aliases.get(alias), reference)) {
690
+ aliases.set(alias, reference);
691
+ }
692
+ }
693
+ return aliases;
694
+ }
695
+
666
696
  const customReferenceMap = buildCustomReferenceMap();
697
+ const customReferenceSuffixAliasMap = buildCustomReferenceSuffixAliasMap(customReferenceMap);
698
+
699
+ const CUSTOM_REFERENCE_TRAILING_MARKER_PATTERN =
700
+ /[-:](?:thinking|customtools|high|low|medium|minimal|xhigh|free|cloud|exacto|nitro|original|optimized|nvfp4|fp8|fp4|bf16|int8|int4|search)$/i;
701
+
702
+ function stripCustomReferenceTrailingMarker(candidate: string): string | undefined {
703
+ const match = CUSTOM_REFERENCE_TRAILING_MARKER_PATTERN.exec(candidate);
704
+ return match ? candidate.slice(0, match.index) : undefined;
705
+ }
667
706
 
668
707
  function getCustomReferenceCandidateIds(modelId: string): string[] {
669
708
  const candidates = new Set<string>();
@@ -673,23 +712,46 @@ function getCustomReferenceCandidateIds(modelId: string): string[] {
673
712
  if (!candidate || candidates.has(candidate)) continue;
674
713
  candidates.add(candidate);
675
714
 
715
+ for (const stripped of getBracketStrippedModelIdCandidates(candidate)) {
716
+ queue.push(stripped);
717
+ }
718
+ for (const segment of getModelLikeIdSegments(candidate)) {
719
+ queue.push(segment);
720
+ }
721
+
676
722
  for (const suffix of [":cloud", "-cloud"] as const) {
677
723
  if (candidate.toLowerCase().endsWith(suffix)) {
678
724
  queue.push(candidate.slice(0, -suffix.length));
679
725
  }
680
726
  }
681
727
 
728
+ const slashIndex = candidate.lastIndexOf("/");
729
+ if (slashIndex !== -1) {
730
+ queue.push(candidate.slice(slashIndex + 1));
731
+ }
732
+
682
733
  const colonToDash = candidate.replace(/:/g, "-");
683
734
  if (colonToDash !== candidate) {
684
735
  queue.push(colonToDash);
685
736
  }
737
+
738
+ const lowercased = candidate.toLowerCase();
739
+ if (lowercased !== candidate) {
740
+ queue.push(lowercased);
741
+ }
742
+
743
+ const strippedMarker = stripCustomReferenceTrailingMarker(candidate);
744
+ if (strippedMarker) {
745
+ queue.push(strippedMarker);
746
+ }
686
747
  }
687
748
  return [...candidates];
688
749
  }
689
750
 
690
751
  function resolveCustomModelReference(modelId: string): Model<Api> | undefined {
691
752
  for (const candidate of getCustomReferenceCandidateIds(modelId)) {
692
- const reference = customReferenceMap.get(candidate);
753
+ const key = normalizeCustomReferenceKey(candidate);
754
+ const reference = customReferenceMap.get(key) ?? customReferenceSuffixAliasMap.get(key);
693
755
  if (reference) return reference;
694
756
  }
695
757
  return undefined;
@@ -1785,7 +1847,7 @@ export class ModelRegistry {
1785
1847
  throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
1786
1848
  }
1787
1849
  const payload = (await response.json()) as {
1788
- data?: Array<{ id?: string; supported_endpoint_types?: string[] }>;
1850
+ data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[] }>;
1789
1851
  };
1790
1852
  const items = payload.data ?? [];
1791
1853
  const discovered: Model<Api>[] = [];
@@ -1800,23 +1862,35 @@ export class ModelRegistry {
1800
1862
  : providerConfig.api;
1801
1863
  if (!api) continue;
1802
1864
  const isAnthropic = api === "anthropic-messages";
1865
+ const reference = resolveCustomModelReference(id);
1866
+ const discoveryName = typeof item.name === "string" ? item.name.trim() : "";
1867
+ const displayName =
1868
+ reference?.name ??
1869
+ (discoveryName && discoveryName !== id ? discoveryName : undefined) ??
1870
+ stripBracketedModelIdAffixes(id) ??
1871
+ id;
1803
1872
  discovered.push(
1804
1873
  enrichModelThinking({
1805
1874
  id,
1806
- name: id,
1875
+ name: displayName,
1807
1876
  api,
1808
1877
  provider: providerConfig.provider,
1809
1878
  baseUrl,
1810
- reasoning: false,
1811
- input: ["text"],
1879
+ reasoning: reference?.reasoning ?? false,
1880
+ thinking: reference?.thinking,
1881
+ input: reference?.input ?? ["text"],
1882
+ // Proxy pricing is provider-specific and usually does not match
1883
+ // upstream bundled catalogs, so keep costs local-unknown even when
1884
+ // we successfully recover the upstream model identity.
1812
1885
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1813
- contextWindow: 128000,
1814
- maxTokens: 8192,
1886
+ contextWindow: reference?.contextWindow ?? 128000,
1887
+ maxTokens: reference?.maxTokens ?? 8192,
1815
1888
  headers,
1816
1889
  // OpenAI-compat fields are no-ops on anthropic models; the
1817
1890
  // Anthropic SDK ignores them. Provider-level disableStrictTools
1818
1891
  // flows in via #applyProviderCompat for the third-party-Anthropic
1819
- // path.
1892
+ // path. Cross-wire bundled compat is intentionally not copied:
1893
+ // request-shaping fields are provider-wire specific.
1820
1894
  compat: isAnthropic
1821
1895
  ? undefined
1822
1896
  : {
@@ -1,6 +1,14 @@
1
1
  import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
2
2
  import { TASK_SIMPLE_MODES } from "../task/simple-mode";
3
3
  import { getThinkingLevelMetadata } from "../thinking";
4
+ import {
5
+ ONLINE_MEMORY_MODEL_KEY,
6
+ ONLINE_TINY_TITLE_MODEL_KEY,
7
+ TINY_MEMORY_MODEL_OPTIONS,
8
+ TINY_MEMORY_MODEL_VALUES,
9
+ TINY_TITLE_MODEL_OPTIONS,
10
+ TINY_TITLE_MODEL_VALUES,
11
+ } from "../tiny/models";
4
12
  import { EDIT_MODES } from "../utils/edit-mode";
5
13
 
6
14
  /** Unified settings schema - single source of truth for all settings.
@@ -1292,23 +1300,192 @@ export const SETTINGS_SCHEMA = {
1292
1300
  "memories.summaryInjectionTokenLimit": { type: "number", default: 5000 },
1293
1301
 
1294
1302
  // Memory backend selector — picks between local memories pipeline,
1295
- // Hindsight remote memory, or off. Legacy `memories.enabled` keeps gating
1296
- // the local backend; see config/settings.ts migration for details.
1303
+ // Mnemosyne local SQLite, Hindsight remote memory, or off. Legacy
1304
+ // `memories.enabled` keeps gating the local backend; see config/settings.ts
1305
+ // migration for details.
1297
1306
  "memory.backend": {
1298
1307
  type: "enum",
1299
- values: ["off", "local", "hindsight"] as const,
1308
+ values: ["off", "local", "hindsight", "mnemosyne"] as const,
1300
1309
  default: "off",
1301
1310
  ui: {
1302
1311
  tab: "memory",
1303
1312
  label: "Memory Backend",
1304
- description: "Off, local memory pipeline, or Hindsight remote memory",
1313
+ description: "Off, local summary pipeline, Mnemosyne SQLite, or Hindsight remote memory",
1305
1314
  options: [
1306
1315
  { value: "off", label: "Off", description: "No memory subsystem runs" },
1307
1316
  { value: "local", label: "Local", description: "Local rollout summarisation pipeline (memory_summary.md)" },
1308
1317
  { value: "hindsight", label: "Hindsight", description: "Vectorize Hindsight remote memory service" },
1318
+ {
1319
+ value: "mnemosyne",
1320
+ label: "Mnemosyne",
1321
+ description: "Local SQLite recall/retain backend with optional embeddings",
1322
+ },
1323
+ ],
1324
+ },
1325
+ },
1326
+
1327
+ // Mnemosyne local SQLite memory backend.
1328
+ "mnemosyne.dbPath": {
1329
+ type: "string",
1330
+ default: undefined,
1331
+ ui: {
1332
+ tab: "memory",
1333
+ label: "Mnemosyne DB Path",
1334
+ description: "Optional SQLite DB path. Defaults to the agent memories directory.",
1335
+ condition: "mnemosyneActive",
1336
+ },
1337
+ },
1338
+ "mnemosyne.bank": {
1339
+ type: "string",
1340
+ default: undefined,
1341
+ ui: {
1342
+ tab: "memory",
1343
+ label: "Mnemosyne Bank",
1344
+ description: "Optional shared bank base name. Per-project modes derive project-local banks from it.",
1345
+ condition: "mnemosyneActive",
1346
+ },
1347
+ },
1348
+ "mnemosyne.scoping": {
1349
+ type: "enum",
1350
+ values: ["global", "per-project", "per-project-tagged"] as const,
1351
+ default: "per-project",
1352
+ ui: {
1353
+ tab: "memory",
1354
+ label: "Mnemosyne Scoping",
1355
+ description:
1356
+ "global = one shared bank; per-project = isolated bank per cwd; per-project-tagged = project-local writes plus global recall visibility",
1357
+ options: [
1358
+ {
1359
+ value: "global",
1360
+ label: "Global",
1361
+ description: "One shared Mnemosyne bank for every project",
1362
+ },
1363
+ {
1364
+ value: "per-project",
1365
+ label: "Per project",
1366
+ description: "Project-local Mnemosyne bank per cwd basename",
1367
+ },
1368
+ {
1369
+ value: "per-project-tagged",
1370
+ label: "Per project (tagged)",
1371
+ description: "Write to a project-local bank but merge project + shared recall results",
1372
+ },
1373
+ ],
1374
+ condition: "mnemosyneActive",
1375
+ },
1376
+ },
1377
+ "mnemosyne.autoRecall": {
1378
+ type: "boolean",
1379
+ default: true,
1380
+ ui: {
1381
+ tab: "memory",
1382
+ label: "Mnemosyne Auto Recall",
1383
+ description: "Recall local memories into the first turn of each session",
1384
+ condition: "mnemosyneActive",
1385
+ },
1386
+ },
1387
+ "mnemosyne.autoRetain": {
1388
+ type: "boolean",
1389
+ default: true,
1390
+ ui: {
1391
+ tab: "memory",
1392
+ label: "Mnemosyne Auto Retain",
1393
+ description: "Retain completed conversation turns into local Mnemosyne memory",
1394
+ condition: "mnemosyneActive",
1395
+ },
1396
+ },
1397
+ "mnemosyne.noEmbeddings": {
1398
+ type: "boolean",
1399
+ default: false,
1400
+ ui: {
1401
+ tab: "memory",
1402
+ label: "Mnemosyne Disable Embeddings",
1403
+ description: "Force deterministic FTS-only recall instead of vector embeddings",
1404
+ condition: "mnemosyneActive",
1405
+ },
1406
+ },
1407
+ "mnemosyne.embeddingModel": {
1408
+ type: "string",
1409
+ default: undefined,
1410
+ ui: {
1411
+ tab: "memory",
1412
+ label: "Mnemosyne Embedding Model",
1413
+ description: "Optional embedding model override passed to Mnemosyne",
1414
+ condition: "mnemosyneActive",
1415
+ },
1416
+ },
1417
+ "mnemosyne.embeddingApiUrl": {
1418
+ type: "string",
1419
+ default: undefined,
1420
+ ui: {
1421
+ tab: "memory",
1422
+ label: "Mnemosyne Embedding API URL",
1423
+ description: "Optional OpenAI-compatible embedding endpoint passed to Mnemosyne",
1424
+ condition: "mnemosyneActive",
1425
+ },
1426
+ },
1427
+ "mnemosyne.embeddingApiKey": {
1428
+ type: "string",
1429
+ default: undefined,
1430
+ ui: {
1431
+ tab: "memory",
1432
+ label: "Mnemosyne Embedding API Key",
1433
+ description: "Optional embedding API key passed to Mnemosyne",
1434
+ condition: "mnemosyneActive",
1435
+ },
1436
+ },
1437
+ "mnemosyne.llmMode": {
1438
+ type: "enum",
1439
+ values: ["none", "smol", "remote"] as const,
1440
+ default: "smol",
1441
+ ui: {
1442
+ tab: "memory",
1443
+ label: "Mnemosyne LLM Mode",
1444
+ description: "Use no LLM, the configured smol model, or a remote OpenAI-compatible endpoint",
1445
+ condition: "mnemosyneActive",
1446
+ options: [
1447
+ { value: "none", label: "None", description: "Disable Mnemosyne LLM-backed extraction" },
1448
+ { value: "smol", label: "Smol", description: "Use the configured pi-ai smol model" },
1449
+ { value: "remote", label: "Remote", description: "Use the Mnemosyne remote LLM settings below" },
1309
1450
  ],
1310
1451
  },
1311
1452
  },
1453
+ "mnemosyne.llmBaseUrl": {
1454
+ type: "string",
1455
+ default: undefined,
1456
+ ui: {
1457
+ tab: "memory",
1458
+ label: "Mnemosyne LLM Base URL",
1459
+ description: "Optional OpenAI-compatible LLM endpoint for Mnemosyne remote mode",
1460
+ condition: "mnemosyneActive",
1461
+ },
1462
+ },
1463
+ "mnemosyne.llmApiKey": {
1464
+ type: "string",
1465
+ default: undefined,
1466
+ ui: {
1467
+ tab: "memory",
1468
+ label: "Mnemosyne LLM API Key",
1469
+ description: "Optional LLM API key for Mnemosyne remote mode",
1470
+ condition: "mnemosyneActive",
1471
+ },
1472
+ },
1473
+ "mnemosyne.llmModel": {
1474
+ type: "string",
1475
+ default: undefined,
1476
+ ui: {
1477
+ tab: "memory",
1478
+ label: "Mnemosyne LLM Model",
1479
+ description: "Optional LLM model name for Mnemosyne remote mode",
1480
+ condition: "mnemosyneActive",
1481
+ },
1482
+ },
1483
+ "mnemosyne.retainEveryNTurns": { type: "number", default: 4 },
1484
+ "mnemosyne.recallLimit": { type: "number", default: 8 },
1485
+ "mnemosyne.recallContextTurns": { type: "number", default: 3 },
1486
+ "mnemosyne.recallMaxQueryChars": { type: "number", default: 4000 },
1487
+ "mnemosyne.injectionTokenLimit": { type: "number", default: 5000 },
1488
+ "mnemosyne.debug": { type: "boolean", default: false },
1312
1489
 
1313
1490
  // Hindsight (https://hindsight.vectorize.io)
1314
1491
  "hindsight.apiUrl": {
@@ -2727,6 +2904,30 @@ export const SETTINGS_SCHEMA = {
2727
2904
  ],
2728
2905
  },
2729
2906
  },
2907
+ "providers.tinyModel": {
2908
+ type: "enum",
2909
+ values: TINY_TITLE_MODEL_VALUES,
2910
+ default: ONLINE_TINY_TITLE_MODEL_KEY,
2911
+ ui: {
2912
+ tab: "providers",
2913
+ label: "Tiny Model",
2914
+ description: "Session-title model: online pi/smol by default, or a local on-device model",
2915
+ options: TINY_TITLE_MODEL_OPTIONS,
2916
+ },
2917
+ },
2918
+ "providers.memoryModel": {
2919
+ type: "enum",
2920
+ values: TINY_MEMORY_MODEL_VALUES,
2921
+ default: ONLINE_MEMORY_MODEL_KEY,
2922
+ ui: {
2923
+ tab: "memory",
2924
+ label: "Memory Model",
2925
+ description:
2926
+ "Mnemosyne LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model",
2927
+ condition: "mnemosyneActive",
2928
+ options: TINY_MEMORY_MODEL_OPTIONS,
2929
+ },
2930
+ },
2730
2931
 
2731
2932
  "providers.kimiApiFormat": {
2732
2933
  type: "enum",
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import {
13
13
  Patch as HashlinePatch,
14
+ missingSnapshotTagMessage,
14
15
  normalizeToLF,
15
16
  type Patch,
16
17
  type PatchSection,
@@ -40,10 +41,6 @@ async function readSectionText(absolutePath: string, sectionPath: string): Promi
40
41
  }
41
42
  }
42
43
 
43
- function hasAnchorScoped(section: PatchSection): boolean {
44
- return section.hasAnchorScopedEdit;
45
- }
46
-
47
44
  function snapshotMatchesCurrent(snapshot: Snapshot, currentText: string): boolean {
48
45
  return snapshot.text === currentText;
49
46
  }
@@ -54,9 +51,10 @@ function validateSectionHash(
54
51
  snapshots: SnapshotStore,
55
52
  ): string | null {
56
53
  if (section.fileHash === undefined) {
57
- return hasAnchorScoped(section)
58
- ? `Missing hashline snapshot tag for anchored edit to ${section.path}; use \`¶${section.path}#tag\` from your latest read.`
59
- : null;
54
+ // The snapshot tag is mandatory on every section — head/tail inserts
55
+ // included to keep this preview path in lockstep with the apply path
56
+ // (`Patcher.prepare`), which rejects tagless sections unconditionally.
57
+ return missingSnapshotTagMessage(section.path);
60
58
  }
61
59
  const snapshot = snapshots.byHash(absolutePath, section.fileHash);
62
60
  if (snapshot && snapshotMatchesCurrent(snapshot, text)) return null;
@@ -492,6 +492,42 @@ display({"label": "A"})`,
492
492
  expect(reloaded.output.trim()).toBe("2");
493
493
  });
494
494
 
495
+ it("loads TypeScript type-only imports in cells and local modules", async () => {
496
+ using tempDir = TempDir.createSync("@omp-eval-js-type-imports-");
497
+ const sessionFile = path.join(tempDir.path(), "session.jsonl");
498
+ const sessionId = `js-type-imports:${crypto.randomUUID()}`;
499
+ const session = createToolSession(tempDir.path(), sessionFile);
500
+ const typesPath = path.join(tempDir.path(), "types.ts");
501
+ const valuesPath = path.join(tempDir.path(), "values.ts");
502
+ const entryPath = path.join(tempDir.path(), "entry.ts");
503
+ const typesSpec = JSON.stringify(typesPath);
504
+ const entrySpec = JSON.stringify(entryPath);
505
+ await Bun.write(typesPath, "export interface TypeOnly { value: number }\n");
506
+ await Bun.write(valuesPath, "export interface InlineOnly { value: number }\nexport const imported = 41;\n");
507
+ await Bun.write(
508
+ entryPath,
509
+ [
510
+ 'import type { TypeOnly } from "./types.ts";',
511
+ 'import { type InlineOnly, imported } from "./values.ts";',
512
+ "export const typeOnly = 1;",
513
+ "export const inlineType = imported;",
514
+ "",
515
+ ].join("\n"),
516
+ );
517
+
518
+ const result = await executeJs(
519
+ `import type { TypeOnly } from ${typesSpec};\nconst mod = await import(${entrySpec});\nreturn mod.typeOnly + mod.inlineType;`,
520
+ {
521
+ sessionId,
522
+ session,
523
+ sessionFile,
524
+ },
525
+ );
526
+
527
+ expect(result.exitCode).toBe(0);
528
+ expect(result.output.trim()).toBe("42");
529
+ });
530
+
495
531
  it("refreshes the Python tool proxy when bridge env appears after kernel warm-up", async () => {
496
532
  using tempDir = TempDir.createSync("@omp-eval-py-tool-proxy-");
497
533
  const sessionFile = path.join(tempDir.path(), "session.jsonl");
@@ -88,7 +88,10 @@ export class LocalModuleLoader {
88
88
 
89
89
  async #buildLocalModule(modulePath: string): Promise<LocalModuleEntry> {
90
90
  const rawSource = fs.readFileSync(modulePath, "utf8");
91
- const stripped = stripTypeScriptSyntax(rawSource);
91
+ const stripped = stripTypeScriptSyntax(rawSource, {
92
+ force: isTypeScriptModulePath(modulePath),
93
+ loader: stripLoaderForPath(modulePath),
94
+ });
92
95
  const moduleDir = path.dirname(modulePath);
93
96
  const localDeps = new Set<string>();
94
97
  for (const specifier of collectModuleSourceSpecifiers(stripped)) {
@@ -251,6 +254,15 @@ function isLocalPathSpecifier(source: string): boolean {
251
254
  );
252
255
  }
253
256
 
257
+ function isTypeScriptModulePath(modulePath: string): boolean {
258
+ const ext = path.extname(modulePath);
259
+ return ext === ".ts" || ext === ".tsx" || ext === ".mts";
260
+ }
261
+
262
+ function stripLoaderForPath(modulePath: string): "ts" | "tsx" {
263
+ return path.extname(modulePath) === ".tsx" ? "tsx" : "ts";
264
+ }
265
+
254
266
  function isManagedLocalModulePath(target: string): boolean {
255
267
  return (
256
268
  path.isAbsolute(target) &&
@@ -75,6 +75,7 @@ function parseProgram(code: string): { program: { body: ReadonlyArray<BabelProgr
75
75
  allowSuperOutsideMethod: true,
76
76
  allowUndeclaredExports: true,
77
77
  errorRecovery: true,
78
+ plugins: ["typescript"],
78
79
  }) as unknown as { program: { body: ReadonlyArray<BabelProgramNode> } };
79
80
  } catch {
80
81
  return null;
@@ -178,8 +179,7 @@ export function rewriteImports(code: string): string {
178
179
  if (node.type !== "CallExpression") return;
179
180
  const call = node as unknown as { callee?: { type?: string; start?: number; end?: number } };
180
181
  const callee = call.callee;
181
- if (!callee || callee.type !== "Import" || typeof callee.start !== "number" || typeof callee.end !== "number")
182
- return;
182
+ if (callee?.type !== "Import" || typeof callee.start !== "number" || typeof callee.end !== "number") return;
183
183
  edits.push({ start: callee.start, end: callee.end, text: "__omp_import__" });
184
184
  });
185
185
 
@@ -252,12 +252,7 @@ export function rewriteDynamicImports(code: string, callee = "__omp_import__"):
252
252
  if (node.type !== "CallExpression") return;
253
253
  const call = node as unknown as { callee?: { type?: string; start?: number; end?: number } };
254
254
  const callCallee = call.callee;
255
- if (
256
- !callCallee ||
257
- callCallee.type !== "Import" ||
258
- typeof callCallee.start !== "number" ||
259
- typeof callCallee.end !== "number"
260
- ) {
255
+ if (callCallee?.type !== "Import" || typeof callCallee.start !== "number" || typeof callCallee.end !== "number") {
261
256
  return;
262
257
  }
263
258
  edits.push({ start: callCallee.start, end: callCallee.end, text: callee });
@@ -453,38 +448,48 @@ function requiresAsyncWrapper(code: string): boolean {
453
448
  }
454
449
 
455
450
  /**
456
- * Strip TypeScript syntax (type annotations, `interface`, `as`, `satisfies`, generics in
457
- * call expressions, etc.) before the import/lexical rewriters parse the code. We use Bun's
458
- * native transpiler in `ts` loader mode — fast, no JSX transforms, preserves `import`/
459
- * `export` declarations so the downstream Babel rewrites keep working.
451
+ * Strip TypeScript syntax (type annotations, type-only imports/exports, `interface`, `as`,
452
+ * `satisfies`, generics in call expressions, etc.) before the import/lexical rewriters parse
453
+ * the code. Bun's native transpiler preserves `import`/`export` declarations, so downstream
454
+ * Babel rewrites still control module resolution.
460
455
  *
461
- * Skipped when the code parses as plain JavaScript already (Babel can accept it), so the
462
- * common case avoids an extra transpile pass. We detect "looks like TS" with a cheap regex
463
- * before invoking the transpiler.
456
+ * Eval cells use a cheap "looks like TS" heuristic to avoid transpiling ordinary JS. Known
457
+ * TypeScript modules pass `force` because a file can contain TS-only module syntax such as
458
+ * `import type` without any value-level type annotations.
464
459
  */
465
- function stripTypeScript(code: string): string {
466
- if (!LOOKS_LIKE_TS.test(code)) return code;
460
+ type TypeScriptStripLoader = "ts" | "tsx";
461
+
462
+ const TS_TRANSPILER = new Bun.Transpiler({ loader: "ts" });
463
+ const TSX_TRANSPILER = new Bun.Transpiler({ loader: "tsx" });
464
+
465
+ function stripTypeScript(code: string, options: { force?: boolean; loader?: TypeScriptStripLoader } = {}): string {
466
+ if (!options.force && !LOOKS_LIKE_TS.test(code)) return code;
467
467
  try {
468
- return new Bun.Transpiler({ loader: "ts" }).transformSync(code);
468
+ const transpiler = options.loader === "tsx" ? TSX_TRANSPILER : TS_TRANSPILER;
469
+ return transpiler.transformSync(code);
469
470
  } catch {
470
471
  // Transpiler failed (e.g. unrecoverable syntax). Hand the original source back so the
471
472
  // downstream rewriter / VM surfaces the real error to the user.
472
473
  return code;
473
474
  }
474
475
  }
475
- export function stripTypeScriptSyntax(code: string): string {
476
- return stripTypeScript(code);
476
+ export function stripTypeScriptSyntax(
477
+ code: string,
478
+ options: { force?: boolean; loader?: TypeScriptStripLoader } = {},
479
+ ): string {
480
+ return stripTypeScript(code, options);
477
481
  }
478
482
 
479
- // Heuristic: any of the obvious TS-only tokens. Plain JS using `as` only inside strings
480
- // won't match because we require a leading word boundary plus a colon/keyword neighbor.
483
+ // Heuristic: obvious TS-only tokens, including type-only module syntax. Plain JS using `as`
484
+ // only inside strings won't match because we require a leading word boundary plus a
485
+ // colon/keyword neighbor.
481
486
  const LOOKS_LIKE_TS =
482
- /(?:\binterface\s+\w|\btype\s+\w+\s*=|\b(?:as|satisfies)\s+(?:[A-Z]|\bconst\b)|:\s*(?:string|number|boolean|any|unknown|void|never|object|[A-Z]\w*)\b|<\s*[A-Z]\w*\s*[,>])/;
487
+ /(?:\bimport\s+type\b|\bexport\s+type\b|\b(?:import|export)\s*\{[^}\n]*\btype\s+\w|\binterface\s+\w|\btype\s+\w+\s*=|\b(?:as|satisfies)\s+(?:[A-Z]|\bconst\b)|:\s*(?:string|number|boolean|any|unknown|void|never|object|[A-Z]\w*)\b|<\s*[A-Z]\w*\s*[,>])/;
483
488
 
484
489
  export function wrapCode(code: string): { source: string; asyncWrapped: boolean; finalExpressionReturned: boolean } {
485
- const stripped = stripTypeScript(code);
486
- const finalExpression = returnFinalExpression(stripped);
487
- const importsRewritten = rewriteImports(finalExpression.source);
490
+ const finalExpression = returnFinalExpression(code);
491
+ const stripped = stripTypeScript(finalExpression.source);
492
+ const importsRewritten = rewriteImports(stripped);
488
493
  const needsAsyncWrapper = requiresAsyncWrapper(importsRewritten);
489
494
  const rewritten = {
490
495
  source: demoteTopLevelLexicals(importsRewritten, { publishGlobals: needsAsyncWrapper }),
@@ -16,7 +16,7 @@ import * as path from "node:path";
16
16
  import { isEnoent } from "@oh-my-pi/pi-utils";
17
17
  import { applyQuery, pathToQuery } from "./json-query";
18
18
  import { artifactsDirsFromRegistry } from "./registry-helpers";
19
- import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
19
+ import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
20
20
 
21
21
  /**
22
22
  * Handler for agent:// URLs.
@@ -126,4 +126,21 @@ export class AgentProtocolHandler implements ProtocolHandler {
126
126
  notes,
127
127
  };
128
128
  }
129
+
130
+ async complete(): Promise<UrlCompletion[]> {
131
+ const ids = new Set<string>();
132
+ for (const dir of artifactsDirsFromRegistry()) {
133
+ let files: string[];
134
+ try {
135
+ files = await fs.readdir(dir);
136
+ } catch (err) {
137
+ if (isEnoent(err)) continue;
138
+ throw err;
139
+ }
140
+ for (const f of files) {
141
+ if (f.endsWith(".md")) ids.add(f.slice(0, -3));
142
+ }
143
+ }
144
+ return [...ids].sort().map(value => ({ value }));
145
+ }
129
146
  }
@@ -13,7 +13,7 @@ import * as fs from "node:fs/promises";
13
13
  import * as path from "node:path";
14
14
  import { isEnoent } from "@oh-my-pi/pi-utils";
15
15
  import { artifactsDirsFromRegistry } from "./registry-helpers";
16
- import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
16
+ import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
17
17
 
18
18
  export class ArtifactProtocolHandler implements ProtocolHandler {
19
19
  readonly scheme = "artifact";
@@ -77,4 +77,22 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
77
77
  sourcePath: foundPath,
78
78
  };
79
79
  }
80
+
81
+ async complete(): Promise<UrlCompletion[]> {
82
+ const ids = new Set<string>();
83
+ for (const dir of artifactsDirsFromRegistry()) {
84
+ let files: string[];
85
+ try {
86
+ files = await fs.readdir(dir);
87
+ } catch (err) {
88
+ if (isEnoent(err)) continue;
89
+ throw err;
90
+ }
91
+ for (const f of files) {
92
+ const m = f.match(/^(\d+)\./);
93
+ if (m) ids.add(m[1]!);
94
+ }
95
+ }
96
+ return [...ids].sort((a, b) => Number(a) - Number(b)).map(value => ({ value }));
97
+ }
80
98
  }