@wrongstack/core 0.300.0 → 0.301.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 (144) hide show
  1. package/dist/chronicle/index.d.ts +2 -0
  2. package/dist/chronicle/index.js +297 -10
  3. package/dist/chronicle/project-server.js +253 -15
  4. package/dist/chronicle/sqlite-compaction.d.ts +20 -0
  5. package/dist/chronicle/sqlite-journal.d.ts +57 -0
  6. package/dist/coordination/agents/capability-manifest.d.ts +7 -0
  7. package/dist/coordination/agents/index.d.ts +3 -2
  8. package/dist/coordination/agents/index.js +818 -570
  9. package/dist/coordination/agents/project-agent-identity.d.ts +6 -6
  10. package/dist/coordination/agents/role-skills.d.ts +1 -1
  11. package/dist/coordination/agents/types.d.ts +6 -0
  12. package/dist/coordination/director.d.ts +2 -2
  13. package/dist/coordination/index.js +6778 -6438
  14. package/dist/coordination/mail-tools.d.ts +3 -3
  15. package/dist/coordination/mailbox-project-server.js +3 -4
  16. package/dist/coordination/mailbox-types.d.ts +6 -0
  17. package/dist/core/agent-response.d.ts +5 -1
  18. package/dist/core/agent-tools.d.ts +3 -0
  19. package/dist/core/context.d.ts +20 -0
  20. package/dist/core/index.d.ts +8 -7
  21. package/dist/core/index.js +893 -284
  22. package/dist/core/instruction-template.d.ts +6 -3
  23. package/dist/core/next-steps-slot.d.ts +88 -0
  24. package/dist/core/system-prompt-builder.d.ts +5 -3
  25. package/dist/core/system-prompt-memory-skills.d.ts +1 -0
  26. package/dist/core/system-prompt-skill-bodies.d.ts +3 -3
  27. package/dist/defaults/index.js +9796 -8897
  28. package/dist/design/index.js +11 -3
  29. package/dist/execution/auto-compaction-middleware.d.ts +17 -0
  30. package/dist/execution/autonomy-brain.d.ts +8 -2
  31. package/dist/execution/brain-runtime.d.ts +3 -1
  32. package/dist/execution/compaction-core.d.ts +41 -2
  33. package/dist/execution/council-brain.d.ts +37 -2
  34. package/dist/execution/council-orchestrator.d.ts +2 -2
  35. package/dist/execution/index.d.ts +11 -10
  36. package/dist/execution/index.js +5178 -4077
  37. package/dist/execution/tool-executor.d.ts +4 -1
  38. package/dist/execution/topic-shift-advisor.d.ts +53 -0
  39. package/dist/extension/index.js +8 -2
  40. package/dist/extension/registry.d.ts +8 -1
  41. package/dist/goal/index.js +11 -3
  42. package/dist/hooks/index.js +42 -9
  43. package/dist/hooks/runner.d.ts +12 -1
  44. package/dist/hq/index.js +41 -17
  45. package/dist/hq/publisher.d.ts +21 -3
  46. package/dist/index.d.ts +21 -20
  47. package/dist/index.js +10887 -8053
  48. package/dist/infrastructure/index.js +108 -21
  49. package/dist/kernel/events/session-events.d.ts +13 -0
  50. package/dist/kernel/events/tool-events.d.ts +3 -3
  51. package/dist/models/index.d.ts +1 -0
  52. package/dist/models/index.js +17 -0
  53. package/dist/models/provider-credentials.d.ts +54 -0
  54. package/dist/plugin/index.d.ts +1 -0
  55. package/dist/plugin/index.js +2024 -502
  56. package/dist/plugins/auto-review-plugin.d.ts +3 -0
  57. package/dist/plugins/cloud-config-sync-plugin.d.ts +22 -0
  58. package/dist/plugins/review-claim-registry.d.ts +23 -8
  59. package/dist/plugins/review-types.d.ts +2 -0
  60. package/dist/registry/index.js +109 -74
  61. package/dist/registry/tool-registry.d.ts +15 -0
  62. package/dist/security/capabilities.d.ts +2 -0
  63. package/dist/security/index.d.ts +1 -1
  64. package/dist/security/index.js +9 -2
  65. package/dist/security/readonly-permission-policy.d.ts +20 -0
  66. package/dist/session-registry-atomic-file.d.ts +32 -5
  67. package/dist/skills/frontmatter.d.ts +6 -0
  68. package/dist/skills/index.js +22 -5
  69. package/dist/storage/cloud-config-sync/sanitize.d.ts +54 -0
  70. package/dist/storage/cloud-config-sync.d.ts +87 -0
  71. package/dist/storage/index.d.ts +1 -0
  72. package/dist/storage/index.js +854 -66
  73. package/dist/tools/index.d.ts +1 -1
  74. package/dist/tools/index.js +511 -234
  75. package/dist/tools/one-shot-llm-tool.d.ts +1 -0
  76. package/dist/tools/plugin-manager.d.ts +27 -0
  77. package/dist/types/config/mcp-features.d.ts +18 -11
  78. package/dist/types/config/root.d.ts +8 -1
  79. package/dist/types/config/runtime.d.ts +20 -6
  80. package/dist/types/config/skills-fleet-brain.d.ts +12 -4
  81. package/dist/types/config/tools.d.ts +16 -0
  82. package/dist/types/context-window.d.ts +1 -0
  83. package/dist/types/hooks.d.ts +14 -0
  84. package/dist/types/index.d.ts +2 -2
  85. package/dist/types/index.js +1 -0
  86. package/dist/types/multi-agent.d.ts +2 -0
  87. package/dist/types/one-shot-llm.d.ts +16 -0
  88. package/dist/types/provider.d.ts +16 -0
  89. package/dist/types/runtime-capability-manifest.d.ts +168 -0
  90. package/dist/types/skill.d.ts +5 -0
  91. package/dist/types/system-prompt.d.ts +3 -1
  92. package/dist/types/tool-executor.d.ts +8 -1
  93. package/dist/utils/context-breakdown.d.ts +8 -2
  94. package/dist/utils/index.d.ts +1 -1
  95. package/dist/utils/index.js +141 -31
  96. package/dist/utils/regex-guard.d.ts +16 -2
  97. package/dist/utils/sage-output-block.d.ts +7 -10
  98. package/dist/utils/wstack-paths.d.ts +11 -3
  99. package/instructions/agents/browser.md +1 -4
  100. package/instructions/agents/e2e.md +17 -15
  101. package/instructions/agents/ios.md +3 -3
  102. package/instructions/agents/search.md +1 -1
  103. package/instructions/autonomy/goal-preamble.md +4 -4
  104. package/instructions/coordination/director-preamble.md +2 -3
  105. package/instructions/coordination/subagent-baseline.md +3 -1
  106. package/instructions/leader-after-task.md +12 -0
  107. package/instructions/llm/chimera-review.md +1 -1
  108. package/instructions/modes/audit-lite.md +1 -1
  109. package/instructions/sections/tool/common-patterns.md +18 -2
  110. package/instructions/sections/tool/mailbox-compact.md +1 -1
  111. package/instructions/sections/tool/mailbox-full.md +1 -1
  112. package/instructions/system-lite.md +10 -0
  113. package/instructions/system-pro.md +23 -14
  114. package/instructions/system.md +22 -13
  115. package/package.json +3 -3
  116. package/skills/api-design/SKILL.md +3 -0
  117. package/skills/audit-log/SKILL.md +2 -0
  118. package/skills/auto-review/SKILL.md +6 -1
  119. package/skills/bug-hunter/SKILL.md +4 -1
  120. package/skills/chimera/SKILL.md +3 -0
  121. package/skills/data-governance/SKILL.md +3 -0
  122. package/skills/design-system/SKILL.md +5 -2
  123. package/skills/docker-deploy/SKILL.md +2 -0
  124. package/skills/git-flow/SKILL.md +2 -0
  125. package/skills/mailbox-bridge/SKILL.md +3 -0
  126. package/skills/mnemosyne/SKILL.md +2 -0
  127. package/skills/multi-agent/SKILL.md +4 -1
  128. package/skills/node-modern/SKILL.md +4 -1
  129. package/skills/observability/SKILL.md +3 -0
  130. package/skills/output-standards/SKILL.md +2 -0
  131. package/skills/plugin-author/SKILL.md +4 -1
  132. package/skills/prompt-engineering/SKILL.md +3 -1
  133. package/skills/react-modern/SKILL.md +6 -3
  134. package/skills/refactor-planner/SKILL.md +2 -0
  135. package/skills/research-web/SKILL.md +3 -0
  136. package/skills/sdd/SKILL.md +3 -1
  137. package/skills/security-scanner/SKILL.md +3 -0
  138. package/skills/skill-creator/SKILL.md +2 -0
  139. package/skills/tech-stack/SKILL.md +3 -0
  140. package/skills/testing/SKILL.md +4 -1
  141. package/skills/typescript-strict/SKILL.md +4 -1
  142. package/skills/wrongstack-kanban/SKILL.md +6 -3
  143. package/skills/wrongstack-mailbox/SKILL.md +4 -1
  144. package/skills/wrongstack-mailbox-mcp/SKILL.md +10 -8
