@wrongstack/cli 1.1.4 → 1.1.6

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 (103) hide show
  1. package/dist/{acp-MZ5MQBFN.js → acp-CZBSFQB7.js} +6 -5
  2. package/dist/acp-leader/acp-leader-confirm.d.ts +2 -0
  3. package/dist/acp-leader/acp-leader-host-binding.d.ts +1 -0
  4. package/dist/acp-leader/acp-leader-host.d.ts +9 -0
  5. package/dist/acp-leader/acp-leader-picker.d.ts +6 -3
  6. package/dist/acp-leader/acp-leader-provider.d.ts +10 -0
  7. package/dist/acp-leader/acp-leader-skills.d.ts +5 -0
  8. package/dist/acp-leader/acp-leader-stream.d.ts +9 -0
  9. package/dist/{auth-IHQS6AFW.js → auth-2ZG5DBHS.js} +4 -4
  10. package/dist/boot/launch-menu-arrow.d.ts +20 -0
  11. package/dist/boot/launch-menu-types.d.ts +36 -0
  12. package/dist/boot/launch-menu.d.ts +1 -57
  13. package/dist/boot/tui-session-resume-rollback.d.ts +31 -0
  14. package/dist/{boot-auto-thin-7D4OVEWA.js → boot-auto-thin-N2CIGTXE.js} +3 -3
  15. package/dist/{chunk-TZPKOS5G.js → chunk-2XQ6SRKG.js} +2 -2
  16. package/dist/{chunk-3EXRPTAU.js → chunk-5TQ65PXQ.js} +17 -17
  17. package/dist/{chunk-VAQXOZKP.js → chunk-AJM4EFCM.js} +139 -136
  18. package/dist/{chunk-C2BBH4S4.js → chunk-E3MJFAFR.js} +2 -2
  19. package/dist/{chunk-COGI7EQU.js → chunk-EAATL6FL.js} +713 -21
  20. package/dist/{chunk-AWRZWPJ3.js → chunk-FWUIKCCO.js} +152 -123
  21. package/dist/{chunk-TAE3EGBM.js → chunk-H5DCQFUI.js} +5 -5
  22. package/dist/{chunk-BIGSVBPI.js → chunk-KNYIEQAY.js} +64 -61
  23. package/dist/chunk-LYWOV4OX.js +252 -0
  24. package/dist/{chunk-75OLD563.js → chunk-M7PSENGF.js} +3 -1
  25. package/dist/{chunk-I5LJLKA3.js → chunk-MKMM5RIW.js} +12 -5
  26. package/dist/{chunk-ZF4HRR5F.js → chunk-P7MEEUGT.js} +275 -250
  27. package/dist/chunk-PGC44T7N.js +255 -0
  28. package/dist/chunk-QLYPIJ7X.js +258 -0
  29. package/dist/{chunk-JKTJSLUT.js → chunk-QRWXHGLB.js} +242 -18
  30. package/dist/chunk-RN5CFAVQ.js +292 -0
  31. package/dist/{chunk-WDXNBYGD.js → chunk-TDP7CZGE.js} +6 -1
  32. package/dist/{chunk-6VPWQ4TG.js → chunk-UROB7MK5.js} +2 -1
  33. package/dist/{chunk-IJLIAPCW.js → chunk-VPSAKGLK.js} +47 -47
  34. package/dist/{chunk-2LTHDKZX.js → chunk-W7X23HI6.js} +18 -18
  35. package/dist/{chunk-GKGWPFW2.js → chunk-X4DEQLAZ.js} +1 -1
  36. package/dist/{cli-context-E44HQ4ZO.js → cli-context-6KS3HV5R.js} +247 -236
  37. package/dist/{cli-main-OT63I2FX.js → cli-main-NF3YGDME.js} +1779 -1494
  38. package/dist/cli-main-boot.d.ts +1 -1
  39. package/dist/config-doctor-known-keys.d.ts +33 -0
  40. package/dist/config-doctor.d.ts +2 -47
  41. package/dist/config-history-support.d.ts +36 -0
  42. package/dist/config-history.d.ts +1 -1
  43. package/dist/{config-transfer-Y7XJMJRN.js → config-transfer-B4GOZTFC.js} +2 -2
  44. package/dist/{execution-OK34CJKJ.js → execution-RVDUXLNT.js} +6522 -6426
  45. package/dist/execution-tui.d.ts +30 -0
  46. package/dist/fleet/acp-wrongstack-bridge.d.ts +11 -3
  47. package/dist/fleet/host-acp.d.ts +3 -0
  48. package/dist/fleet/wrongforge-client.d.ts +116 -0
  49. package/dist/fleet/wrongforge-gateway.d.ts +20 -0
  50. package/dist/fleet/wrongforge-info.d.ts +32 -0
  51. package/dist/fleet/wrongforge-quota.d.ts +10 -0
  52. package/dist/fleet/wrongforge-runner.d.ts +37 -0
  53. package/dist/fleet/wrongforge-runtime.d.ts +10 -0
  54. package/dist/{hq-JD5AZVMG.js → hq-V2M4IYCI.js} +320 -306
  55. package/dist/hq-server/login-attempt-store.d.ts +2 -0
  56. package/dist/hq-server/sweeps.d.ts +28 -0
  57. package/dist/{hq-server-BI23EPM7.js → hq-server-QM7EIVGJ.js} +3 -3
  58. package/dist/index.js +5 -5
  59. package/dist/mcp-22CFBX4H.js +236 -0
  60. package/dist/mcp-serve.d.ts +0 -63
  61. package/dist/{modeldiag-HDIKG37X.js → modeldiag-5PBB777J.js} +4 -4
  62. package/dist/{per-subcommand-help-4GYSCMLU.js → per-subcommand-help-MQGERD4E.js} +2 -2
  63. package/dist/plugin-llm-command.d.ts +9 -0
  64. package/dist/{plugin-usage-TYF5HY4K.js → plugin-usage-UWKYMNQU.js} +3 -3
  65. package/dist/{plugins-XGAXDLPT.js → plugins-VP7JQH47.js} +4 -4
  66. package/dist/{sdd-runtime-6MFUXWJK.js → sdd-runtime-HIJ4Z5TF.js} +2 -2
  67. package/dist/services/sdd/state.d.ts +2 -0
  68. package/dist/session-event-wiring-types.d.ts +62 -0
  69. package/dist/session-event-wiring.d.ts +2 -52
  70. package/dist/{sessions-config-SJVQXCIE.js → sessions-config-7MK742OW.js} +2 -2
  71. package/dist/short-circuit-flags-276OLPIN.js +11 -0
  72. package/dist/slash-commands/command-context-fleet.d.ts +187 -0
  73. package/dist/slash-commands/command-context-sdd-goal.d.ts +102 -0
  74. package/dist/slash-commands/command-context.d.ts +3 -282
  75. package/dist/slash-commands/project-switch-interactive.d.ts +11 -0
  76. package/dist/slash-commands/setmodel-helpers.d.ts +36 -0
  77. package/dist/subcommands/handlers/hq-token.d.ts +3 -0
  78. package/dist/subcommands/handlers/hq.d.ts +0 -21
  79. package/dist/subcommands/handlers/per-subcommand-help-operations.d.ts +8 -0
  80. package/dist/subcommands/handlers/tools-cli-args.d.ts +57 -0
  81. package/dist/subcommands/handlers/tools-cli.d.ts +5 -0
  82. package/dist/subcommands/handlers/tools-serve.d.ts +4 -0
  83. package/dist/{subcommands-YADUV7IJ.js → subcommands-SKJPUJ7N.js} +2 -2
  84. package/dist/tool-gateway-http.d.ts +30 -0
  85. package/dist/tool-gateway.d.ts +185 -0
  86. package/dist/tools-cli-44MLIUQO.js +509 -0
  87. package/dist/tools-serve-TRCET2M7.js +102 -0
  88. package/dist/{tools-skills-55QVTMSK.js → tools-skills-WDTMCE46.js} +7 -2
  89. package/dist/tuneup-runtime-checks.d.ts +7 -0
  90. package/dist/tuneup-types.d.ts +118 -0
  91. package/dist/tuneup.d.ts +3 -122
  92. package/dist/{typesafe-YBEADNAH.js → typesafe-QLSMFU7J.js} +1 -1
  93. package/dist/{version-help-RBZC5GZZ.js → version-help-X6CC4CFY.js} +2 -2
  94. package/dist/{webui-server-CB64CFDG.js → webui-server-6I6QOECV.js} +44 -6
  95. package/dist/wiring/provider-runtime-setup.d.ts +3 -1
  96. package/dist/wiring/session-runtime.d.ts +1 -1
  97. package/dist/wrongforge-gateway-UOFBK56J.js +92 -0
  98. package/dist/wrongforge-leader/wrongforge-leader-gateway.d.ts +9 -0
  99. package/dist/wrongforge-leader/wrongforge-leader-picker.d.ts +12 -0
  100. package/dist/wrongforge-leader/wrongforge-leader-provider.d.ts +56 -0
  101. package/package.json +1 -1
  102. package/dist/mcp-W7A5HUXV.js +0 -560
  103. package/dist/short-circuit-flags-QEXUS3BY.js +0 -11
