@oh-my-pi/pi-coding-agent 16.1.13 → 16.1.15

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 (153) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cli.js +5615 -2833
  3. package/dist/types/advisor/runtime.d.ts +3 -0
  4. package/dist/types/config/settings-schema.d.ts +64 -14
  5. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
  6. package/dist/types/eval/backend-helpers.d.ts +23 -0
  7. package/dist/types/eval/executor-base.d.ts +118 -0
  8. package/dist/types/eval/index.d.ts +2 -0
  9. package/dist/types/eval/jl/executor.d.ts +44 -0
  10. package/dist/types/eval/jl/index.d.ts +11 -0
  11. package/dist/types/eval/jl/kernel.d.ts +28 -0
  12. package/dist/types/eval/jl/prelude.d.ts +1 -0
  13. package/dist/types/eval/jl/runtime.d.ts +22 -0
  14. package/dist/types/eval/kernel-base.d.ts +105 -0
  15. package/dist/types/eval/py/kernel.d.ts +3 -61
  16. package/dist/types/eval/rb/executor.d.ts +77 -0
  17. package/dist/types/eval/rb/index.d.ts +11 -0
  18. package/dist/types/eval/rb/kernel.d.ts +31 -0
  19. package/dist/types/eval/rb/prelude.d.ts +1 -0
  20. package/dist/types/eval/rb/runtime.d.ts +23 -0
  21. package/dist/types/eval/runtime-env.d.ts +24 -0
  22. package/dist/types/eval/types.d.ts +3 -3
  23. package/dist/types/export/share.d.ts +8 -1
  24. package/dist/types/extensibility/extensions/runner.d.ts +2 -15
  25. package/dist/types/extensibility/hooks/loader.d.ts +1 -25
  26. package/dist/types/extensibility/session-handler-types.d.ts +23 -0
  27. package/dist/types/jsonrpc/message-framing.d.ts +35 -0
  28. package/dist/types/mcp/transports/stdio.d.ts +12 -1
  29. package/dist/types/mnemopi/embed-client.d.ts +7 -24
  30. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
  31. package/dist/types/modes/components/custom-editor.d.ts +11 -0
  32. package/dist/types/modes/components/selector-helpers.d.ts +53 -0
  33. package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
  34. package/dist/types/modes/components/status-line/types.d.ts +1 -0
  35. package/dist/types/modes/interactive-mode.d.ts +0 -2
  36. package/dist/types/modes/theme/theme.d.ts +8 -1
  37. package/dist/types/modes/types.d.ts +0 -2
  38. package/dist/types/modes/utils/copy-targets.d.ts +1 -1
  39. package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
  40. package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
  41. package/dist/types/sdk.d.ts +1 -1
  42. package/dist/types/secrets/obfuscator.d.ts +3 -3
  43. package/dist/types/session/agent-session.d.ts +2 -2
  44. package/dist/types/stt/asr-client.d.ts +3 -29
  45. package/dist/types/subprocess/worker-client.d.ts +149 -0
  46. package/dist/types/subprocess/worker-runtime.d.ts +107 -0
  47. package/dist/types/tiny/title-client.d.ts +14 -34
  48. package/dist/types/tools/eval-backends.d.ts +6 -3
  49. package/dist/types/tools/eval-render.d.ts +6 -5
  50. package/dist/types/tools/eval.d.ts +13 -15
  51. package/dist/types/tools/index.d.ts +3 -3
  52. package/dist/types/tts/tts-client.d.ts +3 -28
  53. package/dist/types/tui/code-cell.d.ts +7 -0
  54. package/dist/types/utils/file-display-mode.d.ts +1 -1
  55. package/dist/types/utils/shell-snapshot.d.ts +10 -0
  56. package/dist/types/web/parallel.d.ts +6 -0
  57. package/dist/types/web/search/providers/perplexity.d.ts +17 -3
  58. package/package.json +12 -12
  59. package/src/advisor/__tests__/advisor.test.ts +114 -0
  60. package/src/advisor/runtime.ts +129 -1
  61. package/src/config/model-registry.ts +12 -4
  62. package/src/config/settings-schema.ts +74 -18
  63. package/src/config/settings.ts +5 -0
  64. package/src/dap/client.ts +13 -107
  65. package/src/eval/__tests__/julia-prelude.test.ts +77 -0
  66. package/src/eval/backend-helpers.ts +48 -0
  67. package/src/eval/executor-base.ts +425 -0
  68. package/src/eval/index.ts +2 -0
  69. package/src/eval/jl/executor.ts +540 -0
  70. package/src/eval/jl/index.ts +54 -0
  71. package/src/eval/jl/kernel.ts +235 -0
  72. package/src/eval/jl/prelude.jl +930 -0
  73. package/src/eval/jl/prelude.ts +3 -0
  74. package/src/eval/jl/runner.jl +634 -0
  75. package/src/eval/jl/runtime.ts +118 -0
  76. package/src/eval/js/index.ts +3 -14
  77. package/src/eval/kernel-base.ts +569 -0
  78. package/src/eval/py/executor.ts +43 -252
  79. package/src/eval/py/index.ts +9 -20
  80. package/src/eval/py/kernel.ts +29 -544
  81. package/src/eval/rb/executor.ts +504 -0
  82. package/src/eval/rb/index.ts +54 -0
  83. package/src/eval/rb/kernel.ts +230 -0
  84. package/src/eval/rb/prelude.rb +721 -0
  85. package/src/eval/rb/prelude.ts +3 -0
  86. package/src/eval/rb/runner.rb +474 -0
  87. package/src/eval/rb/runtime.ts +132 -0
  88. package/src/eval/runtime-env.ts +104 -0
  89. package/src/eval/types.ts +3 -3
  90. package/src/exec/bash-executor.ts +44 -0
  91. package/src/export/share.ts +51 -28
  92. package/src/extensibility/extensions/runner.ts +4 -11
  93. package/src/extensibility/hooks/loader.ts +3 -21
  94. package/src/extensibility/session-handler-types.ts +21 -0
  95. package/src/internal-urls/docs-index.generated.txt +1 -1
  96. package/src/jsonrpc/message-framing.ts +142 -0
  97. package/src/lsp/client.ts +13 -109
  98. package/src/mcp/transports/stdio.ts +20 -4
  99. package/src/mnemopi/embed-client.ts +43 -198
  100. package/src/modes/components/agent-dashboard.ts +17 -40
  101. package/src/modes/components/chat-transcript-builder.ts +18 -102
  102. package/src/modes/components/custom-editor.test.ts +22 -0
  103. package/src/modes/components/custom-editor.ts +29 -1
  104. package/src/modes/components/extensions/extension-dashboard.ts +4 -13
  105. package/src/modes/components/extensions/extension-list.ts +16 -44
  106. package/src/modes/components/footer.ts +4 -3
  107. package/src/modes/components/history-search.ts +4 -16
  108. package/src/modes/components/selector-helpers.ts +129 -0
  109. package/src/modes/components/settings-selector.ts +7 -5
  110. package/src/modes/components/status-line/component.ts +5 -1
  111. package/src/modes/components/status-line/context-thresholds.ts +11 -3
  112. package/src/modes/components/status-line/segments.ts +1 -1
  113. package/src/modes/components/status-line/types.ts +1 -0
  114. package/src/modes/components/tree-selector.ts +13 -18
  115. package/src/modes/controllers/command-controller.ts +1 -0
  116. package/src/modes/controllers/event-controller.ts +3 -9
  117. package/src/modes/controllers/input-controller.ts +32 -54
  118. package/src/modes/interactive-mode.ts +5 -7
  119. package/src/modes/theme/theme.ts +35 -0
  120. package/src/modes/types.ts +0 -2
  121. package/src/modes/utils/copy-targets.ts +3 -2
  122. package/src/modes/utils/interactive-context-helpers.ts +27 -0
  123. package/src/modes/utils/transcript-render-helpers.ts +157 -0
  124. package/src/modes/utils/ui-helpers.ts +21 -126
  125. package/src/prompts/system/system-prompt.md +5 -5
  126. package/src/prompts/tools/bash.md +2 -3
  127. package/src/prompts/tools/eval.md +6 -4
  128. package/src/prompts/tools/find.md +0 -4
  129. package/src/prompts/tools/read.md +1 -2
  130. package/src/prompts/tools/replace.md +1 -1
  131. package/src/sdk.ts +13 -7
  132. package/src/secrets/obfuscator.ts +3 -9
  133. package/src/session/agent-session.ts +42 -9
  134. package/src/slash-commands/builtin-registry.ts +2 -1
  135. package/src/stt/asr-client.ts +35 -215
  136. package/src/stt/asr-worker.ts +29 -181
  137. package/src/subprocess/worker-client.ts +297 -0
  138. package/src/subprocess/worker-runtime.ts +277 -0
  139. package/src/task/executor.ts +4 -4
  140. package/src/tiny/title-client.ts +53 -219
  141. package/src/tiny/worker.ts +29 -180
  142. package/src/tools/eval-backends.ts +10 -3
  143. package/src/tools/eval-render.ts +17 -8
  144. package/src/tools/eval.ts +187 -22
  145. package/src/tools/index.ts +51 -28
  146. package/src/tts/tts-client.ts +38 -206
  147. package/src/tts/tts-worker.ts +23 -97
  148. package/src/tui/code-cell.ts +12 -1
  149. package/src/utils/file-display-mode.ts +2 -3
  150. package/src/utils/shell-snapshot.ts +63 -1
  151. package/src/web/parallel.ts +43 -42
  152. package/src/web/search/providers/parallel.ts +10 -99
  153. package/src/web/search/providers/perplexity.ts +18 -6
