@oh-my-pi/pi-coding-agent 17.2.1 → 17.2.2

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 (114) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/{CHANGELOG-dj46zzrm.md → CHANGELOG-xfpkakrn.md} +47 -0
  3. package/dist/cli.js +3891 -3885
  4. package/dist/types/config/keybindings.d.ts +6 -1
  5. package/dist/types/config/settings-schema.d.ts +16 -5
  6. package/dist/types/edit/hashline/block-resolver.d.ts +5 -5
  7. package/dist/types/edit/hashline/diff.d.ts +4 -4
  8. package/dist/types/eval/executor-base.d.ts +27 -0
  9. package/dist/types/eval/js/context-manager.d.ts +7 -0
  10. package/dist/types/eval/js/executor.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/runner.d.ts +64 -5
  12. package/dist/types/extensibility/extensions/types.d.ts +15 -0
  13. package/dist/types/extensibility/extensions/wrapper.d.ts +1 -1
  14. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  15. package/dist/types/live/visualizer.d.ts +3 -1
  16. package/dist/types/modes/acp/acp-agent.d.ts +3 -3
  17. package/dist/types/modes/components/keybinding-hints.d.ts +1 -1
  18. package/dist/types/modes/controllers/mcp-command-controller.d.ts +13 -0
  19. package/dist/types/modes/interactive-mode.d.ts +1 -0
  20. package/dist/types/modes/theme/theme.d.ts +1 -1
  21. package/dist/types/modes/types.d.ts +2 -0
  22. package/dist/types/secrets/index.d.ts +22 -0
  23. package/dist/types/secrets/obfuscator.d.ts +1 -1
  24. package/dist/types/session/agent-session-types.d.ts +4 -0
  25. package/dist/types/session/agent-session.d.ts +9 -0
  26. package/dist/types/session/codex-auto-reset.d.ts +182 -64
  27. package/dist/types/session/session-storage.d.ts +5 -2
  28. package/dist/types/session/session-tools.d.ts +2 -1
  29. package/dist/types/session/turn-recovery.d.ts +7 -1
  30. package/dist/types/system-prompt.d.ts +8 -0
  31. package/dist/types/task/executor.d.ts +27 -0
  32. package/dist/types/task/persisted-revive.d.ts +7 -0
  33. package/dist/types/tools/bash-interceptor.d.ts +1 -1
  34. package/dist/types/tools/browser/attach.d.ts +9 -1
  35. package/dist/types/tools/browser/launch.d.ts +20 -1
  36. package/dist/types/tools/browser/registry.d.ts +2 -0
  37. package/dist/types/tools/browser/tab-protocol.d.ts +8 -0
  38. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -0
  39. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  40. package/dist/types/tools/shell-tokenize.d.ts +11 -0
  41. package/dist/types/utils/image-loading.d.ts +2 -0
  42. package/dist/types/web/search/providers/duckduckgo.d.ts +9 -0
  43. package/dist/types/web/search/types.d.ts +2 -2
  44. package/package.json +12 -12
  45. package/src/commit/agentic/index.ts +2 -1
  46. package/src/commit/pipeline.ts +3 -2
  47. package/src/config/keybindings.ts +6 -1
  48. package/src/config/settings-schema.ts +16 -3
  49. package/src/edit/hashline/block-resolver.ts +5 -5
  50. package/src/edit/hashline/diff.ts +22 -15
  51. package/src/edit/hashline/execute.ts +14 -13
  52. package/src/edit/renderer.ts +3 -3
  53. package/src/eval/executor-base.ts +39 -0
  54. package/src/eval/jl/executor.ts +13 -2
  55. package/src/eval/js/context-manager.ts +97 -14
  56. package/src/eval/js/executor.ts +3 -0
  57. package/src/eval/js/index.ts +1 -0
  58. package/src/eval/py/executor.ts +55 -15
  59. package/src/eval/rb/executor.ts +8 -1
  60. package/src/extensibility/extensions/runner.ts +121 -4
  61. package/src/extensibility/extensions/types.ts +15 -0
  62. package/src/extensibility/extensions/wrapper.ts +19 -2
  63. package/src/extensibility/legacy-pi-coding-agent-shim.ts +5 -5
  64. package/src/live/visualizer.ts +8 -1
  65. package/src/lsp/index.ts +51 -11
  66. package/src/main.ts +3 -0
  67. package/src/modes/acp/acp-agent.ts +16 -5
  68. package/src/modes/components/assistant-message.ts +5 -10
  69. package/src/modes/components/custom-editor.ts +1 -1
  70. package/src/modes/components/keybinding-hints.ts +4 -13
  71. package/src/modes/components/status-line/segments.ts +4 -5
  72. package/src/modes/components/tool-execution.ts +4 -5
  73. package/src/modes/controllers/input-controller.ts +18 -40
  74. package/src/modes/controllers/live-command-controller.ts +1 -0
  75. package/src/modes/controllers/mcp-command-controller.ts +28 -11
  76. package/src/modes/interactive-mode.ts +59 -2
  77. package/src/modes/theme/theme.ts +13 -6
  78. package/src/modes/types.ts +2 -0
  79. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  80. package/src/prompts/system/plan-mode-active.md +5 -5
  81. package/src/sdk.ts +47 -16
  82. package/src/secrets/index.ts +83 -3
  83. package/src/secrets/obfuscator.ts +70 -15
  84. package/src/session/agent-session-types.ts +7 -1
  85. package/src/session/agent-session.ts +393 -112
  86. package/src/session/codex-auto-reset.ts +594 -123
  87. package/src/session/eval-runner.ts +2 -0
  88. package/src/session/messages.ts +1 -1
  89. package/src/session/session-maintenance.ts +48 -1
  90. package/src/session/session-manager.ts +4 -3
  91. package/src/session/session-stats.ts +14 -3
  92. package/src/session/session-storage.ts +47 -15
  93. package/src/session/session-tools.ts +30 -2
  94. package/src/session/turn-recovery.ts +15 -2
  95. package/src/slash-commands/builtin-registry.ts +21 -15
  96. package/src/slash-commands/helpers/reset-usage.ts +2 -0
  97. package/src/system-prompt.ts +13 -1
  98. package/src/task/executor.ts +168 -15
  99. package/src/task/persisted-revive.ts +26 -1
  100. package/src/telemetry-export.ts +8 -4
  101. package/src/tools/bash-interceptor.ts +85 -8
  102. package/src/tools/bash.ts +1 -1
  103. package/src/tools/browser/attach.ts +35 -10
  104. package/src/tools/browser/launch.ts +60 -11
  105. package/src/tools/browser/registry.ts +18 -2
  106. package/src/tools/browser/tab-protocol.ts +8 -0
  107. package/src/tools/browser/tab-supervisor.ts +31 -4
  108. package/src/tools/browser/tab-worker.ts +32 -2
  109. package/src/tools/output-meta.ts +16 -1
  110. package/src/tools/shell-tokenize.ts +101 -0
  111. package/src/utils/image-loading.ts +8 -3
  112. package/src/utils/shell-snapshot.ts +31 -21
  113. package/src/web/search/providers/duckduckgo.ts +183 -15
  114. package/src/web/search/types.ts +6 -1
