@posthog/wizard 2.45.0 → 2.46.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 (112) hide show
  1. package/README.md +5 -1
  2. package/dist/{add-mcp-server-to-clients-CNzZMSat.js → add-mcp-server-to-clients-Bv2CBSil.js} +53 -6
  3. package/dist/add-mcp-server-to-clients-Bv2CBSil.js.map +1 -0
  4. package/dist/{agent-interface-CF7GsdFf.js → agent-interface-B8roB2UP.js} +833 -8
  5. package/dist/agent-interface-B8roB2UP.js.map +1 -0
  6. package/dist/{agent-runner-B0LTVFfw.js → agent-runner-CV3qh_2p.js} +204 -48
  7. package/dist/agent-runner-CV3qh_2p.js.map +1 -0
  8. package/dist/analytics-D7vUZgzf.js +2 -0
  9. package/dist/{analytics-DQEuL6qS.js → analytics-k76OWsl0.js} +20 -6
  10. package/dist/analytics-k76OWsl0.js.map +1 -0
  11. package/dist/{api-uCMvDII5.js → api-CmIWcW-U.js} +3 -3
  12. package/dist/{api-uCMvDII5.js.map → api-CmIWcW-U.js.map} +1 -1
  13. package/dist/bin.js +228 -985
  14. package/dist/bin.js.map +1 -1
  15. package/dist/{ci-install-Bow8-f0j.js → ci-install-CNEJxdHf.js} +4 -4
  16. package/dist/{ci-install-Bow8-f0j.js.map → ci-install-CNEJxdHf.js.map} +1 -1
  17. package/dist/{commandments-CyB7Icr1.js → commandments-DSNrOeOC.js} +1 -1
  18. package/dist/{commandments-CyB7Icr1.js.map → commandments-DSNrOeOC.js.map} +1 -1
  19. package/dist/commandments-qTW7pwaT.js +2 -0
  20. package/dist/{debug-BixHZmHv.js → debug-BwIo7nwn.js} +1 -1
  21. package/dist/{debug-DBmxx59Q.js → debug-C9HmIrwx.js} +19 -5
  22. package/dist/debug-C9HmIrwx.js.map +1 -0
  23. package/dist/{defaults-8zjNjGGi.js → defaults-CtWMmC8s.js} +1 -1
  24. package/dist/{defaults-8zjNjGGi.js.map → defaults-CtWMmC8s.js.map} +1 -1
  25. package/dist/{env-api-key-AcC_tz8G.js → env-api-key-C3LzH8Lr.js} +1 -1
  26. package/dist/{env-api-key-AcC_tz8G.js.map → env-api-key-C3LzH8Lr.js.map} +1 -1
  27. package/dist/{environment-kZtkp7eG.js → environment-NJykOp33.js} +3 -3
  28. package/dist/{environment-kZtkp7eG.js.map → environment-NJykOp33.js.map} +1 -1
  29. package/dist/{file-utils-BAhYy5vC.js → file-utils-CLMcNIKi.js} +2 -2
  30. package/dist/{file-utils-BAhYy5vC.js.map → file-utils-CLMcNIKi.js.map} +1 -1
  31. package/dist/{file-watcher-BL-IRbR7.js → file-watcher-CBDCNQVV.js} +2 -2
  32. package/dist/{file-watcher-BL-IRbR7.js.map → file-watcher-CBDCNQVV.js.map} +1 -1
  33. package/dist/{headless-ui-DWmMyhaP.js → headless-ui-B3QKxbz8.js} +2 -2
  34. package/dist/{headless-ui-DWmMyhaP.js.map → headless-ui-B3QKxbz8.js.map} +1 -1
  35. package/dist/{interactive-Cyic0bGL.js → interactive-CIJRex02.js} +2 -2
  36. package/dist/{interactive-Cyic0bGL.js.map → interactive-CIJRex02.js.map} +1 -1
  37. package/dist/{mcp-prompt-streaming-C227vG1X.js → mcp-prompt-streaming-D6OgW3lC.js} +3 -3
  38. package/dist/{mcp-prompt-streaming-C227vG1X.js.map → mcp-prompt-streaming-D6OgW3lC.js.map} +1 -1
  39. package/dist/{mcp-BSGmEWjt.js → mcp-qTjKHqXN.js} +2 -2
  40. package/dist/{mcp-BSGmEWjt.js.map → mcp-qTjKHqXN.js.map} +1 -1
  41. package/dist/{non-interactive-DQA9vn9f.js → non-interactive-8Eypq_h-.js} +2 -2
  42. package/dist/{non-interactive-DQA9vn9f.js.map → non-interactive-8Eypq_h-.js.map} +1 -1
  43. package/dist/{orchestrator-tools-NuWONNkF.js → orchestrator-tools-DVXOV-kz.js} +3 -3
  44. package/dist/{orchestrator-tools-NuWONNkF.js.map → orchestrator-tools-DVXOV-kz.js.map} +1 -1
  45. package/dist/{package-manager-DKtR3xzM.js → package-manager-CAD9Uklp.js} +2 -2
  46. package/dist/{package-manager-DKtR3xzM.js.map → package-manager-CAD9Uklp.js.map} +1 -1
  47. package/dist/{pi-DJCln9jJ.js → pi-C9iHv5uG.js} +48 -32
  48. package/dist/pi-C9iHv5uG.js.map +1 -0
  49. package/dist/{playground-CbEndzc8.js → playground-C-9Q3ETa.js} +7 -7
  50. package/dist/{playground-CbEndzc8.js.map → playground-C-9Q3ETa.js.map} +1 -1
  51. package/dist/{posthog-DFyux4Vw.js → posthog-DZZbziJ1.js} +1 -1
  52. package/dist/{posthog-DFyux4Vw.js.map → posthog-DZZbziJ1.js.map} +1 -1
  53. package/dist/{posthog-integration-dmWJZ8mj.js → posthog-integration-Do997QON.js} +2006 -17
  54. package/dist/posthog-integration-Do997QON.js.map +1 -0
  55. package/dist/{provisioning-CcxCry5p.js → provisioning-DpqnGk3z.js} +3 -3
  56. package/dist/{provisioning-CcxCry5p.js.map → provisioning-DpqnGk3z.js.map} +1 -1
  57. package/dist/{queue-tools-Bw-AANLM.js → queue-tools-CTUgbOU8.js} +2 -2
  58. package/dist/{queue-tools-Bw-AANLM.js.map → queue-tools-CTUgbOU8.js.map} +1 -1
  59. package/dist/{registry-D2V88NoC.js → registry-DT58jEYr.js} +4 -4
  60. package/dist/{registry-D2V88NoC.js.map → registry-DT58jEYr.js.map} +1 -1
  61. package/dist/{security-DOtBCBvx.js → security-CTigdAWm.js} +10 -8
  62. package/dist/security-CTigdAWm.js.map +1 -0
  63. package/dist/{setup-utils-Qp-u6BZk.js → setup-utils-Cb5uACEO.js} +31 -9
  64. package/dist/setup-utils-Cb5uACEO.js.map +1 -0
  65. package/dist/{start-tui-DHa_9aiB.js → start-tui-Cwc-PFma.js} +36 -30
  66. package/dist/start-tui-Cwc-PFma.js.map +1 -0
  67. package/dist/{steps-D07HUQyE.js → steps-DyEKYM4I.js} +7 -7
  68. package/dist/{steps-D07HUQyE.js.map → steps-DyEKYM4I.js.map} +1 -1
  69. package/dist/{store-DSwsITKX.js → store-4sj-vWr5.js} +4 -4
  70. package/dist/{store-DSwsITKX.js.map → store-4sj-vWr5.js.map} +1 -1
  71. package/dist/{subagent-Dk4K5MDa.js → subagent-D8_yDgCB.js} +2 -2
  72. package/dist/{subagent-Dk4K5MDa.js.map → subagent-D8_yDgCB.js.map} +1 -1
  73. package/dist/{task-CQnV9Qd2.js → task-D8Y8pkzd.js} +12 -13
  74. package/dist/task-D8Y8pkzd.js.map +1 -0
  75. package/dist/{task-stream-B7qia9eW.js → task-stream-DQT3vjwS.js} +4 -4
  76. package/dist/{task-stream-B7qia9eW.js.map → task-stream-DQT3vjwS.js.map} +1 -1
  77. package/dist/{tasks-C7BQp659.js → tasks-D2WKssA5.js} +2 -2
  78. package/dist/{tasks-C7BQp659.js.map → tasks-D2WKssA5.js.map} +1 -1
  79. package/dist/{telemetry-DgahJGSx.js → telemetry-CYr4HgS2.js} +3 -3
  80. package/dist/{telemetry-DgahJGSx.js.map → telemetry-CYr4HgS2.js.map} +1 -1
  81. package/dist/{terminal-Bb6tZkf-.js → terminal-1RfyHQC2.js} +12 -12
  82. package/dist/{terminal-Bb6tZkf-.js.map → terminal-1RfyHQC2.js.map} +1 -1
  83. package/dist/tools-DQCUOQZX.js +199 -0
  84. package/dist/tools-DQCUOQZX.js.map +1 -0
  85. package/dist/{wizard-abort-DpDmuCfA.js → wizard-abort-D_-v1zvS.js} +1 -1
  86. package/dist/{wizard-abort-BxtdkgI-.js → wizard-abort-FuyIaI_0.js} +3 -3
  87. package/dist/{wizard-abort-BxtdkgI-.js.map → wizard-abort-FuyIaI_0.js.map} +1 -1
  88. package/dist/yara-hooks-6gNnPFPo.js +2 -0
  89. package/dist/{yara-hooks-pw86rZ8y.js → yara-hooks-DdYRRxfH.js} +82 -21
  90. package/dist/yara-hooks-DdYRRxfH.js.map +1 -0
  91. package/package.json +1 -1
  92. package/dist/add-mcp-server-to-clients-CNzZMSat.js.map +0 -1
  93. package/dist/agent-interface-CF7GsdFf.js.map +0 -1
  94. package/dist/agent-runner-B0LTVFfw.js.map +0 -1
  95. package/dist/analytics-DQEuL6qS.js.map +0 -1
  96. package/dist/analytics-YD3D38z2.js +0 -2
  97. package/dist/commandments-DguPfCX8.js +0 -2
  98. package/dist/debug-DBmxx59Q.js.map +0 -1
  99. package/dist/pi-DJCln9jJ.js.map +0 -1
  100. package/dist/posthog-integration-dmWJZ8mj.js.map +0 -1
  101. package/dist/security-DOtBCBvx.js.map +0 -1
  102. package/dist/setup-utils-Qp-u6BZk.js.map +0 -1
  103. package/dist/start-tui-DHa_9aiB.js.map +0 -1
  104. package/dist/task-CQnV9Qd2.js.map +0 -1
  105. package/dist/tools-BJEvSUWN.js +0 -122
  106. package/dist/tools-BJEvSUWN.js.map +0 -1
  107. package/dist/types-BRe_EzTW.js +0 -46
  108. package/dist/types-BRe_EzTW.js.map +0 -1
  109. package/dist/wizard-tools-Dg8o2lcH.js +0 -783
  110. package/dist/wizard-tools-Dg8o2lcH.js.map +0 -1
  111. package/dist/yara-hooks-CncaWRuC.js +0 -2
  112. package/dist/yara-hooks-pw86rZ8y.js.map +0 -1