@@ -1,5 +1,7 @@
1
1
  import { type FetchImpl } from "@oh-my-pi/pi-ai";
2
2
  import type { AgentStorage } from "../session/agent-storage";
3
+ export declare const PARALLEL_SEARCH_URL = "https://api.parallel.ai/v1beta/search";
4
+ export declare const PARALLEL_BETA_HEADER = "search-extract-2025-10-10";
3
5
  export interface ParallelUsageItem {
4
6
  name?: string;
5
7
  count?: number;
@@ -55,6 +57,10 @@ export declare class ParallelApiError extends Error {
55
57
  readonly statusCode?: number;
56
58
  constructor(message: string, statusCode?: number);
57
59
  }
60
+ export declare function parseParallelErrorResponse(statusCode: number, responseText: string): ParallelApiError;
61
+ export declare function parseParallelSearchPayload(payload: unknown, options?: {
62
+ parseMetadata?: boolean;
63
+ }): ParallelSearchResult;
58
64
  export declare function findParallelApiKey(storage: AgentStorage | null | undefined): string | null;
59
65
  export declare function getParallelExtractContent(document: ParallelExtractDocument): string;
60
66
  export declare function searchWithParallel(objective: string, queries: string[], options: ParallelSearchOptions, storage: AgentStorage | null | undefined): Promise<ParallelSearchResult>;
@@ -33,11 +33,25 @@ export declare function searchPerplexity(params: PerplexitySearchParams): Promis
33
33
  export declare class PerplexityProvider extends SearchProvider {
34
34
  readonly id = "perplexity";
35
35
  readonly label = "Perplexity";
36
+ /**
37
+ * Auto-chain admission. Requires a direct Perplexity credential
38
+ * (`PERPLEXITY_COOKIES`, OAuth session, or `PERPLEXITY_API_KEY`).
39
+ *
40
+ * OpenRouter auth is intentionally NOT accepted here: silently using
41
+ * OpenRouter's `perplexity/sonar-pro` whenever any OpenRouter key is
42
+ * configured surprises users (and bills them) for a path they never
43
+ * asked for. The auto chain skips Perplexity in that case and falls
44
+ * through to the next configured provider. Users who DO want the
45
+ * OpenRouter-backed Perplexity path can still opt in by setting
46
+ * `webSearch: perplexity` explicitly — see {@link isExplicitlyAvailable}.
47
+ */
36
48
  isAvailable(authStorage: AuthStorage): boolean;
37
49
  /**
38
- * Perplexity accepts anonymous browser-style ask requests, but keep auto
39
- * provider selection credential-gated so a configured provider keeps priority
40
- * over the anonymous fallback.
50
+ * Perplexity accepts anonymous browser-style ask requests, and the
51
+ * OpenRouter-backed `perplexity/sonar-pro` path is opt-in through
52
+ * explicit selection. Keep auto-chain admission credential-gated so a
53
+ * configured provider keeps priority over the anonymous/OpenRouter
54
+ * fallbacks.
41
55
  */
42
56
  isExplicitlyAvailable(_authStorage: AuthStorage): boolean;
43
57
  search(params: SearchParams): Promise<SearchResponse>;
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": "16.1.13",
4
+ "version": "16.1.15",
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",
@@ -48,17 +48,17 @@
48
48
  "@agentclientprotocol/sdk": "0.25.0",
49
49
  "@babel/parser": "^7.29.7",
50
50
  "@mozilla/readability": "^0.6.0",
51
- "@oh-my-pi/hashline": "16.1.13",
52
- "@oh-my-pi/omp-stats": "16.1.13",
53
- "@oh-my-pi/pi-agent-core": "16.1.13",
54
- "@oh-my-pi/pi-ai": "16.1.13",
55
- "@oh-my-pi/pi-catalog": "16.1.13",
56
- "@oh-my-pi/pi-mnemopi": "16.1.13",
57
- "@oh-my-pi/pi-natives": "16.1.13",
58
- "@oh-my-pi/pi-tui": "16.1.13",
59
- "@oh-my-pi/pi-utils": "16.1.13",
60
- "@oh-my-pi/pi-wire": "16.1.13",
61
- "@oh-my-pi/snapcompact": "16.1.13",
51
+ "@oh-my-pi/hashline": "16.1.15",
52
+ "@oh-my-pi/omp-stats": "16.1.15",
53
+ "@oh-my-pi/pi-agent-core": "16.1.15",
54
+ "@oh-my-pi/pi-ai": "16.1.15",
55
+ "@oh-my-pi/pi-catalog": "16.1.15",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.15",
57
+ "@oh-my-pi/pi-natives": "16.1.15",
58
+ "@oh-my-pi/pi-tui": "16.1.15",
59
+ "@oh-my-pi/pi-utils": "16.1.15",
60
+ "@oh-my-pi/pi-wire": "16.1.15",
61
+ "@oh-my-pi/snapcompact": "16.1.15",
62
62
  "@opentelemetry/api": "^1.9.1",
63
63
  "@opentelemetry/context-async-hooks": "^2.7.1",
64
64
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -3,6 +3,7 @@ import type { AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core
3
3
  import { type } from "arktype";
4
4
  import { createAdvisorMessageCard } from "../../modes/components/advisor-message";
5
5
  import { getThemeByName } from "../../modes/theme/theme";
6
+ import { SecretObfuscator } from "../../secrets/obfuscator";
6
7
  import { formatSessionHistoryMarkdown } from "../../session/session-history-format";
7
8
  import { YieldQueue } from "../../session/yield-queue";
8
9
  import {
@@ -447,6 +448,119 @@ describe("advisor", () => {
447
448
  expect(promptInputs[0]).not.toContain("note");
448
449
  });
449
450
 
451
+ it("obfuscates session updates before prompting the advisor", async () => {
452
+ const secret = "ADVISOR_SECRET_TOKEN_123";
453
+ const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
454
+ const placeholder = obfuscator.obfuscate(secret);
455
+ const promptInputs: string[] = [];
456
+ const agent = makeAgent(promptInputs);
457
+ const messages: AgentMessage[] = [{ role: "user", content: `token ${secret}`, timestamp: 1 } as AgentMessage];
458
+ const host: AdvisorRuntimeHost = {
459
+ snapshotMessages: () => messages,
460
+ enqueueAdvice: () => {},
461
+ obfuscator,
462
+ };
463
+ const runtime = new AdvisorRuntime(agent, host);
464
+
465
+ runtime.onTurnEnd();
466
+ await Promise.resolve();
467
+
468
+ expect(promptInputs).toHaveLength(1);
469
+ expect(promptInputs[0]).toContain(placeholder);
470
+ expect(promptInputs[0]).not.toContain(secret);
471
+ });
472
+
473
+ it("redacts expanded primary context before XML escaping", async () => {
474
+ const secret = "ADVISOR&SECRET<TOKEN>123";
475
+ const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
476
+ const placeholder = obfuscator.obfuscate(secret);
477
+ const promptInputs: string[] = [];
478
+ const agent = makeAgent(promptInputs);
479
+ const messages: AgentMessage[] = [
480
+ {
481
+ role: "custom",
482
+ customType: "plan-mode-context",
483
+ content: `Plan mode carries ${secret}`,
484
+ display: false,
485
+ timestamp: 1,
486
+ } as AgentMessage,
487
+ ];
488
+ const host: AdvisorRuntimeHost = {
489
+ snapshotMessages: () => messages,
490
+ enqueueAdvice: () => {},
491
+ obfuscator,
492
+ };
493
+ const runtime = new AdvisorRuntime(agent, host);
494
+
495
+ runtime.onTurnEnd();
496
+ await Promise.resolve();
497
+
498
+ expect(promptInputs).toHaveLength(1);
499
+ expect(promptInputs[0]).toContain(placeholder);
500
+ expect(promptInputs[0]).not.toContain(secret);
501
+ expect(promptInputs[0]).not.toContain("ADVISOR&amp;SECRET&lt;TOKEN&gt;123");
502
+ });
503
+
504
+ it("redacts file-mention paths before formatting", async () => {
505
+ const secret = "MENTION_SECRET_TOKEN_123";
506
+ const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
507
+ const placeholder = obfuscator.obfuscate(secret);
508
+ const promptInputs: string[] = [];
509
+ const agent = makeAgent(promptInputs);
510
+ const messages: AgentMessage[] = [
511
+ {
512
+ role: "fileMention",
513
+ files: [{ path: `notes/${secret}.txt`, content: "ignored" }],
514
+ timestamp: 1,
515
+ } as unknown as AgentMessage,
516
+ ];
517
+ const host: AdvisorRuntimeHost = {
518
+ snapshotMessages: () => messages,
519
+ enqueueAdvice: () => {},
520
+ obfuscator,
521
+ };
522
+ const runtime = new AdvisorRuntime(agent, host);
523
+
524
+ runtime.onTurnEnd();
525
+ await Promise.resolve();
526
+
527
+ expect(promptInputs).toHaveLength(1);
528
+ expect(promptInputs[0]).toContain(placeholder);
529
+ expect(promptInputs[0]).not.toContain(secret);
530
+ });
531
+
532
+ it("redacts nested async-result job labels before formatting", async () => {
533
+ const secret = "JOB_LABEL_SECRET_TOKEN_123";
534
+ const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
535
+ const placeholder = obfuscator.obfuscate(secret);
536
+ const promptInputs: string[] = [];
537
+ const agent = makeAgent(promptInputs);
538
+ const messages: AgentMessage[] = [
539
+ {
540
+ role: "custom",
541
+ customType: "async-result",
542
+ content: "",
543
+ details: { jobs: [{ label: `bash: echo ${secret}`, jobId: "j1" }] },
544
+ display: true,
545
+ attribution: "agent",
546
+ timestamp: 1,
547
+ } as unknown as AgentMessage,
548
+ ];
549
+ const host: AdvisorRuntimeHost = {
550
+ snapshotMessages: () => messages,
551
+ enqueueAdvice: () => {},
552
+ obfuscator,
553
+ };
554
+ const runtime = new AdvisorRuntime(agent, host);
555
+
556
+ runtime.onTurnEnd();
557
+ await Promise.resolve();
558
+
559
+ expect(promptInputs).toHaveLength(1);
560
+ expect(promptInputs[0]).toContain(placeholder);
561
+ expect(promptInputs[0]).not.toContain(secret);
562
+ });
563
+
450
564
  it("expands plan-mode context once, then collapses an unchanged re-injection", async () => {
451
565
  const promptInputs: string[] = [];
452
566
  const agent = makeAgent(promptInputs);
@@ -1,6 +1,8 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
3
+ import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
3
4
  import { logger } from "@oh-my-pi/pi-utils";
5
+ import { obfuscateToolArguments, type SecretObfuscator } from "../secrets/obfuscator";
4
6
  import { formatSessionHistoryMarkdown, PRIMARY_CONTEXT_CUSTOM_TYPES } from "../session/session-history-format";
5
7
 
6
8
  /** Minimal slice of `Agent` the runtime drives — satisfied by pi-agent-core `Agent`. */
@@ -16,6 +18,8 @@ export interface AdvisorRuntimeHost {
16
18
  snapshotMessages(): AgentMessage[];
17
19
  /** Surface one advice note to the primary (enqueues into the session YieldQueue). */
18
20
  enqueueAdvice(note: string, severity?: "nit" | "concern" | "blocker"): void;
21
+ /** Redact primary transcript bytes before they reach the advisor model. */
22
+ obfuscator?: SecretObfuscator;
19
23
  /**
20
24
  * Pre-prompt context maintenance for the advisor's own append-only context.
21
25
  * Promotes the advisor model to a larger sibling when its context nears the
@@ -174,7 +178,9 @@ export class AdvisorRuntime {
174
178
  .map(m => this.#dedupContextMessage(m));
175
179
  this.#lastCount = all.length;
176
180
  if (delta.length === 0) return null;
177
- const md = formatSessionHistoryMarkdown(delta, {
181
+ const obfuscator = this.host.obfuscator;
182
+ const formattedDelta = obfuscator?.hasSecrets() ? obfuscateAdvisorDelta(obfuscator, delta) : delta;
183
+ const md = formatSessionHistoryMarkdown(formattedDelta, {
178
184
  includeThinking: true,
179
185
  includeToolIntent: true,
180
186
  watchedRoles: true,
@@ -304,3 +310,125 @@ export class AdvisorRuntime {
304
310
  }
305
311
  }
306
312
  }
313
+
314
+ type TextualContent = string | readonly (TextContent | ImageContent)[];
315
+
316
+ function obfuscateTextualContent(obfuscator: SecretObfuscator, content: TextualContent): TextualContent {
317
+ if (typeof content === "string") return obfuscator.obfuscate(content);
318
+ let changed = false;
319
+ const result = content.map((block): TextContent | ImageContent => {
320
+ if (block.type !== "text") return block;
321
+ const text = obfuscator.obfuscate(block.text);
322
+ if (text === block.text) return block;
323
+ changed = true;
324
+ return { ...block, text };
325
+ });
326
+ return changed ? result : content;
327
+ }
328
+
329
+ function obfuscateAssistantMessage(obfuscator: SecretObfuscator, message: AssistantMessage): AssistantMessage {
330
+ let changed = false;
331
+ const content = message.content.map((block): AssistantMessage["content"][number] => {
332
+ if (block.type === "text") {
333
+ const text = obfuscator.obfuscate(block.text);
334
+ if (text === block.text) return block;
335
+ changed = true;
336
+ return { ...block, text };
337
+ }
338
+ if (block.type === "toolCall") {
339
+ const args = obfuscateToolArguments(obfuscator, block.arguments);
340
+ if (args === block.arguments) return block;
341
+ changed = true;
342
+ return { ...block, arguments: args };
343
+ }
344
+ return block;
345
+ });
346
+ return changed ? { ...message, content } : message;
347
+ }
348
+
349
+ function obfuscateDetails(
350
+ obfuscator: SecretObfuscator,
351
+ details: Record<string, unknown> | undefined,
352
+ ): Record<string, unknown> | undefined {
353
+ if (!details) return details;
354
+ // Walk strings at every depth: `customOneLiner` renders nested fields
355
+ // (e.g. `async-result` reads `details.jobs[].label`/`jobId`), so a shallow
356
+ // pass leaks any secret a background job's label happens to contain.
357
+ return obfuscateToolArguments(obfuscator, details);
358
+ }
359
+
360
+ function obfuscateAdvisorMessage(obfuscator: SecretObfuscator, message: AgentMessage): AgentMessage {
361
+ switch (message.role) {
362
+ case "user":
363
+ case "developer":
364
+ case "toolResult": {
365
+ const content = obfuscateTextualContent(obfuscator, message.content as TextualContent);
366
+ return content === message.content ? message : ({ ...(message as object), content } as AgentMessage);
367
+ }
368
+ case "assistant":
369
+ return obfuscateAssistantMessage(obfuscator, message as AssistantMessage) as AgentMessage;
370
+ case "custom":
371
+ case "hookMessage": {
372
+ const msg = message as AgentMessage & {
373
+ content: TextualContent;
374
+ details?: Record<string, unknown>;
375
+ };
376
+ const content = obfuscateTextualContent(obfuscator, msg.content);
377
+ const details = obfuscateDetails(obfuscator, msg.details);
378
+ if (content === msg.content && details === msg.details) return message;
379
+ return { ...(message as object), content, details } as AgentMessage;
380
+ }
381
+ case "bashExecution": {
382
+ const msg = message as AgentMessage & { command: string; output: string };
383
+ const command = obfuscator.obfuscate(msg.command);
384
+ const output = obfuscator.obfuscate(msg.output);
385
+ return command === msg.command && output === msg.output
386
+ ? message
387
+ : ({ ...(message as object), command, output } as AgentMessage);
388
+ }
389
+ case "pythonExecution": {
390
+ const msg = message as AgentMessage & { code: string; output: string };
391
+ const code = obfuscator.obfuscate(msg.code);
392
+ const output = obfuscator.obfuscate(msg.output);
393
+ return code === msg.code && output === msg.output
394
+ ? message
395
+ : ({ ...(message as object), code, output } as AgentMessage);
396
+ }
397
+ case "branchSummary": {
398
+ const msg = message as AgentMessage & { summary: string };
399
+ const summary = obfuscator.obfuscate(msg.summary);
400
+ return summary === msg.summary ? message : ({ ...(message as object), summary } as AgentMessage);
401
+ }
402
+ case "compactionSummary": {
403
+ const msg = message as AgentMessage & { summary: string };
404
+ const summary = obfuscator.obfuscate(msg.summary);
405
+ return summary === msg.summary ? message : ({ ...(message as object), summary } as AgentMessage);
406
+ }
407
+ case "fileMention": {
408
+ const msg = message as AgentMessage & {
409
+ files: Array<{ path: string; content: string; image?: unknown }>;
410
+ };
411
+ let changed = false;
412
+ const files = msg.files.map(file => {
413
+ const path = obfuscator.obfuscate(file.path);
414
+ const content = obfuscator.obfuscate(file.content);
415
+ if (path === file.path && content === file.content) return file;
416
+ changed = true;
417
+ return { ...file, path, content };
418
+ });
419
+ return changed ? ({ ...(message as object), files } as AgentMessage) : message;
420
+ }
421
+ default:
422
+ return message;
423
+ }
424
+ }
425
+
426
+ function obfuscateAdvisorDelta(obfuscator: SecretObfuscator, messages: AgentMessage[]): AgentMessage[] {
427
+ let changed = false;
428
+ const result = messages.map(message => {
429
+ const next = obfuscateAdvisorMessage(obfuscator, message);
430
+ if (next !== message) changed = true;
431
+ return next;
432
+ });
433
+ return changed ? result : messages;
434
+ }
@@ -992,6 +992,7 @@ export class ModelRegistry {
992
992
  });
993
993
  continue;
994
994
  }
995
+ const configStale = this.#isDiscoveryCacheOlderThanModelsConfig(cache.updatedAt);
995
996
  const models = this.#applyProviderModelOverrides(
996
997
  providerConfig.provider,
997
998
  this.#normalizeDiscoverableModels(
@@ -1007,7 +1008,7 @@ export class ModelRegistry {
1007
1008
  provider: providerConfig.provider,
1008
1009
  status: "cached",
1009
1010
  optional: providerConfig.optional ?? false,
1010
- stale: !cache.fresh || !cache.authoritative,
1011
+ stale: !cache.fresh || !cache.authoritative || configStale,
1011
1012
  fetchedAt: cache.updatedAt,
1012
1013
  models: models.map(model => model.id),
1013
1014
  });
@@ -1259,12 +1260,19 @@ export class ModelRegistry {
1259
1260
  return providerConfig.provider;
1260
1261
  }
1261
1262
 
1263
+ #isDiscoveryCacheOlderThanModelsConfig(cacheUpdatedAt: number): boolean {
1264
+ const configMtime = this.#modelsConfigFile.getMtimeMs();
1265
+ return configMtime !== null && cacheUpdatedAt < Math.floor(configMtime);
1266
+ }
1267
+
1262
1268
  async #discoverProviderModels(
1263
1269
  providerConfig: DiscoveryProviderConfig,
1264
1270
  strategy: ModelRefreshStrategy,
1265
1271
  ): Promise<Model<Api>[]> {
1266
1272
  const cacheProviderId = this.#configuredDiscoveryCacheProviderId(providerConfig);
1267
1273
  const cached = readModelCache<Api>(cacheProviderId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
1274
+ const cacheOlderThanConfig = cached !== null && this.#isDiscoveryCacheOlderThanModelsConfig(cached.updatedAt);
1275
+ const effectiveStrategy = strategy === "online-if-uncached" && cacheOlderThanConfig ? "online" : strategy;
1268
1276
  const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
1269
1277
  if (requiresAuth) {
1270
1278
  const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
@@ -1311,12 +1319,12 @@ export class ModelRegistry {
1311
1319
  cacheTtlMs: 24 * 60 * 60 * 1000,
1312
1320
  fetchDynamicModels,
1313
1321
  });
1314
- const result = await manager.refresh(strategy);
1322
+ const result = await manager.refresh(effectiveStrategy);
1315
1323
  const status = discoveryError
1316
1324
  ? result.models.length > 0
1317
1325
  ? "cached"
1318
1326
  : "unavailable"
1319
- : strategy === "offline"
1327
+ : effectiveStrategy === "offline"
1320
1328
  ? cached
1321
1329
  ? "cached"
1322
1330
  : "idle"
@@ -1327,7 +1335,7 @@ export class ModelRegistry {
1327
1335
  provider: providerId,
1328
1336
  status,
1329
1337
  optional: providerConfig.optional ?? false,
1330
- stale: result.stale || status === "cached",
1338
+ stale: result.stale || status === "cached" || (cacheOlderThanConfig && status !== "ok"),
1331
1339
  fetchedAt: discoveryError ? cached?.updatedAt : Date.now(),
1332
1340
  models: result.models.map(model => model.id),
1333
1341
  error: discoveryError,
@@ -122,7 +122,7 @@ export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
122
122
  context: ["General", "Compaction", "Rules (TTSR)", "Experimental"],
123
123
  memory: ["General", "Auto-Learn", "Mnemopi", "Hindsight"],
124
124
  files: ["Editing", "Reading", "Read Summaries", "LSP"],
125
- shell: ["Bash", "Eval & Python"],
125
+ shell: ["Bash", "Eval & Runtimes"],
126
126
  tools: [
127
127
  "Available Tools",
128
128
  "Todos",
@@ -1592,6 +1592,30 @@ export const SETTINGS_SCHEMA = {
1592
1592
  },
1593
1593
  },
1594
1594
 
1595
+ "share.store": {
1596
+ type: "enum",
1597
+ values: ["blob", "gist"] as const,
1598
+ default: "blob",
1599
+ ui: {
1600
+ tab: "interaction",
1601
+ group: "Collab",
1602
+ label: "Share Store",
1603
+ description: "Where /share uploads the encrypted session blob",
1604
+ options: [
1605
+ {
1606
+ value: "blob",
1607
+ label: "Encrypted Blob",
1608
+ description: "Upload to the share server (no GitHub account needed; avoids gist API rate limits)",
1609
+ },
1610
+ {
1611
+ value: "gist",
1612
+ label: "GitHub Gist",
1613
+ description: "Push to a secret gist (needs authenticated gh), falling back to the share server",
1614
+ },
1615
+ ],
1616
+ },
1617
+ },
1618
+
1595
1619
  "share.redactSecrets": {
1596
1620
  type: "boolean",
1597
1621
  default: true,
@@ -2703,18 +2727,6 @@ export const SETTINGS_SCHEMA = {
2703
2727
  },
2704
2728
  },
2705
2729
 
2706
- readHashLines: {
2707
- type: "boolean",
2708
- default: true,
2709
- ui: {
2710
- tab: "files",
2711
- group: "Reading",
2712
- label: "Hash Lines",
2713
- description:
2714
- "Include snapshot-tag headers and line numbers in read output for hashline edit mode ([PATH#TAG] plus LINE:content)",
2715
- },
2716
- },
2717
-
2718
2730
  "read.defaultLimit": {
2719
2731
  type: "number",
2720
2732
  default: 300,
@@ -2981,7 +2993,7 @@ export const SETTINGS_SCHEMA = {
2981
2993
  default: true,
2982
2994
  ui: {
2983
2995
  tab: "shell",
2984
- group: "Eval & Python",
2996
+ group: "Eval & Runtimes",
2985
2997
  label: "Python Eval Backend",
2986
2998
  description: "Allow the eval tool to dispatch Python cells to the IPython kernel",
2987
2999
  },
@@ -2992,20 +3004,42 @@ export const SETTINGS_SCHEMA = {
2992
3004
  default: true,
2993
3005
  ui: {
2994
3006
  tab: "shell",
2995
- group: "Eval & Python",
3007
+ group: "Eval & Runtimes",
2996
3008
  label: "JavaScript Eval Backend",
2997
3009
  description: "Allow the eval tool to dispatch JavaScript cells to the in-process runtime",
2998
3010
  },
2999
3011
  },
3000
3012
 
3001
- // Python kernel knobs (consumed by the eval py backend and the /python slash command)
3013
+ "eval.rb": {
3014
+ type: "boolean",
3015
+ default: false,
3016
+ ui: {
3017
+ tab: "shell",
3018
+ group: "Eval & Runtimes",
3019
+ label: "Ruby Eval Backend",
3020
+ description: "Allow the eval tool to dispatch Ruby cells to the persistent Ruby kernel",
3021
+ },
3022
+ },
3023
+
3024
+ "eval.jl": {
3025
+ type: "boolean",
3026
+ default: false,
3027
+ ui: {
3028
+ tab: "shell",
3029
+ group: "Eval & Runtimes",
3030
+ label: "Julia Eval Backend",
3031
+ description: "Allow the eval tool to dispatch Julia cells to the persistent Julia kernel",
3032
+ },
3033
+ },
3034
+
3035
+ // Runtime knobs (consumed by eval backends and the /python slash command)
3002
3036
  "python.kernelMode": {
3003
3037
  type: "enum",
3004
3038
  values: ["session", "per-call"] as const,
3005
3039
  default: "session",
3006
3040
  ui: {
3007
3041
  tab: "shell",
3008
- group: "Eval & Python",
3042
+ group: "Eval & Runtimes",
3009
3043
  label: "Python Kernel Mode",
3010
3044
  description: "Keep the IPython kernel alive across eval calls or start fresh each time",
3011
3045
  },
@@ -3015,12 +3049,34 @@ export const SETTINGS_SCHEMA = {
3015
3049
  default: "",
3016
3050
  ui: {
3017
3051
  tab: "shell",
3018
- group: "Eval & Python",
3052
+ group: "Eval & Runtimes",
3019
3053
  label: "Python Interpreter",
3020
3054
  description:
3021
3055
  "Optional path to an exact Python executable. When set, automatic Python runtime discovery is skipped.",
3022
3056
  },
3023
3057
  },
3058
+ "ruby.interpreter": {
3059
+ type: "string",
3060
+ default: "",
3061
+ ui: {
3062
+ tab: "shell",
3063
+ group: "Eval & Runtimes",
3064
+ label: "Ruby Interpreter",
3065
+ description:
3066
+ "Optional path to an exact Ruby executable. When set, automatic Ruby runtime discovery is skipped.",
3067
+ },
3068
+ },
3069
+ "julia.interpreter": {
3070
+ type: "string",
3071
+ default: "",
3072
+ ui: {
3073
+ tab: "shell",
3074
+ group: "Eval & Runtimes",
3075
+ label: "Julia Interpreter",
3076
+ description:
3077
+ "Optional path to an exact Julia executable. When set, automatic Julia runtime discovery is skipped.",
3078
+ },
3079
+ },
3024
3080
 
3025
3081
  // ────────────────────────────────────────────────────────────────────────
3026
3082
  // Tools
@@ -949,6 +949,11 @@ export class Settings {
949
949
  delete raw["power.preventDisplaySleep"];
950
950
  }
951
951
 
952
+ // readHashLines: removed. Hashline anchors are now driven solely by
953
+ // edit.mode === "hashline"; the separate read toggle only ever produced
954
+ // the incoherent "hashline edits without addressable anchors" state.
955
+ delete raw.readHashLines;
956
+
952
957
  return raw;
953
958
  }
954
959