privateer-agent 0.12.42 → 0.12.45

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.12.42",
3
+ "version": "0.12.45",
4
4
  "description": "Privacy-first terminal coding agent — bring your own model across 20 providers (Anthropic, OpenAI, OpenRouter, Google, local Ollama…). Safe-by-default permissions, MCP, sub-agents, workflows, and verifiable TEE inference. Built on the Pi toolkit.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -70,21 +70,21 @@
70
70
  "node": ">=22.19.0"
71
71
  },
72
72
  "dependencies": {
73
- "@earendil-works/pi-ai": "0.84.4",
74
- "@earendil-works/pi-coding-agent": "0.84.4",
75
- "@earendil-works/pi-tui": "0.84.4",
76
- "@juicesharp/rpiv-ask-user-question": "2.2.0",
77
- "@juicesharp/rpiv-web-tools": "1.20.0",
73
+ "@earendil-works/pi-ai": "0.86.0",
74
+ "@earendil-works/pi-coding-agent": "0.86.0",
75
+ "@earendil-works/pi-tui": "0.86.0",
76
+ "@juicesharp/rpiv-ask-user-question": "2.10.1",
77
+ "@juicesharp/rpiv-web-tools": "2.10.1",
78
78
  "@noble/ciphers": "2.2.0",
79
79
  "@noble/curves": "1.9.7",
80
80
  "@noble/hashes": "1.8.0",
81
81
  "@phala/dcap-qvl": "0.5.2",
82
82
  "@zed-industries/agent-client-protocol": "0.4.5",
83
83
  "patch-package": "8.0.1",
84
- "pi-background-tasks": "2.4.2",
85
- "pi-mcp-adapter": "2.11.0",
86
- "pi-privacy": "0.13.0",
87
- "pi-subagents": "0.34.0",
84
+ "pi-background-tasks": "2.5.0",
85
+ "pi-mcp-adapter": "2.34.0",
86
+ "pi-privacy": "0.14.1",
87
+ "pi-subagents": "0.70.0",
88
88
  "picomatch": "4.0.5",
89
89
  "privateer-speak": "0.2.2",
90
90
  "privateer-workflow": "0.1.0",
@@ -1,8 +1,8 @@
1
1
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/config.js b/node_modules/@earendil-works/pi-coding-agent/dist/config.js
2
- index f04269f..7160497 100644
2
+ index 872dd55..edd61db 100644
3
3
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/config.js
4
4
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/config.js
5
- @@ -398,9 +398,72 @@ catch (e) {
5
+ @@ -399,9 +399,72 @@ catch (e) {
6
6
  const piConfigName = pkg.piConfig?.name;
7
7
  export const PACKAGE_NAME = pkg.name || "@earendil-works/pi-coding-agent";
8
8
  export const APP_NAME = piConfigName || "pi";
@@ -77,10 +77,10 @@ index f04269f..7160497 100644
77
77
  export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
78
78
  export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
79
79
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
80
- index fd82b71..7c10ba9 100644
80
+ index 638646d..ba1da98 100644
81
81
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
82
82
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
83
- @@ -38,6 +38,122 @@ import { createLocalBashOperations } from "./tools/bash.js";
83
+ @@ -39,6 +39,129 @@ import { createLocalBashOperations } from "./tools/bash.js";
84
84
  import { createAllToolDefinitions } from "./tools/index.js";
85
85
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
86
86
  import { addUsageToTotals, createUsageTotals } from "./usage-totals.js";
@@ -192,7 +192,14 @@ index fd82b71..7c10ba9 100644
192
192
  +// A server-stated delay wins outright — it is the only number here that is a fact.
193
193
  +// The jitter is the point of the rest: without it every parallel request that tripped
194
194
  +// the same limit wakes at the identical millisecond and trips it again.
195
- +export function retryDelayMs(errorText, attempt, baseDelayMs) {
195
+ +//
196
+ +// Privateer patch: named pvRetryDelayMs, not retryDelayMs — pi-ai now ships its OWN
197
+ +// `retryDelayMs(policy, attempt)` (plain exponential backoff, no retry-after, no
198
+ +// jitter), imported by this file below. A same-named top-level function here would be
199
+ +// a SyntaxError (duplicate declaration) that takes the whole CLI down at launch,
200
+ +// exactly the failure mode the MIN_ANSWER_TOKENS comment in the pi-ai patch warns
201
+ +// about. Ours still fully supersedes pi-ai's at the one call site that mattered.
202
+ +export function pvRetryDelayMs(errorText, attempt, baseDelayMs) {
196
203
  + const stated = retryAfterMs(errorText);
197
204
  + if (stated != null)
198
205
  + return stated;
@@ -203,7 +210,7 @@ index fd82b71..7c10ba9 100644
203
210
  /**
204
211
  * Parse a skill block from message text.
205
212
  * Returns null if the text doesn't contain a skill block.
206
- @@ -102,6 +218,8 @@ export class AgentSession {
213
+ @@ -101,6 +224,8 @@ export class AgentSession {
207
214
  // Retry state
208
215
  _retryAbortController = undefined;
209
216
  _retryAttempt = 0;
@@ -212,7 +219,7 @@ index fd82b71..7c10ba9 100644
212
219
  // Bash execution state
213
220
  _bashAbortControllers = new Set();
214
221
  _pendingBashMessages = [];
215
- @@ -186,6 +304,14 @@ export class AgentSession {
222
+ @@ -192,6 +317,14 @@ export class AgentSession {
216
223
  }
217
224
  const isOAuth = this._modelRuntime.isUsingOAuth(model.provider);
218
225
  if (isOAuth) {
@@ -227,7 +234,7 @@ index fd82b71..7c10ba9 100644
227
234
  throw new Error(`Authentication failed for "${model.provider}". ` +
228
235
  `Credentials may have expired or network is unavailable. ` +
229
236
  `Run '/login ${model.provider}' to re-authenticate.`);
230
- @@ -380,6 +506,16 @@ export class AgentSession {
237
+ @@ -401,6 +534,16 @@ export class AgentSession {
231
238
  }
232
239
  }
233
240
  }
@@ -244,7 +251,7 @@ index fd82b71..7c10ba9 100644
244
251
  // Emit to extensions first
245
252
  await this._emitExtensionEvent(event);
246
253
  // Notify all listeners
247
- @@ -801,6 +937,33 @@ export class AgentSession {
254
+ @@ -904,6 +1047,33 @@ export class AgentSession {
248
255
  finalError: msg.errorMessage,
249
256
  });
250
257
  this._retryAttempt = 0;
@@ -277,8 +284,8 @@ index fd82b71..7c10ba9 100644
277
284
  + return false;
278
285
  }
279
286
  if (await this._checkCompaction(msg)) {
280
- return true;
281
- @@ -882,6 +1045,14 @@ export class AgentSession {
287
+ return !this._agentRunAbortRequested;
288
+ @@ -991,6 +1161,14 @@ export class AgentSession {
282
289
  if (!hasConfiguredAuth) {
283
290
  const isOAuth = this._modelRuntime.isUsingOAuth(this.model.provider);
284
291
  if (isOAuth) {
@@ -293,7 +300,7 @@ index fd82b71..7c10ba9 100644
293
300
  throw new Error(`Authentication failed for "${this.model.provider}". ` +
294
301
  `Credentials may have expired or network is unavailable. ` +
295
302
  `Run '/login ${this.model.provider}' to re-authenticate.`);
296
- @@ -891,7 +1062,13 @@ export class AgentSession {
303
+ @@ -1000,7 +1178,13 @@ export class AgentSession {
297
304
  // Check if we need to compact before sending (catches aborted responses).
298
305
  // The user's new prompt is sent below, so do not call agent.continue() here.
299
306
  const lastAssistant = this._findLastAssistantMessage();
@@ -308,7 +315,7 @@ index fd82b71..7c10ba9 100644
308
315
  await this._checkCompaction(lastAssistant, false);
309
316
  }
310
317
  // Build messages array (custom message if any, then user message)
311
- @@ -2242,6 +2419,34 @@ export class AgentSession {
318
+ @@ -2351,6 +2535,34 @@ export class AgentSession {
312
319
  // Context overflow is handled by compaction, not retry.
313
320
  if (isContextOverflow(message, this.model?.contextWindow ?? 0))
314
321
  return false;
@@ -343,16 +350,15 @@ index fd82b71..7c10ba9 100644
343
350
  return isRetryableAssistantError(message);
344
351
  }
345
352
  /**
346
- @@ -2287,7 +2492,11 @@ export class AgentSession {
353
+ @@ -2408,7 +2620,10 @@ export class AgentSession {
347
354
  this._retryAttempt--;
348
355
  return false;
349
356
  }
350
- - const delayMs = settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
357
+ - const delayMs = retryDelayMs(settings, this._retryAttempt);
351
358
  + // Privateer patch: honour a server-stated `retry-after` and jitter the fallback.
352
- + // Stock Pi's fixed 2s/4s/8s ladder spends its whole budget inside a rate-limit
353
- + // window it was never long enough to outlast, and fires every parallel retry at
354
- + // the same instant. See retryDelayMs above.
355
- + const delayMs = retryDelayMs(message.errorMessage, this._retryAttempt, settings.baseDelayMs);
359
+ + // Stock Pi's own retryDelayMs (imported above, from pi-ai) is a fixed 2s/4s/8s
360
+ + // ladder with no jitter and no retry-after — see pvRetryDelayMs above.
361
+ + const delayMs = pvRetryDelayMs(message.errorMessage, this._retryAttempt, settings.baseDelayMs);
356
362
  this._emit({
357
363
  type: "auto_retry_start",
358
364
  attempt: this._retryAttempt,
@@ -402,35 +408,37 @@ index 197bccc..bc9ac3f 100644
402
408
  //# sourceMappingURL=auth-guidance.js.map
403
409
 
404
410
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
405
- index bb022d4..207f3ba 100644
411
+ index be748bf..378f4c7 100644
406
412
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
407
413
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
408
- @@ -18,7 +18,7 @@ import { createJiti } from "jiti/static";
409
- import * as _bundledTypebox from "typebox";
410
- import * as _bundledTypeboxCompile from "typebox/compile";
411
- import * as _bundledTypeboxValue from "typebox/value";
412
- -import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js";
413
- +import { getAgentDir, isBunBinary, projectConfigDirCandidates } from "../../config.js";
414
- // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
415
- // avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
416
- import * as _bundledPiCodingAgent from "../../index.js";
417
- @@ -419,11 +419,14 @@ async function loadExtensionModule(extensionPath, cacheToken) {
418
- // Compiled binaries and the bundled Node distribution use embedded modules.
419
- // Source TypeScript reuses host modules and root tsconfig paths. Unbundled
420
- // Node builds use dist aliases.
421
- + // Privateer: reuse the host's Pi/Typebox namespaces in Node builds too.
422
- + // Otherwise each extension can reload that graph through Jiti. Aliases
423
- + // remain a fallback for non-virtual imports; extension reload is unchanged.
424
- ...(isBunBinary || isNodeSeaBinary || isBundledNode
425
- ? { virtualModules: VIRTUAL_MODULES, tryNative: false }
426
- : isTypeScriptSourceRuntime
427
- ? { virtualModules: VIRTUAL_MODULES, tsconfigPaths: true }
428
- - : { alias: getAliases() }),
429
- + : { virtualModules: VIRTUAL_MODULES, alias: getAliases() }),
430
- });
431
- const module = await jiti.import(extensionPath, { default: true });
432
- const factory = module;
433
- @@ -621,9 +624,23 @@ export async function discoverAndLoadExtensions(configuredPaths, cwd, agentDir =
414
+ @@ -6,7 +6,7 @@ import * as fs from "node:fs";
415
+ import { createRequire } from "node:module";
416
+ import * as path from "node:path";
417
+ import { fileURLToPath } from "node:url";
418
+ -import { CONFIG_DIR_NAME, getAgentDir, isBunBinary, isBundledNode } from "../../config.js";
419
+ +import { getAgentDir, isBunBinary, isBundledNode, projectConfigDirCandidates } from "../../config.js";
420
+ import { resolvePath } from "../../utils/paths.js";
421
+ import { createEventBus } from "../event-bus.js";
422
+ import { execCommand } from "../exec.js";
423
+ @@ -401,12 +401,15 @@ async function loadExtensionModule(extensionPath, cacheToken) {
424
+ const createJitiImpl = await getCreateJiti();
425
+ // Compiled binaries and the bundled Node distribution use embedded modules.
426
+ // Source TypeScript reuses host modules and root tsconfig paths. Unbundled
427
+ - // Node builds use dist aliases and do not need the bundled virtual modules.
428
+ + // Node builds use dist aliases.
429
+ + // Privateer: reuse the host's Pi/Typebox namespaces in Node builds too.
430
+ + // Otherwise each extension can reload that graph through Jiti. Aliases
431
+ + // remain a fallback for non-virtual imports; extension reload is unchanged.
432
+ const resolutionOptions = usesEmbeddedModules
433
+ ? { virtualModules: await getVirtualModules(), tryNative: false }
434
+ : isTypeScriptSourceRuntime
435
+ ? { virtualModules: await getVirtualModules(), tsconfigPaths: true }
436
+ - : { alias: getAliases() };
437
+ + : { virtualModules: await getVirtualModules(), alias: getAliases() };
438
+ const jiti = createJitiImpl(import.meta.url, {
439
+ moduleCache: false,
440
+ ...resolutionOptions,
441
+ @@ -607,9 +610,23 @@ export async function discoverAndLoadExtensions(configuredPaths, cwd, agentDir =
434
442
  }
435
443
  }
436
444
  };
@@ -810,10 +818,10 @@ index c5ed4d7..d9f50ae 100644
810
818
  }
811
819
  const globalPath = join(this.agentDir, "APPEND_SYSTEM.md");
812
820
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
813
- index f0f936e..ad164d2 100644
821
+ index ed9d8d2..34d344c 100644
814
822
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
815
823
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
816
- @@ -2,7 +2,7 @@ import { randomUUID } from "crypto";
824
+ @@ -3,7 +3,7 @@ import { randomUUID } from "crypto";
817
825
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
818
826
  import { dirname, join } from "path";
819
827
  import lockfile from "proper-lockfile";
@@ -822,7 +830,7 @@ index f0f936e..ad164d2 100644
822
830
  import { normalizePath, resolvePath } from "../utils/paths.js";
823
831
  import { stripBom } from "../utils/text.js";
824
832
  import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.js";
825
- @@ -45,14 +45,112 @@ function toSettingsError(scope, error, path) {
833
+ @@ -52,14 +52,112 @@ function toSettingsError(scope, error, path) {
826
834
  error: error instanceof Error ? error : new Error(String(error)),
827
835
  };
828
836
  }
@@ -936,7 +944,7 @@ index f0f936e..ad164d2 100644
936
944
  }
937
945
  acquireLockSyncWithRetry(path) {
938
946
  const maxAttempts = 10;
939
- @@ -79,7 +177,13 @@ export class FileSettingsStorage {
947
+ @@ -86,7 +184,13 @@ export class FileSettingsStorage {
940
948
  throw lastError ?? new Error("Failed to acquire settings lock");
941
949
  }
942
950
  withLock(scope, fn) {
@@ -951,7 +959,7 @@ index f0f936e..ad164d2 100644
951
959
  const dir = dirname(path);
952
960
  let release;
953
961
  try {
954
- @@ -157,7 +261,7 @@ export class SettingsManager {
962
+ @@ -164,7 +268,7 @@ export class SettingsManager {
955
963
  const storage = new FileSettingsStorage(resolvedCwd, resolvedAgentDir);
956
964
  return SettingsManager.fromStorageWithPaths(storage, options, {
957
965
  global: join(resolvedAgentDir, "settings.json"),
@@ -960,7 +968,7 @@ index f0f936e..ad164d2 100644
960
968
  });
961
969
  }
962
970
  /** Create a SettingsManager from an arbitrary storage backend */
963
- @@ -610,7 +714,15 @@ export class SettingsManager {
971
+ @@ -646,7 +750,15 @@ export class SettingsManager {
964
972
  getProviderRetrySettings() {
965
973
  return {
966
974
  timeoutMs: this.settings.retry?.provider?.timeoutMs,
@@ -978,7 +986,7 @@ index f0f936e..ad164d2 100644
978
986
  };
979
987
  }
980
988
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js
981
- index 56cf7a3..2a32170 100644
989
+ index d101003..540b972 100644
982
990
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js
983
991
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js
984
992
  @@ -1,7 +1,7 @@
@@ -990,7 +998,7 @@ index 56cf7a3..2a32170 100644
990
998
  import { parseFrontmatter } from "../utils/frontmatter.js";
991
999
  import { canonicalizePath, resolvePath } from "../utils/paths.js";
992
1000
  import { createSyntheticSourceInfo } from "./source-info.js";
993
- @@ -346,10 +346,15 @@ export function loadSkills(options) {
1001
+ @@ -348,10 +348,15 @@ export function loadSkills(options) {
994
1002
  }
995
1003
  if (includeDefaults) {
996
1004
  addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", true));
@@ -1008,7 +1016,7 @@ index 56cf7a3..2a32170 100644
1008
1016
  const isUnderPath = (target, root) => {
1009
1017
  const normalizedRoot = resolve(root);
1010
1018
  if (target === normalizedRoot) {
1011
- @@ -362,7 +367,7 @@ export function loadSkills(options) {
1019
+ @@ -364,7 +369,7 @@ export function loadSkills(options) {
1012
1020
  if (!includeDefaults) {
1013
1021
  if (isUnderPath(resolvedPath, userSkillsDir))
1014
1022
  return "user";
@@ -1161,7 +1169,7 @@ index 0877ee7..faa2559 100644
1161
1169
  while (true) {
1162
1170
  const agentsSkillsDir = join(currentDir, ".agents", "skills");
1163
1171
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts b/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts
1164
- index 3e9241d..bfbfcbf 100644
1172
+ index 205144d..14080a2 100644
1165
1173
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts
1166
1174
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts
1167
1175
  @@ -1,7 +1,7 @@
@@ -1170,9 +1178,9 @@ index 3e9241d..bfbfcbf 100644
1170
1178
  export { AgentSession, type AgentSessionConfig, type AgentSessionEvent, type AgentSessionEventListener, type ModelCycleResult, type ParsedSkillBlock, type PromptOptions, parseSkillBlock, type SessionStats, } from "./core/agent-session.ts";
1171
1179
  -export { readStoredCredential } from "./core/auth-storage.ts";
1172
1180
  +export { AuthStorage, readStoredCredential } from "./core/auth-storage.ts";
1181
+ export type { CacheWarmingDecision, CacheWarmingStatus } from "./core/cache-warmer.ts";
1173
1182
  export { type BranchPreparation, type BranchSummaryResult, type CollectEntriesResult, type CompactionResult, type CutPointResult, calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, type FileOperations, findCutPoint, findTurnStartIndex, type GenerateBranchSummaryOptions, generateBranchSummary, generateSummary, generateSummaryWithUsage, getLastAssistantUsage, prepareBranchEntries, serializeConversation, shouldCompact, } from "./core/compaction/index.ts";
1174
1183
  export { createEventBus, type EventBus, type EventBusController } from "./core/event-bus.ts";
1175
- export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextUsage, CustomToolCallEvent, EditToolCallEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, GrepToolCallEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, MarkdownTransformContext, MarkdownTransformer, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, PowerShellToolCallEvent, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, RegisteredCommand, RegisteredTool, ResolvedCommand, SessionBeforeCompactEvent, SessionBeforeForkEvent, SessionBeforeSwitchEvent, SessionBeforeTreeEvent, SessionCompactEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SlashCommandInfo, SlashCommandSource, SourceInfo, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, TurnEndEvent, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, } from "./core/extensions/index.ts";
1176
1184
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/index.js b/node_modules/@earendil-works/pi-coding-agent/dist/index.js
1177
1185
  index 01017cd..c2b8722 100644
1178
1186
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/index.js
@@ -1187,10 +1195,10 @@ index 01017cd..c2b8722 100644
1187
1195
  export { calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, findTurnStartIndex, generateBranchSummary, generateSummary, generateSummaryWithUsage, getLastAssistantUsage, prepareBranchEntries, serializeConversation, shouldCompact, } from "./core/compaction/index.js";
1188
1196
  export { createEventBus } from "./core/event-bus.js";
1189
1197
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/main.js b/node_modules/@earendil-works/pi-coding-agent/dist/main.js
1190
- index 940e0f4..c0f3d11 100644
1198
+ index be10307..636fbf4 100644
1191
1199
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/main.js
1192
1200
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/main.js
1193
- @@ -795,7 +795,58 @@ export async function main(args, options) {
1201
+ @@ -802,7 +802,58 @@ export async function main(args, options) {
1194
1202
  if (exitCode !== 0) {
1195
1203
  process.exitCode = exitCode;
1196
1204
  }
@@ -1251,7 +1259,7 @@ index 940e0f4..c0f3d11 100644
1251
1259
  //# sourceMappingURL=main.js.map
1252
1260
 
1253
1261
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js b/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js
1254
- index 072268a..472ee6d 100644
1262
+ index c30d6b8..ad1654a 100644
1255
1263
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js
1256
1264
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js
1257
1265
  @@ -4,7 +4,7 @@
@@ -1262,7 +1270,7 @@ index 072268a..472ee6d 100644
1262
1270
  +import { getAgentDir, getBinDir, projectConfigDirs } from "./config.js";
1263
1271
  import { migrateKeybindingsConfig } from "./core/keybindings.js";
1264
1272
  import { stripBom } from "./utils/text.js";
1265
- const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";
1273
+ const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";
1266
1274
  @@ -231,14 +231,17 @@ function checkDeprecatedExtensionDirs(baseDir, label) {
1267
1275
  */
1268
1276
  function migrateExtensionSystem(cwd) {
@@ -1330,7 +1338,7 @@ index 6f7ed6a..b0ed0ae 100644
1330
1338
  getResourcePattern(item) {
1331
1339
  const scope = item.metadata.scope;
1332
1340
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js
1333
- index 1d9f046..c0326e3 100644
1341
+ index 0e2811e..c0326e3 100644
1334
1342
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js
1335
1343
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js
1336
1344
  @@ -1,7 +1,6 @@
@@ -1400,7 +1408,7 @@ index 1d9f046..c0326e3 100644
1400
1408
  */
1401
1409
  export class FooterComponent {
1402
1410
  autoCompactEnabled = true;
1403
- @@ -74,29 +113,9 @@ export class FooterComponent {
1411
+ @@ -74,32 +113,9 @@ export class FooterComponent {
1404
1412
  }
1405
1413
  render(width) {
1406
1414
  const state = this.session.state;
@@ -1408,7 +1416,10 @@ index 1d9f046..c0326e3 100644
1408
1416
  - const usageTotals = createUsageTotals();
1409
1417
  - let latestCacheHitRate;
1410
1418
  - for (const entry of this.session.sessionManager.getEntries()) {
1411
- - if (entry.type === "message" && entry.message.role === "assistant") {
1419
+ - if (entry.type === "usage") {
1420
+ - addUsageToTotals(usageTotals, entry.usage);
1421
+ - }
1422
+ - else if (entry.type === "message" && entry.message.role === "assistant") {
1412
1423
  - addUsageToTotals(usageTotals, entry.message.usage);
1413
1424
  - const latestPromptTokens = entry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;
1414
1425
  - latestCacheHitRate =
@@ -1433,7 +1444,7 @@ index 1d9f046..c0326e3 100644
1433
1444
  // Replace home directory with ~
1434
1445
  let pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
1435
1446
  // Add git branch if available
1436
- @@ -109,56 +128,24 @@ export class FooterComponent {
1447
+ @@ -112,56 +128,24 @@ export class FooterComponent {
1437
1448
  if (sessionName) {
1438
1449
  pwd = `${pwd} • ${sessionName}`;
1439
1450
  }
@@ -1505,7 +1516,7 @@ index 1d9f046..c0326e3 100644
1505
1516
  const minPadding = 2;
1506
1517
  // Add thinking level indicator if model supports reasoning
1507
1518
  let rightSideWithoutProvider = modelName;
1508
- @@ -171,50 +158,48 @@ export class FooterComponent {
1519
+ @@ -174,50 +158,48 @@ export class FooterComponent {
1509
1520
  let rightSide = rightSideWithoutProvider;
1510
1521
  if (this.footerData.getAvailableProviderCount() > 1 && state.model) {
1511
1522
  rightSide = `(${state.model.provider}) ${rightSideWithoutProvider}`;
@@ -1575,13 +1586,12 @@ index 1d9f046..c0326e3 100644
1575
1586
  return lines;
1576
1587
  }
1577
1588
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
1578
- index c33fc39..060a38f 100644
1589
+ index 6d0356c..ca0aa98 100644
1579
1590
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
1580
1591
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
1581
- @@ -1,10 +1,37 @@
1582
- -import { Box, Container, getCapabilities, Image, Spacer, Text } from "@earendil-works/pi-tui";
1583
- +import { Box, Container, getCapabilities, Image, Spacer, visibleWidth, Text } from "@earendil-works/pi-tui";
1584
- import { createAllToolDefinitions } from "../../../core/tools/index.js";
1592
+ @@ -1,9 +1,36 @@
1593
+ -import { Box, Container, getCapabilities, Image, MouseRegion, Spacer, Text, } from "@earendil-works/pi-tui";
1594
+ +import { Box, Container, getCapabilities, Image, MouseRegion, Spacer, visibleWidth, Text, } from "@earendil-works/pi-tui";
1585
1595
  import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.js";
1586
1596
  import { convertToPng } from "../../../utils/image-convert.js";
1587
1597
  import { theme } from "../theme/theme.js";
@@ -1617,7 +1627,7 @@ index c33fc39..060a38f 100644
1617
1627
  export class ToolExecutionComponent extends Container {
1618
1628
  contentBox;
1619
1629
  contentText;
1620
- @@ -45,8 +72,13 @@ export class ToolExecutionComponent extends Container {
1630
+ @@ -44,8 +71,13 @@ export class ToolExecutionComponent extends Container {
1621
1631
  // Always create all shell variants. contentBox is used for default renderer-based composition.
1622
1632
  // selfRenderContainer is used when the tool renders its own framing.
1623
1633
  // contentText is reserved for generic fallback rendering when no tool definition exists.
@@ -1630,10 +1640,10 @@ index c33fc39..060a38f 100644
1630
1640
  + // matches what the self-render path above emits (a single lines.push("")).
1631
1641
  + this.contentBox = new Box(1, 0, (text) => theme.bg("toolPendingBg", text));
1632
1642
  + this.contentText = new Text("", 1, 0, (text) => theme.bg("toolPendingBg", text));
1643
+ this.contentTextRegion = this.createResultRegion(this.contentText);
1633
1644
  this.selfRenderContainer = new Container();
1634
1645
  if (this.hasRendererDefinition()) {
1635
- this.addChild(this.getRenderShell() === "self" ? this.selfRenderContainer : this.contentBox);
1636
- @@ -108,6 +140,13 @@ export class ToolExecutionComponent extends Container {
1646
+ @@ -90,6 +122,13 @@ export class ToolExecutionComponent extends Container {
1637
1647
  createCallFallback() {
1638
1648
  return new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0);
1639
1649
  }
@@ -1647,12 +1657,12 @@ index c33fc39..060a38f 100644
1647
1657
  createResultFallback() {
1648
1658
  const output = this.getTextOutput();
1649
1659
  if (!output) {
1650
- @@ -227,19 +266,22 @@ export class ToolExecutionComponent extends Container {
1660
+ @@ -237,19 +276,22 @@ export class ToolExecutionComponent extends Container {
1651
1661
  renderContainer.clear();
1652
1662
  const callRenderer = this.getCallRenderer();
1653
1663
  if (!callRenderer) {
1654
- - renderContainer.addChild(this.createCallFallback());
1655
- + renderContainer.addChild(this.decorateCall(this.createCallFallback()));
1664
+ - renderContainer.addChild(this.createResultRegion(this.createCallFallback()));
1665
+ + renderContainer.addChild(this.createResultRegion(this.decorateCall(this.createCallFallback())));
1656
1666
  hasContent = true;
1657
1667
  }
1658
1668
  else {
@@ -1662,18 +1672,18 @@ index c33fc39..060a38f 100644
1662
1672
  + // next render and the tool's renderer calls its own methods on it,
1663
1673
  + // so handing it a wrapper would break every stateful renderer.
1664
1674
  this.callRendererComponent = component;
1665
- - renderContainer.addChild(component);
1666
- + renderContainer.addChild(this.decorateCall(component));
1675
+ - renderContainer.addChild(this.createResultRegion(component));
1676
+ + renderContainer.addChild(this.createResultRegion(this.decorateCall(component)));
1667
1677
  hasContent = true;
1668
1678
  }
1669
1679
  catch {
1670
1680
  this.callRendererComponent = undefined;
1671
- - renderContainer.addChild(this.createCallFallback());
1672
- + renderContainer.addChild(this.decorateCall(this.createCallFallback()));
1681
+ - renderContainer.addChild(this.createResultRegion(this.createCallFallback()));
1682
+ + renderContainer.addChild(this.createResultRegion(this.decorateCall(this.createCallFallback())));
1673
1683
  hasContent = true;
1674
1684
  }
1675
1685
  }
1676
- @@ -311,7 +353,8 @@ export class ToolExecutionComponent extends Container {
1686
+ @@ -322,7 +364,8 @@ export class ToolExecutionComponent extends Container {
1677
1687
  return getRenderedTextOutput(this.result, this.showImages);
1678
1688
  }
1679
1689
  formatToolExecution() {
@@ -1684,11 +1694,11 @@ index c33fc39..060a38f 100644
1684
1694
  if (content) {
1685
1695
  text += `\n\n${content}`;
1686
1696
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
1687
- index a27754f..3403ca8 100644
1697
+ index d82182c..41426bc 100644
1688
1698
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
1689
1699
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
1690
1700
  @@ -10,7 +10,7 @@ import * as TuiLayouts from "@earendil-works/pi-tui";
1691
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setCapabilityOverrides, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
1701
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, Spacer, setCapabilityOverrides, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
1692
1702
  import chalk from "chalk";
1693
1703
  import { spawn } from "child_process";
1694
1704
  -import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, VERSION, } from "../../config.js";
@@ -1696,7 +1706,7 @@ index a27754f..3403ca8 100644
1696
1706
  import { parseSkillBlock } from "../../core/agent-session.js";
1697
1707
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
1698
1708
  import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
1699
- @@ -126,7 +126,12 @@ export function formatResumeCommand(sessionManager) {
1709
+ @@ -141,7 +141,12 @@ export function formatResumeCommand(sessionManager) {
1700
1710
  const sessionFile = sessionManager.getSessionFile();
1701
1711
  if (!sessionFile || !fs.existsSync(sessionFile))
1702
1712
  return undefined;
@@ -1710,9 +1720,9 @@ index a27754f..3403ca8 100644
1710
1720
  if (!sessionManager.usesDefaultSessionDir()) {
1711
1721
  args.push("--session-dir", quoteIfNeeded(sessionManager.getSessionDir()));
1712
1722
  }
1713
- @@ -232,6 +237,64 @@ export function createInteractiveTuiReference(getTui) {
1714
- getPrototypeOf: () => Reflect.getPrototypeOf(getTui()),
1715
- });
1723
+ @@ -192,6 +197,64 @@ function formatLoginProviderCompletionDescription(provider) {
1724
+ const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
1725
+ return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
1716
1726
  }
1717
1727
  +// Privateer patch: describe an error we only have the TEXT of. Mirrors
1718
1728
  +// describeErrorText in src/engine/errors.ts — see the incident note there.
@@ -1775,7 +1785,7 @@ index a27754f..3403ca8 100644
1775
1785
  export class InteractiveMode {
1776
1786
  runtimeHost;
1777
1787
  renderer;
1778
- @@ -439,9 +502,15 @@ export class InteractiveMode {
1788
+ @@ -404,9 +467,15 @@ export class InteractiveMode {
1779
1789
  }
1780
1790
  getBuiltInCommandConflictDiagnostics(extensionRunner) {
1781
1791
  const builtinNames = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
@@ -1792,7 +1802,7 @@ index a27754f..3403ca8 100644
1792
1802
  .map((command) => ({
1793
1803
  type: "warning",
1794
1804
  message: command.invocationName === command.name
1795
- @@ -815,20 +884,18 @@ export class InteractiveMode {
1805
+ @@ -775,20 +844,18 @@ export class InteractiveMode {
1796
1806
  this.showNewVersionNotification(newRelease);
1797
1807
  }
1798
1808
  });
@@ -1825,7 +1835,7 @@ index a27754f..3403ca8 100644
1825
1835
  // Check tmux keyboard setup asynchronously
1826
1836
  this.checkTmuxKeyboardSetup().then((warning) => {
1827
1837
  if (warning) {
1828
- @@ -2393,7 +2460,17 @@ export class InteractiveMode {
1838
+ @@ -2419,7 +2486,17 @@ export class InteractiveMode {
1829
1839
  if (text === "/model" || text.startsWith("/model ")) {
1830
1840
  const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
1831
1841
  this.editor.setText("");
@@ -1844,7 +1854,7 @@ index a27754f..3403ca8 100644
1844
1854
  return;
1845
1855
  }
1846
1856
  if (text === "/thinking" || text.startsWith("/thinking ")) {
1847
- @@ -2465,12 +2542,42 @@ export class InteractiveMode {
1857
+ @@ -2497,12 +2574,42 @@ export class InteractiveMode {
1848
1858
  if (text === "/login" || text.startsWith("/login ")) {
1849
1859
  const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
1850
1860
  this.editor.setText("");
@@ -1889,7 +1899,7 @@ index a27754f..3403ca8 100644
1889
1899
  return;
1890
1900
  }
1891
1901
  if (text === "/new") {
1892
- @@ -2788,9 +2895,19 @@ export class InteractiveMode {
1902
+ @@ -2827,9 +2934,19 @@ export class InteractiveMode {
1893
1903
  if (entries[0]?.type !== "compaction") {
1894
1904
  throw new Error("Completed compaction is missing from the session context");
1895
1905
  }
@@ -1912,7 +1922,7 @@ index a27754f..3403ca8 100644
1912
1922
  this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
1913
1923
  if (event.result.usage) {
1914
1924
  this.addCompactionCostNotice({
1915
- @@ -3164,7 +3281,7 @@ export class InteractiveMode {
1925
+ @@ -3253,7 +3370,7 @@ export class InteractiveMode {
1916
1926
  if (this.chatContainer.children.length > 0) {
1917
1927
  this.chatContainer.addChild(new Spacer(1));
1918
1928
  }
@@ -1921,7 +1931,7 @@ index a27754f..3403ca8 100644
1921
1931
  }
1922
1932
  async getUserInput() {
1923
1933
  const queuedInput = this.pendingUserInputs.shift();
1924
- @@ -3504,7 +3621,17 @@ export class InteractiveMode {
1934
+ @@ -3597,7 +3714,17 @@ export class InteractiveMode {
1925
1935
  }
1926
1936
  showError(errorMessage) {
1927
1937
  this.chatContainer.addChild(new Spacer(1));
@@ -1941,10 +1951,10 @@ index a27754f..3403ca8 100644
1941
1951
  }
1942
1952
  showWarning(warningMessage) {
1943
1953
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
1944
- index 01d1e02..c591e1d 100644
1954
+ index 3f4c698..e0152d0 100644
1945
1955
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
1946
1956
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
1947
- @@ -41,9 +41,9 @@
1957
+ @@ -42,9 +42,9 @@
1948
1958
  "customMessageBg": "customMsgBg",
1949
1959
  "customMessageText": "text",
1950
1960
  "customMessageLabel": "#9575cd",
@@ -1958,10 +1968,10 @@ index 01d1e02..c591e1d 100644
1958
1968
  "toolOutput": "gray",
1959
1969
 
1960
1970
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
1961
- index 0fde42b..cc3b7c5 100644
1971
+ index 20f6783..49fc151 100644
1962
1972
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
1963
1973
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
1964
- @@ -40,9 +40,9 @@
1974
+ @@ -41,9 +41,9 @@
1965
1975
  "customMessageBg": "customMsgBg",
1966
1976
  "customMessageText": "text",
1967
1977
  "customMessageLabel": "#7e57c2",
package/src/cli/chat.ts CHANGED
@@ -53,8 +53,14 @@ async function main() {
53
53
  const { modelRegistryOf } = await import("../providers/piAuthStore.ts");
54
54
  const { agentVersion } = await import("../config/version.ts");
55
55
  const { pickerCatalog, hiddenAccountNotice, hiddenAccountTitleSuffix } = await import("../providers/modelCatalog.ts");
56
- const { resolveDefaultModel, resolveSignedInModel, savedPiDefaultSpec, writePiDefaultModel } =
57
- await import("../providers/defaultModel.ts");
56
+ const {
57
+ resolveDefaultModel,
58
+ resolveSignedInModel,
59
+ savedPiDefaultSpec,
60
+ writePiDefaultModel,
61
+ visionWarningAcknowledged,
62
+ acknowledgeVisionWarning,
63
+ } = await import("../providers/defaultModel.ts");
58
64
  const { acceptsImages } = await import("../providers/vision.ts");
59
65
  const { postOutbox } = await import("../outbox/cloudOutbox.ts");
60
66
  const { addPendingCloud } = await import("../routines/store.ts");
@@ -440,8 +446,11 @@ async function main() {
440
446
  // blocks for a model that doesn't declare the modality with NO error — `read` on a
441
447
  // screenshot just quietly answers about nothing — so make that failure visible
442
448
  // instead of letting a user discover it turn by turn. /model to switch.
443
- if (!acceptsImages(spec)) {
449
+ // Shown once per distinct non-vision spec (visionWarningAcknowledged), not on every
450
+ // launch — a user who keeps a deliberate non-vision pick already knows the tradeoff.
451
+ if (!acceptsImages(spec) && !visionWarningAcknowledged(spec)) {
444
452
  console.log(`${YELLOW}⚠ ${provider}/${modelId} can't see images — @file/read on a picture or video frame will be dropped silently. Run /model to switch.${RESET}`);
453
+ acknowledgeVisionWarning(spec);
445
454
  }
446
455
  if (noQuarterActive()) applyNoQuarter(true); // launched with --no-quarter: say so up front
447
456
 
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Mirrors the patched clampMaxTokensToContext in pi-ai (api/simple-options.js) —
4
4
  // that copy is what actually runs; this one is where the behaviour is specified and
5
- // tested. See patches/@earendil-works+pi-ai+0.84.4.patch.
5
+ // tested. See patches/@earendil-works+pi-ai+*.patch.
6
6
  //
7
7
  // ── The bug this exists to fix ───────────────────────────────────────────────
8
8
  //
@@ -227,6 +227,43 @@ export function resolveSignedInModel(env: NodeJS.ProcessEnv = process.env): stri
227
227
  return resolveDefaultModel({ env, signedIn: true, saved: null });
228
228
  }
229
229
 
230
+ // Whether the startup "can't see images" nag (cli/chat.ts) has already been shown for
231
+ // this exact spec. Without this, a user who deliberately keeps a non-vision saved pick
232
+ // (speed over sight, say) sees the same warning on every single launch forever, which
233
+ // trains people to stop reading warnings at all. Keyed by spec so switching to a
234
+ // DIFFERENT non-vision model warns again once — this tracks "have they seen this
235
+ // specific tradeoff", not "should we ever mention it again".
236
+ export function visionWarningAcknowledged(spec: string): boolean {
237
+ try {
238
+ const raw = readFileSync(join(agentDir(), "settings.json"), "utf8").trim();
239
+ if (!raw) return false;
240
+ const s = JSON.parse(raw) as Record<string, unknown>;
241
+ return s.visionWarningAcknowledgedFor === spec;
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+
247
+ // Record that the nag has been shown for `spec`, so the next launch on the same pick
248
+ // stays quiet. Best-effort and silent like the writers below — losing this write just
249
+ // means the warning repeats once more, never a crash.
250
+ export function acknowledgeVisionWarning(spec: string): void {
251
+ const dir = agentDir();
252
+ const settingsPath = join(dir, "settings.json");
253
+ try {
254
+ mkdirSync(dir, { recursive: true });
255
+ let settings: Record<string, unknown> = {};
256
+ if (existsSync(settingsPath)) {
257
+ const raw = readFileSync(settingsPath, "utf8").trim();
258
+ if (raw) settings = JSON.parse(raw) as Record<string, unknown>;
259
+ }
260
+ settings.visionWarningAcknowledgedFor = spec;
261
+ writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
262
+ } catch {
263
+ // best-effort: the warning already printed either way.
264
+ }
265
+ }
266
+
230
267
  // Split a "provider/id" spec on its first slash (model ids themselves contain "/", so
231
268
  // only the first delimiter separates provider from model). Returns null for a spec
232
269
  // with no provider prefix.
@@ -738,6 +738,10 @@ interface AudioResponse {
738
738
  audioBase64?: string;
739
739
  mimeType?: string;
740
740
  model?: string;
741
+ /** The exact wire voice the provider actually received (post-resolution —
742
+ * e.g. an aura-2 override of 'jupiter' comes back as 'aura-2-jupiter-en').
743
+ * Absent only on models that take no voice at all. */
744
+ voice?: string;
741
745
  /** Present only for the models that take a length — an sfx model always does. */
742
746
  durationSeconds?: number;
743
747
  }
@@ -749,11 +753,21 @@ export const generateSpeechToolDefinition = {
749
753
  "Turn text into spoken audio and save it to disk. Use it to narrate a video you are assembling, or " +
750
754
  "to produce a spoken version of a written answer. Billed to the user's Privateer account; the " +
751
755
  "account's default voice model is a confidential-compute one, so the text is processed inside an " +
752
- "enclave rather than by a retaining provider. Mux the result onto video with video_compose.",
756
+ "enclave rather than by a retaining provider. Mux the result onto video with video_compose. " +
757
+ "Call media_capabilities first if you want to override `voice` or `model` — it lists the exact " +
758
+ "wire voice ids for the account's current TTS model (and any model you pass it), which is not " +
759
+ "the same as a spoken character or brand name.",
753
760
  parameters: Type.Object({
754
761
  text: Type.String({ description: "The words to speak. Write them as they should be read aloud." }),
755
762
  path: Type.String({ description: "Where to write the audio, relative to cwd or absolute (e.g. 'audio/narration.mp3')." }),
756
- voice: Type.Optional(Type.String({ description: "Voice name, if the account's TTS model offers a choice. Leave unset for its default." })),
763
+ voice: Type.Optional(Type.String({
764
+ description:
765
+ "Voice id, if the account's TTS model offers a choice. Leave unset for its default. These are " +
766
+ "PROVIDER wire ids, not free text — e.g. Deepgram Aura-2 voices are 'aura-2-<name>-<lang>' " +
767
+ "('aura-2-jupiter-en', not 'jupiter' or 'Jupiter'). Get the exact legal list for the model in " +
768
+ "play from media_capabilities' `speech.voices` before guessing one; an unrecognised id is " +
769
+ "refused with VOICE_UNSUPPORTED rather than silently served on a different voice.",
770
+ })),
757
771
  model: Type.Optional(Type.String({ description: "Override the account's text-to-speech model." })),
758
772
  }),
759
773
  async execute(
@@ -783,7 +797,13 @@ export const generateSpeechToolDefinition = {
783
797
  const target = abs(cwd, params.path);
784
798
  const ext = extname(target) || extForMime(r.data.mimeType ?? "", ".mp3");
785
799
  const out = `${target.slice(0, target.length - extname(target).length)}${ext}`;
786
- return text(`Generated speech: ${writeOut(out, Buffer.from(r.data.audioBase64, "base64"))}`);
800
+ // Report what actually ran, not what was requested — the two can differ
801
+ // (a resolved default, a normalized voice id) and the caller has no other
802
+ // way to find out short of listening to the file.
803
+ const via = [r.data.model, r.data.voice].filter(Boolean).join(" / ");
804
+ return text(
805
+ `Generated speech${via ? ` with ${via}` : ""}: ${writeOut(out, Buffer.from(r.data.audioBase64, "base64"))}`,
806
+ );
787
807
  },
788
808
  };
789
809
 
@@ -951,6 +971,20 @@ interface CapabilitiesResponse {
951
971
  * the account's own privacy setting. They are different refusals with different
952
972
  * remedies, and only one of them is the user's to fix. */
953
973
  sfx?: { model?: string; configured?: boolean; blockedByZdr?: boolean; maxDurationSeconds?: number };
974
+ /** `voices` (on the described model) is populated only for models with an
975
+ * enumerable, closed voice set (Deepgram Aura-2, Tinfoil, fal) — pass
976
+ * `ttsModel` to describe one other than the account default. Empty for
977
+ * models whose voices aren't data this server holds (Gemini, OpenAI-shaped);
978
+ * guessing one there is on you. `catalog` is EVERY TTS model this account
979
+ * can reach, summarized — the only place other than the default to
980
+ * discover an id, since there is no TTS picker in this CLI. */
981
+ speech?: {
982
+ model?: string; blockedByZdr?: boolean; voices?: string[];
983
+ catalog?: {
984
+ id: string; name?: string; provider?: string;
985
+ isZdr?: boolean; isTee?: boolean; blockedByZdr?: boolean; voiceCount?: number;
986
+ }[];
987
+ };
954
988
  privacy?: { requireZdr?: boolean; allowNonZdrMedia?: boolean };
955
989
  }
956
990
 
@@ -1060,6 +1094,44 @@ export function describeSfx(sfx: CapabilitiesResponse["sfx"]): string[] {
1060
1094
  ];
1061
1095
  }
1062
1096
 
1097
+ /**
1098
+ * Speech, worded the same way describeSfx is: a [BLOCKED] model is the
1099
+ * account's own ZDR setting, not something retrying fixes. `voices` prints
1100
+ * only when the model publishes an enumerable set — printing all 90 of
1101
+ * Aura-2's inline is the whole point (a bare character name is not a legal
1102
+ * id on the wire), but a model with none gets no fabricated list either.
1103
+ */
1104
+ export function describeSpeech(speech: CapabilitiesResponse["speech"]): string[] {
1105
+ const blocked = speech?.blockedByZdr ? " [BLOCKED by this account's ZDR setting]" : "";
1106
+ const lines = [`Speech model: ${speech?.model ?? "unknown"}${blocked}`];
1107
+ if (speech?.blockedByZdr) {
1108
+ lines.push(
1109
+ " This voice model is non-ZDR, so this account cannot use it until its owner enables non-ZDR " +
1110
+ "media (Settings → Privacy). Omit `model` to use the account's confidential-compute default " +
1111
+ "instead of retrying this one.",
1112
+ );
1113
+ }
1114
+ if (speech?.voices?.length) {
1115
+ lines.push(
1116
+ ` ${speech.voices.length} voice id(s) for this model — pass one of these EXACTLY as generate_speech's ` +
1117
+ `\`voice\`, never a guessed name: ${speech.voices.join(", ")}`,
1118
+ );
1119
+ } else {
1120
+ lines.push(" No enumerable voice list for this model; omit `voice` to use its default.");
1121
+ }
1122
+ if (speech?.catalog?.length) {
1123
+ lines.push(" every TTS model available (pass `ttsModel` to this tool to see its voices):");
1124
+ for (const m of speech.catalog) {
1125
+ const tags = [m.isTee ? "confidential" : m.isZdr ? "ZDR" : "non-ZDR", `${m.voiceCount ?? 0} voice(s)`];
1126
+ if (m.blockedByZdr) tags.push("BLOCKED");
1127
+ lines.push(
1128
+ ` ${m.id} (${tags.join(", ")})${m.id === speech.model ? " [described above]" : ""}`,
1129
+ );
1130
+ }
1131
+ }
1132
+ return lines;
1133
+ }
1134
+
1063
1135
  export const mediaCapabilitiesToolDefinition = {
1064
1136
  name: "media_capabilities",
1065
1137
  label: "Media Capabilities",
@@ -1072,7 +1144,10 @@ export const mediaCapabilitiesToolDefinition = {
1072
1144
  "It is also the ONLY way to find out which 3D models exist and what options each one takes: they " +
1073
1145
  "range from $0.14 to $2.41 a mesh and no two take the same options, so call this with `model` set " +
1074
1146
  "to the id you are considering BEFORE generate_model, or you will pay the default model's price " +
1075
- "for a job a cheaper one could have done.",
1147
+ "for a job a cheaper one could have done.\n" +
1148
+ "It is also the ONLY way to find a text-to-speech model's real voice ids — Deepgram Aura-2's are " +
1149
+ "'aura-2-<name>-<lang>', not a bare character name — so call this with `ttsModel` set BEFORE " +
1150
+ "generate_speech whenever you plan to pass `voice`.",
1076
1151
  parameters: Type.Object({
1077
1152
  model: Type.Optional(
1078
1153
  Type.String({
@@ -1082,13 +1157,27 @@ export const mediaCapabilitiesToolDefinition = {
1082
1157
  "their legal values and the price — is per-model.",
1083
1158
  }),
1084
1159
  ),
1160
+ ttsModel: Type.Optional(
1161
+ Type.String({
1162
+ description:
1163
+ "A text-to-speech model id to describe instead of the account default (e.g. 'deepgram/aura-2'). " +
1164
+ "The response's `speech.voices` lists every legal voice id for THIS model — voice ids are not " +
1165
+ "shared across models.",
1166
+ }),
1167
+ ),
1085
1168
  }),
1086
- async execute(_toolCallId: string, params: { model?: string }, signal?: AbortSignal) {
1087
- const query = params?.model ? `?model=${encodeURIComponent(params.model)}` : "";
1088
- const r = await callAccount<CapabilitiesResponse>(`/api/agent/media/capabilities${query}`, { method: "GET", signal });
1169
+ async execute(_toolCallId: string, params: { model?: string; ttsModel?: string }, signal?: AbortSignal) {
1170
+ const query = new URLSearchParams();
1171
+ if (params?.model) query.set("model", params.model);
1172
+ if (params?.ttsModel) query.set("ttsModel", params.ttsModel);
1173
+ const qs = query.toString();
1174
+ const r = await callAccount<CapabilitiesResponse>(
1175
+ `/api/agent/media/capabilities${qs ? `?${qs}` : ""}`,
1176
+ { method: "GET", signal },
1177
+ );
1089
1178
  if (!r.ok) return text(`Could not read media capabilities: ${r.message}`);
1090
1179
 
1091
- const { image, video, model3d, sfx, privacy } = r.data;
1180
+ const { image, video, model3d, sfx, speech, privacy } = r.data;
1092
1181
  const lines = [
1093
1182
  `Image model: ${image?.model ?? "unknown"}${image?.blockedByZdr ? " [BLOCKED by this account's ZDR setting]" : ""}`,
1094
1183
  ` up to ${image?.maxPerCall ?? 1} image(s) per call`,
@@ -1099,18 +1188,19 @@ export const mediaCapabilitiesToolDefinition = {
1099
1188
 
1100
1189
  lines.push(...describeModel3d(model3d));
1101
1190
  lines.push(...describeSfx(sfx));
1191
+ lines.push(...describeSpeech(speech));
1102
1192
 
1103
1193
  lines.push(`Privacy: requireZdr=${privacy?.requireZdr ?? "?"}, allowNonZdrMedia=${privacy?.allowNonZdrMedia ?? "?"}`);
1104
- if (image?.blockedByZdr || video?.blockedByZdr || model3d?.blockedByZdr || sfx?.blockedByZdr) {
1194
+ if (image?.blockedByZdr || video?.blockedByZdr || model3d?.blockedByZdr || sfx?.blockedByZdr || speech?.blockedByZdr) {
1105
1195
  lines.push(
1106
1196
  "A [BLOCKED] model means the account requires Zero Data Retention and that model has no ZDR endpoint. " +
1107
1197
  "Only the account owner can change it (Settings → Privacy); do not keep retrying.",
1108
1198
  );
1109
1199
  }
1110
1200
  lines.push(
1111
- "Speech and music are always available; sound effects are not (see above). Speech runs confidentially, " +
1112
- "music has no ZDR gate at all, and effects are gated like image and video — so a blocked effect must " +
1113
- "never be answered with music.",
1201
+ "Music is always available; sound effects and (per above) speech's CURRENT model may not be. Music has " +
1202
+ "no ZDR gate at all; effects are gated like image and video, so a blocked effect must never be " +
1203
+ "answered with music instead.",
1114
1204
  );
1115
1205
  return text(lines.join("\n"));
1116
1206
  },