@@ -1,6 +1,7 @@
1
1
  import * as crypto from "node:crypto";
2
- import * as fs from "node:fs/promises";
2
+ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { SENSITIVE_TOKEN_RE } from "@oh-my-pi/pi-ai/providers/transform-messages";
4
5
  import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
5
6
  import { YAML } from "bun";
6
7
  import { regexHasUnresolvableShortMatchFallback, type SecretEntry, sanitizeSecretFriendlyName } from "./obfuscator";
@@ -31,9 +32,9 @@ export async function getSecretPlaceholderKey(keyDir: string = getAgentDir()): P
31
32
  }
32
33
 
33
34
  const generated = crypto.randomBytes(32).toString("base64url");
34
- await fs.mkdir(keyDir, { recursive: true });
35
+ await fs.promises.mkdir(keyDir, { recursive: true });
35
36
  try {
36
- await fs.writeFile(keyPath, generated, { flag: "wx", mode: 0o600 });
37
+ await fs.promises.writeFile(keyPath, generated, { flag: "wx", mode: 0o600 });
37
38
  cachedPlaceholderKeys.set(keyPath, generated);
38
39
  return generated;
39
40
  } catch (err) {
@@ -71,6 +72,60 @@ export async function getExistingSecretPlaceholderKey(keyDir: string = getAgentD
71
72
  return existing;
72
73
  }
73
74
 
75
+ // Process-stable fallback for the sync lazy path when the key file cannot be
76
+ // persisted (e.g. unwritable config root on a headless run). Memoized so
77
+ // re-obfuscation within the process stays idempotent; placeholders simply lose
78
+ // cross-session stability, matching `defaultPlaceholderKey()` in obfuscator.ts.
79
+ let ephemeralSyncPlaceholderKey: string | undefined;
80
+
81
+ /**
82
+ * Synchronous variant of `getSecretPlaceholderKey` for the lazy key provider
83
+ * `SecretObfuscator` invokes inside its synchronous `obfuscate()` path when a
84
+ * built-in credential-pattern entry first matches session content. Never
85
+ * throws: an unreadable or unwritable key file degrades to a process-ephemeral
86
+ * key (with a warning) instead of breaking the session.
87
+ */
88
+ export function getSecretPlaceholderKeySync(keyDir: string = getAgentDir()): string {
89
+ const keyPath = path.join(keyDir, "secret-placeholder.key");
90
+ const cached = cachedPlaceholderKeys.get(keyPath);
91
+ if (cached !== undefined) return cached;
92
+ try {
93
+ const existing = fs.readFileSync(keyPath, "utf8").trim();
94
+ if (PLACEHOLDER_KEY_RE.test(existing)) {
95
+ cachedPlaceholderKeys.set(keyPath, existing);
96
+ return existing;
97
+ }
98
+ } catch {
99
+ // Missing or unreadable — attempt creation below.
100
+ }
101
+ const generated = crypto.randomBytes(32).toString("base64url");
102
+ try {
103
+ fs.mkdirSync(keyDir, { recursive: true });
104
+ fs.writeFileSync(keyPath, generated, { flag: "wx", mode: 0o600 });
105
+ cachedPlaceholderKeys.set(keyPath, generated);
106
+ return generated;
107
+ } catch (err) {
108
+ if ((err as NodeJS.ErrnoException).code === "EEXIST") {
109
+ // Another process won the create race; accept its key if valid.
110
+ try {
111
+ const winner = fs.readFileSync(keyPath, "utf8").trim();
112
+ if (PLACEHOLDER_KEY_RE.test(winner)) {
113
+ cachedPlaceholderKeys.set(keyPath, winner);
114
+ return winner;
115
+ }
116
+ } catch {
117
+ // Fall through to the ephemeral key.
118
+ }
119
+ }
120
+ logger.warn("Could not persist secret placeholder key, using a process-ephemeral key", {
121
+ path: keyPath,
122
+ error: String(err),
123
+ });
124
+ ephemeralSyncPlaceholderKey ??= crypto.randomBytes(32).toString("base64url");
125
+ return ephemeralSyncPlaceholderKey;
126
+ }
127
+ }
128
+
74
129
  /** Read and validate the key file, optionally retrying briefly until a valid key lands. */
75
130
  async function readPlaceholderKeyFile(keyPath: string, retry: boolean): Promise<string | undefined> {
76
131
  const attempts = retry ? 50 : 1;
@@ -145,6 +200,31 @@ export function collectEnvSecrets(): SecretEntry[] {
145
200
  return entries;
146
201
  }
147
202
 
203
+ /**
204
+ * Built-in entries covering credential-shaped tokens (GitHub/GitLab/OpenAI-style
205
+ * API keys) that are NOT configured via secrets.yml or the environment. Without
206
+ * these, such a token in a tool result falls through to pi-ai's irreversible
207
+ * provider-boundary redaction (`[openai_token_redacted]`); the model then echoes
208
+ * that placeholder into edit-tool `old_text`, which can never match the real
209
+ * bytes on disk (issue #6968). Routing the same shapes through the obfuscator
210
+ * mints reversible keyed placeholders that `deobfuscateToolArguments` restores
211
+ * before tool execution, keeping exact-match edits working while the credential
212
+ * bytes still never reach the provider. Unlike the pi-ai redaction there is no
213
+ * entropy gate here — a false positive only over-obfuscates, which stays
214
+ * transparent because the round trip is lossless.
215
+ */
216
+ export function builtinCredentialSecretEntries(): SecretEntry[] {
217
+ return [
218
+ {
219
+ type: "regex",
220
+ content: SENSITIVE_TOKEN_RE.source,
221
+ flags: "i",
222
+ mode: "obfuscate",
223
+ friendlyName: "Credential",
224
+ },
225
+ ];
226
+ }
227
+
148
228
  async function loadSecretsFile(filePath: string): Promise<SecretEntry[]> {
149
229
  try {
150
230
  const text = await Bun.file(filePath).text();
@@ -594,18 +594,22 @@ export class SecretObfuscator {
594
594
  /** Whether any secrets were configured */
595
595
  #hasAny: boolean;
596
596
 
597
- /** Private per-install (or per-process) key for the keyed placeholder digest. */
598
- readonly #key: string;
597
+ /**
598
+ * Private per-install (or per-process) key for the keyed placeholder digest.
599
+ * Resolved lazily when the constructor received a key PROVIDER: the first
600
+ * placeholder mint or keyed fallback marker triggers resolution, so callers
601
+ * can defer persisting `secret-placeholder.key` until a dynamic regex entry
602
+ * actually matches session content.
603
+ */
604
+ #key: string | undefined;
605
+ #keyProvider: (() => string) | undefined;
599
606
 
600
- constructor(entries: SecretEntry[], key: string = defaultPlaceholderKey()) {
601
- this.#key = key;
602
- // The keyed-hash key makes obfuscate-mode placeholder bases un-dictionaryable,
603
- // but it can be persisted in a user-readable file (`secret-placeholder.key`).
604
- // A prompt-injected tool read (read/bash) could otherwise surface it to the
605
- // provider verbatim and undo that protection, so redact the key itself from
606
- // obfuscated (provider-visible) output as a one-way secret.
607
- this.#replaceMappings.set(key, this.#generateSecretReplacement(key));
608
- this.#configuredSecretValues.add(key);
607
+ constructor(entries: SecretEntry[], key: string | (() => string) = defaultPlaceholderKey()) {
608
+ if (typeof key === "function") {
609
+ this.#keyProvider = key;
610
+ } else {
611
+ this.#setPlaceholderKey(key);
612
+ }
609
613
  // Collect every configured plain-secret literal AND compile every regex
610
614
  // entry BEFORE minting any placeholder below, so a placeholder's friendly
611
615
  // name (checked against both in `#createPlaceholder`) can never embed a
@@ -671,6 +675,44 @@ export class SecretObfuscator {
671
675
  this.#hasAny = hasRealSec;
672
676
  }
673
677
 
678
+ /**
679
+ * The keyed-hash key makes obfuscate-mode placeholder bases un-dictionaryable,
680
+ * but it can be persisted in a user-readable file (`secret-placeholder.key`).
681
+ * A prompt-injected tool read (read/bash) could otherwise surface it to the
682
+ * provider verbatim and undo that protection, so redact the key itself from
683
+ * obfuscated (provider-visible) output as a one-way secret.
684
+ */
685
+ #setPlaceholderKey(key: string): void {
686
+ this.#key = key;
687
+ this.#replaceMappings.set(key, this.#generateSecretReplacement(key));
688
+ this.#configuredSecretValues.add(key);
689
+ }
690
+
691
+ /** Resolve the placeholder key once, minting its self-redaction on first use. */
692
+ #getKey(): string {
693
+ let key = this.#key;
694
+ if (key === undefined) {
695
+ key = this.#keyProvider?.() ?? defaultPlaceholderKey();
696
+ this.#keyProvider = undefined;
697
+ this.#setPlaceholderKey(key);
698
+ }
699
+ return key;
700
+ }
701
+
702
+ /** Whether this pass will mint a keyed placeholder from a regex match. */
703
+ #willMintRegexPlaceholder(secretValues: ReadonlySet<string>): boolean {
704
+ for (const entry of this.#regexEntries) {
705
+ if (entry.mode !== "obfuscate") continue;
706
+ for (const value of secretValues) {
707
+ entry.regex.lastIndex = 0;
708
+ const matches = entry.regex.test(value);
709
+ entry.regex.lastIndex = 0;
710
+ if (matches) return true;
711
+ }
712
+ }
713
+ return false;
714
+ }
715
+
674
716
  hasSecrets(): boolean {
675
717
  return this.#hasAny;
676
718
  }
@@ -682,6 +724,13 @@ export class SecretObfuscator {
682
724
  for (const secretValue of sharedRegexSecretValues ?? []) {
683
725
  this.#currentRegexSecretValues.add(secretValue);
684
726
  }
727
+ // Resolve a lazy key before the replace phase whenever this pass will mint
728
+ // a regex placeholder. The key registers itself as a replace-mode secret;
729
+ // resolving it later, while processing the regex match, would expose key
730
+ // bytes already present in this same provider-visible input.
731
+ if (this.#keyProvider !== undefined && this.#willMintRegexPlaceholder(this.#currentRegexSecretValues)) {
732
+ this.#getKey();
733
+ }
685
734
  let result = text;
686
735
  // `origin` runs parallel to `result` (one tag char per result char): "I" for
687
736
  // bytes carried from the INPUT (placeholders from a PRIOR obfuscate() call)
@@ -937,7 +986,9 @@ export class SecretObfuscator {
937
986
  // remainder bytes that merely look sentinel-shaped (e.g. `ZZZZ`) cannot equal
938
987
  // the marker and are still redacted instead of passed through.
939
988
  const replacement =
940
- chunk.length <= 2 ? "Z".repeat(chunk.length) : `ZZ${buildKeyedReplacementRun(this.#key, chunk.length - 2)}`;
989
+ chunk.length <= 2
990
+ ? "Z".repeat(chunk.length)
991
+ : `ZZ${buildKeyedReplacementRun(this.#getKey(), chunk.length - 2)}`;
941
992
  this.#generatedReplaceChunks.add(replacement);
942
993
  return replacement;
943
994
  }
@@ -1023,7 +1074,9 @@ export class SecretObfuscator {
1023
1074
  // the ordinary keyed-run fallback #generateReplacement already uses.
1024
1075
  replacement =
1025
1076
  stable ??
1026
- (value.length <= 2 ? buildKeyedReplacementRun(this.#key, value.length) : this.#generateReplacement(value));
1077
+ (value.length <= 2
1078
+ ? buildKeyedReplacementRun(this.#getKey(), value.length)
1079
+ : this.#generateReplacement(value));
1027
1080
  regex.lastIndex = 0;
1028
1081
  }
1029
1082
  this.#generatedReplaceChunks.add(replacement);
@@ -1183,7 +1236,9 @@ export class SecretObfuscator {
1183
1236
 
1184
1237
  for (let attempt = 0; ; attempt++) {
1185
1238
  const base =
1186
- attempt === 0 ? buildHashBase(this.#key, baseKey) : buildHashBase(this.#key, `${baseKey}\0${attempt}`);
1239
+ attempt === 0
1240
+ ? buildHashBase(this.#getKey(), baseKey)
1241
+ : buildHashBase(this.#getKey(), `${baseKey}\0${attempt}`);
1187
1242
  const owner = this.#placeholderBaseOwners.get(base);
1188
1243
  if (owner !== undefined && owner !== baseKey) continue;
1189
1244
  this.#placeholderBaseOwners.set(base, baseKey);
@@ -1195,7 +1250,7 @@ export class SecretObfuscator {
1195
1250
  #reserveFallbackPlaceholderBase(baseKey: string, startAttempt: number): string {
1196
1251
  for (let attempt = startAttempt; ; attempt++) {
1197
1252
  const owner = `${baseKey}\0collision\0${attempt}`;
1198
- const base = buildHashBase(this.#key, `${baseKey}\0collision\0${attempt}`);
1253
+ const base = buildHashBase(this.#getKey(), `${baseKey}\0collision\0${attempt}`);
1199
1254
  if (this.#placeholderBaseOwners.has(base)) continue;
1200
1255
  this.#placeholderBaseOwners.set(base, owner);
1201
1256
  return base;
@@ -35,6 +35,7 @@ import type { FileSlashCommand } from "../extensibility/slash-commands";
35
35
  import type { SecretObfuscator } from "../secrets/obfuscator";
36
36
  import type { ConfiguredThinkingLevel } from "../thinking";
37
37
  import type { XdevState } from "../tools/xdev";
38
+ import type { CodexAutoRedeemCoordinator } from "./codex-auto-reset";
38
39
  import type { SessionManager } from "./session-manager";
39
40
 
40
41
  /** Maximum time the interactive shutdown path waits for Mnemopi consolidation. */
@@ -173,6 +174,8 @@ export interface AgentSessionConfig {
173
174
  initialAdvisorCosts?: ReadonlyMap<string, number>;
174
175
  /** Prefer websocket transport for OpenAI Codex requests when supported. */
175
176
  preferWebsockets?: boolean;
177
+ /** Codex saved-reset coordinator; defaults to the process-wide singleton so concurrent sessions can't double-spend. Inject a fresh one in tests. */
178
+ codexResetCoordinator?: CodexAutoRedeemCoordinator;
176
179
  /** Provider payload hook used by the active session request path. */
177
180
  onPayload?: SimpleStreamOptions["onPayload"];
178
181
  /** Provider response hook used by the active session request path. */
@@ -184,7 +187,10 @@ export interface AgentSessionConfig {
184
187
  /** Current session message-to-LLM conversion pipeline. */
185
188
  convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
186
189
  /** System prompt builder that can consider tool availability. */
187
- rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
190
+ rebuildSystemPrompt?: (
191
+ toolNames: string[],
192
+ tools: Map<string, AgentTool>,
193
+ ) => Promise<{ systemPrompt: string[]; xdevCatalogNames?: readonly string[] }>;
188
194
  /** Local calendar date provider used by prompt-cache invalidation. */
189
195
  getLocalCalendarDate?: () => string;
190
196
  /** Tools mounted under `xd://`, for `/tools` display. */