@@ -35,27 +35,81 @@ var init_pid = __esm({
35
35
  // src/session-registry-atomic-file.ts
36
36
  import { randomUUID } from "node:crypto";
37
37
  import * as fs6 from "node:fs/promises";
38
+ import { hostname } from "node:os";
38
39
  import * as path8 from "node:path";
40
+ function lockOwnerStamp() {
41
+ return `${HOST_NAME}:${process.pid}`;
42
+ }
43
+ async function maybeUnlinkOwnedLock(lockPath) {
44
+ const owner = await fs6.readFile(lockPath, "utf8").catch(() => null);
45
+ if (owner === null || owner.trim() !== lockOwnerStamp()) return;
46
+ for (let attempt = 0; attempt < 3; attempt++) {
47
+ try {
48
+ await fs6.unlink(lockPath);
49
+ return;
50
+ } catch {
51
+ if (attempt < 2) await new Promise((resolve5) => setTimeout(resolve5, 5));
52
+ }
53
+ }
54
+ }
39
55
  async function breakStaleLock(lockPath) {
40
56
  try {
41
- const [stat5, content] = await Promise.all([
42
- fs6.stat(lockPath),
43
- /* v8 ignore start -- best-effort lock-content read; .catch only fires if the lock vanished */
44
- fs6.readFile(lockPath, "utf8").catch(() => "")
45
- /* v8 ignore stop */
46
- ]);
47
- const ageMs = Date.now() - stat5.mtimeMs;
48
- const ownerPid = Number.parseInt(content.trim(), 10);
49
- const ownerDead = Number.isInteger(ownerPid) && ownerPid > 0 && ownerPid !== process.pid && !isPidAlive(ownerPid);
50
- if (ownerDead || ageMs > STALE_LOCK_MS) {
51
- await fs6.unlink(lockPath).catch(() => void 0);
52
- return true;
57
+ const content = await fs6.readFile(lockPath, "utf8").catch(() => "");
58
+ const trimmed = content.trim();
59
+ const colonIdx = trimmed.indexOf(":");
60
+ if (colonIdx === -1) {
61
+ const barePid = Number.parseInt(trimmed, 10);
62
+ if (Number.isInteger(barePid) && barePid > 0 && !isPidAlive(barePid)) {
63
+ return await breakStaleLockVerified(lockPath, async () => {
64
+ const reread = await fs6.readFile(lockPath, "utf8").catch(() => "");
65
+ return reread.trim() === trimmed;
66
+ });
67
+ }
68
+ return await breakStaleLockVerified(lockPath, async () => {
69
+ const st = await fs6.stat(lockPath);
70
+ return Date.now() - st.mtimeMs > STALE_LOCK_MS;
71
+ });
53
72
  }
54
- return false;
73
+ const ownerHost = trimmed.slice(0, colonIdx);
74
+ const ownerPid = Number.parseInt(trimmed.slice(colonIdx + 1), 10);
75
+ if (ownerHost === HOST_NAME && Number.isInteger(ownerPid) && ownerPid > 0) {
76
+ if (isPidAlive(ownerPid)) {
77
+ const stat5 = await fs6.stat(lockPath);
78
+ if (Date.now() - stat5.mtimeMs > SAME_HOST_STALE_MS) {
79
+ return await breakStaleLockVerified(lockPath, async () => {
80
+ const st = await fs6.stat(lockPath);
81
+ return Date.now() - st.mtimeMs > SAME_HOST_STALE_MS;
82
+ });
83
+ }
84
+ return false;
85
+ }
86
+ return await breakStaleLockVerified(lockPath, async () => {
87
+ const reread = await fs6.readFile(lockPath, "utf8").catch(() => "");
88
+ return reread.trim() === trimmed;
89
+ });
90
+ }
91
+ return await breakStaleLockVerified(lockPath, async () => {
92
+ const st = await fs6.stat(lockPath);
93
+ return Date.now() - st.mtimeMs > STALE_LOCK_MS;
94
+ });
55
95
  } catch {
56
96
  return true;
57
97
  }
58
98
  }
99
+ async function breakStaleLockVerified(lockPath, verify) {
100
+ if (!await verify()) return false;
101
+ return await breakLockAtomically(lockPath) === true;
102
+ }
103
+ async function breakLockAtomically(lockPath) {
104
+ const tombstone = `${lockPath}.stale-${randomUUID()}.tmp`;
105
+ try {
106
+ await fs6.rename(lockPath, tombstone);
107
+ } catch {
108
+ return false;
109
+ }
110
+ void fs6.unlink(tombstone).catch(() => void 0);
111
+ return true;
112
+ }
59
113
  async function writeAtomicFile(filePath, registry) {
60
114
  const tmp = path8.join(
61
115
  path8.dirname(filePath),
@@ -120,14 +174,16 @@ async function pruneStaleTempFiles(filePath) {
120
174
  } catch {
121
175
  }
122
176
  }
123
- var STALE_LOCK_MS, STALE_TMP_MS, MAX_STALE_TMP_FILES;
177
+ var STALE_LOCK_MS, SAME_HOST_STALE_MS, STALE_TMP_MS, MAX_STALE_TMP_FILES, HOST_NAME;
124
178
  var init_session_registry_atomic_file = __esm({
125
179
  "src/session-registry-atomic-file.ts"() {
126
180
  "use strict";
127
181
  init_pid();
128
182
  STALE_LOCK_MS = 1e4;
183
+ SAME_HOST_STALE_MS = 2 * STALE_LOCK_MS;
129
184
  STALE_TMP_MS = 6e4;
130
185
  MAX_STALE_TMP_FILES = 20;
186
+ HOST_NAME = hostname();
131
187
  }
132
188
  });
133
189
 
@@ -552,16 +608,33 @@ var init_session_registry = __esm({
552
608
  continue;
553
609
  }
554
610
  }
611
+ let stampFailed = false;
555
612
  try {
556
- await lockHandle.writeFile(String(process.pid)).catch(() => void 0);
613
+ let stamped = false;
614
+ for (let stampAttempt = 0; stampAttempt < 3 && !stamped; stampAttempt++) {
615
+ try {
616
+ await lockHandle.writeFile(lockOwnerStamp());
617
+ stamped = true;
618
+ } catch {
619
+ if (stampAttempt < 2) await new Promise((resolve5) => setTimeout(resolve5, 5));
620
+ }
621
+ }
622
+ if (!stamped) {
623
+ stampFailed = true;
624
+ throw new Error("failed to stamp session-registry lock owner");
625
+ }
557
626
  const raw = await fs7.readFile(this.filePath, "utf8").catch(() => "{}");
558
627
  const registry = parseRegistry(raw);
559
628
  fn(registry);
560
629
  await this.writeAtomicWithRetry(registry);
561
630
  return;
562
631
  } finally {
563
- await lockHandle.close();
564
- await fs7.unlink(lockPath).catch(() => void 0);
632
+ await lockHandle.close().catch(() => void 0);
633
+ if (stampFailed) {
634
+ await fs7.unlink(lockPath).catch(() => void 0);
635
+ } else {
636
+ await maybeUnlinkOwnedLock(lockPath);
637
+ }
565
638
  }
566
639
  } catch (err) {
567
640
  if (err instanceof SessionOwnershipConflictError) throw err;
@@ -2911,6 +2984,9 @@ function canonicalProjectRoot(absRoot) {
2911
2984
  const commonDirFile = path5.join(gitDir, "commondir");
2912
2985
  if (!fs4.statSync(commonDirFile).isFile()) return checkoutRoot;
2913
2986
  const commonDir = path5.resolve(gitDir, fs4.readFileSync(commonDirFile, "utf8").trim());
2987
+ const worktreesDir = path5.dirname(gitDir);
2988
+ if (path5.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
2989
+ if (path5.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
2914
2990
  if (path5.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2915
2991
  return path5.dirname(commonDir);
2916
2992
  } catch {
@@ -2933,7 +3009,7 @@ function safeProfileName(name) {
2933
3009
  const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
2934
3010
  return safe || "default";
2935
3011
  }
2936
- function activeProfileName(globalRoot) {
3012
+ function bootstrapProfileName(globalRoot) {
2937
3013
  try {
2938
3014
  const parsed = JSON.parse(fs4.readFileSync(path5.join(globalRoot, "config.json"), "utf8"));
2939
3015
  return safeProfileName(
@@ -2951,7 +3027,7 @@ function wstackGlobalRoot() {
2951
3027
  function resolveWstackPaths(opts) {
2952
3028
  const globalRoot = opts.globalRoot ?? (opts.userHome ? path5.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2953
3029
  const homeDir = opts.userHome ?? os2.homedir();
2954
- const profileName = safeProfileName(opts.profileName ?? activeProfileName(globalRoot));
3030
+ const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
2955
3031
  const profileDir = path5.join(globalRoot, "profiles", profileName);
2956
3032
  const hash = projectHash(opts.projectRoot);
2957
3033
  const slug = projectSlug(opts.projectRoot);
@@ -3021,7 +3097,12 @@ function resolveWstackPaths(opts) {
3021
3097
  projectRequirementIntakes: path5.join(projectDir, "requirement-intakes"),
3022
3098
  syncConfig: path5.join(profileDir, "sync.json"),
3023
3099
  configHistoryDir: path5.join(globalRoot, "config-history"),
3024
- projectStatus: (projectHash2) => path5.join(globalRoot, "projects", projectHash2, "status.json")
3100
+ projectStatus: (statusProjectSlug) => {
3101
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
3102
+ throw new Error(`Invalid project slug: ${statusProjectSlug}`);
3103
+ }
3104
+ return path5.join(globalRoot, "projects", statusProjectSlug, "status.json");
3105
+ }
3025
3106
  };
3026
3107
  }
3027
3108
 
@@ -3035,6 +3116,7 @@ function isBootstrapOnly(config) {
3035
3116
  function normalizeTokenSavingTier(val) {
3036
3117
  if (val === void 0) return "off";
3037
3118
  if (typeof val === "boolean") return val ? "medium" : "off";
3119
+ if (val === "auto") return "medium";
3038
3120
  const validTiers = /* @__PURE__ */ new Set([
3039
3121
  "off",
3040
3122
  "minimal",
@@ -3176,7 +3258,7 @@ var CONFIG_BEHAVIOR_DEFAULTS = {
3176
3258
  enabled: false
3177
3259
  }
3178
3260
  },
3179
- skills: { readClaudeSkills: true },
3261
+ skills: { readClaudeSkills: true, mode: "progressive" },
3180
3262
  mcpServers: {},
3181
3263
  fallbackAuto: true,
3182
3264
  maxConcurrent: 4,
@@ -3378,6 +3460,10 @@ var KNOWN_DENIED_IN_PROJECT = [
3378
3460
  reason: "Controls which plugin boot states the LLM may mutate; user-owned trust policy."
3379
3461
  },
3380
3462
  { key: "sync", reason: "Carries githubToken credential and target repo." },
3463
+ {
3464
+ key: "cloudSync",
3465
+ reason: "Carries the my.wrongstack.com machine token credential and endpoint URL."
3466
+ },
3381
3467
  { key: "yolo", reason: "Disables all permission confirmation prompts." },
3382
3468
  { key: "extensions", reason: "Per-plugin config can carry command/credential fields." },
3383
3469
  { key: "hq", reason: "Carries HQ client token credential and endpoint URL." },
@@ -3440,6 +3526,7 @@ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
3440
3526
  "modelRuntime",
3441
3527
  "hq",
3442
3528
  "sync",
3529
+ "cloudSync",
3443
3530
  "extensions",
3444
3531
  "acp",
3445
3532
  "fleet",
@@ -11,6 +11,13 @@ export interface SessionEventMap {
11
11
  id: string;
12
12
  sessionId?: string | undefined;
13
13
  usage: Usage;
14
+ /**
15
+ * Register asynchronous session-end producer work during the synchronous
16
+ * `session.ended` callback. Cleanup waits for every registered promise to
17
+ * settle (fulfilled or rejected) before draining review work and closing.
18
+ * Calls made after the callback returns are outside the join contract.
19
+ */
20
+ waitUntil?: ((work: Promise<void>) => void) | undefined;
14
21
  };
15
22
  'session.damaged': {
16
23
  sessionId: string;
@@ -72,6 +79,12 @@ export interface SessionEventMap {
72
79
  providerId: string;
73
80
  modelId: string;
74
81
  maxContext: number;
82
+ /** Previous positive limit when this event represents a live change. */
83
+ previousMaxContext?: number | undefined;
84
+ /** Whether the value came from static resolution or a live provider probe. */
85
+ source?: 'configured' | 'provider' | 'provider_overflow' | undefined;
86
+ /** True when the positive limit decreased and UI should surface a warning. */
87
+ decreased?: boolean | undefined;
75
88
  };
76
89
  'context.repaired': {
77
90
  sessionId?: string | undefined;
@@ -202,9 +202,9 @@ export interface ToolEventMap {
202
202
  input?: unknown | undefined;
203
203
  output?: string | undefined;
204
204
  /**
205
- * The `--- SAGE: … (Memory Injector) ---` block the SAGE middleware
206
- * appended to the result, split off `output` BEFORE the preview cap and
207
- * carried as whole lines (header first). Absent when nothing was injected.
205
+ * Legacy inline `--- SAGE: … (Memory Injector) ---` content split off
206
+ * `output` before the preview cap. Current retrievals travel through
207
+ * `memory.injector_run` and provider memory-evidence blocks instead.
208
208
  *
209
209
  * Surfaces render it as a memory card, never as tool output: sharing the
210
210
  * `output` budget used to cut a memory line mid-fence on short tool
@@ -8,4 +8,5 @@ export { ALIBABA_TOKEN_PLAN_MODELS, alibabaTokenPlanModelMeta, type AlibabaToken
8
8
  export { MODELS_DEV_MODALITY_VALUES, modelsDevCostSchema, modelsDevLimitSchema, modelsDevModalitiesSchema, modelsDevModelSchema, modelsDevPayloadSchema, modelsDevProviderSchema, type ModelsDevModelParsed, type ModelsDevPayloadParsed, type ModelsDevProviderParsed, type ModelsDevSchemaSyncProof, } from './models-dev-schema.js';
9
9
  export { MODEL_PROFILES, TASK_TO_ROLE, inferTaskType, findModelProfile, scoreModelForTask, type ModelProfile, type TaskType, } from './model-intelligence.js';
10
10
  export { ModelRouter, type RouterConfig, type ModelPick, type RouterCosts, type ModelIntelligenceEntry, } from './model-router.js';
11
+ export { hasProviderCredential, hasProviderKeyInConfig, hasProviderKeyInEnv, type ProviderCredentialConfig, type ProviderCredentialSubject, } from './provider-credentials.js';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -2391,6 +2391,20 @@ var ModelRouter = class {
2391
2391
  return matrix;
2392
2392
  }
2393
2393
  };
2394
+
2395
+ // src/models/provider-credentials.ts
2396
+ function hasProviderKeyInEnv(provider, env = process.env) {
2397
+ return (provider.envVars ?? []).some((name) => !!env[name]);
2398
+ }
2399
+ function hasProviderKeyInConfig(providerId, config) {
2400
+ const entry = config?.providers?.[providerId];
2401
+ if (!entry) return false;
2402
+ if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) return true;
2403
+ return Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => !!k?.apiKey);
2404
+ }
2405
+ function hasProviderCredential(provider, config, env = process.env) {
2406
+ return hasProviderKeyInEnv(provider, env) || hasProviderKeyInConfig(provider.id, config);
2407
+ }
2394
2408
  export {
2395
2409
  ALIBABA_TOKEN_PLAN_MODELS,
2396
2410
  CODEX_MODELS,
@@ -2406,6 +2420,9 @@ export {
2406
2420
  codexModelMeta,
2407
2421
  describeCatalogModel,
2408
2422
  findModelProfile,
2423
+ hasProviderCredential,
2424
+ hasProviderKeyInConfig,
2425
+ hasProviderKeyInEnv,
2409
2426
  inferTaskType,
2410
2427
  loadProjectModes,
2411
2428
  loadUserModes,
@@ -0,0 +1,54 @@
1
+ /**
2
+ * "Does this provider have a usable credential?" — one answer.
3
+ *
4
+ * There were three, and they disagreed:
5
+ *
6
+ * - `cli/src/provider-helpers.ts` — env var OR `config.providers[id].apiKey`
7
+ * OR any entry in `apiKeys[]`. The complete rule.
8
+ * - `cli/src/slash-commands/modelcaps.ts` — config only. `/modelcaps` printed
9
+ * a hollow `○` next to every provider keyed purely through
10
+ * `ANTHROPIC_API_KEY`, which is the most common way to key one.
11
+ * - `webui-server/src/server/provider-handlers.ts` — `savedIds.has(id) || env`.
12
+ * `savedIds` means "appears in the saved config at all", so a provider saved
13
+ * with a `baseUrl` and no key reported `hasApiKey: true` and the WebUI
14
+ * offered it as ready to use.
15
+ *
16
+ * The rule itself is small; that it lives in three places is the defect. This
17
+ * module is in core because the CLI and the WebUI server both need it and
18
+ * neither may import the other.
19
+ */
20
+ /** The provider fields the check reads. Both call sites can supply these. */
21
+ export interface ProviderCredentialSubject {
22
+ readonly id: string;
23
+ /** Environment variables that carry this provider's key, if any. */
24
+ readonly envVars?: readonly string[] | undefined;
25
+ }
26
+ /** The saved-config shape the check reads. */
27
+ export interface ProviderCredentialConfig {
28
+ readonly providers?: Readonly<Record<string, {
29
+ readonly apiKey?: string | undefined;
30
+ readonly apiKeys?: readonly ({
31
+ readonly apiKey?: string | undefined;
32
+ } | undefined)[] | undefined;
33
+ } | undefined>> | undefined;
34
+ }
35
+ /**
36
+ * Is a key present in the environment for this provider?
37
+ *
38
+ * Split out because the WebUI reports the two sources separately in places, and
39
+ * because a caller with no config still has a meaningful answer.
40
+ */
41
+ export declare function hasProviderKeyInEnv(provider: ProviderCredentialSubject, env?: Readonly<Record<string, string | undefined>>): boolean;
42
+ /** Is a non-empty key stored in the saved config for this provider? */
43
+ export declare function hasProviderKeyInConfig(providerId: string, config: ProviderCredentialConfig | undefined): boolean;
44
+ /**
45
+ * Can the user call this provider right now?
46
+ *
47
+ * Deliberately does NOT cover keyless local gateways (loopback servers that
48
+ * declare no env vars). They need no credential at all, which is a different
49
+ * question with a different answer — see `isKeylessLocalProvider`. Folding the
50
+ * two together here would make "has a key" mean "is usable" and quietly lose
51
+ * the distinction the picker needs.
52
+ */
53
+ export declare function hasProviderCredential(provider: ProviderCredentialSubject, config: ProviderCredentialConfig | undefined, env?: Readonly<Record<string, string | undefined>>): boolean;
54
+ //# sourceMappingURL=provider-credentials.d.ts.map
@@ -12,4 +12,5 @@ export { persistReviewReport, type ReportIntegrationResult, } from '../plugins/r
12
12
  export { maybeCompactReviewStores, type ReviewStoreMaintenanceResult, } from '../plugins/review-store-maintenance.js';
13
13
  export { createSkillsPlugin } from '../plugins/skills-plugin.js';
14
14
  export { createSyncPlugin } from '../plugins/sync-plugin.js';
15
+ export { createCloudConfigSyncPlugin } from '../plugins/cloud-config-sync-plugin.js';
15
16
  //# sourceMappingURL=index.d.ts.map