@wrongstack/core 0.305.0 → 0.306.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/chronicle/project-server.js +18 -48
  2. package/dist/coordination/agents/index.js +3 -2
  3. package/dist/coordination/agents/types.d.ts +1 -1
  4. package/dist/coordination/index.d.ts +1 -0
  5. package/dist/coordination/index.js +5 -2
  6. package/dist/coordination/mailbox-project-server.js +28 -57
  7. package/dist/core/index.d.ts +2 -1
  8. package/dist/core/index.js +2764 -2632
  9. package/dist/core/system-prompt-blocks.d.ts +1 -1
  10. package/dist/core/system-prompt-builder.d.ts +7 -1
  11. package/dist/core/system-prompt-glossary.d.ts +0 -23
  12. package/dist/defaults/index.js +3 -2
  13. package/dist/execution/index.js +3 -2
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +898 -531
  16. package/dist/observability/index.js +7 -3
  17. package/dist/plugin/index.d.ts +4 -3
  18. package/dist/plugin/index.js +589 -143
  19. package/dist/plugins/auto-review-plugin.d.ts +14 -7
  20. package/dist/plugins/chimera-plugin.d.ts +15 -1
  21. package/dist/plugins/review-finding-integration.d.ts +15 -3
  22. package/dist/plugins/review-finding-parser.d.ts +36 -0
  23. package/dist/plugins/review-finding-types.d.ts +46 -0
  24. package/dist/plugins/review-finding-verification.d.ts +53 -0
  25. package/dist/plugins/review-report-integration.d.ts +1 -0
  26. package/dist/plugins/review-report-store.d.ts +7 -0
  27. package/dist/plugins/review-report-types.d.ts +14 -0
  28. package/dist/plugins/review-types.d.ts +74 -0
  29. package/dist/replay/replay-provider-runner.d.ts +5 -4
  30. package/dist/session-catalog/project-server.js +36 -65
  31. package/dist/tools/fallback-manage-tool-options.d.ts +9 -0
  32. package/dist/tools/index.js +91 -38
  33. package/dist/tools/one-shot-llm-tool.d.ts +6 -0
  34. package/dist/types/blocks.d.ts +10 -0
  35. package/dist/types/config/ui.d.ts +7 -4
  36. package/dist/types/index.js +21 -1
  37. package/dist/utils/index.js +8 -9
  38. package/instructions/agents/browser.md +1 -0
  39. package/instructions/agents/e2e.md +2 -0
  40. package/instructions/llm/chimera-review.md +52 -1
  41. package/instructions/system-lite.md +17 -6
  42. package/instructions/system-pro.md +25 -20
  43. package/instructions/system.md +25 -12
  44. package/package.json +3 -3
@@ -2,10 +2,10 @@
2
2
 
3
3
  // src/session-catalog/project-server.ts
4
4
  import { randomBytes as randomBytes2, randomUUID as randomUUID2 } from "node:crypto";
5
- import * as fs3 from "node:fs";
6
5
  import * as fsp from "node:fs/promises";
7
6
  import * as net from "node:net";
8
7
  import * as path3 from "node:path";
8
+ import { bindProjectEndpoint } from "@wrongstack/persistence";
9
9
 
10
10
  // src/security/file-permissions.ts
11
11
  import { chmod } from "node:fs/promises";
