@oh-my-pi/pi-coding-agent 16.1.21 → 16.1.23

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 (46) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/dist/cli.js +3659 -3517
  3. package/dist/types/cli/gc-cli.d.ts +58 -0
  4. package/dist/types/commands/gc.d.ts +37 -0
  5. package/dist/types/config/settings-schema.d.ts +54 -0
  6. package/dist/types/config/settings.d.ts +11 -0
  7. package/dist/types/mcp/transports/stdio.d.ts +25 -1
  8. package/dist/types/mcp/transports/stdio.test.d.ts +1 -0
  9. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +1 -0
  10. package/dist/types/modes/theme/theme.d.ts +1 -0
  11. package/dist/types/session/session-listing.d.ts +10 -1
  12. package/dist/types/system-prompt.d.ts +2 -0
  13. package/dist/types/tools/plan-mode-guard.d.ts +7 -0
  14. package/package.json +12 -12
  15. package/scripts/bench-guard.ts +1 -1
  16. package/src/cli/gc-cli.ts +939 -0
  17. package/src/cli-commands.ts +1 -0
  18. package/src/commands/gc.ts +46 -0
  19. package/src/config/settings-schema.ts +45 -0
  20. package/src/config/settings.ts +44 -6
  21. package/src/edit/hashline/filesystem.ts +11 -6
  22. package/src/eval/__tests__/julia-prelude.test.ts +18 -0
  23. package/src/eval/jl/runner.jl +7 -1
  24. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +2 -0
  25. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +6 -0
  26. package/src/internal-urls/docs-index.generated.txt +1 -1
  27. package/src/lsp/index.ts +8 -1
  28. package/src/mcp/oauth-discovery.ts +62 -15
  29. package/src/mcp/transports/stdio.test.ts +45 -0
  30. package/src/mcp/transports/stdio.ts +46 -12
  31. package/src/modes/controllers/input-controller.ts +2 -2
  32. package/src/modes/controllers/selector-controller.ts +10 -0
  33. package/src/modes/interactive-mode.ts +26 -9
  34. package/src/modes/theme/mermaid-rendering.test.ts +53 -0
  35. package/src/modes/theme/theme.ts +33 -14
  36. package/src/prompts/system/plan-mode-active.md +9 -0
  37. package/src/prompts/system/system-prompt.md +2 -0
  38. package/src/sdk.ts +43 -4
  39. package/src/session/agent-session.ts +323 -62
  40. package/src/session/session-listing.ts +35 -2
  41. package/src/slash-commands/builtin-registry.ts +20 -2
  42. package/src/system-prompt.ts +4 -0
  43. package/src/tools/acp-bridge.ts +6 -1
  44. package/src/tools/plan-mode-guard.ts +26 -13
  45. package/src/utils/edit-mode.ts +19 -2
  46. package/src/utils/shell-snapshot.ts +1 -1