@@ -1,783 +0,0 @@
1
- import { s as logToFile } from "./debug-DBmxx59Q.js";
2
- import { n as analytics } from "./analytics-DQEuL6qS.js";
3
- import { c as makeMutex, i as buildOrchestratorTools, l as writeJsonAtomic } from "./queue-tools-Bw-AANLM.js";
4
- import { a as coerceAuditChecks, t as AUDIT_CHECKS_FILE } from "./types-BRe_EzTW.js";
5
- import fs from "fs";
6
- import path from "path";
7
- import { z } from "zod";
8
- import { unzipSync } from "fflate";
9
- import { randomUUID } from "crypto";
10
- //#region src/lib/wizard-ask-bridge.ts
11
- /**
12
- * WizardAskBridge — host-side promise broker for the `wizard_ask` MCP tool.
13
- *
14
- * The `wizard_ask` tool needs to (a) read information from the wizard
15
- * session (the active skill id, used as the analytics `source`) and
16
- * (b) drive the TUI overlay. Wiring `wizard-tools.ts` directly to either
17
- * would couple our pure-data MCP server to the runtime UI layer.
18
- *
19
- * The bridge is the seam: `wizard-tools.ts` depends on this interface,
20
- * and `agent-runner.ts` constructs an implementation that knows about
21
- * both the session and `getUI()`.
22
- */
23
- /** Sentinel returned for unanswered fields on cancellation or timeout. */
24
- const CANCELLED_SENTINEL = "__cancelled__";
25
- function buildCancelledAnswers(questions) {
26
- const out = {};
27
- for (const q of questions) out[q.id] = CANCELLED_SENTINEL;
28
- return out;
29
- }
30
- function isFullyCancelled(answers) {
31
- const values = Object.values(answers);
32
- if (values.length === 0) return false;
33
- return values.every((v) => v === CANCELLED_SENTINEL);
34
- }
35
- function createWizardAskBridge(opts) {
36
- const timeoutMs = opts.timeoutMs ?? 3e5;
37
- return { async request({ questions }) {
38
- const pending = {
39
- id: randomUUID(),
40
- questions,
41
- source: opts.getSource(),
42
- richLinks: opts.richLinks ?? false
43
- };
44
- const startedAt = Date.now();
45
- let timer;
46
- const timeoutPromise = new Promise((resolve) => {
47
- timer = setTimeout(() => {
48
- opts.cancelQuestion?.();
49
- resolve(buildCancelledAnswers(questions));
50
- }, timeoutMs);
51
- });
52
- try {
53
- const answers = await Promise.race([opts.showQuestion(pending), timeoutPromise]);
54
- const durationMs = Date.now() - startedAt;
55
- if (isFullyCancelled(answers)) analytics.wizardCapture("wizard_ask cancelled", {
56
- source: pending.source,
57
- question_count: questions.length,
58
- duration_ms: durationMs,
59
- timed_out: durationMs >= timeoutMs
60
- });
61
- else analytics.wizardCapture("wizard_ask answered", {
62
- source: pending.source,
63
- question_count: questions.length,
64
- duration_ms: durationMs
65
- });
66
- return answers;
67
- } finally {
68
- if (timer) clearTimeout(timer);
69
- }
70
- } };
71
- }
72
- //#endregion
73
- //#region src/lib/secret-vault.ts
74
- /**
75
- * Session-scoped secret vault.
76
- *
77
- * Tools that handle sensitive values (personal API keys pasted by the user
78
- * or minted by the wizard, OAuth tokens, etc.) store them here and return
79
- * an opaque `secret:<uuid>` ref to the agent. The agent passes the ref
80
- * around to subsequent tools — `set_env_values` resolves it host-side
81
- * before writing — but the raw value never enters the LLM conversation.
82
- *
83
- * The vault is created once per `createWizardToolsServer` call and lives
84
- * for the duration of a single wizard run. There is no persistence and
85
- * no cross-session sharing; refs minted in one run cannot be resolved in
86
- * another.
87
- */
88
- const REF_PREFIX = "secret:";
89
- function createSecretVault() {
90
- const store = /* @__PURE__ */ new Map();
91
- return {
92
- put(value, meta) {
93
- const ref = `${REF_PREFIX}${randomUUID()}`;
94
- store.set(ref, {
95
- value,
96
- meta: {
97
- ref,
98
- label: meta.label,
99
- source: meta.source,
100
- createdAt: Date.now()
101
- }
102
- });
103
- return ref;
104
- },
105
- get(ref) {
106
- return store.get(ref)?.value;
107
- },
108
- has(ref) {
109
- return store.has(ref);
110
- },
111
- list() {
112
- return [...store.values()].map((s) => s.meta);
113
- },
114
- clear() {
115
- store.clear();
116
- }
117
- };
118
- }
119
- //#endregion
120
- //#region src/lib/fetch-retry.ts
121
- /**
122
- * Retry wrapper for fetching from the skills/agents server (GitHub releases in
123
- * production). GitHub releases blips transiently, so every fetch on the run's
124
- * critical path — skill menu, skill zips, agent menu, agent prompt bodies —
125
- * goes through here rather than a bare `fetch`.
126
- */
127
- const DEFAULT_TIMEOUT_MS = 6e4;
128
- const DEFAULT_MAX_ATTEMPTS = 3;
129
- const DEFAULT_BACKOFF_MS = 500;
130
- function sleep(ms) {
131
- return new Promise((resolve) => setTimeout(resolve, ms));
132
- }
133
- /** Fetch a URL, retrying transient failures (network error or non-ok HTTP) with backoff. */
134
- async function fetchWithRetry(url, opts = {}) {
135
- const { fetchImpl = fetch, sleepImpl = sleep, timeoutMs = DEFAULT_TIMEOUT_MS, maxAttempts = DEFAULT_MAX_ATTEMPTS, backoffMs = DEFAULT_BACKOFF_MS } = opts;
136
- const failures = [];
137
- for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
138
- const resp = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
139
- if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
140
- return resp;
141
- } catch (err) {
142
- failures.push(`attempt ${attempt}: ${err.message}`);
143
- if (attempt < maxAttempts) await sleepImpl(backoffMs * 2 ** (attempt - 1));
144
- }
145
- throw new Error(`fetch ${url} failed — ${failures.join("; ")}`);
146
- }
147
- //#endregion
148
- //#region src/lib/wizard-tools.ts
149
- /**
150
- * Unified in-process MCP server for the PostHog wizard.
151
- *
152
- * Provides tools that run locally (secret values never leave the machine):
153
- * - check_env_keys: Check which env var keys exist in a .env file
154
- * - set_env_values: Create/update env vars in a .env file
155
- * - detect_package_manager: Detect the project's package manager(s)
156
- * - load_skill_menu / install_skill: Skill installation
157
- * - audit_seed_checks / audit_add_checks / audit_resolve_checks: Audit ledger ownership
158
- */
159
- let _sdkModule = null;
160
- async function getSDKModule() {
161
- if (!_sdkModule) _sdkModule = await import("@anthropic-ai/claude-agent-sdk");
162
- return _sdkModule;
163
- }
164
- /** Expand a bundle entry into one entry per variant, so the menu reads the same whether a group ships bundled or as zips. */
165
- function expandBundleEntry(entry) {
166
- if (!entry.bundle || !entry.variants) return [entry];
167
- return entry.variants.map((variant) => ({
168
- ...variant,
169
- name: entry.name,
170
- group: entry.group,
171
- bundle: true,
172
- downloadUrl: entry.downloadUrl
173
- }));
174
- }
175
- /**
176
- * Fetch the skill menu from the skills server.
177
- * Returns parsed data on success, `null` on failure.
178
- */
179
- async function fetchSkillMenu(skillsBaseUrl, opts = {}) {
180
- const menuUrl = `${skillsBaseUrl}/skill-menu.json`;
181
- try {
182
- logToFile(`fetchSkillMenu: fetching from ${menuUrl}`);
183
- const data = await (await fetchWithRetry(menuUrl, opts)).json();
184
- for (const [category, entries] of Object.entries(data.categories)) data.categories[category] = entries.flatMap(expandBundleEntry);
185
- logToFile(`fetchSkillMenu: loaded (${Object.keys(data.categories).length} categories)`);
186
- return data;
187
- } catch (err) {
188
- logToFile(`fetchSkillMenu: error: ${err.message}`);
189
- return null;
190
- }
191
- }
192
- /** Extract a zip buffer, refusing entries that escape destDir (zip-slip). */
193
- function extractZipArchive(zip, destDir) {
194
- const root = path.resolve(destDir);
195
- let written = 0;
196
- for (const [entryPath, data] of Object.entries(unzipSync(zip))) {
197
- const target = path.resolve(root, entryPath);
198
- if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`zip entry escapes destination: ${entryPath}`);
199
- if (entryPath.endsWith("/")) {
200
- fs.mkdirSync(target, { recursive: true });
201
- continue;
202
- }
203
- fs.mkdirSync(path.dirname(target), { recursive: true });
204
- fs.writeFileSync(target, data);
205
- written++;
206
- }
207
- return written;
208
- }
209
- /** Unpack the one variant this entry names out of a bundle; the rest is noise and never hits disk. */
210
- function extractBundle(bundle, destDir, entryId) {
211
- if (typeof bundle?.id !== "string" || typeof bundle?.variants !== "object" || bundle.variants === null) throw new Error("malformed bundle: expected { id, variants }");
212
- const files = bundle.variants[entryId.slice(bundle.id.length + 1)];
213
- if (!files) throw new Error(`bundle ${bundle.id} has no variant "${entryId}"`);
214
- const root = path.resolve(destDir);
215
- let written = 0;
216
- for (const [entryPath, contents] of Object.entries(files)) {
217
- const target = path.resolve(root, entryPath);
218
- if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`bundle entry escapes destination: ${entryPath}`);
219
- fs.mkdirSync(path.dirname(target), { recursive: true });
220
- fs.writeFileSync(target, contents);
221
- written++;
222
- }
223
- return written;
224
- }
225
- /** Download a URL to a buffer, retrying transient failures with backoff. */
226
- async function downloadWithRetry(url, opts = {}) {
227
- const resp = await fetchWithRetry(url, opts);
228
- return new Uint8Array(await resp.arrayBuffer());
229
- }
230
- /**
231
- * Download and extract a skill.
232
- * By default installs to `<installDir>/.claude/skills/<id>/`.
233
- * Pass `skillsRoot` to override the base directory (e.g. `.posthog/skills`).
234
- */
235
- async function downloadSkill(skillEntry, installDir, skillsRoot) {
236
- const skillDir = skillsRoot ? path.join(installDir, skillsRoot, skillEntry.id) : path.join(installDir, ".claude", "skills", skillEntry.id);
237
- let step = "download";
238
- try {
239
- fs.mkdirSync(skillDir, { recursive: true });
240
- const data = await downloadWithRetry(skillEntry.downloadUrl);
241
- step = "extract";
242
- const fileCount = skillEntry.bundle ? extractBundle(JSON.parse(Buffer.from(data).toString("utf8")), skillDir, skillEntry.id) : extractZipArchive(data, skillDir);
243
- fs.writeFileSync(path.join(skillDir, ".posthog-wizard"), "");
244
- logToFile(`downloadSkill: installed ${skillEntry.id} from ${skillEntry.downloadUrl} (${fileCount} files)`);
245
- return { success: true };
246
- } catch (err) {
247
- logToFile(`downloadSkill: error: ${err.message}`);
248
- analytics.wizardCapture("skill install failed", {
249
- skill_id: skillEntry.id,
250
- step,
251
- platform: process.platform,
252
- error: String(err.message).slice(0, 500)
253
- });
254
- return {
255
- success: false,
256
- error: err.message
257
- };
258
- }
259
- }
260
- /**
261
- * High-level "install a skill by ID" helper. Fetches the skill menu,
262
- * finds the skill, downloads and extracts it. Programs should use this
263
- * instead of composing fetchSkillMenu + downloadSkill themselves.
264
- */
265
- async function installSkillById(skillId, installDir, skillsBaseUrl, skillsRoot) {
266
- const menu = await fetchSkillMenu(skillsBaseUrl);
267
- if (!menu) return { kind: "menu-fetch-failed" };
268
- const skill = Object.values(menu.categories).flat().find((s) => s.id === skillId);
269
- if (!skill) return {
270
- kind: "skill-not-found",
271
- skillId
272
- };
273
- const result = await downloadSkill(skill, installDir, skillsRoot);
274
- if (!result.success) return {
275
- kind: "download-failed",
276
- message: result.error ?? "unknown"
277
- };
278
- return {
279
- kind: "ok",
280
- path: skillsRoot ? `${skillsRoot}/${skillId}` : `.claude/skills/${skillId}`
281
- };
282
- }
283
- /**
284
- * Pure decision function for the wizard_ask caps. Returns whether the
285
- * upcoming call should proceed and, if not, the error message to surface
286
- * to the agent. Extracted so the policy can be unit-tested without
287
- * spinning up an MCP server.
288
- *
289
- * The adjacency nudge fires exactly once per run (the caller records it
290
- * via `adjacencyNudged`) — flows that legitimately need several
291
- * sequential, answer-dependent asks then proceed up to `maxQuestions`.
292
- * Without the flag the rejected call would never advance the counter and
293
- * every later call would be rejected, making caps above the threshold
294
- * unreachable.
295
- */
296
- function evaluateAskCap(callCount, maxQuestions, adjacencyNudged = false) {
297
- if (callCount >= maxQuestions) return {
298
- kind: "capped",
299
- reason: "max_questions",
300
- message: `Error: wizard_ask cap reached (${maxQuestions} calls in this run). Proceed with sensible defaults using the answers you already have, or emit [ABORT] requirements-incomplete.`
301
- };
302
- if (!adjacencyNudged && callCount >= 3) return {
303
- kind: "capped",
304
- reason: "adjacency",
305
- message: `Error: too many wizard_ask calls in a row (${callCount} so far). Batch the remaining questions into a single call — the schema accepts up to 8 questions per invocation. If the remaining questions truly depend on earlier answers, ask again and they will go through.`
306
- };
307
- return { kind: "ok" };
308
- }
309
- /**
310
- * Resolve filePath relative to workingDirectory, rejecting path traversal.
311
- */
312
- function resolveEnvPath(workingDirectory, filePath) {
313
- const resolved = path.resolve(workingDirectory, filePath);
314
- if (!resolved.startsWith(workingDirectory + path.sep) && resolved !== workingDirectory) throw new Error(`Path traversal rejected: "${filePath}" resolves outside working directory`);
315
- return resolved;
316
- }
317
- /**
318
- * Ensure the given env file basename is covered by .gitignore in the working directory.
319
- * Creates .gitignore if it doesn't exist; appends the entry if missing.
320
- */
321
- function ensureGitignoreCoverage(workingDirectory, envFileName) {
322
- const gitignorePath = path.join(workingDirectory, ".gitignore");
323
- if (fs.existsSync(gitignorePath)) {
324
- const content = fs.readFileSync(gitignorePath, "utf8");
325
- if (content.split("\n").some((line) => line.trim() === envFileName)) return;
326
- const newContent = content.endsWith("\n") ? `${content}${envFileName}\n` : `${content}\n${envFileName}\n`;
327
- fs.writeFileSync(gitignorePath, newContent, "utf8");
328
- } else fs.writeFileSync(gitignorePath, `${envFileName}\n`, "utf8");
329
- }
330
- /**
331
- * Parse a .env file's content and return the set of defined key names.
332
- */
333
- function parseEnvKeys(content) {
334
- const keys = /* @__PURE__ */ new Set();
335
- for (const line of content.split("\n")) {
336
- const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
337
- if (match) keys.add(match[1]);
338
- }
339
- return keys;
340
- }
341
- /**
342
- * Merge key-value pairs into existing .env content.
343
- * Updates existing keys in-place, appends new keys at the end.
344
- */
345
- function mergeEnvValues(content, values) {
346
- let result = content;
347
- const updatedKeys = /* @__PURE__ */ new Set();
348
- for (const [key, value] of Object.entries(values)) {
349
- const regex = new RegExp(`^(\\s*${key}\\s*=).*$`, "m");
350
- if (regex.test(result)) {
351
- result = result.replace(regex, `$1${value}`);
352
- updatedKeys.add(key);
353
- }
354
- }
355
- const newKeys = Object.entries(values).filter(([key]) => !updatedKeys.has(key));
356
- if (newKeys.length > 0) {
357
- if (result.length > 0 && !result.endsWith("\n")) result += "\n";
358
- for (const [key, value] of newKeys) result += `${key}=${value}\n`;
359
- }
360
- return result;
361
- }
362
- const AUDIT_STATUSES = [
363
- "pending",
364
- "pass",
365
- "error",
366
- "warning",
367
- "suggestion"
368
- ];
369
- const auditCheckSchema = z.object({
370
- id: z.string().min(1),
371
- area: z.string().min(1),
372
- label: z.string().min(1),
373
- status: z.enum(AUDIT_STATUSES),
374
- file: z.string().optional(),
375
- details: z.string().optional()
376
- });
377
- const auditUpdateSchema = z.object({
378
- id: z.string().min(1),
379
- status: z.enum(AUDIT_STATUSES),
380
- file: z.string().optional(),
381
- details: z.string().optional()
382
- });
383
- /** Atomically write the audit ledger. Thin typed wrapper over writeJsonAtomic. */
384
- function writeLedgerAtomic(targetPath, checks) {
385
- writeJsonAtomic(targetPath, checks);
386
- }
387
- /**
388
- * Apply a batch of patches to the ledger by id. Returns the new array and the
389
- * list of update ids that didn't match any existing check.
390
- */
391
- function applyAuditUpdates(current, updates) {
392
- const byId = new Map(current.map((c) => [c.id, c]));
393
- const unknown = [];
394
- for (const u of updates) {
395
- const existing = byId.get(u.id);
396
- if (!existing) {
397
- unknown.push(u.id);
398
- continue;
399
- }
400
- byId.set(u.id, {
401
- ...existing,
402
- status: u.status,
403
- ...u.file !== void 0 ? { file: u.file } : {},
404
- ...u.details !== void 0 ? { details: u.details } : {}
405
- });
406
- }
407
- return {
408
- next: current.map((c) => byId.get(c.id) ?? c),
409
- unknown
410
- };
411
- }
412
- /**
413
- * Append new checks to a seeded ledger. Duplicate ids are reported without
414
- * mutating the current ledger, including duplicates inside the additions.
415
- */
416
- function applyAuditAdditions(current, additions) {
417
- const existingIds = new Set(current.map((c) => c.id));
418
- const additionIds = /* @__PURE__ */ new Set();
419
- const duplicates = [];
420
- for (const check of additions) {
421
- if (existingIds.has(check.id) || additionIds.has(check.id)) {
422
- duplicates.push(check.id);
423
- continue;
424
- }
425
- additionIds.add(check.id);
426
- }
427
- if (duplicates.length > 0) return {
428
- next: current,
429
- duplicates
430
- };
431
- return {
432
- next: [...current, ...additions],
433
- duplicates: []
434
- };
435
- }
436
- function readLedger(targetPath) {
437
- if (!fs.existsSync(targetPath)) return [];
438
- try {
439
- return coerceAuditChecks(JSON.parse(fs.readFileSync(targetPath, "utf8")));
440
- } catch {
441
- return [];
442
- }
443
- }
444
- function appendAuditChecksToLedger(targetPath, additions) {
445
- if (!fs.existsSync(targetPath)) return {
446
- ok: false,
447
- reason: "missing-ledger"
448
- };
449
- const { next, duplicates } = applyAuditAdditions(readLedger(targetPath), additions);
450
- if (duplicates.length > 0) return {
451
- ok: false,
452
- reason: "duplicate-ids",
453
- ids: duplicates
454
- };
455
- writeLedgerAtomic(targetPath, next);
456
- return {
457
- ok: true,
458
- added: additions.length
459
- };
460
- }
461
- const SERVER_NAME = "wizard-tools";
462
- /**
463
- * Create the unified in-process MCP server with all wizard tools.
464
- * Must be called asynchronously because the SDK is an ESM module loaded via dynamic import.
465
- */
466
- async function createWizardToolsServer(options) {
467
- const { workingDirectory, detectPackageManager, skillsBaseUrl, askBridge, askMaxQuestions = 10, secretVault = createSecretVault(), orchestrator } = options;
468
- const { tool, createSdkMcpServer } = await getSDKModule();
469
- let askCallCount = 0;
470
- let askAdjacencyNudged = false;
471
- let cachedSkillMenu = {};
472
- let categoryNames = ["integration"];
473
- const menu = await fetchSkillMenu(skillsBaseUrl);
474
- if (menu) cachedSkillMenu = menu.categories;
475
- const keys = Object.keys(cachedSkillMenu);
476
- if (keys.length > 0) categoryNames = keys;
477
- const checkEnvKeys = tool("check_env_keys", "Check which environment variable keys are present or missing in a .env file. Never reveals values.", {
478
- filePath: z.string().describe("Path to the .env file, relative to the project root"),
479
- keys: z.array(z.string()).describe("Environment variable key names to check")
480
- }, (args) => {
481
- const resolved = resolveEnvPath(workingDirectory, args.filePath);
482
- logToFile(`check_env_keys: ${resolved}, keys: ${args.keys.join(", ")}`);
483
- const existingKeys = fs.existsSync(resolved) ? parseEnvKeys(fs.readFileSync(resolved, "utf8")) : /* @__PURE__ */ new Set();
484
- const results = {};
485
- for (const key of args.keys) results[key] = existingKeys.has(key) ? "present" : "missing";
486
- return { content: [{
487
- type: "text",
488
- text: JSON.stringify(results, null, 2)
489
- }] };
490
- });
491
- const setEnvValues = tool("set_env_values", "Create or update environment variable keys in a .env file. Creates the file if it does not exist. Ensures .gitignore coverage. Each value can be either a literal string or a secret reference of the form `{ \"secretRef\": \"secret:...\" }` returned by another tool (e.g. wizard_ask). Secret references are resolved locally — the actual value is written to the file but never returned to the agent.", {
492
- filePath: z.string().describe("Path to the .env file, relative to the project root"),
493
- values: z.record(z.string(), z.union([z.string(), z.object({ secretRef: z.string() })])).describe("Key → (literal string OR { secretRef } pointing to a vaulted secret)")
494
- }, (args) => {
495
- const forbidden = Object.keys(args.values).find((k) => k.toUpperCase() === "POSTHOG_KEY");
496
- if (forbidden) return {
497
- content: [{
498
- type: "text",
499
- text: `Error: "${forbidden}" is not a valid PostHog env var name. Use the project-specific key name from your framework's integration guide (e.g. NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN).`
500
- }],
501
- isError: true
502
- };
503
- const resolvedValues = {};
504
- const resolvedRefKeys = [];
505
- for (const [key, val] of Object.entries(args.values)) if (typeof val === "string") resolvedValues[key] = val;
506
- else {
507
- const secret = secretVault.get(val.secretRef);
508
- if (secret === void 0) return {
509
- content: [{
510
- type: "text",
511
- text: `Error: secret reference "${val.secretRef}" for key "${key}" is not known to the vault. The ref may have expired, been minted in a different run, or been mistyped.`
512
- }],
513
- isError: true
514
- };
515
- resolvedValues[key] = secret;
516
- resolvedRefKeys.push(key);
517
- }
518
- const resolved = resolveEnvPath(workingDirectory, args.filePath);
519
- logToFile(`set_env_values: ${resolved}, keys: ${Object.keys(resolvedValues).join(", ")}${resolvedRefKeys.length > 0 ? ` (secret refs: ${resolvedRefKeys.join(", ")})` : ""}`);
520
- const content = mergeEnvValues(fs.existsSync(resolved) ? fs.readFileSync(resolved, "utf8") : "", resolvedValues);
521
- const dir = path.dirname(resolved);
522
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
523
- fs.writeFileSync(resolved, content, "utf8");
524
- ensureGitignoreCoverage(workingDirectory, path.basename(resolved));
525
- return { content: [{
526
- type: "text",
527
- text: `Updated ${Object.keys(args.values).length} key(s) in ${args.filePath}`
528
- }] };
529
- });
530
- const detectPM = tool("detect_package_manager", "Detect which package manager(s) the project uses. Returns the name, install command, and run command for each detected package manager. Call this before running any install commands.", {}, async () => {
531
- logToFile(`detect_package_manager: scanning ${workingDirectory}`);
532
- const result = await detectPackageManager(workingDirectory);
533
- logToFile(`detect_package_manager: detected ${result.detected.length} package manager(s)`);
534
- return { content: [{
535
- type: "text",
536
- text: JSON.stringify(result, null, 2)
537
- }] };
538
- });
539
- const loadSkillMenu = tool("load_skill_menu", "Load available PostHog skills for a category. Returns skill IDs and names. Call this first, then use install_skill with the chosen ID.", { category: z.enum(categoryNames).describe("Skill category") }, (args) => {
540
- const skills = cachedSkillMenu[args.category];
541
- if (!skills || skills.length === 0) return {
542
- content: [{
543
- type: "text",
544
- text: `No skills found for category "${args.category}".`
545
- }],
546
- isError: true
547
- };
548
- const menuText = skills.map((s) => `- ${s.id}: ${s.name}`).join("\n");
549
- logToFile(`load_skill_menu: returning ${skills.length} skills for "${args.category}"`);
550
- return { content: [{
551
- type: "text",
552
- text: menuText
553
- }] };
554
- });
555
- const installSkill = tool("install_skill", "Download and install a PostHog skill by ID. Call load_skill_menu first to see available skills. Extracts the skill to .claude/skills/<skillId>/.", { skillId: z.string().describe("Skill ID from the skill menu (e.g., \"integration-nextjs-app-router\")") }, async (args) => {
556
- if (!/^[a-z0-9][a-z0-9_-]*$/.test(args.skillId)) return {
557
- content: [{
558
- type: "text",
559
- text: "Error: skillId must be lowercase alphanumeric with hyphens."
560
- }],
561
- isError: true
562
- };
563
- const skill = Object.values(cachedSkillMenu).flat().find((s) => s.id === args.skillId);
564
- if (!skill) return {
565
- content: [{
566
- type: "text",
567
- text: `Error: skill "${args.skillId}" not found. Use load_skill_menu to see available skills.`
568
- }],
569
- isError: true
570
- };
571
- const result = await downloadSkill(skill, workingDirectory);
572
- if (result.success) return { content: [{
573
- type: "text",
574
- text: `Skill installed to .claude/skills/${args.skillId}/`
575
- }] };
576
- else {
577
- analytics.captureException(/* @__PURE__ */ new Error("Skill install failed: download-failed"), {
578
- source: "install_skill_tool",
579
- skill_id: args.skillId,
580
- error_detail: String(result.error).slice(0, 500),
581
- platform: process.platform
582
- });
583
- return {
584
- content: [{
585
- type: "text",
586
- text: `Error installing skill: ${result.error}`
587
- }],
588
- isError: true
589
- };
590
- }
591
- });
592
- const auditLedgerPath = path.join(workingDirectory, AUDIT_CHECKS_FILE);
593
- const auditMutex = makeMutex();
594
- const auditSeedChecks = tool("audit_seed_checks", "Seed the audit ledger at .posthog-audit-checks.json with the full set of pending checks. Call this once at the start of the audit. Atomically replaces any existing ledger.", { checks: z.array(auditCheckSchema).describe("Full pending checklist to write to the ledger") }, async (args) => {
595
- return auditMutex(() => {
596
- writeLedgerAtomic(auditLedgerPath, args.checks);
597
- logToFile(`audit_seed_checks: wrote ${args.checks.length} entries`);
598
- return { content: [{
599
- type: "text",
600
- text: `Seeded ${args.checks.length} audit checks.`
601
- }] };
602
- });
603
- });
604
- const auditAddChecks = tool("audit_add_checks", "Append one or more pending checks to the existing audit ledger at .posthog-audit-checks.json. Call audit_seed_checks first. Atomically rejects duplicate ids without changing the ledger.", { checks: z.array(auditCheckSchema).min(1).describe("Additional checks to append to the existing ledger") }, async (args) => {
605
- return auditMutex(() => {
606
- const result = appendAuditChecksToLedger(auditLedgerPath, args.checks);
607
- if (!result.ok) {
608
- if (result.reason === "missing-ledger") return {
609
- content: [{
610
- type: "text",
611
- text: "Error: audit ledger does not exist. Run audit_seed_checks first."
612
- }],
613
- isError: true
614
- };
615
- return {
616
- content: [{
617
- type: "text",
618
- text: `Error: duplicate check id(s): ${result.ids.join(", ")}. Check ids must be unique.`
619
- }],
620
- isError: true
621
- };
622
- }
623
- logToFile(`audit_add_checks: added ${result.added} entries`);
624
- return { content: [{
625
- type: "text",
626
- text: `Added ${result.added} audit check(s).`
627
- }] };
628
- });
629
- });
630
- const auditResolveChecks = tool("audit_resolve_checks", "Resolve one or more audit checks by id. Patches each entry's status (and optional file/details) and writes the ledger back atomically. Concurrent calls serialize.", { updates: z.array(auditUpdateSchema).min(1).describe("Patches to apply, keyed by check id") }, async (args) => {
631
- return auditMutex(() => {
632
- const { next, unknown } = applyAuditUpdates(readLedger(auditLedgerPath), args.updates);
633
- if (unknown.length > 0) return {
634
- content: [{
635
- type: "text",
636
- text: `Error: unknown check id(s): ${unknown.join(", ")}. Run audit_seed_checks first or check the id.`
637
- }],
638
- isError: true
639
- };
640
- writeLedgerAtomic(auditLedgerPath, next);
641
- logToFile(`audit_resolve_checks: applied ${args.updates.length} update(s)`);
642
- return { content: [{
643
- type: "text",
644
- text: `Resolved ${args.updates.length} check(s).`
645
- }] };
646
- });
647
- });
648
- const askQuestionSchema = z.object({
649
- id: z.string().min(1).describe("Stable key for the answer in the response map"),
650
- prompt: z.string().min(1).describe("Question text shown to the user"),
651
- kind: z.enum([
652
- "single",
653
- "multi",
654
- "text"
655
- ]).describe("'single' = pick one option, 'multi' = pick any, 'text' = free-form single-line answer"),
656
- options: z.array(z.object({
657
- label: z.string(),
658
- value: z.string(),
659
- description: z.string().optional().describe("Optional secondary line shown dimmed and wrapped beneath the label (multi-select only). Use when a choice needs more than a title — e.g. what a custom scout watches and what makes it speak up.")
660
- })).optional().describe("Required for kind=single|multi; ignored for kind=text"),
661
- required: z.boolean().optional().describe("Defaults to true"),
662
- sensitive: z.boolean().optional().describe("Only valid for kind='text'. When true, the user's answer is stored in the wizard's secret vault and returned to you as { secretRef: 'secret:...' } instead of the raw string. Use for API keys, tokens, and any other secret the user types in. The secretRef is only resolved by wizard-tools that accept it (e.g. set_env_values) — it is NOT resolved when passed to other MCP tools (e.g. PostHog data-warehouse tools), which will reject it. For a secret that must reach another tool, write it to the env with set_env_values first, or use that tool's own credential-reference flow.")
663
- });
664
- return createSdkMcpServer({
665
- name: SERVER_NAME,
666
- version: "1.0.0",
667
- tools: [
668
- checkEnvKeys,
669
- setEnvValues,
670
- detectPM,
671
- loadSkillMenu,
672
- installSkill,
673
- auditSeedChecks,
674
- auditAddChecks,
675
- auditResolveChecks,
676
- tool("wizard_ask", "Ask the user one or more structured questions and wait for their answers. Use this whenever you would otherwise inline a question in your text output. Batch related questions into a single call (up to 8) rather than asking one at a time; sequential calls are fine when later questions genuinely depend on earlier answers. A fully cancelled or timed-out response does NOT count against the per-run cap — treat it as \"the user declined\" and fall back gracefully (e.g. hand over a deep link) without worrying about a wasted call.", { questions: z.array(askQuestionSchema).min(1).max(8) }, async (args) => {
677
- if (!askBridge) return {
678
- content: [{
679
- type: "text",
680
- text: "Error: wizard_ask is not available in this environment (CI / non-interactive). Proceed with sensible defaults or emit [ABORT] requirements-incomplete."
681
- }],
682
- isError: true
683
- };
684
- const capDecision = evaluateAskCap(askCallCount, askMaxQuestions, askAdjacencyNudged);
685
- if (capDecision.kind === "capped") {
686
- if (capDecision.reason === "adjacency") askAdjacencyNudged = true;
687
- analytics.wizardCapture("wizard_ask capped", {
688
- reason: capDecision.reason,
689
- call_count: askCallCount,
690
- max_questions: askMaxQuestions
691
- });
692
- return {
693
- content: [{
694
- type: "text",
695
- text: capDecision.message
696
- }],
697
- isError: true
698
- };
699
- }
700
- for (const q of args.questions) {
701
- if ((q.kind === "single" || q.kind === "multi") && (!q.options || q.options.length === 0)) return {
702
- content: [{
703
- type: "text",
704
- text: `Error: question "${q.id}" has kind="${q.kind}" but no options. Provide at least one { label, value } option, or change kind to "text".`
705
- }],
706
- isError: true
707
- };
708
- if (q.sensitive && q.kind !== "text") return {
709
- content: [{
710
- type: "text",
711
- text: `Error: question "${q.id}" sets sensitive=true but kind="${q.kind}". Only kind="text" answers can be vaulted as secrets.`
712
- }],
713
- isError: true
714
- };
715
- }
716
- const ids = /* @__PURE__ */ new Set();
717
- for (const q of args.questions) {
718
- if (ids.has(q.id)) return {
719
- content: [{
720
- type: "text",
721
- text: `Error: duplicate question id "${q.id}". Each question must have a unique id.`
722
- }],
723
- isError: true
724
- };
725
- ids.add(q.id);
726
- }
727
- askCallCount += 1;
728
- try {
729
- const answers = await askBridge.request({ questions: args.questions });
730
- if (isFullyCancelled(answers)) askCallCount -= 1;
731
- const sensitiveById = new Map(args.questions.filter((q) => q.sensitive).map((q) => [q.id, q.prompt]));
732
- const sanitised = {};
733
- for (const [id, answer] of Object.entries(answers)) {
734
- const label = sensitiveById.get(id);
735
- if (label !== void 0 && typeof answer === "string" && answer !== "__cancelled__") {
736
- const ref = secretVault.put(answer, {
737
- label,
738
- source: "wizard_ask"
739
- });
740
- sanitised[id] = { secretRef: ref };
741
- logToFile(`wizard_ask: vaulted answer for "${id}" as ${ref}`);
742
- } else sanitised[id] = answer;
743
- }
744
- logToFile(`wizard_ask: resolved ${Object.keys(answers).length} answer(s) for ${args.questions.length} question(s)`);
745
- return { content: [{
746
- type: "text",
747
- text: JSON.stringify({ answers: sanitised }, null, 2)
748
- }] };
749
- } catch (err) {
750
- askCallCount -= 1;
751
- logToFile(`wizard_ask: error: ${err?.message ?? err}`);
752
- return {
753
- content: [{
754
- type: "text",
755
- text: `Error: wizard_ask failed: ${err?.message ?? String(err)}`
756
- }],
757
- isError: true
758
- };
759
- }
760
- }),
761
- ...orchestrator ? buildOrchestratorTools(tool, orchestrator) : []
762
- ]
763
- });
764
- }
765
- /** Tool names exposed by the wizard-tools server, keyed for selective use. */
766
- const WIZARD_TOOL_NAMES = {
767
- checkEnvKeys: `mcp__${SERVER_NAME}__check_env_keys`,
768
- setEnvValues: `mcp__${SERVER_NAME}__set_env_values`,
769
- detectPackageManager: `mcp__${SERVER_NAME}__detect_package_manager`,
770
- loadSkillMenu: `mcp__${SERVER_NAME}__load_skill_menu`,
771
- installSkill: `mcp__${SERVER_NAME}__install_skill`,
772
- auditSeedChecks: `mcp__${SERVER_NAME}__audit_seed_checks`,
773
- auditAddChecks: `mcp__${SERVER_NAME}__audit_add_checks`,
774
- auditResolveChecks: `mcp__${SERVER_NAME}__audit_resolve_checks`,
775
- wizardAsk: `mcp__${SERVER_NAME}__wizard_ask`,
776
- enqueueTask: `mcp__${SERVER_NAME}__enqueue_task`,
777
- completeTask: `mcp__${SERVER_NAME}__complete_task`,
778
- readHandoffs: `mcp__${SERVER_NAME}__read_handoffs`
779
- };
780
- //#endregion
781
- export { installSkillById as a, resolveEnvPath as c, fetchSkillMenu as i, fetchWithRetry as l, createWizardToolsServer as n, mergeEnvValues as o, downloadSkill as r, parseEnvKeys as s, WIZARD_TOOL_NAMES as t, createWizardAskBridge as u };
782
-
783
- //# sourceMappingURL=wizard-tools-Dg8o2lcH.js.map