@@ -177,17 +177,9 @@ function useDaemonPerfDefaults() {
177
177
 
178
178
  // src/session-catalog/endpoint.ts
179
179
  import { createHash } from "node:crypto";
180
- import * as fs from "node:fs";
181
180
  import * as os from "node:os";
182
181
  import * as path from "node:path";
183
182
 
184
- // src/utils/socket-path.ts
185
- import {
186
- assertUnixSocketPathWithinLimit,
187
- checkUnixSocketPath,
188
- unixSocketPathLimit
189
- } from "@wrongstack/persistence";
190
-
191
183
  // src/session-catalog/protocol.ts
192
184
  var SESSION_CATALOG_PROTOCOL_VERSION = 1;
193
185
  var SESSION_CATALOG_MAX_FRAME_CHARS = 4 * 1024 * 1024;
@@ -218,15 +210,10 @@ function sessionCatalogProjectServerEndpoint(projectDir) {
218
210
  function sessionCatalogProjectServerMetadataPath(projectDir) {
219
211
  return path.join(projectDir, SESSION_CATALOG_METADATA_FILE);
220
212
  }
221
- function ensureSessionCatalogSocketDirectory(endpoint2) {
222
- if (process.platform === "win32") return;
223
- assertUnixSocketPathWithinLimit(endpoint2, "session-catalog");
224
- fs.mkdirSync(path.dirname(endpoint2), { recursive: true, mode: 448 });
225
- }
226
213
 
227
214
  // src/session-catalog/store.ts
228
215
  import { createHash as createHash2, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
229
- import * as fs2 from "node:fs";
216
+ import * as fs from "node:fs";
230
217
  import * as path2 from "node:path";
231
218
 
232
219
  // src/coordination/sqlite-mailbox-schema.ts
@@ -632,7 +619,7 @@ var SessionCatalogStore = class {
632
619
  constructor(projectDir) {
633
620
  this.projectDir = projectDir;
634
621
  this.sessionsDir = path2.join(projectDir, "sessions");
635
- fs2.mkdirSync(this.sessionsDir, { recursive: true, mode: 448 });
622
+ fs.mkdirSync(this.sessionsDir, { recursive: true, mode: 448 });
636
623
  this.databasePath = path2.join(this.sessionsDir, "catalog.sqlite");
637
624
  const Database = loadDatabaseSync();
638
625
  this.db = new Database(this.databasePath);
@@ -650,12 +637,12 @@ var SessionCatalogStore = class {
650
637
  this.db.close();
651
638
  const quarantine = `${this.databasePath}.corrupt-${Date.now()}`;
652
639
  try {
653
- fs2.renameSync(this.databasePath, quarantine);
640
+ fs.renameSync(this.databasePath, quarantine);
654
641
  } catch {
655
642
  }
656
643
  for (const suffix of ["-wal", "-shm"]) {
657
644
  try {
658
- fs2.renameSync(`${this.databasePath}${suffix}`, `${quarantine}${suffix}`);
645
+ fs.renameSync(`${this.databasePath}${suffix}`, `${quarantine}${suffix}`);
659
646
  } catch {
660
647
  }
661
648
  }
@@ -881,7 +868,7 @@ var SessionCatalogStore = class {
881
868
  if (this.maintenanceExists(targetSessionId))
882
869
  throw conflict(`Session ${targetSessionId} is under maintenance`);
883
870
  const catalog = this.db.prepare("SELECT 1 AS yes FROM sessions WHERE session_id=?").get(targetSessionId);
884
- if (!catalog && !fs2.existsSync(this.containedPath(`${targetSessionId}.jsonl`)))
871
+ if (!catalog && !fs.existsSync(this.containedPath(`${targetSessionId}.jsonl`)))
885
872
  throw new Error(`Session not found: ${targetSessionId}`);
886
873
  const reservationId = randomUUID();
887
874
  const now = Date.now();
@@ -1027,7 +1014,7 @@ var SessionCatalogStore = class {
1027
1014
  transcriptRelativePath = normalizedTranscript;
1028
1015
  summaryRelativePath = normalizedSummary;
1029
1016
  const transcript = this.containedPath(transcriptRelativePath);
1030
- const stat = fs2.existsSync(transcript) ? fs2.statSync(transcript) : void 0;
1017
+ const stat = fs.existsSync(transcript) ? fs.statSync(transcript) : void 0;
1031
1018
  const now = (/* @__PURE__ */ new Date()).toISOString();
1032
1019
  return this.transaction(() => {
1033
1020
  const prior = this.db.prepare("SELECT summary_revision FROM sessions WHERE session_id=?").get(summary.id);
@@ -1150,7 +1137,7 @@ var SessionCatalogStore = class {
1150
1137
  if (trimmed) summary.name = this.scrubber.scrub(trimmed).slice(0, 500);
1151
1138
  else delete summary.name;
1152
1139
  const summaryPath = this.containedPath(current.summaryRelativePath);
1153
- fs2.mkdirSync(path2.dirname(summaryPath), { recursive: true, mode: 448 });
1140
+ fs.mkdirSync(path2.dirname(summaryPath), { recursive: true, mode: 448 });
1154
1141
  await atomicWrite(summaryPath, `${JSON.stringify(summary)}
1155
1142
  `, { mode: 384 });
1156
1143
  try {
@@ -1213,13 +1200,13 @@ var SessionCatalogStore = class {
1213
1200
  path2.join(path2.dirname(transcript), path2.basename(sessionId))
1214
1201
  ];
1215
1202
  const trashRoot = path2.join(this.sessionsDir, "_trash", lease.leaseId);
1216
- fs2.mkdirSync(trashRoot, { recursive: true, mode: 448 });
1203
+ fs.mkdirSync(trashRoot, { recursive: true, mode: 448 });
1217
1204
  const moved = [];
1218
1205
  try {
1219
1206
  artifacts.forEach((artifact, index) => {
1220
- if (!fs2.existsSync(artifact)) return;
1207
+ if (!fs.existsSync(artifact)) return;
1221
1208
  const target = path2.join(trashRoot, `${index}-${path2.basename(artifact)}`);
1222
- fs2.renameSync(artifact, target);
1209
+ fs.renameSync(artifact, target);
1223
1210
  moved.push({ from: artifact, to: target });
1224
1211
  });
1225
1212
  this.transaction(() => {
@@ -1234,17 +1221,17 @@ var SessionCatalogStore = class {
1234
1221
  } catch (error) {
1235
1222
  for (const item of moved.reverse()) {
1236
1223
  try {
1237
- fs2.mkdirSync(path2.dirname(item.from), { recursive: true, mode: 448 });
1238
- fs2.renameSync(item.to, item.from);
1224
+ fs.mkdirSync(path2.dirname(item.from), { recursive: true, mode: 448 });
1225
+ fs.renameSync(item.to, item.from);
1239
1226
  } catch {
1240
1227
  }
1241
1228
  }
1242
1229
  throw error;
1243
1230
  }
1244
1231
  try {
1245
- fs2.rmSync(trashRoot, { recursive: true, force: true });
1232
+ fs.rmSync(trashRoot, { recursive: true, force: true });
1246
1233
  const trashParent = path2.dirname(trashRoot);
1247
- if (fs2.readdirSync(trashParent).length === 0) fs2.rmdirSync(trashParent);
1234
+ if (fs.readdirSync(trashParent).length === 0) fs.rmdirSync(trashParent);
1248
1235
  } catch {
1249
1236
  }
1250
1237
  }
@@ -1281,10 +1268,10 @@ var SessionCatalogStore = class {
1281
1268
  for (const id of ids) {
1282
1269
  try {
1283
1270
  const summaryFile = this.containedPath(`${id}.summary.json`);
1284
- const summary = fs2.existsSync(summaryFile) ? parseJson(fs2.readFileSync(summaryFile, "utf8")) : this.summarizeTranscript(id);
1271
+ const summary = fs.existsSync(summaryFile) ? parseJson(fs.readFileSync(summaryFile, "utf8")) : this.summarizeTranscript(id);
1285
1272
  if (!summary || summary.id !== id) throw new Error("summary identity mismatch");
1286
1273
  const transcript = this.containedPath(`${id}.jsonl`);
1287
- const stat = fs2.existsSync(transcript) ? fs2.statSync(transcript) : void 0;
1274
+ const stat = fs.existsSync(transcript) ? fs.statSync(transcript) : void 0;
1288
1275
  const now = (/* @__PURE__ */ new Date()).toISOString();
1289
1276
  this.db.prepare(
1290
1277
  "INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,transcript_size,transcript_mtime_ms,summary_revision,indexed_at,damaged) VALUES (?,?,?,?,?,?,?,?,0)"
@@ -1325,7 +1312,7 @@ var SessionCatalogStore = class {
1325
1312
  walkFiles(root, suffix) {
1326
1313
  const result = [];
1327
1314
  const visit = (dir) => {
1328
- for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
1315
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1329
1316
  if (entry.isDirectory()) {
1330
1317
  if (entry.name !== "_cas" && entry.name !== "_trash") visit(path2.join(dir, entry.name));
1331
1318
  } else if (entry.isFile() && entry.name.endsWith(suffix))
@@ -1337,7 +1324,7 @@ var SessionCatalogStore = class {
1337
1324
  }
1338
1325
  summarizeTranscript(id) {
1339
1326
  const file = this.containedPath(`${id}.jsonl`);
1340
- const lines = fs2.readFileSync(file, "utf8").split(/\r?\n/);
1327
+ const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
1341
1328
  let start;
1342
1329
  let endedAt;
1343
1330
  let lastActivityAt;
@@ -1846,7 +1833,6 @@ async function stop(_reason) {
1846
1833
  if (process.platform !== "win32") await fsp.rm(endpoint, { force: true }).catch(() => void 0);
1847
1834
  await removeOwnedMetadata();
1848
1835
  }
1849
- ensureSessionCatalogSocketDirectory(endpoint);
1850
1836
  var server = net.createServer((socket) => {
1851
1837
  if (stopping) {
1852
1838
  socket.destroy();
@@ -1870,42 +1856,28 @@ var server = net.createServer((socket) => {
1870
1856
  scheduleIdleStop(disconnectedIdleMs);
1871
1857
  });
1872
1858
  });
1873
- var probing = false;
1874
- function listen() {
1875
- server.listen(endpoint);
1876
- }
1877
- server.on("error", (error) => {
1878
- if (error.code === "EADDRINUSE" && process.platform === "win32") {
1859
+ void (async () => {
1860
+ const bind = await bindProjectEndpoint({ server, endpoint, service: "session-catalog" });
1861
+ if (bind.outcome === "already-owned") {
1879
1862
  process.exitCode = 0;
1880
1863
  return;
1881
1864
  }
1882
- if (error.code === "EADDRINUSE" && !probing) {
1883
- probing = true;
1884
- const probe = net.createConnection(endpoint);
1885
- probe.once("connect", () => {
1886
- probe.destroy();
1887
- process.exitCode = 0;
1888
- });
1889
- probe.once("error", () => {
1890
- probe.destroy();
1891
- try {
1892
- fs3.rmSync(endpoint, { force: true });
1893
- } catch {
1894
- }
1895
- probing = false;
1896
- listen();
1897
- });
1865
+ if (bind.outcome === "failed") {
1866
+ process.stderr.write(`session-catalog project server failed: ${bind.error.message}
1867
+ `);
1868
+ process.exitCode = 1;
1898
1869
  return;
1899
1870
  }
1900
- process.exitCode = 1;
1901
- });
1902
- server.on("listening", () => {
1903
- if (process.platform !== "win32") {
1904
- try {
1905
- fs3.chmodSync(endpoint, 384);
1906
- } catch {
1907
- }
1871
+ if (bind.reclaimedStaleEndpoint) {
1872
+ process.stderr.write(`session-catalog project server reclaimed stale endpoint ${endpoint}
1873
+ `);
1908
1874
  }
1875
+ server.on("error", (error) => {
1876
+ if (stopping) return;
1877
+ process.stderr.write(`session-catalog project server error: ${error.message}
1878
+ `);
1879
+ process.exitCode = 1;
1880
+ });
1909
1881
  try {
1910
1882
  store = new SessionCatalogStore(parsed.projectDir);
1911
1883
  void writeMetadata().then(() => metadataReadyResolve?.()).catch(() => void stop("metadata write failed"));
@@ -1914,8 +1886,7 @@ server.on("listening", () => {
1914
1886
  return;
1915
1887
  }
1916
1888
  scheduleIdleStop();
1917
- });
1889
+ })();
1918
1890
  process.once("SIGINT", () => void stop("SIGINT"));
1919
1891
  process.once("SIGTERM", () => void stop("SIGTERM"));
1920
- listen();
1921
1892
  //# sourceMappingURL=project-server.js.map
@@ -10,6 +10,15 @@ export interface FallbackManageToolOptions {
10
10
  updateConfig: (mutate: (cfg: Record<string, unknown>) => void) => Promise<void>;
11
11
  /** Optional secure interactive input callback for secrets such as API keys. */
12
12
  requestInput?: ((prompt: string) => Promise<string>) | undefined;
13
+ /**
14
+ * Optional live provider/model switch (the host's switchProviderAndModel).
15
+ * When present, `leader_model_set` routes leader changes through it so the
16
+ * live agent context (provider instance, model, context caps) follows the
17
+ * config write. Returns an error string on failure, null on success —
18
+ * mirroring cli-main's switch callback. When absent, the tool persists the
19
+ * config and reports that the live session keeps its current model.
20
+ */
21
+ switchProviderAndModel?: ((providerId: string, modelId: string) => Promise<string | null>) | undefined;
13
22
  /** Optional logger for internal warnings. */
14
23
  logger?: Logger | undefined;
15
24
  }
@@ -3908,6 +3908,7 @@ var TOOLS = {
3908
3908
  "glob",
3909
3909
  "search",
3910
3910
  "tree",
3911
+ "diff",
3911
3912
  "write",
3912
3913
  "edit",
3913
3914
  "replace",
@@ -4939,7 +4940,7 @@ var VERIFY_AGENTS = [
4939
4940
  id: "e2e",
4940
4941
  name: "E2E",
4941
4942
  role: "e2e",
4942
- tools: [...TOOLS.build, "fetch", ...SPECIALIST_TOOLS.browser],
4943
+ tools: [...TOOLS.build, "fetch", "e2e_plan", ...SPECIALIST_TOOLS.browser],
4943
4944
  prompt: agentPrompt("e2e")
4944
4945
  },
4945
4946
  budget: HEAVY_BUDGET,
@@ -5452,7 +5453,7 @@ var DOMAIN_AGENTS = [
5452
5453
  id: "designer",
5453
5454
  name: "Designer",
5454
5455
  role: "designer",
5455
- tools: [...TOOLS.docs],
5456
+ tools: [...TOOLS.docs, "design"],
5456
5457
  prompt: agentPrompt("designer")
5457
5458
  },
5458
5459
  budget: MEDIUM_BUDGET,
@@ -6661,7 +6662,7 @@ function createFallbackChainManageTool(opts) {
6661
6662
  name: FALLBACK_CHAIN_MANAGE_TOOL_NAME,
6662
6663
  description: "View or change the active rate-limit fallback chain. When the primary model is overloaded (429/5xx), the agent rotates through this chain in order. Every new entry must be a FAVORITE model \u2014 add it via favorite_manage first. Use insert to place a fallback at a specific position; use remove to delete an entry.",
6663
6664
  usageHint: '"list" to see the current chain. "add" with a favorite model to append. "insert" with an index (1-based) to place before that position. "remove" with index or model ref. "clear" to empty the chain (auto fallback takes over).',
6664
- category: "Config",
6665
+ category: "config",
6665
6666
  inputSchema: FALLBACK_CHAIN_SCHEMA,
6666
6667
  permission: "auto",
6667
6668
  mutating: true,
@@ -6811,7 +6812,7 @@ function createFavoriteManageTool(opts) {
6811
6812
  name: FAVORITE_MANAGE_TOOL_NAME,
6812
6813
  description: "Manage your favorite provider/model list. Favorites are the only models that can be added to fallback chains and profiles. The LLM uses this tool to curate which models are available for fallback and role assignment.",
6813
6814
  usageHint: 'Start with "list" to see current favorites. Use "add <provider/model>" to add. Use "remove <index|ref>" to remove.',
6814
- category: "Config",
6815
+ category: "config",
6815
6816
  inputSchema: FAVORITE_MANAGE_SCHEMA,
6816
6817
  permission: "auto",
6817
6818
  mutating: true,
@@ -6891,7 +6892,7 @@ async function storeProviderKey(providers, input, keyValue, opts) {
6891
6892
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
6892
6893
  });
6893
6894
  entry.apiKeys = existingKeys;
6894
- entry.apiKey = void 0;
6895
+ delete entry.apiKey;
6895
6896
  if (input.setActive !== false) {
6896
6897
  entry.activeKey = label;
6897
6898
  }
@@ -6934,7 +6935,7 @@ function createSystemConfigViewTool(opts) {
6934
6935
  name: SYSTEM_CONFIG_VIEW_TOOL_NAME,
6935
6936
  description: "Get a comprehensive view of all provider, model, fallback, and matrix configuration. Shows the complete state across all configurable areas so you can see what is available and make informed decisions when assigning models, creating fallback profiles, or managing providers. Use the section parameter to focus on specific areas.",
6936
6937
  usageHint: '"section: all" for everything. "section: providers" for configured providers and key status. "section: models" for leader model and favorites. "section: fallbacks" for chains, profiles, and toggles. "section: matrix" for per-role assignments. "section: refiner" for goal refinement config.',
6937
- category: "Config",
6938
+ category: "config",
6938
6939
  inputSchema: SYSTEM_CONFIG_VIEW_SCHEMA,
6939
6940
  permission: "auto",
6940
6941
  mutating: false,
@@ -7238,7 +7239,7 @@ function createFallbackProfileManageTool(opts) {
7238
7239
  name: FALLBACK_PROFILE_MANAGE_TOOL_NAME,
7239
7240
  description: "Manage named fallback profiles. A profile is a reusable, ordered list of model references that can be assigned to agent roles. Every entry in a profile must be a FAVORITE model \u2014 add it via favorite_manage first. Use /setmodel or agent_model_assign to assign a profile to a role.",
7240
7241
  usageHint: '"list" to see all profiles. "set" with name and chain (array of model refs) to create or replace a profile. "delete" with name to remove a profile.',
7241
- category: "Config",
7242
+ category: "config",
7242
7243
  inputSchema: FALLBACK_PROFILE_SCHEMA,
7243
7244
  permission: "auto",
7244
7245
  mutating: true,
@@ -7349,7 +7350,7 @@ function createAgentModelAssignTool(opts) {
7349
7350
  name: AGENT_MODEL_ASSIGN_TOOL_NAME,
7350
7351
  description: "Assign a provider/model or a fallback profile to a specific agent role, phase, or the fleet-wide default. This is the LLM-accessible equivalent of /setmodel set. The provider+model combination must be in your favorites list (unless only clearing). Resolution precedence: exact role \u2192 phase \u2192 * \u2192 leader model.",
7351
7352
  usageHint: 'Use "list" as role to see current assignments. Set with role + model, or role + provider + model, or role + profile. Set role + clear=true to remove a matrix entry. The provider/model must be a favorite.',
7352
- category: "Config",
7353
+ category: "config",
7353
7354
  inputSchema: AGENT_MODEL_ASSIGN_SCHEMA,
7354
7355
  permission: "auto",
7355
7356
  mutating: true,
@@ -7497,7 +7498,7 @@ function createProviderManageTool(opts) {
7497
7498
  name: PROVIDER_MANAGE_TOOL_NAME,
7498
7499
  description: "View or configure provider entries. List all configured providers with their type, model lists, base URL, and key status. Add new providers, update their settings, or remove unused ones. API keys should be set via provider_key_set instead of passing them here \u2014 they are visible in the LLM output.",
7499
7500
  usageHint: '"list" to see all providers. "add" with provider id and type to create. "configure" to update models, baseUrl, family, or envVars. "remove" to delete a provider. Use provider_key_set for API key management.',
7500
- category: "Config",
7501
+ category: "config",
7501
7502
  inputSchema: PROVIDER_MANAGE_SCHEMA,
7502
7503
  permission: "auto",
7503
7504
  mutating: true,
@@ -7646,9 +7647,11 @@ function createProviderKeySetTool(opts) {
7646
7647
  name: PROVIDER_KEY_SET_TOOL_NAME,
7647
7648
  description: "Set the API key for a provider. For security, prefer using envVar (reads from environment variable, value never visible to the LLM) over passing the key directly. When neither key nor envVar is provided, the tool returns a prompt for interactive key entry \u2014 the UI will present an input field and the key is stored without LLM visibility.\n\nAfter setting a key, the provider becomes usable for model assignments and fallback chains. Add its models to favorites with favorite_manage to unlock them for fallback/profile use.",
7648
7649
  usageHint: 'Preferred: provider_key_set({ provider: "openai", envVar: "OPENAI_API_KEY" }). For interactive input: provider_key_set({ provider: "openai" }) \u2014 the UI will prompt. Direct key: provider_key_set({ provider: "openai", key: "sk-..." }) \u2014 visible to LLM.',
7649
- category: "Config",
7650
+ category: "config",
7650
7651
  inputSchema: PROVIDER_KEY_SET_SCHEMA,
7651
- permission: "auto",
7652
+ // 'confirm', not 'auto' — this tool writes credentials to disk (and can
7653
+ // read arbitrary env vars into the config file), so the user must see it.
7654
+ permission: "confirm",
7652
7655
  mutating: true,
7653
7656
  riskTier: "standard",
7654
7657
  icon: "settings",
@@ -7744,7 +7747,7 @@ function createLeaderModelSetTool(opts) {
7744
7747
  name: LEADER_MODEL_SET_TOOL_NAME,
7745
7748
  description: 'View or change the leader provider/model and system toggles. The leader is the primary model used for the main agent interactions. "set" changes it directly. "profile" derives it from a named fallback profile (first entry becomes leader, rest become the fallback chain). "toggle" controls fallbackAuto (smart default fallback) and favoriteModelsOnly (restrict auto-fallback to favorites only).',
7746
7749
  usageHint: '"show" to see current state. "set" with provider+model to change. "profile" with name to derive from a profile. "toggle" with toggle name and value to change a boolean setting.',
7747
- category: "Config",
7750
+ category: "config",
7748
7751
  inputSchema: LEADER_MODEL_SET_SCHEMA,
7749
7752
  permission: "auto",
7750
7753
  mutating: true,
@@ -7768,11 +7771,21 @@ function createLeaderModelSetTool(opts) {
7768
7771
  if (!input.provider || !input.model) {
7769
7772
  return { status: "error", message: 'Provide "provider" and "model" for the leader.' };
7770
7773
  }
7774
+ if (opts.switchProviderAndModel) {
7775
+ const switchError = await opts.switchProviderAndModel(input.provider, input.model);
7776
+ if (switchError) {
7777
+ return {
7778
+ status: "error",
7779
+ message: `Could not switch to ${input.provider}/${input.model}: ${switchError}. Config was not changed.`
7780
+ };
7781
+ }
7782
+ }
7771
7783
  await opts.updateConfig((cfg) => {
7772
7784
  cfg.provider = input.provider;
7773
7785
  cfg.model = input.model;
7774
7786
  });
7775
- return { status: "ok", message: `\u2713 Leader \u2192 ${input.provider}/${input.model}` };
7787
+ const liveNote = opts.switchProviderAndModel ? "" : " (config updated \u2014 the live session keeps its current model until restart or /setmodel)";
7788
+ return { status: "ok", message: `\u2713 Leader \u2192 ${input.provider}/${input.model}${liveNote}` };
7776
7789
  }
7777
7790
  if (input.action === "profile") {
7778
7791
  if (!input.profile) {
@@ -7791,15 +7804,25 @@ function createLeaderModelSetTool(opts) {
7791
7804
  return { status: "error", message: `Cannot parse "${first}" as a valid model reference.` };
7792
7805
  }
7793
7806
  const rest = chain.slice(1);
7807
+ if (opts.switchProviderAndModel) {
7808
+ const switchError = await opts.switchProviderAndModel(provider, model);
7809
+ if (switchError) {
7810
+ return {
7811
+ status: "error",
7812
+ message: `Could not switch to ${provider}/${model}: ${switchError}. Config was not changed.`
7813
+ };
7814
+ }
7815
+ }
7794
7816
  await opts.updateConfig((cfg) => {
7795
7817
  cfg.provider = provider;
7796
7818
  cfg.model = model;
7797
7819
  cfg.fallbackModels = rest;
7798
7820
  });
7821
+ const profileLiveNote = opts.switchProviderAndModel ? "" : "\n (config updated \u2014 the live session keeps its current model until restart or /setmodel)";
7799
7822
  return {
7800
7823
  status: "ok",
7801
7824
  message: `\u2713 Leader \u2192 ${provider}/${model} (profile: ${input.profile})` + (rest.length > 0 ? `
7802
- Fallback chain: ${rest.join(" \u2192 ")}` : "")
7825
+ Fallback chain: ${rest.join(" \u2192 ")}` : "") + profileLiveNote
7803
7826
  };
7804
7827
  }
7805
7828
  if (input.action === "toggle") {
@@ -8283,20 +8306,22 @@ async function runEnable(name, deps) {
8283
8306
  const known = Object.keys(all).join(", ");
8284
8307
  return `Unknown server "${name}". Available presets: ${known}`;
8285
8308
  }
8286
- await updateJsonObjectFile(deps.configPath, (full) => {
8309
+ const persistEnabled = () => updateJsonObjectFile(deps.configPath, (full) => {
8287
8310
  const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
8288
8311
  setJsonPath(full, ["mcpServers", name], { ...current[name], ...cfg, enabled: true });
8289
8312
  });
8290
8313
  try {
8291
8314
  const live = deps.registry.describe().find((s) => s.name === name);
8292
8315
  if (live && live.state === "connected") {
8293
- return `${green("\u25CF")} Server "${name}" is already running (${live.toolCount} tools registered).`;
8316
+ await persistEnabled();
8317
+ return `Server "${name}" is already running (${live.toolCount} tools registered).`;
8294
8318
  }
8295
8319
  await deps.registry.start({ ...cfg, enabled: true });
8320
+ await persistEnabled();
8296
8321
  const updated = deps.registry.describe().find((s) => s.name === name);
8297
- return `${green("\u2713 Enabled and started")} "${name}"${updated ? ` (${updated.toolCount} tools registered).` : "."}`;
8322
+ return `Enabled and started "${name}"${updated ? ` (${updated.toolCount} tools registered).` : "."}`;
8298
8323
  } catch (err) {
8299
- return `${red("\u2717 Failed to start")} "${name}": ${toErrorMessage(err)}`;
8324
+ return `Failed to start "${name}": ${toErrorMessage(err)}. Config was left unchanged (server stays disabled).`;
8300
8325
  }
8301
8326
  }
8302
8327
  async function runDisable(name, deps) {
@@ -8370,34 +8395,34 @@ function isMcpServerRecord(value) {
8370
8395
  return !!value && typeof value === "object" && !Array.isArray(value);
8371
8396
  }
8372
8397
  function bold(s) {
8373
- return `\x1B[1m${s}\x1B[0m`;
8398
+ return s;
8374
8399
  }
8375
8400
  function dim(s) {
8376
- return `\x1B[2m${s}\x1B[0m`;
8401
+ return s;
8377
8402
  }
8378
8403
  function green(s) {
8379
- return `\x1B[32m${s}\x1B[0m`;
8404
+ return s;
8380
8405
  }
8381
8406
  function yellow(s) {
8382
- return `\x1B[33m${s}\x1B[0m`;
8407
+ return s;
8383
8408
  }
8384
8409
  function red(s) {
8385
- return `\x1B[31m${s}\x1B[0m`;
8410
+ return s;
8386
8411
  }
8387
8412
  function badge(state) {
8388
8413
  switch (state) {
8389
8414
  case "connected":
8390
- return green("\u25CF connected");
8415
+ return "\u25CF connected";
8391
8416
  case "connecting":
8392
- return `\x1B[36m\u25D0 connecting\x1B[0m`;
8417
+ return "\u25D0 connecting";
8393
8418
  case "reconnecting":
8394
- return `\x1B[36m\u25D1 reconnecting\x1B[0m`;
8419
+ return "\u25D1 reconnecting";
8395
8420
  case "disconnected":
8396
- return dim("\u25CB disconnected");
8421
+ return "\u25CB disconnected";
8397
8422
  case "failed":
8398
- return red("\u2717 failed");
8423
+ return "\u2717 failed";
8399
8424
  default:
8400
- return dim(state);
8425
+ return state;
8401
8426
  }
8402
8427
  }
8403
8428
 
@@ -8462,13 +8487,19 @@ function createMcpUseTool(opts) {
8462
8487
  const servers = registry.describe();
8463
8488
  const serverInfo = servers.find((s) => s.name === serverName);
8464
8489
  if (!serverInfo) {
8465
- return `Server "${serverName}" not found. Available: ${servers.map((s) => s.name).join(", ") || "none"}.`;
8490
+ throw new Error(
8491
+ `Server "${serverName}" not found. Available: ${servers.map((s) => s.name).join(", ") || "none"}.`
8492
+ );
8466
8493
  }
8467
8494
  if (serverInfo.state !== "connected") {
8468
- return `Server "${serverName}" is not connected (state: ${serverInfo.state}). Use \`mcp_control({ action: "enable", server: "${serverName}" })\` first.`;
8495
+ throw new Error(
8496
+ `Server "${serverName}" is not connected (state: ${serverInfo.state}). Use \`mcp_control({ action: "enable", server: "${serverName}" })\` first.`
8497
+ );
8469
8498
  }
8470
- if (registry.activateServer) {
8471
- registry.activateServer(serverName);
8499
+ const alreadyActive = registry.isActivated?.(serverName) === true;
8500
+ const didActivate = !alreadyActive && Boolean(registry.activateServer);
8501
+ if (didActivate) {
8502
+ registry.activateServer?.(serverName);
8472
8503
  }
8473
8504
  try {
8474
8505
  const qualifiedName = mcpQualifiedToolName(serverName, toolName);
@@ -8476,7 +8507,7 @@ function createMcpUseTool(opts) {
8476
8507
  if (!mcpTool) {
8477
8508
  const allTools = toolRegistry.list().filter((t) => t.name.startsWith(mcpServerToolPrefix(serverName))).map((t) => t.name.replace(mcpServerToolPrefix(serverName), ""));
8478
8509
  const hint = allTools.length > 0 ? `Available tools on "${serverName}": ${allTools.join(", ")}.` : `No tools found on "${serverName}". The server may not have published any tools.`;
8479
- return `Tool "${toolName}" not found on server "${serverName}". ${hint}`;
8510
+ throw new Error(`Tool "${toolName}" not found on server "${serverName}". ${hint}`);
8480
8511
  }
8481
8512
  const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
8482
8513
  if (typeof governedExecute !== "function") {
@@ -8486,7 +8517,7 @@ function createMcpUseTool(opts) {
8486
8517
  if (!result.success) throw new Error(result.error ?? "MCP tool execution failed");
8487
8518
  return result.result;
8488
8519
  } finally {
8489
- if (registry.deactivateServer) {
8520
+ if (didActivate && registry.deactivateServer) {
8490
8521
  registry.deactivateServer(serverName);
8491
8522
  }
8492
8523
  }
@@ -8781,6 +8812,7 @@ function asTextBlocks(system) {
8781
8812
 
8782
8813
  // src/tools/one-shot-llm-tool.ts
8783
8814
  var ONE_SHOT_LLM_TOOL_NAME = "llm";
8815
+ var MAX_TIMEOUT_MS = 12e4;
8784
8816
  var INPUT_SCHEMA2 = {
8785
8817
  type: "object",
8786
8818
  properties: {
@@ -8857,9 +8889,10 @@ var INPUT_SCHEMA2 = {
8857
8889
  },
8858
8890
  timeoutMs: {
8859
8891
  type: "number",
8860
- description: "Hard timeout in ms (default 30s)."
8892
+ description: `Hard timeout in ms (default 30s, clamped to a maximum of ${MAX_TIMEOUT_MS}).`
8861
8893
  }
8862
- }
8894
+ },
8895
+ additionalProperties: false
8863
8896
  };
8864
8897
  function createOneShotLLMTool(opts) {
8865
8898
  const orchestrator = new OneShotOrchestrator({
@@ -8867,6 +8900,7 @@ function createOneShotLLMTool(opts) {
8867
8900
  getConfig: opts.getConfig,
8868
8901
  fallbackProfileManager: opts.fallbackProfileManager,
8869
8902
  modelRouter: opts.modelRouter,
8903
+ statusTracker: opts.statusTracker,
8870
8904
  logger: opts.logger,
8871
8905
  wrapProviderCall: opts.wrapProviderCall
8872
8906
  });
@@ -8875,8 +8909,23 @@ function createOneShotLLMTool(opts) {
8875
8909
  description: "Make a one-shot LLM call with a system prompt and user input. Supports provider selection, model routing by role, fallback chains, and timeout. Returns the response text, model info, token usage, and whether a fallback was used. Use this for summarization, classification, extraction, and any single-turn LLM task.",
8876
8910
  usageHint: "Provide `system` for the instruction and `userPrompt` for the input. Either set `model`+`providerId`, or have defaults configured on the tool. Set `fallbackModels` for resilience. Check `error` on the result for failure details.",
8877
8911
  inputSchema: INPUT_SCHEMA2,
8912
+ // Metadata mirrors council-tool.ts — both are read-only meta tools that
8913
+ // spend tokens but never touch the workspace.
8914
+ category: "meta",
8878
8915
  permission: "auto",
8879
8916
  mutating: false,
8917
+ riskTier: "safe",
8918
+ maxOutputBytes: 262144,
8919
+ validate(input) {
8920
+ const hasPrompt = typeof input.userPrompt === "string" && input.userPrompt.trim().length > 0;
8921
+ const hasMessages = Array.isArray(input.messages) && input.messages.length > 0;
8922
+ if (!hasPrompt && !hasMessages) {
8923
+ return [
8924
+ "Provide `userPrompt` (a single user turn) or `messages` (a conversation array) \u2014 without either the llm tool has nothing to send to the model."
8925
+ ];
8926
+ }
8927
+ return [];
8928
+ },
8880
8929
  async execute(input, _ctx, { signal }) {
8881
8930
  if (!input.model && !input.providerId && !opts.defaultModel && !opts.defaultProvider) {
8882
8931
  return {
@@ -8893,7 +8942,11 @@ function createOneShotLLMTool(opts) {
8893
8942
  ...input,
8894
8943
  signal: input.signal ? AbortSignal.any([input.signal, signal]) : signal,
8895
8944
  model: input.model ?? opts.defaultModel,
8896
- providerId: input.providerId ?? opts.defaultProvider
8945
+ providerId: input.providerId ?? opts.defaultProvider,
8946
+ // Clamp runaway timeouts (documented on the schema). Non-positive
8947
+ // values fall back to the orchestrator default rather than making the
8948
+ // call instantly un-completable.
8949
+ ...typeof input.timeoutMs === "number" && input.timeoutMs > 0 ? { timeoutMs: Math.min(input.timeoutMs, MAX_TIMEOUT_MS) } : { timeoutMs: void 0 }
8897
8950
  };
8898
8951
  return orchestrator.call(effectiveInput);
8899
8952
  }
@@ -17,6 +17,12 @@ export interface CreateOneShotLLMToolOptions {
17
17
  /** Shared live FallbackProfileManager — required. */
18
18
  fallbackProfileManager: OneShotOrchestratorOptions['fallbackProfileManager'];
19
19
  modelRouter?: OneShotOrchestratorOptions['modelRouter'];
20
+ /**
21
+ * Shared provider/model status tracker. Without it the orchestrator never
22
+ * records failures/successes for `llm` calls, so provider health stays
23
+ * blind to this tool's traffic and blocked entries are not skipped.
24
+ */
25
+ statusTracker?: OneShotOrchestratorOptions['statusTracker'];
20
26
  logger?: OneShotOrchestratorOptions['logger'];
21
27
  wrapProviderCall?: OneShotOrchestratorOptions['wrapProviderCall'];
22
28
  /**
@@ -36,6 +36,16 @@ export interface ToolResultBlock {
36
36
  name?: string | undefined;
37
37
  content: string;
38
38
  is_error?: boolean | undefined;
39
+ /**
40
+ * Provider cache boundary. Set at request-composition time on a CLONE of the
41
+ * trailing durable block (never on the stored message) so the conversation
42
+ * prefix becomes an incrementally extendable cache entry. Wires that use
43
+ * explicit markers (Anthropic) emit it; every other wire rebuilds
44
+ * tool_result content explicitly and drops it.
45
+ */
46
+ cache_control?: {
47
+ type: 'ephemeral' | undefined;
48
+ };
39
49
  /**
40
50
  * Structured tool error information. Present on error results produced
41
51
  * by the unified tool error taxonomy (tool-error-taxonomy.ts). Consumed
@@ -5,10 +5,13 @@
5
5
  * the TUI runtime can all reference the same string union without
6
6
  * importing the TUI package (which would invert the dependency direction).
7
7
  *
8
- * Keep in lockstep with `THEME_OPTIONS` in `packages/tui/src/theme.ts`.
9
- * Adding a preset here is intentional it also requires updating the TUI
10
- * presets map AND the CLI `VALID_PRESETS` set in `tui-theme-adapter.ts`.
8
+ * This array is the CANONICAL list. The CLI `/theme` command and the boot
9
+ * theme adapter both derive their valid-id sets from it, so adding an entry
10
+ * here needs exactly ONE follow-up: a matching palette + `THEME_OPTIONS` row
11
+ * in `packages/tui/src/theme.ts`. Both are compile-enforced — `themePresets`
12
+ * is typed `Record<ThemePresetId, Theme>` (no cast) and the CLI's `THEME_META`
13
+ * is a total record — so a missing preset fails `tsc`, not the runtime.
11
14
  */
12
- export declare const THEME_PRESET_IDS: readonly ['catppuccin', 'tokyo-night', 'nord', 'cyberpunk', 'dracula', 'gruvbox-dark', 'solarized-dark', 'one-dark', 'monokai', 'rose-pine', 'kanagawa', 'ayu-dark', 'everforest', 'night-owl', 'synthwave'];
15
+ export declare const THEME_PRESET_IDS: readonly ['catppuccin', 'tokyo-night', 'nord', 'cyberpunk', 'dracula', 'gruvbox-dark', 'solarized-dark', 'one-dark', 'monokai', 'rose-pine', 'kanagawa', 'ayu-dark', 'everforest', 'night-owl', 'synthwave', 'github-dark', 'material-ocean', 'nightfox', 'oxocarbon', 'catppuccin-macchiato', 'catppuccin-frappe', 'gruvbox-material', 'tokyo-night-storm', 'rose-pine-moon', 'zenburn', 'palenight', 'horizon', 'sonokai', 'edge-dark', 'moonfly', 'melange', 'poimandres', 'vitesse-dark', 'aura', 'dark-plus'];
13
16
  export type ThemePresetId = (typeof THEME_PRESET_IDS)[number];
14
17
  //# sourceMappingURL=ui.d.ts.map