@@ -1,3 +1,7 @@
1
+ import {
2
+ resolveWrongForgeEndpoint,
3
+ wrongForgeCall
4
+ } from "./chunk-PGC44T7N.js";
1
5
 
2
6
  // src/fleet/acp-wrongstack-bridge.ts
3
7
  import { statSync } from "node:fs";
@@ -17,6 +21,43 @@ function wstackLaunch(args, entry = process.argv[1]) {
17
21
  }
18
22
  return { command: "wstack", args };
19
23
  }
24
+ function wstackToolsCommand(entry = process.argv[1], platform = process.platform) {
25
+ const launch = wstackLaunch(["tools"], entry);
26
+ return [launch.command, ...launch.args].map((word) => shellWord(word, platform)).join(" ");
27
+ }
28
+ function shellWord(word, platform) {
29
+ const w = platform === "win32" ? word.replace(/\\/g, "/") : word;
30
+ return /^[A-Za-z0-9_./:@=+-]+$/.test(w) ? w : `"${w.replace(/(["\\$`])/g, "\\$1")}"`;
31
+ }
32
+ function cliIndexNote(cmd) {
33
+ return [
34
+ `- \`${cmd} run <tool> -- --<field> <value>\` (run through your shell): a prebuilt, ranked`,
35
+ " symbol index of this repository, caught up with the disk on every call. Prefer it over",
36
+ ' grep/glob and whole-file reads for code navigation: codebase-context --query "<task>"',
37
+ " (files relevant to a task), codebase-search --query <name> (where something is defined),",
38
+ " codebase-incoming-calls / codebase-outgoing-calls --symbol <name> (callers, callees),",
39
+ " codebase-read-symbol --file <path> --symbol <name> (one symbol's source),",
40
+ " codebase-skeleton --path <file-or-dir> (an outline), codebase-impact-analysis --symbol",
41
+ ` <name> (what a change affects). \`${cmd} describe <tool>\` lists a tool's fields;`,
42
+ ` \`${cmd} batch\` runs several JSONL calls ({"tool":\u2026,"input":\u2026}) in one process.`,
43
+ " Use grep for literal text such as strings, comments and config values."
44
+ ];
45
+ }
46
+ function serviceNotes(servers, delegated, indexCli) {
47
+ const viaCli = indexCli !== void 0 && servers.includes("codebase-index");
48
+ const mcp = viaCli ? servers.filter((s) => s !== "codebase-index") : servers;
49
+ return [
50
+ ...mcp.length > 0 ? [
51
+ "WrongStack's project services are available as MCP servers:",
52
+ ...mcp.map((s) => SERVER_NOTES[s](delegated))
53
+ ] : [],
54
+ ...viaCli ? [
55
+ ...mcp.length > 0 ? [""] : [],
56
+ "WrongStack's codebase index is available through its CLI:",
57
+ ...cliIndexNote(indexCli)
58
+ ] : []
59
+ ];
60
+ }
20
61
  var SERVER_NAMES = {
21
62
  sage: "wrongstack-sage",
22
63
  mailbox: "wrongstack-mailbox",
@@ -31,9 +72,15 @@ var SERVER_NOTES = {
31
72
  "codebase-index": () => "- wrongstack-index: a prebuilt, ranked symbol index of this repository, kept fresh by WrongStack. Prefer it over grep/glob and whole-file reads for code navigation: codebase_search (where something is defined), codebase_incoming_calls / codebase_outgoing_calls (callers, callees), codebase_read_symbol (one symbol's source), codebase_skeleton (a file or folder outline), codebase_impact_analysis (what a change affects), codebase_context (files relevant to a task), codebase_repo_map (overview). Use grep for literal text such as strings, comments and config values.",
32
73
  "requirement-intake": () => "- wrongstack-intake: the project's requirements intake. List open requests; file a new one when you are handed an unrecorded software request."
33
74
  };
34
- function serviceWorkflow(servers) {
75
+ function serviceWorkflow(servers, indexCli) {
35
76
  return [
36
- ...servers.includes("codebase-index") ? [
77
+ ...servers.includes("codebase-index") ? indexCli !== void 0 ? [
78
+ "Use the WrongStack index first for code navigation; use your file tools for edits,",
79
+ "exact literal searches, and verification. Start a code task with",
80
+ `${indexCli} run codebase-context -- --query "<the task in a few words>"; for a named`,
81
+ `symbol use ${indexCli} run codebase-search -- --query <name>. If the index lacks a`,
82
+ "result, fall back to your tools."
83
+ ] : [
37
84
  "Use wrongstack-index first for code navigation; use your file tools for edits, exact",
38
85
  "literal searches, and verification. Start a code task with",
39
86
  "mcp__wrongstack-index__codebase_context (task description); for a named symbol use",
@@ -57,10 +104,12 @@ function serviceWorkflow(servers) {
57
104
  "If a project service is unavailable, continue with available tools and report the gap."
58
105
  ];
59
106
  }
60
- function bridgeTaskKickoff(servers) {
107
+ function bridgeTaskKickoff(servers, indexCli) {
61
108
  return [
62
109
  "Project-service kickoff for this task:",
63
- ...servers.includes("codebase-index") ? ["First use mcp__wrongstack-index__codebase_context to locate relevant code."] : [],
110
+ ...servers.includes("codebase-index") ? [
111
+ indexCli !== void 0 ? `First run ${indexCli} run codebase-context -- --query "<the task>" to locate relevant code.` : "First use mcp__wrongstack-index__codebase_context to locate relevant code."
112
+ ] : [],
64
113
  ...servers.includes("sage") ? ["Use mcp__wrongstack-sage__memory_search for prior project guidance."] : [],
65
114
  ...servers.includes("mailbox") ? ['Check mcp__wrongstack-mailbox__mailbox_read with action "inbox".'] : [],
66
115
  ...servers.includes("kanban") ? ["If the task names a board card, inspect it with mcp__wrongstack-kanban__kanban_read."] : [],
@@ -93,7 +142,7 @@ function serverArgs(server, projectRoot, ctx) {
93
142
  return [...base, "--writable"];
94
143
  }
95
144
  }
96
- function bridgeNote(servers, delegated = true) {
145
+ function bridgeNote(servers, delegated = true, indexCli) {
97
146
  return [
98
147
  "You are working as a delegated worker inside a WrongStack project.",
99
148
  "WrongStack is the orchestrator the user runs: it sent you this task and receives your",
@@ -104,10 +153,9 @@ function bridgeNote(servers, delegated = true) {
104
153
  "from the user who delegated the task, and answer what it asks."
105
154
  ] : [],
106
155
  "",
107
- "WrongStack's project services are available as MCP servers:",
108
- ...servers.map((s) => SERVER_NOTES[s](delegated)),
156
+ ...serviceNotes(servers, delegated, indexCli),
109
157
  "",
110
- ...serviceWorkflow(servers),
158
+ ...serviceWorkflow(servers, indexCli),
111
159
  "",
112
160
  "WrongStack applies the user's rules to your edits and commands. A refused permission",
113
161
  "comes from them (a deny rule the user wrote, a history-rewriting git command, a",
@@ -115,15 +163,14 @@ function bridgeNote(servers, delegated = true) {
115
163
  "was refused in your final reply."
116
164
  ].join("\n");
117
165
  }
118
- function leaderBridgeNote(servers) {
166
+ function leaderBridgeNote(servers, indexCli) {
119
167
  return [
120
168
  "You are the leader of a WrongStack session. The user talks to you through WrongStack",
121
169
  "(its terminal UI or web UI), which shows your replies, your tool calls and their results.",
122
170
  "",
123
- "WrongStack's project services are available as MCP servers:",
124
- ...servers.map((s) => SERVER_NOTES[s](false)),
171
+ ...serviceNotes(servers, false, indexCli),
125
172
  "",
126
- ...serviceWorkflow(servers),
173
+ ...serviceWorkflow(servers, indexCli),
127
174
  "",
128
175
  "WrongStack applies the user's rules to your edits and commands and asks the user when one",
129
176
  "needs approval. A refused permission is the user's decision or rule: do not retry it or",
@@ -167,21 +214,24 @@ function installedBridgeAgents(acp, isOnPath = (c) => onPath(c)) {
167
214
  }
168
215
  function wrongstackBridgeSetup(agentId, projectRoot, config, role = "worker") {
169
216
  const agents = config?.agents ?? DEFAULT_BRIDGE_AGENTS;
170
- if (!agents.some((a) => a === agentId || REGISTRY_ID_ALIASES[a] === agentId)) return void 0;
217
+ const listed = (a) => a === agentId || REGISTRY_ID_ALIASES[a] === agentId || REGISTRY_ID_ALIASES[agentId] === a;
218
+ if (!agents.some(listed)) return void 0;
171
219
  const servers = [...new Set(config?.servers ?? DEFAULT_BRIDGE_SERVERS)].filter(
172
220
  (s) => Object.hasOwn(SERVER_NAMES, s)
173
221
  );
174
222
  if (servers.length === 0) return void 0;
223
+ const indexCli = config?.indexSurface === "cli" && servers.includes("codebase-index") ? wstackToolsCommand() : void 0;
224
+ const attached = indexCli ? servers.filter((s) => s !== "codebase-index") : servers;
175
225
  return (_task, ctx) => ({
176
- promptPrefix: bridgeTaskKickoff(servers),
177
- mcpServers: servers.map((server) => {
226
+ promptPrefix: bridgeTaskKickoff(servers, indexCli),
227
+ mcpServers: attached.map((server) => {
178
228
  const launch = wstackLaunch(serverArgs(server, projectRoot, ctx));
179
229
  return { name: SERVER_NAMES[server], ...launch, env: [] };
180
230
  }),
181
231
  meta: {
182
232
  systemPrompt: {
183
233
  // `@session` only reaches someone when a WrongStack session delegated the task.
184
- append: role === "leader" ? leaderBridgeNote(servers) : bridgeNote(servers, ctx.sessionId !== void 0)
234
+ append: role === "leader" ? leaderBridgeNote(servers, indexCli) : bridgeNote(servers, ctx.sessionId !== void 0, indexCli)
185
235
  }
186
236
  }
187
237
  });
@@ -517,12 +567,186 @@ ${command}
517
567
  }
518
568
  }
519
569
 
570
+ // src/fleet/wrongforge-info.ts
571
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
572
+ import path from "node:path";
573
+ import { wstackGlobalRoot } from "@wrongstack/core/utils";
574
+ function wrongForgeInfoCacheFile() {
575
+ return path.join(wstackGlobalRoot(), "cache", "wrongforge-info.json");
576
+ }
577
+ function parseWrongForgeInfo(body) {
578
+ const o = body && typeof body === "object" ? body : {};
579
+ const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
580
+ const def = o["default_model"] ?? o["defaultModel"];
581
+ const details = o["model_details"] ?? o["modelDetails"];
582
+ const effort = o["effort"];
583
+ const levels = effort?.levels ?? o["effortLevels"];
584
+ return {
585
+ models: strings(o["models"]),
586
+ ...typeof def === "string" && def ? { defaultModel: def } : {},
587
+ features: strings(o["features"]),
588
+ ...Array.isArray(levels) ? { effortLevels: strings(levels) } : {},
589
+ ...Array.isArray(details) ? {
590
+ modelDetails: details.flatMap((value) => {
591
+ if (!value || typeof value !== "object") return [];
592
+ const d = value;
593
+ if (typeof d["id"] !== "string" || !d["id"]) return [];
594
+ return [
595
+ {
596
+ id: d["id"],
597
+ description: typeof d["description"] === "string" ? d["description"] : "",
598
+ effortLevels: strings(d["effort_levels"] ?? d["effortLevels"])
599
+ }
600
+ ];
601
+ })
602
+ } : {}
603
+ };
604
+ }
605
+ function readCachedWrongForgeInfo(file = wrongForgeInfoCacheFile()) {
606
+ try {
607
+ const info = parseWrongForgeInfo(JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, "")));
608
+ return info.models.length > 0 ? info : void 0;
609
+ } catch {
610
+ return void 0;
611
+ }
612
+ }
613
+ function writeWrongForgeInfoCache(info, file = wrongForgeInfoCacheFile()) {
614
+ if (info.models.length === 0) return;
615
+ try {
616
+ mkdirSync(path.dirname(file), { recursive: true });
617
+ const tmp = `${file}.${process.pid}.tmp`;
618
+ writeFileSync(tmp, `${JSON.stringify(info)}
619
+ `);
620
+ renameSync(tmp, file);
621
+ } catch {
622
+ }
623
+ }
624
+ async function fetchWrongForgeInfo(ep, opts = {}) {
625
+ const res = await (opts.fetchImpl ?? fetch)(`${ep.url}/v1/info`, {
626
+ headers: { authorization: `Bearer ${ep.apiKey}` },
627
+ ...opts.signal ? { signal: opts.signal } : {}
628
+ });
629
+ if (!res.ok) throw new Error(`WrongForge /v1/info answered ${res.status}`);
630
+ return parseWrongForgeInfo(await res.json());
631
+ }
632
+ function refreshWrongForgeInfoCache(ep) {
633
+ if (!ep) return;
634
+ void fetchWrongForgeInfo(ep, { signal: AbortSignal.timeout(3e3) }).then((info) => writeWrongForgeInfoCache(info)).catch(() => void 0);
635
+ }
636
+
637
+ // src/fleet/wrongforge-quota.ts
638
+ import {
639
+ DEFAULT_QUOTA_METER,
640
+ recordProviderQuota
641
+ } from "@wrongstack/core/quota";
642
+ var reads = /* @__PURE__ */ new Map();
643
+ var REFRESH_MS = 2e4;
644
+ function object(value) {
645
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
646
+ }
647
+ function percent(value) {
648
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100;
649
+ }
650
+ async function reportWrongForgeQuota(opts = {}) {
651
+ const endpoint = opts.endpoint ?? resolveWrongForgeEndpoint();
652
+ if (!endpoint) return false;
653
+ const providerId = opts.providerId ?? "wrongforge";
654
+ const key = `${providerId}\0${endpoint.url}\0${endpoint.apiKey}`;
655
+ const prior = reads.get(key);
656
+ if (prior?.pending) {
657
+ if (opts.force && !prior.forced) {
658
+ await prior.pending;
659
+ return reportWrongForgeQuota(opts);
660
+ }
661
+ return prior.pending;
662
+ }
663
+ if (!opts.force && prior && Date.now() - prior.at < REFRESH_MS) return prior.ok;
664
+ const state = { at: Date.now(), ok: false, forced: opts.force === true };
665
+ reads.set(key, state);
666
+ if (reads.size > 128)
667
+ for (const [id, entry] of reads) {
668
+ if (!entry.pending && id !== key) {
669
+ reads.delete(id);
670
+ break;
671
+ }
672
+ }
673
+ state.pending = (async () => {
674
+ try {
675
+ const signal = AbortSignal.timeout(opts.timeoutMs ?? 1e4);
676
+ const body = object(
677
+ await wrongForgeCall(endpoint, opts.fetchImpl)(
678
+ "GET",
679
+ `/v1/usage${opts.force ? "?force=1" : ""}`,
680
+ void 0,
681
+ signal
682
+ )
683
+ );
684
+ if (signal.aborted) return false;
685
+ const windows = [];
686
+ for (const spec of [
687
+ { key: "five_hour", id: "primary", label: "5h", windowMinutes: 300 },
688
+ { key: "weekly", id: "secondary", label: "weekly", windowMinutes: 10080 }
689
+ ]) {
690
+ const raw = object(body[spec.key]);
691
+ if (!percent(raw.percent)) continue;
692
+ const reset = typeof raw.resetsAt === "string" ? Date.parse(raw.resetsAt) : Number.NaN;
693
+ windows.push({
694
+ id: spec.id,
695
+ label: spec.label,
696
+ windowMinutes: spec.windowMinutes,
697
+ usedPercent: raw.percent,
698
+ ...Number.isFinite(reset) ? { resetsAt: Math.floor(reset / 1e3) } : {}
699
+ });
700
+ }
701
+ if (windows.length === 0) return false;
702
+ const extra = object(body.extra_usage);
703
+ const snapshot = {
704
+ providerId,
705
+ meterId: DEFAULT_QUOTA_METER,
706
+ meterLabel: "WrongForge \xB7 Claude subscription",
707
+ windows,
708
+ capturedAt: Date.now()
709
+ };
710
+ if (extra.enabled === false) snapshot.credits = { hasCredits: false, unlimited: false };
711
+ if (extra.enabled === true) {
712
+ snapshot.note = percent(extra.utilization) ? `Extra usage enabled \xB7 ${extra.utilization}% used` : "Extra usage enabled";
713
+ if (typeof extra.usedCredits === "number" && Number.isFinite(extra.usedCredits) && extra.usedCredits >= 0 && typeof extra.monthlyLimit === "number" && Number.isFinite(extra.monthlyLimit) && extra.monthlyLimit >= 0) {
714
+ const remaining = Math.max(0, extra.monthlyLimit - extra.usedCredits);
715
+ snapshot.credits = {
716
+ hasCredits: remaining > 0,
717
+ unlimited: false,
718
+ balance: `${remaining} credits remaining (${extra.usedCredits}/${extra.monthlyLimit})`
719
+ };
720
+ }
721
+ } else if (extra.enabled === false) snapshot.note = "Extra usage disabled";
722
+ const exhausted = windows.filter(
723
+ (window) => window.usedPercent >= 100 && (window.resetsAt === void 0 || window.resetsAt * 1e3 > snapshot.capturedAt)
724
+ );
725
+ exhausted.sort((a, b) => (b.resetsAt ?? Infinity) - (a.resetsAt ?? Infinity));
726
+ if (exhausted[0]) snapshot.reachedWindowId = exhausted[0].id;
727
+ recordProviderQuota(providerId, [snapshot]);
728
+ state.ok = true;
729
+ return true;
730
+ } catch {
731
+ return false;
732
+ } finally {
733
+ delete state.pending;
734
+ }
735
+ })();
736
+ return state.pending;
737
+ }
738
+
520
739
  export {
521
740
  DEFAULT_BRIDGE_SERVERS,
522
741
  installedBridgeAgents,
523
742
  wrongstackBridgeSetup,
524
743
  createProjectKanbanTransitionHandler,
525
744
  createProjectKanbanAssignHandler,
526
- createHqCommandDispatcher
745
+ createHqCommandDispatcher,
746
+ parseWrongForgeInfo,
747
+ readCachedWrongForgeInfo,
748
+ writeWrongForgeInfoCache,
749
+ refreshWrongForgeInfoCache,
750
+ reportWrongForgeQuota
527
751
  };
528
- //# sourceMappingURL=chunk-JKTJSLUT.js.map
752
+ //# sourceMappingURL=chunk-QRWXHGLB.js.map
@@ -0,0 +1,292 @@
1
+ import {
2
+ withheldReason
3
+ } from "./chunk-LYWOV4OX.js";
4
+
5
+ // src/tool-gateway-http.ts
6
+ import { randomUUID } from "node:crypto";
7
+ import { realpath } from "node:fs/promises";
8
+ import { createServer } from "node:http";
9
+ import * as path from "node:path";
10
+ import { toErrorMessage } from "@wrongstack/core/utils";
11
+ import {
12
+ bearerTokenMatches,
13
+ hostHeaderIsAcceptable,
14
+ isJsonContentType,
15
+ isLoopbackHost,
16
+ originIsAcceptable
17
+ } from "@wrongstack/mcp";
18
+ var BODY_CAP_BYTES = 4 * 1024 * 1024;
19
+ var DEFAULT_MAX_CONCURRENT_CALLS = 16;
20
+ var CALLBACK_TIMEOUT_MS = 3e4;
21
+ function serveToolGatewayHttp(opts) {
22
+ const host = opts.host ?? "127.0.0.1";
23
+ if (!opts.token || opts.token.length < 16) {
24
+ return Promise.reject(
25
+ new Error("tools serve: a bearer token of at least 16 characters is required")
26
+ );
27
+ }
28
+ const maxConcurrent = opts.maxConcurrentCalls ?? DEFAULT_MAX_CONCURRENT_CALLS;
29
+ const pending = /* @__PURE__ */ new Set();
30
+ let inFlight = 0;
31
+ const server = createServer((req, res) => {
32
+ void handle(req, res).catch((err) => {
33
+ opts.log?.(`tools serve: request failed: ${toErrorMessage(err)}`);
34
+ if (!res.headersSent) send(res, 500, { ok: false, error: "internal_error" });
35
+ });
36
+ });
37
+ async function handle(req, res) {
38
+ if (!originIsAcceptable(req, host))
39
+ return send(res, 403, { ok: false, error: "cross_origin_forbidden" });
40
+ if (!hostHeaderIsAcceptable(req, host))
41
+ return send(res, 403, { ok: false, error: "untrusted_host_header" });
42
+ const url = new URL(req.url ?? "/", "http://local");
43
+ if (req.method === "GET" && url.pathname === "/health") return send(res, 200, { ok: true });
44
+ if (!bearerTokenMatches(req.headers.authorization, opts.token)) {
45
+ return send(res, 401, { ok: false, error: "unauthorized" });
46
+ }
47
+ let parts;
48
+ try {
49
+ parts = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
50
+ } catch {
51
+ return send(res, 400, { ok: false, error: "invalid_path_encoding" });
52
+ }
53
+ if (parts[0] !== "v1" || parts[1] !== "tools")
54
+ return send(res, 404, { ok: false, error: "not_found" });
55
+ const name = parts[2];
56
+ if (req.method === "GET" && parts.length === 2) {
57
+ return send(res, 200, {
58
+ mode: opts.gateway.mode,
59
+ capabilities: opts.gateway.capabilities,
60
+ tools: opts.gateway.tools.map(describe)
61
+ });
62
+ }
63
+ if (req.method === "GET" && name && parts.length === 3) {
64
+ const tool = opts.gateway.get(name);
65
+ if (!tool) return send(res, 404, notExposedBody(name));
66
+ return send(res, 200, describe(tool));
67
+ }
68
+ if (req.method === "POST" && name && parts.length === 4 && parts[3] === "call") {
69
+ return call(req, res, name);
70
+ }
71
+ return send(res, req.method === "GET" || req.method === "POST" ? 404 : 405, {
72
+ ok: false,
73
+ error: req.method === "GET" || req.method === "POST" ? "not_found" : "method_not_allowed"
74
+ });
75
+ }
76
+ async function call(req, res, name) {
77
+ if (!isJsonContentType(req.headers["content-type"])) {
78
+ return send(res, 415, { ok: false, error: "content_type_must_be_json" });
79
+ }
80
+ const body = await readBody(req);
81
+ if (body === "too_large") return send(res, 413, { ok: false, error: "payload_too_large" });
82
+ let parsed;
83
+ try {
84
+ parsed = parseCallRequest(body, opts.allowRemoteCallbacks === true);
85
+ } catch (err) {
86
+ return send(res, 400, { ok: false, error: "bad_request", message: toErrorMessage(err) });
87
+ }
88
+ if (!opts.gateway.get(name)) {
89
+ return send(res, 404, { call_id: parsed.callId, ...notExposedBody(name) });
90
+ }
91
+ let cwd;
92
+ if (parsed.workdir !== void 0) {
93
+ const resolved = await resolveWorkdir(parsed.workdir, opts.gateway.projectRoot);
94
+ if ("error" in resolved) {
95
+ return send(res, 400, {
96
+ call_id: parsed.callId,
97
+ ok: false,
98
+ tool: name,
99
+ error: "bad_workdir",
100
+ message: resolved.error
101
+ });
102
+ }
103
+ cwd = resolved.cwd;
104
+ }
105
+ if (inFlight >= maxConcurrent) {
106
+ return send(res, 429, { call_id: parsed.callId, ok: false, error: "too_many_calls" });
107
+ }
108
+ inFlight++;
109
+ const settle = run(name, parsed, cwd).finally(() => {
110
+ inFlight--;
111
+ });
112
+ if (!parsed.callback) {
113
+ return send(res, 200, await settle);
114
+ }
115
+ send(res, 202, { call_id: parsed.callId, status: "accepted" });
116
+ const delivery = settle.then((result) => deliverCallback(parsed.callback, result)).catch(
117
+ (err) => opts.log?.(`tools serve: callback for ${parsed.callId} failed: ${toErrorMessage(err)}`)
118
+ );
119
+ pending.add(delivery);
120
+ void delivery.finally(() => pending.delete(delivery));
121
+ }
122
+ async function run(name, req, cwd) {
123
+ const base = { call_id: req.callId, tool: name };
124
+ try {
125
+ const outcome = await opts.gateway.call(name, req.input, { cwd });
126
+ if (outcome.kind === "not_exposed") return { ...base, ...notExposedBody(name) };
127
+ if (outcome.kind === "confirm_required") {
128
+ return {
129
+ ...base,
130
+ ok: false,
131
+ error: "confirm_required",
132
+ message: `Tool "${name}" needs an interactive confirmation for this input, which this server cannot give.`
133
+ };
134
+ }
135
+ return {
136
+ ...base,
137
+ ok: !outcome.isError && outcome.settlement === "completed",
138
+ settlement: outcome.settlement,
139
+ duration_ms: outcome.durationMs,
140
+ content: outcome.content
141
+ };
142
+ } catch (err) {
143
+ return { ...base, ok: false, error: "tool_threw", message: toErrorMessage(err) };
144
+ }
145
+ }
146
+ async function deliverCallback(callback, result) {
147
+ const doFetch = opts.fetchImpl ?? fetch;
148
+ const res = await doFetch(callback.url, {
149
+ method: "POST",
150
+ headers: {
151
+ "content-type": "application/json",
152
+ ...callback.token ? { authorization: `Bearer ${callback.token}` } : {}
153
+ },
154
+ body: JSON.stringify(result),
155
+ signal: AbortSignal.timeout(CALLBACK_TIMEOUT_MS),
156
+ redirect: "error"
157
+ });
158
+ if (!res.ok) throw new Error(`callback answered HTTP ${res.status}`);
159
+ }
160
+ function notExposedBody(name) {
161
+ return {
162
+ ok: false,
163
+ tool: name,
164
+ error: "not_exposed",
165
+ message: withheldReason(opts.gateway, opts.registry, name)
166
+ };
167
+ }
168
+ return new Promise((resolve2, reject) => {
169
+ server.once("error", reject);
170
+ server.listen(opts.port ?? 0, host, () => {
171
+ server.removeListener("error", reject);
172
+ if (opts.unref) server.unref();
173
+ const port = server.address().port;
174
+ const shown = host === "::1" ? "[::1]" : host;
175
+ resolve2({
176
+ url: `http://${shown}:${port}/`,
177
+ port,
178
+ host,
179
+ close: async () => {
180
+ await new Promise((done) => server.close(() => done()));
181
+ await Promise.allSettled([...pending]);
182
+ }
183
+ });
184
+ });
185
+ });
186
+ }
187
+ function describe(tool) {
188
+ return {
189
+ name: tool.name,
190
+ description: tool.description,
191
+ input_schema: tool.inputSchema ?? { type: "object" },
192
+ capabilities: tool.capabilities ?? [],
193
+ mutating: tool.mutating
194
+ };
195
+ }
196
+ function parseCallRequest(body, allowRemoteCallbacks) {
197
+ const text = body.trim();
198
+ const raw = text === "" ? {} : JSON.parse(text);
199
+ if (!isRecord(raw)) throw new Error("the body must be a JSON object");
200
+ const input = raw["input"] ?? {};
201
+ if (!isRecord(input)) throw new Error(`"input" must be a JSON object (the tool's input)`);
202
+ const callId = raw["call_id"] ?? randomUUID();
203
+ if (typeof callId !== "string" || callId.length === 0 || callId.length > 200) {
204
+ throw new Error('"call_id" must be a non-empty string of at most 200 characters');
205
+ }
206
+ const cb = raw["callback"];
207
+ const workdir = raw["workdir"];
208
+ if (workdir !== void 0 && workdir !== null && (typeof workdir !== "string" || workdir === "")) {
209
+ throw new Error('"workdir" must be a non-empty path');
210
+ }
211
+ const base = { input, callId, ...typeof workdir === "string" ? { workdir } : {} };
212
+ if (cb === void 0) return base;
213
+ if (!isRecord(cb) || typeof cb["url"] !== "string") {
214
+ throw new Error('"callback" must be { "url": string, "token"?: string }');
215
+ }
216
+ let target;
217
+ try {
218
+ target = new URL(cb["url"]);
219
+ } catch {
220
+ throw new Error('"callback.url" is not a valid URL');
221
+ }
222
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
223
+ throw new Error('"callback.url" must be http(s)');
224
+ }
225
+ if (!allowRemoteCallbacks && !isLoopbackHost(target.hostname.replace(/^\[(.*)\]$/, "$1"))) {
226
+ throw new Error(
227
+ '"callback.url" must be a loopback address (start the server with --allow-remote-callbacks)'
228
+ );
229
+ }
230
+ const token = cb["token"];
231
+ if (token !== void 0 && typeof token !== "string")
232
+ throw new Error('"callback.token" must be a string');
233
+ return { ...base, callback: { url: target.toString(), token } };
234
+ }
235
+ async function resolveWorkdir(workdir, projectRoot) {
236
+ let root;
237
+ let target;
238
+ try {
239
+ root = await realpath(projectRoot);
240
+ target = await realpath(path.resolve(projectRoot, workdir));
241
+ } catch {
242
+ return { error: `workdir "${workdir}" does not exist on the WrongStack side` };
243
+ }
244
+ const fold = (p) => process.platform === "win32" ? p.toLowerCase() : p;
245
+ const rel = path.relative(fold(root), fold(target));
246
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
247
+ return {
248
+ error: `workdir "${workdir}" is outside the project this server serves (${root}); start \`wstack tools serve\` in the task's directory`
249
+ };
250
+ }
251
+ return { cwd: target };
252
+ }
253
+ function readBody(req) {
254
+ return new Promise((resolve2, reject) => {
255
+ const chunks = [];
256
+ let size = 0;
257
+ let tooLarge = false;
258
+ req.on("data", (chunk) => {
259
+ if (tooLarge) return;
260
+ size += chunk.byteLength;
261
+ if (size > BODY_CAP_BYTES) {
262
+ tooLarge = true;
263
+ resolve2("too_large");
264
+ return;
265
+ }
266
+ chunks.push(chunk);
267
+ });
268
+ req.on("end", () => {
269
+ if (!tooLarge) resolve2(Buffer.concat(chunks).toString("utf8"));
270
+ });
271
+ req.on("error", reject);
272
+ });
273
+ }
274
+ function send(res, status, body) {
275
+ res.writeHead(status, {
276
+ "content-type": "application/json",
277
+ // This endpoint executes tools: nothing about it should be embedded,
278
+ // sniffed into another type, or leak its URL onward. No CORS headers.
279
+ "x-content-type-options": "nosniff",
280
+ "x-frame-options": "DENY",
281
+ "referrer-policy": "no-referrer"
282
+ });
283
+ res.end(JSON.stringify(body));
284
+ }
285
+ function isRecord(value) {
286
+ return typeof value === "object" && value !== null && !Array.isArray(value);
287
+ }
288
+
289
+ export {
290
+ serveToolGatewayHttp
291
+ };
292
+ //# sourceMappingURL=chunk-RN5CFAVQ.js.map
@@ -61,6 +61,11 @@ var SDDState = class {
61
61
  if (this.versioning === null) this.versioning = new SpecVersioning();
62
62
  return this.versioning;
63
63
  }
64
+ /** Drain fire-and-forget tracker writes before deleting or replacing the store. */
65
+ async flushTaskWrites() {
66
+ await this.taskTracker?.flushPersistence();
67
+ await this.taskStore?.flush?.();
68
+ }
64
69
  clearTaskState() {
65
70
  this.taskStore = null;
66
71
  this.taskTracker = null;
@@ -406,4 +411,4 @@ export {
406
411
  getTaskGraphId,
407
412
  getTaskTrackerExport
408
413
  };
409
- //# sourceMappingURL=chunk-WDXNBYGD.js.map
414
+ //# sourceMappingURL=chunk-TDP7CZGE.js.map
@@ -63,6 +63,7 @@ var helpCmd = async (_args, deps) => {
63
63
  ["wstack sage sync", "Keep project SAGE memory synchronized through HQ"],
64
64
  ["wstack plugin", "Plugins: list, install, enable, disable, remove"],
65
65
  ["wstack tools | skills", "List registered tools / discovered skills"],
66
+ ["wstack tools run <tool>", "Call a built-in tool from a script or external agent"],
66
67
  ["wstack typesafe", "TypeSafe account: status, login, test"],
67
68
  ["wstack update", "Self-update (--check-only)"]
68
69
  ]),
@@ -157,4 +158,4 @@ export {
157
158
  versionCmd,
158
159
  helpCmd
159
160
  };
160
- //# sourceMappingURL=chunk-6VPWQ4TG.js.map
161
+ //# sourceMappingURL=chunk-UROB7MK5.js.map