@@ -23,6 +23,7 @@ export const commands: CommandEntry[] = [
23
23
  { name: "__complete", load: () => import("./commands/complete").then(m => m.default) },
24
24
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
25
25
  { name: "dry-balance", load: () => import("./commands/dry-balance").then(m => m.default) },
26
+ { name: "gc", load: () => import("./commands/gc").then(m => m.default) },
26
27
  { name: "grep", load: () => import("./commands/grep").then(m => m.default) },
27
28
  { name: "gallery", load: () => import("./commands/gallery").then(m => m.default) },
28
29
  { name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Run on-disk storage maintenance.
3
+ */
4
+ import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
+ import { collectGcErrors, type GcCommandArgs, runGcCommand } from "../cli/gc-cli";
6
+
7
+ export default class Gc extends Command {
8
+ static description = "Run storage garbage collection";
9
+
10
+ static flags = {
11
+ apply: Flags.boolean({ description: "Apply changes (default is dry-run)" }),
12
+ json: Flags.boolean({ description: "Output JSON" }),
13
+ "agent-dir": Flags.string({ description: "Agent directory to maintain" }),
14
+ blobs: Flags.boolean({ description: "Sweep unreferenced blobs" }),
15
+ archive: Flags.boolean({ description: "Archive cold sessions" }),
16
+ wal: Flags.boolean({ description: "Checkpoint history/model database WAL files" }),
17
+ "cold-archive-after-days": Flags.integer({ description: "Minimum session age before archiving" }),
18
+ "retain-newest-global": Flags.integer({ description: "Always keep this many newest sessions active" }),
19
+ "retain-newest-per-cwd": Flags.integer({ description: "Always keep this many newest sessions per cwd active" }),
20
+ };
21
+
22
+ async run(): Promise<void> {
23
+ const { flags } = await this.parse(Gc);
24
+ const cmd: GcCommandArgs = {
25
+ flags: {
26
+ apply: flags.apply,
27
+ json: flags.json,
28
+ agentDir: flags["agent-dir"],
29
+ blobs: flags.blobs,
30
+ archive: flags.archive,
31
+ wal: flags.wal,
32
+ coldArchiveAfterDays: flags["cold-archive-after-days"],
33
+ retainNewestGlobal: flags["retain-newest-global"],
34
+ retainNewestPerCwd: flags["retain-newest-per-cwd"],
35
+ },
36
+ };
37
+ const result = await runGcCommand(cmd);
38
+ const errors = collectGcErrors(result);
39
+ if (errors.length > 0) {
40
+ process.stderr.write(
41
+ `GC completed with ${errors.length} error${errors.length === 1 ? "" : "s"}:\n${errors.map(error => `- ${error}`).join("\n")}\n`,
42
+ );
43
+ process.exitCode = 1;
44
+ }
45
+ }
46
+ }
@@ -790,6 +790,17 @@ export const SETTINGS_SCHEMA = {
790
790
  },
791
791
  },
792
792
 
793
+ "tui.renderMermaid": {
794
+ type: "boolean",
795
+ default: true,
796
+ ui: {
797
+ tab: "appearance",
798
+ group: "Display",
799
+ label: "Render Mermaid Diagrams",
800
+ description: "Render Mermaid fenced code blocks as ASCII diagrams",
801
+ },
802
+ },
803
+
793
804
  "tui.hyperlinks": {
794
805
  type: "enum",
795
806
  values: ["off", "auto", "always"] as const,
@@ -1700,6 +1711,17 @@ export const SETTINGS_SCHEMA = {
1700
1711
  },
1701
1712
  },
1702
1713
 
1714
+ "compaction.midTurnEnabled": {
1715
+ type: "boolean",
1716
+ default: true,
1717
+ ui: {
1718
+ tab: "context",
1719
+ group: "Compaction",
1720
+ label: "Mid-Turn Compaction",
1721
+ description: "Check thresholds at safe mid-turn tool-loop boundaries before the next provider request",
1722
+ },
1723
+ },
1724
+
1703
1725
  "compaction.strategy": {
1704
1726
  type: "enum",
1705
1727
  values: ["context-full", "handoff", "shake", "snapcompact", "off"] as const,
@@ -4630,6 +4652,18 @@ export const SETTINGS_SCHEMA = {
4630
4652
  default: "unset" as const,
4631
4653
  },
4632
4654
 
4655
+ "gc.blobs": { type: "boolean", default: true },
4656
+
4657
+ "gc.archive": { type: "boolean", default: true },
4658
+
4659
+ "gc.wal": { type: "boolean", default: true },
4660
+
4661
+ "gc.coldArchiveAfterDays": { type: "number", default: 30 },
4662
+
4663
+ "gc.retainNewestGlobal": { type: "number", default: 20 },
4664
+
4665
+ "gc.retainNewestPerCwd": { type: "number", default: 10 },
4666
+
4633
4667
  "thinkingBudgets.minimal": { type: "number", default: 1024 },
4634
4668
 
4635
4669
  "thinkingBudgets.low": { type: "number", default: 2048 },
@@ -4731,6 +4765,7 @@ export interface CompactionSettings {
4731
4765
  thresholdTokens: number;
4732
4766
  reserveTokens: number;
4733
4767
  keepRecentTokens: number;
4768
+ midTurnEnabled: boolean;
4734
4769
  handoffSaveToDisk: boolean;
4735
4770
  autoContinue: boolean;
4736
4771
  remoteEnabled: boolean;
@@ -4875,6 +4910,15 @@ export interface CodexResetsSettings {
4875
4910
  keepCredits: number;
4876
4911
  }
4877
4912
 
4913
+ export interface GcSettings {
4914
+ blobs: boolean;
4915
+ archive: boolean;
4916
+ wal: boolean;
4917
+ coldArchiveAfterDays: number;
4918
+ retainNewestGlobal: number;
4919
+ retainNewestPerCwd: number;
4920
+ }
4921
+
4878
4922
  /** Map group prefix -> typed settings interface */
4879
4923
  export interface GroupTypeMap {
4880
4924
  compaction: CompactionSettings;
@@ -4894,6 +4938,7 @@ export interface GroupTypeMap {
4894
4938
  cycleOrder: string[];
4895
4939
  shellMinimizer: ShellMinimizerSettings;
4896
4940
  codexResets: CodexResetsSettings;
4941
+ gc: GcSettings;
4897
4942
  }
4898
4943
 
4899
4944
  export type GroupPrefix = keyof GroupTypeMap;
@@ -61,6 +61,8 @@ export interface SettingsOptions {
61
61
  agentDir?: string;
62
62
  /** Don't persist to disk (for tests) */
63
63
  inMemory?: boolean;
64
+ /** Read config sources without opening storage or writing migrations */
65
+ readOnly?: boolean;
64
66
  /** Initial overrides */
65
67
  overrides?: Partial<Record<SettingPath, unknown>>;
66
68
  /** Extra config.yml-style overlays loaded after global/project settings */
@@ -136,11 +138,17 @@ function stringArrayFromUnknown(value: unknown): string[] {
136
138
  return [];
137
139
  }
138
140
 
141
+ function isRecord(value: unknown): value is Record<string, unknown> {
142
+ return !!value && typeof value === "object" && !Array.isArray(value);
143
+ }
144
+
139
145
  function shallowStringRecord(value: unknown): Record<string, string> {
140
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
146
+ if (!isRecord(value)) return {};
141
147
 
142
148
  const result: Record<string, string> = {};
143
- for (const [key, item] of Object.entries(value)) {
149
+ for (const key in value) {
150
+ if (!Object.hasOwn(value, key)) continue;
151
+ const item = value[key];
144
152
  if (typeof item === "string") {
145
153
  result[key] = item;
146
154
  }
@@ -228,7 +236,7 @@ export class Settings {
228
236
  this.#agentDir = path.normalize(options.agentDir ?? getAgentDir());
229
237
  this.#configPath = options.inMemory ? null : path.join(this.#agentDir, "config.yml");
230
238
  this.#configFiles = options.configFiles?.map(file => path.resolve(this.#cwd, expandTilde(file))) ?? [];
231
- this.#persist = !options.inMemory;
239
+ this.#persist = !options.inMemory && options.readOnly !== true;
232
240
 
233
241
  if (options.overrides) {
234
242
  for (const [key, value] of Object.entries(options.overrides)) {
@@ -270,6 +278,23 @@ export class Settings {
270
278
  );
271
279
  }
272
280
 
281
+ /**
282
+ * Load effective settings from config.yml and project providers without
283
+ * opening agent.db, migrating legacy settings, or writing marker files.
284
+ */
285
+ static loadReadOnly(options: SettingsOptions = {}): Promise<Settings> {
286
+ const instance = new Settings({ ...options, readOnly: true });
287
+ return instance.#loadReadOnly();
288
+ }
289
+
290
+ /**
291
+ * Load a persisted settings instance without touching the global singleton.
292
+ */
293
+ static loadIsolated(options: SettingsOptions = {}): Promise<Settings> {
294
+ const instance = new Settings(options);
295
+ return instance.#load();
296
+ }
297
+
273
298
  /**
274
299
  * Create an isolated instance for testing.
275
300
  * Does not affect the global singleton.
@@ -481,10 +506,10 @@ export class Settings {
481
506
  */
482
507
  getEditVariantForModel(model: string | undefined): EditMode | null {
483
508
  if (!model) return null;
484
- const variants = (this.#merged.edit as { modelVariants?: Record<string, string> })?.modelVariants;
485
- if (!variants) return null;
509
+ const variants = shallowStringRecord(getByPath(this.#merged, ["edit", "modelVariants"]));
510
+ const modelLower = model.toLowerCase();
486
511
  for (const pattern in variants) {
487
- if (model.includes(pattern)) {
512
+ if (modelLower.includes(pattern.toLowerCase())) {
488
513
  const value = normalizeEditMode(variants[pattern]);
489
514
  if (value) {
490
515
  return value;
@@ -583,6 +608,19 @@ export class Settings {
583
608
  return this;
584
609
  }
585
610
 
611
+ async #loadReadOnly(): Promise<Settings> {
612
+ const projectPromise = this.#loadProjectSettings();
613
+
614
+ if (this.#configPath) {
615
+ this.#global = await this.#loadYaml(this.#configPath);
616
+ }
617
+
618
+ this.#project = await projectPromise;
619
+ this.#configOverlay = await this.#loadConfigOverlays();
620
+ this.#rebuildMerged();
621
+ return this;
622
+ }
623
+
586
624
  async #loadYaml(filePath: string): Promise<RawSettings> {
587
625
  try {
588
626
  const content = await Bun.file(filePath).text();
@@ -25,7 +25,7 @@ import { routeWriteThroughBridge } from "../../tools/acp-bridge";
25
25
  import { assertEditableFileContent } from "../../tools/auto-generated-guard";
26
26
  import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
27
27
  import { isInternalUrlPath } from "../../tools/path-utils";
28
- import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
28
+ import { enforcePlanModeWrite, resolvePlanPath, targetsLocalSandbox } from "../../tools/plan-mode-guard";
29
29
  import { canonicalSnapshotKey } from "../file-snapshot-store";
30
30
  import { readEditFileText, serializeEditFileText } from "../read-file";
31
31
  import type { LspBatchRequest } from "../renderer";
@@ -92,12 +92,17 @@ export class HashlineFilesystem extends Filesystem {
92
92
  // Internal-URL authored targets (`local://`, `vault://`, …) are approved
93
93
  // at the lower "read" privilege; never let one redirect onto a "write".
94
94
  if (isInternalUrlPath(authoredPath)) return false;
95
- // Recovery only fixes a mistyped working-tree path: confine the resolved
96
- // target to the working tree (realpath-normalized). A plain-path "write"
97
- // approval must not redirect into the artifact sandbox, the secret vault,
98
- // or any out-of-tree file the user never named.
95
+ // Recovery rebinds a bare/mis-typed authored path onto the file its
96
+ // snapshot tag uniquely names. Confine the redirect to locations a plain
97
+ // "write" may legitimately target:
98
+ // 1. the working tree (the model dropped the directory), or
99
+ // 2. the session `local://` sandbox where plan/scratch artifacts live —
100
+ // the snapshot tag proves the model wrote/read that exact file this
101
+ // session, so a bare `plan.md#tag` should land on `local://plan.md`.
102
+ // The secret vault and any other out-of-tree path stay refused.
99
103
  const root = canonicalSnapshotKey(this.session.cwd);
100
- return resolvedPath === root || resolvedPath.startsWith(`${root}${path.sep}`);
104
+ if (resolvedPath === root || resolvedPath.startsWith(`${root}${path.sep}`)) return true;
105
+ return targetsLocalSandbox(this.session, resolvedPath);
101
106
  }
102
107
 
103
108
  async readText(relativePath: string): Promise<string> {
@@ -45,4 +45,22 @@ nothing
45
45
  expect(result.output).toContain("META=alpha:true");
46
46
  expect(result.output).toContain("MULTI=2:alpha:json");
47
47
  }, 30_000);
48
+
49
+ it("surfaces the exception type and message in the error output, not just stack frames", async () => {
50
+ using tempDir = TempDir.createSync("@omp-eval-julia-error-");
51
+ const result = await executeJulia(`println("="^8)\nmissing_var_xyz + 1`, {
52
+ cwd: tempDir.path(),
53
+ sessionId: `julia-prelude-error:${crypto.randomUUID()}`,
54
+ kernelOwnerId: OWNER_ID,
55
+ reset: true,
56
+ });
57
+
58
+ // The rendered error must carry the actual exception, not only the
59
+ // runner-internal backtrace frames (regression: traceback-only output
60
+ // hid `ename`/`evalue`).
61
+ expect(result.output).toContain("UndefVarError");
62
+ expect(result.output).toContain("missing_var_xyz");
63
+ // Frames are still present alongside the message.
64
+ expect(result.output).toContain("top-level scope");
65
+ }, 30_000);
48
66
  });
@@ -522,7 +522,13 @@ function emit_error(rid, err, bt)
522
522
  end
523
523
  err_str = String(take!(io))
524
524
 
525
- tb = String[]
525
+ # Seed the traceback with the rendered exception text so the array is a
526
+ # self-contained error display, matching the Python and Ruby runners. The
527
+ # host shows `traceback` verbatim when present and only falls back to
528
+ # `ename: evalue` when it is empty, so a frames-only traceback would hide
529
+ # the real error. Julia's `showerror` output already embeds the exception
530
+ # type for nearly every error and mirrors what the REPL prints after `ERROR: `.
531
+ tb = isempty(err_str) ? String[] : String[err_str]
526
532
  for frame in stacktrace(bt)
527
533
  file = string(frame.file)
528
534
  line = frame.line
@@ -249,6 +249,7 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
249
249
  "@oh-my-pi/pi-coding-agent/cli/flag-tables",
250
250
  "@oh-my-pi/pi-coding-agent/cli/gallery-cli",
251
251
  "@oh-my-pi/pi-coding-agent/cli/gallery-screenshot",
252
+ "@oh-my-pi/pi-coding-agent/cli/gc-cli",
252
253
  "@oh-my-pi/pi-coding-agent/cli/grep-cli",
253
254
  "@oh-my-pi/pi-coding-agent/cli/grievances-cli",
254
255
  "@oh-my-pi/pi-coding-agent/cli/initial-message",
@@ -293,6 +294,7 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
293
294
  "@oh-my-pi/pi-coding-agent/commands/config",
294
295
  "@oh-my-pi/pi-coding-agent/commands/dry-balance",
295
296
  "@oh-my-pi/pi-coding-agent/commands/gallery",
297
+ "@oh-my-pi/pi-coding-agent/commands/gc",
296
298
  "@oh-my-pi/pi-coding-agent/commands/grep",
297
299
  "@oh-my-pi/pi-coding-agent/commands/grievances",
298
300
  "@oh-my-pi/pi-coding-agent/commands/install",
@@ -223,6 +223,7 @@ import * as bundledPiCodingAgentCliGalleryFixturesShell from "@oh-my-pi/pi-codin
223
223
  import * as bundledPiCodingAgentCliGalleryFixturesTypes from "@oh-my-pi/pi-coding-agent/cli/gallery-fixtures/types";
224
224
  import * as bundledPiCodingAgentCliGalleryFixturesWeb from "@oh-my-pi/pi-coding-agent/cli/gallery-fixtures/web";
225
225
  import * as bundledPiCodingAgentCliGalleryScreenshot from "@oh-my-pi/pi-coding-agent/cli/gallery-screenshot";
226
+ import * as bundledPiCodingAgentCliGcCli from "@oh-my-pi/pi-coding-agent/cli/gc-cli";
226
227
  import * as bundledPiCodingAgentCliGrepCli from "@oh-my-pi/pi-coding-agent/cli/grep-cli";
227
228
  import * as bundledPiCodingAgentCliGrievancesCli from "@oh-my-pi/pi-coding-agent/cli/grievances-cli";
228
229
  import * as bundledPiCodingAgentCliInitialMessage from "@oh-my-pi/pi-coding-agent/cli/initial-message";
@@ -255,6 +256,7 @@ import * as bundledPiCodingAgentCommandsCompletions from "@oh-my-pi/pi-coding-ag
255
256
  import * as bundledPiCodingAgentCommandsConfig from "@oh-my-pi/pi-coding-agent/commands/config";
256
257
  import * as bundledPiCodingAgentCommandsDryBalance from "@oh-my-pi/pi-coding-agent/commands/dry-balance";
257
258
  import * as bundledPiCodingAgentCommandsGallery from "@oh-my-pi/pi-coding-agent/commands/gallery";
259
+ import * as bundledPiCodingAgentCommandsGc from "@oh-my-pi/pi-coding-agent/commands/gc";
258
260
  import * as bundledPiCodingAgentCommandsGrep from "@oh-my-pi/pi-coding-agent/commands/grep";
259
261
  import * as bundledPiCodingAgentCommandsGrievances from "@oh-my-pi/pi-coding-agent/commands/grievances";
260
262
  import * as bundledPiCodingAgentCommandsInstall from "@oh-my-pi/pi-coding-agent/commands/install";
@@ -1503,6 +1505,7 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
1503
1505
  "@oh-my-pi/pi-coding-agent/cli/gallery-screenshot": bundledPiCodingAgentCliGalleryScreenshot as unknown as Readonly<
1504
1506
  Record<string, unknown>
1505
1507
  >,
1508
+ "@oh-my-pi/pi-coding-agent/cli/gc-cli": bundledPiCodingAgentCliGcCli as unknown as Readonly<Record<string, unknown>>,
1506
1509
  "@oh-my-pi/pi-coding-agent/cli/grep-cli": bundledPiCodingAgentCliGrepCli as unknown as Readonly<
1507
1510
  Record<string, unknown>
1508
1511
  >,
@@ -1625,6 +1628,9 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
1625
1628
  "@oh-my-pi/pi-coding-agent/commands/gallery": bundledPiCodingAgentCommandsGallery as unknown as Readonly<
1626
1629
  Record<string, unknown>
1627
1630
  >,
1631
+ "@oh-my-pi/pi-coding-agent/commands/gc": bundledPiCodingAgentCommandsGc as unknown as Readonly<
1632
+ Record<string, unknown>
1633
+ >,
1628
1634
  "@oh-my-pi/pi-coding-agent/commands/grep": bundledPiCodingAgentCommandsGrep as unknown as Readonly<
1629
1635
  Record<string, unknown>
1630
1636
  >,