@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.
- package/README.md +5 -1
- package/dist/{add-mcp-server-to-clients-CNzZMSat.js → add-mcp-server-to-clients-Bv2CBSil.js} +53 -6
- package/dist/add-mcp-server-to-clients-Bv2CBSil.js.map +1 -0
- package/dist/{agent-interface-CF7GsdFf.js → agent-interface-B8roB2UP.js} +833 -8
- package/dist/agent-interface-B8roB2UP.js.map +1 -0
- package/dist/{agent-runner-B0LTVFfw.js → agent-runner-CV3qh_2p.js} +204 -48
- package/dist/agent-runner-CV3qh_2p.js.map +1 -0
- package/dist/analytics-D7vUZgzf.js +2 -0
- package/dist/{analytics-DQEuL6qS.js → analytics-k76OWsl0.js} +20 -6
- package/dist/analytics-k76OWsl0.js.map +1 -0
- package/dist/{api-uCMvDII5.js → api-CmIWcW-U.js} +3 -3
- package/dist/{api-uCMvDII5.js.map → api-CmIWcW-U.js.map} +1 -1
- package/dist/bin.js +228 -985
- package/dist/bin.js.map +1 -1
- package/dist/{ci-install-Bow8-f0j.js → ci-install-CNEJxdHf.js} +4 -4
- package/dist/{ci-install-Bow8-f0j.js.map → ci-install-CNEJxdHf.js.map} +1 -1
- package/dist/{commandments-CyB7Icr1.js → commandments-DSNrOeOC.js} +1 -1
- package/dist/{commandments-CyB7Icr1.js.map → commandments-DSNrOeOC.js.map} +1 -1
- package/dist/commandments-qTW7pwaT.js +2 -0
- package/dist/{debug-BixHZmHv.js → debug-BwIo7nwn.js} +1 -1
- package/dist/{debug-DBmxx59Q.js → debug-C9HmIrwx.js} +19 -5
- package/dist/debug-C9HmIrwx.js.map +1 -0
- package/dist/{defaults-8zjNjGGi.js → defaults-CtWMmC8s.js} +1 -1
- package/dist/{defaults-8zjNjGGi.js.map → defaults-CtWMmC8s.js.map} +1 -1
- package/dist/{env-api-key-AcC_tz8G.js → env-api-key-C3LzH8Lr.js} +1 -1
- package/dist/{env-api-key-AcC_tz8G.js.map → env-api-key-C3LzH8Lr.js.map} +1 -1
- package/dist/{environment-kZtkp7eG.js → environment-NJykOp33.js} +3 -3
- package/dist/{environment-kZtkp7eG.js.map → environment-NJykOp33.js.map} +1 -1
- package/dist/{file-utils-BAhYy5vC.js → file-utils-CLMcNIKi.js} +2 -2
- package/dist/{file-utils-BAhYy5vC.js.map → file-utils-CLMcNIKi.js.map} +1 -1
- package/dist/{file-watcher-BL-IRbR7.js → file-watcher-CBDCNQVV.js} +2 -2
- package/dist/{file-watcher-BL-IRbR7.js.map → file-watcher-CBDCNQVV.js.map} +1 -1
- package/dist/{headless-ui-DWmMyhaP.js → headless-ui-B3QKxbz8.js} +2 -2
- package/dist/{headless-ui-DWmMyhaP.js.map → headless-ui-B3QKxbz8.js.map} +1 -1
- package/dist/{interactive-Cyic0bGL.js → interactive-CIJRex02.js} +2 -2
- package/dist/{interactive-Cyic0bGL.js.map → interactive-CIJRex02.js.map} +1 -1
- package/dist/{mcp-prompt-streaming-C227vG1X.js → mcp-prompt-streaming-D6OgW3lC.js} +3 -3
- package/dist/{mcp-prompt-streaming-C227vG1X.js.map → mcp-prompt-streaming-D6OgW3lC.js.map} +1 -1
- package/dist/{mcp-BSGmEWjt.js → mcp-qTjKHqXN.js} +2 -2
- package/dist/{mcp-BSGmEWjt.js.map → mcp-qTjKHqXN.js.map} +1 -1
- package/dist/{non-interactive-DQA9vn9f.js → non-interactive-8Eypq_h-.js} +2 -2
- package/dist/{non-interactive-DQA9vn9f.js.map → non-interactive-8Eypq_h-.js.map} +1 -1
- package/dist/{orchestrator-tools-NuWONNkF.js → orchestrator-tools-DVXOV-kz.js} +3 -3
- package/dist/{orchestrator-tools-NuWONNkF.js.map → orchestrator-tools-DVXOV-kz.js.map} +1 -1
- package/dist/{package-manager-DKtR3xzM.js → package-manager-CAD9Uklp.js} +2 -2
- package/dist/{package-manager-DKtR3xzM.js.map → package-manager-CAD9Uklp.js.map} +1 -1
- package/dist/{pi-DJCln9jJ.js → pi-C9iHv5uG.js} +48 -32
- package/dist/pi-C9iHv5uG.js.map +1 -0
- package/dist/{playground-CbEndzc8.js → playground-C-9Q3ETa.js} +7 -7
- package/dist/{playground-CbEndzc8.js.map → playground-C-9Q3ETa.js.map} +1 -1
- package/dist/{posthog-DFyux4Vw.js → posthog-DZZbziJ1.js} +1 -1
- package/dist/{posthog-DFyux4Vw.js.map → posthog-DZZbziJ1.js.map} +1 -1
- package/dist/{posthog-integration-dmWJZ8mj.js → posthog-integration-Do997QON.js} +2006 -17
- package/dist/posthog-integration-Do997QON.js.map +1 -0
- package/dist/{provisioning-CcxCry5p.js → provisioning-DpqnGk3z.js} +3 -3
- package/dist/{provisioning-CcxCry5p.js.map → provisioning-DpqnGk3z.js.map} +1 -1
- package/dist/{queue-tools-Bw-AANLM.js → queue-tools-CTUgbOU8.js} +2 -2
- package/dist/{queue-tools-Bw-AANLM.js.map → queue-tools-CTUgbOU8.js.map} +1 -1
- package/dist/{registry-D2V88NoC.js → registry-DT58jEYr.js} +4 -4
- package/dist/{registry-D2V88NoC.js.map → registry-DT58jEYr.js.map} +1 -1
- package/dist/{security-DOtBCBvx.js → security-CTigdAWm.js} +10 -8
- package/dist/security-CTigdAWm.js.map +1 -0
- package/dist/{setup-utils-Qp-u6BZk.js → setup-utils-Cb5uACEO.js} +31 -9
- package/dist/setup-utils-Cb5uACEO.js.map +1 -0
- package/dist/{start-tui-DHa_9aiB.js → start-tui-Cwc-PFma.js} +36 -30
- package/dist/start-tui-Cwc-PFma.js.map +1 -0
- package/dist/{steps-D07HUQyE.js → steps-DyEKYM4I.js} +7 -7
- package/dist/{steps-D07HUQyE.js.map → steps-DyEKYM4I.js.map} +1 -1
- package/dist/{store-DSwsITKX.js → store-4sj-vWr5.js} +4 -4
- package/dist/{store-DSwsITKX.js.map → store-4sj-vWr5.js.map} +1 -1
- package/dist/{subagent-Dk4K5MDa.js → subagent-D8_yDgCB.js} +2 -2
- package/dist/{subagent-Dk4K5MDa.js.map → subagent-D8_yDgCB.js.map} +1 -1
- package/dist/{task-CQnV9Qd2.js → task-D8Y8pkzd.js} +12 -13
- package/dist/task-D8Y8pkzd.js.map +1 -0
- package/dist/{task-stream-B7qia9eW.js → task-stream-DQT3vjwS.js} +4 -4
- package/dist/{task-stream-B7qia9eW.js.map → task-stream-DQT3vjwS.js.map} +1 -1
- package/dist/{tasks-C7BQp659.js → tasks-D2WKssA5.js} +2 -2
- package/dist/{tasks-C7BQp659.js.map → tasks-D2WKssA5.js.map} +1 -1
- package/dist/{telemetry-DgahJGSx.js → telemetry-CYr4HgS2.js} +3 -3
- package/dist/{telemetry-DgahJGSx.js.map → telemetry-CYr4HgS2.js.map} +1 -1
- package/dist/{terminal-Bb6tZkf-.js → terminal-1RfyHQC2.js} +12 -12
- package/dist/{terminal-Bb6tZkf-.js.map → terminal-1RfyHQC2.js.map} +1 -1
- package/dist/tools-DQCUOQZX.js +199 -0
- package/dist/tools-DQCUOQZX.js.map +1 -0
- package/dist/{wizard-abort-DpDmuCfA.js → wizard-abort-D_-v1zvS.js} +1 -1
- package/dist/{wizard-abort-BxtdkgI-.js → wizard-abort-FuyIaI_0.js} +3 -3
- package/dist/{wizard-abort-BxtdkgI-.js.map → wizard-abort-FuyIaI_0.js.map} +1 -1
- package/dist/yara-hooks-6gNnPFPo.js +2 -0
- package/dist/{yara-hooks-pw86rZ8y.js → yara-hooks-DdYRRxfH.js} +82 -21
- package/dist/yara-hooks-DdYRRxfH.js.map +1 -0
- package/package.json +1 -1
- package/dist/add-mcp-server-to-clients-CNzZMSat.js.map +0 -1
- package/dist/agent-interface-CF7GsdFf.js.map +0 -1
- package/dist/agent-runner-B0LTVFfw.js.map +0 -1
- package/dist/analytics-DQEuL6qS.js.map +0 -1
- package/dist/analytics-YD3D38z2.js +0 -2
- package/dist/commandments-DguPfCX8.js +0 -2
- package/dist/debug-DBmxx59Q.js.map +0 -1
- package/dist/pi-DJCln9jJ.js.map +0 -1
- package/dist/posthog-integration-dmWJZ8mj.js.map +0 -1
- package/dist/security-DOtBCBvx.js.map +0 -1
- package/dist/setup-utils-Qp-u6BZk.js.map +0 -1
- package/dist/start-tui-DHa_9aiB.js.map +0 -1
- package/dist/task-CQnV9Qd2.js.map +0 -1
- package/dist/tools-BJEvSUWN.js +0 -122
- package/dist/tools-BJEvSUWN.js.map +0 -1
- package/dist/types-BRe_EzTW.js +0 -46
- package/dist/types-BRe_EzTW.js.map +0 -1
- package/dist/wizard-tools-Dg8o2lcH.js +0 -783
- package/dist/wizard-tools-Dg8o2lcH.js.map +0 -1
- package/dist/yara-hooks-CncaWRuC.js +0 -2
- package/dist/yara-hooks-pw86rZ8y.js.map +0 -1
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { n as __require } from "./rolldown-runtime-B_-DWIq7.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as analytics } from "./analytics-
|
|
2
|
+
import { Ct as runtimeEnv, J as POSTHOG_FLAG_HEADER_PREFIX, Z as POSTHOG_PROPERTY_HEADER_PREFIX, a as getLogFilePath, f as getUI, lt as WIZARD_ORCHESTRATOR_FLAG_KEY, o as initLogFile, pt as WIZARD_REMARK_EVENT_NAME, r as debug, s as logToFile, yt as wizardUserAgentForProgram } from "./debug-C9HmIrwx.js";
|
|
3
|
+
import { n as analytics } from "./analytics-k76OWsl0.js";
|
|
4
4
|
import { n as ADDITIONAL_FEATURE_PROMPTS } from "./wizard-session-tp4Zdbwl.js";
|
|
5
|
-
import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-
|
|
6
|
-
import {
|
|
7
|
-
import { l as prewarmYaraScanner, n as createPostToolUseYaraHooks, r as createPreToolUseYaraHooks } from "./yara-hooks-
|
|
8
|
-
import { t as getWizardCommandments } from "./commandments-
|
|
5
|
+
import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-FuyIaI_0.js";
|
|
6
|
+
import { c as makeMutex, i as buildOrchestratorTools, l as writeJsonAtomic } from "./queue-tools-CTUgbOU8.js";
|
|
7
|
+
import { _ as AUDIT_CHECKS_FILE, l as prewarmYaraScanner, n as createPostToolUseYaraHooks, r as createPreToolUseYaraHooks, x as coerceAuditChecks } from "./yara-hooks-DdYRRxfH.js";
|
|
8
|
+
import { t as getWizardCommandments } from "./commandments-DSNrOeOC.js";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
import * as fs$1 from "fs";
|
|
11
|
+
import fs from "fs";
|
|
11
12
|
import path from "path";
|
|
12
13
|
import { spawnSync } from "node:child_process";
|
|
13
14
|
import axios from "axios";
|
|
15
|
+
import { z } from "zod";
|
|
14
16
|
import * as os from "os";
|
|
17
|
+
import { unzipSync } from "fflate";
|
|
18
|
+
import { randomUUID } from "crypto";
|
|
15
19
|
//#region src/utils/custom-headers.ts
|
|
16
20
|
/**
|
|
17
21
|
* Builds a list of custom headers for ANTHROPIC_CUSTOM_HEADERS.
|
|
@@ -453,6 +457,827 @@ function evaluateBashCommand(rawCommand) {
|
|
|
453
457
|
return commandDecision(normalized);
|
|
454
458
|
}
|
|
455
459
|
//#endregion
|
|
460
|
+
//#region src/lib/wizard-ask-bridge.ts
|
|
461
|
+
/**
|
|
462
|
+
* WizardAskBridge — host-side promise broker for the `wizard_ask` MCP tool.
|
|
463
|
+
*
|
|
464
|
+
* The `wizard_ask` tool needs to (a) read information from the wizard
|
|
465
|
+
* session (the active skill id, used as the analytics `source`) and
|
|
466
|
+
* (b) drive the TUI overlay. Wiring `wizard-tools.ts` directly to either
|
|
467
|
+
* would couple our pure-data MCP server to the runtime UI layer.
|
|
468
|
+
*
|
|
469
|
+
* The bridge is the seam: `wizard-tools.ts` depends on this interface,
|
|
470
|
+
* and `agent-runner.ts` constructs an implementation that knows about
|
|
471
|
+
* both the session and `getUI()`.
|
|
472
|
+
*/
|
|
473
|
+
/** Sentinel returned for unanswered fields on cancellation or timeout. */
|
|
474
|
+
const CANCELLED_SENTINEL = "__cancelled__";
|
|
475
|
+
function buildCancelledAnswers(questions) {
|
|
476
|
+
const out = {};
|
|
477
|
+
for (const q of questions) out[q.id] = CANCELLED_SENTINEL;
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
function isFullyCancelled(answers) {
|
|
481
|
+
const values = Object.values(answers);
|
|
482
|
+
if (values.length === 0) return false;
|
|
483
|
+
return values.every((v) => v === CANCELLED_SENTINEL);
|
|
484
|
+
}
|
|
485
|
+
function createWizardAskBridge(opts) {
|
|
486
|
+
const timeoutMs = opts.timeoutMs ?? 3e5;
|
|
487
|
+
return { async request({ questions }) {
|
|
488
|
+
const pending = {
|
|
489
|
+
id: randomUUID(),
|
|
490
|
+
questions,
|
|
491
|
+
source: opts.getSource(),
|
|
492
|
+
richLinks: opts.richLinks ?? false
|
|
493
|
+
};
|
|
494
|
+
const startedAt = Date.now();
|
|
495
|
+
let timer;
|
|
496
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
497
|
+
timer = setTimeout(() => {
|
|
498
|
+
opts.cancelQuestion?.();
|
|
499
|
+
resolve(buildCancelledAnswers(questions));
|
|
500
|
+
}, timeoutMs);
|
|
501
|
+
});
|
|
502
|
+
try {
|
|
503
|
+
const answers = await Promise.race([opts.showQuestion(pending), timeoutPromise]);
|
|
504
|
+
const durationMs = Date.now() - startedAt;
|
|
505
|
+
if (isFullyCancelled(answers)) analytics.wizardCapture("wizard_ask cancelled", {
|
|
506
|
+
source: pending.source,
|
|
507
|
+
question_count: questions.length,
|
|
508
|
+
duration_ms: durationMs,
|
|
509
|
+
timed_out: durationMs >= timeoutMs
|
|
510
|
+
});
|
|
511
|
+
else analytics.wizardCapture("wizard_ask answered", {
|
|
512
|
+
source: pending.source,
|
|
513
|
+
question_count: questions.length,
|
|
514
|
+
duration_ms: durationMs
|
|
515
|
+
});
|
|
516
|
+
return answers;
|
|
517
|
+
} finally {
|
|
518
|
+
if (timer) clearTimeout(timer);
|
|
519
|
+
}
|
|
520
|
+
} };
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/lib/fetch-retry.ts
|
|
524
|
+
/**
|
|
525
|
+
* Retry wrapper for fetching from the skills/agents server (GitHub releases in
|
|
526
|
+
* production). GitHub releases blips transiently, so every fetch on the run's
|
|
527
|
+
* critical path — skill menu, skill zips, agent menu, agent prompt bodies —
|
|
528
|
+
* goes through here rather than a bare `fetch`.
|
|
529
|
+
*/
|
|
530
|
+
const DEFAULT_TIMEOUT_MS = 6e4;
|
|
531
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
532
|
+
const DEFAULT_BACKOFF_MS = 500;
|
|
533
|
+
function sleep(ms) {
|
|
534
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
535
|
+
}
|
|
536
|
+
/** Fetch a URL, retrying transient failures (network error or non-ok HTTP) with backoff. */
|
|
537
|
+
async function fetchWithRetry(url, opts = {}) {
|
|
538
|
+
const { fetchImpl = fetch, sleepImpl = sleep, timeoutMs = DEFAULT_TIMEOUT_MS, maxAttempts = DEFAULT_MAX_ATTEMPTS, backoffMs = DEFAULT_BACKOFF_MS } = opts;
|
|
539
|
+
const failures = [];
|
|
540
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
541
|
+
const resp = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
542
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
|
|
543
|
+
return resp;
|
|
544
|
+
} catch (err) {
|
|
545
|
+
failures.push(`attempt ${attempt}: ${err.message}`);
|
|
546
|
+
if (attempt < maxAttempts) await sleepImpl(backoffMs * 2 ** (attempt - 1));
|
|
547
|
+
}
|
|
548
|
+
throw new Error(`fetch ${url} failed — ${failures.join("; ")}`);
|
|
549
|
+
}
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/lib/wizard-tools/tools.ts
|
|
552
|
+
/**
|
|
553
|
+
* Shared wizard-tool behavior — skill discovery/install, env-file helpers,
|
|
554
|
+
* the wizard_ask cap policy, secret-vault plumbing, and the audit ledger.
|
|
555
|
+
* One implementation consumed by both protocol facades: the MCP server
|
|
556
|
+
* (`./mcp`) and the pi-native tools (`harness/pi/tools.ts`) — tool behavior
|
|
557
|
+
* cannot drift between harnesses.
|
|
558
|
+
*/
|
|
559
|
+
/** Expand a bundle entry into one entry per variant, so the menu reads the same whether a group ships bundled or as zips. */
|
|
560
|
+
function expandBundleEntry(entry) {
|
|
561
|
+
if (!entry.bundle || !entry.variants) return [entry];
|
|
562
|
+
return entry.variants.map((variant) => ({
|
|
563
|
+
...variant,
|
|
564
|
+
name: entry.name,
|
|
565
|
+
group: entry.group,
|
|
566
|
+
bundle: true,
|
|
567
|
+
downloadUrl: entry.downloadUrl
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Fetch the skill menu from the skills server.
|
|
572
|
+
* Returns parsed data on success, `null` on failure.
|
|
573
|
+
*/
|
|
574
|
+
async function fetchSkillMenu(skillsBaseUrl, opts = {}) {
|
|
575
|
+
const menuUrl = `${skillsBaseUrl}/skill-menu.json`;
|
|
576
|
+
try {
|
|
577
|
+
logToFile(`fetchSkillMenu: fetching from ${menuUrl}`);
|
|
578
|
+
const data = await (await fetchWithRetry(menuUrl, opts)).json();
|
|
579
|
+
for (const [category, entries] of Object.entries(data.categories)) data.categories[category] = entries.flatMap(expandBundleEntry);
|
|
580
|
+
logToFile(`fetchSkillMenu: loaded (${Object.keys(data.categories).length} categories)`);
|
|
581
|
+
return data;
|
|
582
|
+
} catch (err) {
|
|
583
|
+
logToFile(`fetchSkillMenu: error: ${err.message}`);
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/** Extract a zip buffer, refusing entries that escape destDir (zip-slip). */
|
|
588
|
+
function extractZipArchive(zip, destDir) {
|
|
589
|
+
const root = path.resolve(destDir);
|
|
590
|
+
let written = 0;
|
|
591
|
+
for (const [entryPath, data] of Object.entries(unzipSync(zip))) {
|
|
592
|
+
const target = path.resolve(root, entryPath);
|
|
593
|
+
if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`zip entry escapes destination: ${entryPath}`);
|
|
594
|
+
if (entryPath.endsWith("/")) {
|
|
595
|
+
fs.mkdirSync(target, { recursive: true });
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
599
|
+
fs.writeFileSync(target, data);
|
|
600
|
+
written++;
|
|
601
|
+
}
|
|
602
|
+
return written;
|
|
603
|
+
}
|
|
604
|
+
/** Unpack the one variant this entry names out of a bundle; the rest is noise and never hits disk. */
|
|
605
|
+
function extractBundle(bundle, destDir, entryId) {
|
|
606
|
+
if (typeof bundle?.id !== "string" || typeof bundle?.variants !== "object" || bundle.variants === null) throw new Error("malformed bundle: expected { id, variants }");
|
|
607
|
+
const files = bundle.variants[entryId.slice(bundle.id.length + 1)];
|
|
608
|
+
if (!files) throw new Error(`bundle ${bundle.id} has no variant "${entryId}"`);
|
|
609
|
+
const root = path.resolve(destDir);
|
|
610
|
+
let written = 0;
|
|
611
|
+
for (const [entryPath, contents] of Object.entries(files)) {
|
|
612
|
+
const target = path.resolve(root, entryPath);
|
|
613
|
+
if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`bundle entry escapes destination: ${entryPath}`);
|
|
614
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
615
|
+
fs.writeFileSync(target, contents);
|
|
616
|
+
written++;
|
|
617
|
+
}
|
|
618
|
+
return written;
|
|
619
|
+
}
|
|
620
|
+
/** Download a URL to a buffer, retrying transient failures with backoff. */
|
|
621
|
+
async function downloadWithRetry(url, opts = {}) {
|
|
622
|
+
const resp = await fetchWithRetry(url, opts);
|
|
623
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Download and extract a skill.
|
|
627
|
+
* By default installs to `<installDir>/.claude/skills/<id>/`.
|
|
628
|
+
* Pass `skillsRoot` to override the base directory (e.g. `.posthog/skills`).
|
|
629
|
+
*/
|
|
630
|
+
async function downloadSkill(skillEntry, installDir, skillsRoot) {
|
|
631
|
+
const skillDir = skillsRoot ? path.join(installDir, skillsRoot, skillEntry.id) : path.join(installDir, ".claude", "skills", skillEntry.id);
|
|
632
|
+
let step = "download";
|
|
633
|
+
try {
|
|
634
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
635
|
+
const data = await downloadWithRetry(skillEntry.downloadUrl);
|
|
636
|
+
step = "extract";
|
|
637
|
+
const fileCount = skillEntry.bundle ? extractBundle(JSON.parse(Buffer.from(data).toString("utf8")), skillDir, skillEntry.id) : extractZipArchive(data, skillDir);
|
|
638
|
+
fs.writeFileSync(path.join(skillDir, ".posthog-wizard"), "");
|
|
639
|
+
logToFile(`downloadSkill: installed ${skillEntry.id} from ${skillEntry.downloadUrl} (${fileCount} files)`);
|
|
640
|
+
return { success: true };
|
|
641
|
+
} catch (err) {
|
|
642
|
+
logToFile(`downloadSkill: error: ${err.message}`);
|
|
643
|
+
analytics.wizardCapture("skill install failed", {
|
|
644
|
+
skill_id: skillEntry.id,
|
|
645
|
+
step,
|
|
646
|
+
platform: process.platform,
|
|
647
|
+
error: String(err.message).slice(0, 500)
|
|
648
|
+
});
|
|
649
|
+
return {
|
|
650
|
+
success: false,
|
|
651
|
+
error: err.message
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* High-level "install a skill by ID" helper. Fetches the skill menu,
|
|
657
|
+
* finds the skill, downloads and extracts it. Programs should use this
|
|
658
|
+
* instead of composing fetchSkillMenu + downloadSkill themselves.
|
|
659
|
+
*/
|
|
660
|
+
async function installSkillById(skillId, installDir, skillsBaseUrl, skillsRoot) {
|
|
661
|
+
const menu = await fetchSkillMenu(skillsBaseUrl);
|
|
662
|
+
if (!menu) return { kind: "menu-fetch-failed" };
|
|
663
|
+
const skill = Object.values(menu.categories).flat().find((s) => s.id === skillId);
|
|
664
|
+
if (!skill) return {
|
|
665
|
+
kind: "skill-not-found",
|
|
666
|
+
skillId
|
|
667
|
+
};
|
|
668
|
+
const result = await downloadSkill(skill, installDir, skillsRoot);
|
|
669
|
+
if (!result.success) return {
|
|
670
|
+
kind: "download-failed",
|
|
671
|
+
message: result.error ?? "unknown"
|
|
672
|
+
};
|
|
673
|
+
return {
|
|
674
|
+
kind: "ok",
|
|
675
|
+
path: skillsRoot ? `${skillsRoot}/${skillId}` : `.claude/skills/${skillId}`
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Pure decision function for the wizard_ask caps. Returns whether the
|
|
680
|
+
* upcoming call should proceed and, if not, the error message to surface
|
|
681
|
+
* to the agent. Extracted so the policy can be unit-tested without
|
|
682
|
+
* spinning up an MCP server.
|
|
683
|
+
*
|
|
684
|
+
* The adjacency nudge fires exactly once per run (the caller records it
|
|
685
|
+
* via `adjacencyNudged`) — flows that legitimately need several
|
|
686
|
+
* sequential, answer-dependent asks then proceed up to `maxQuestions`.
|
|
687
|
+
* Without the flag the rejected call would never advance the counter and
|
|
688
|
+
* every later call would be rejected, making caps above the threshold
|
|
689
|
+
* unreachable.
|
|
690
|
+
*/
|
|
691
|
+
function evaluateAskCap(callCount, maxQuestions, adjacencyNudged = false) {
|
|
692
|
+
if (callCount >= maxQuestions) return {
|
|
693
|
+
kind: "capped",
|
|
694
|
+
reason: "max_questions",
|
|
695
|
+
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.`
|
|
696
|
+
};
|
|
697
|
+
if (!adjacencyNudged && callCount >= 3) return {
|
|
698
|
+
kind: "capped",
|
|
699
|
+
reason: "adjacency",
|
|
700
|
+
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.`
|
|
701
|
+
};
|
|
702
|
+
return { kind: "ok" };
|
|
703
|
+
}
|
|
704
|
+
const ENV_FILE_PATH_DESCRIPTION = "Path to the .env file, relative to the wizard working directory. Pass \".env\" for a file in that directory, or include the selected subproject path (for example, \"packages/app/.env\") for a nested project. Never prefix it with the wizard working directory's path inside an ancestor repository.";
|
|
705
|
+
/**
|
|
706
|
+
* Resolve filePath relative to workingDirectory, rejecting path traversal.
|
|
707
|
+
*/
|
|
708
|
+
function resolveEnvPath(workingDirectory, filePath) {
|
|
709
|
+
const resolved = path.resolve(workingDirectory, filePath);
|
|
710
|
+
if (!resolved.startsWith(workingDirectory + path.sep) && resolved !== workingDirectory) throw new Error(`Path traversal rejected: "${filePath}" resolves outside working directory`);
|
|
711
|
+
return resolved;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Ensure the given env file basename is covered by .gitignore in the working directory.
|
|
715
|
+
* Creates .gitignore if it doesn't exist; appends the entry if missing.
|
|
716
|
+
*/
|
|
717
|
+
function ensureGitignoreCoverage(workingDirectory, envFileName) {
|
|
718
|
+
const gitignorePath = path.join(workingDirectory, ".gitignore");
|
|
719
|
+
if (fs.existsSync(gitignorePath)) {
|
|
720
|
+
const content = fs.readFileSync(gitignorePath, "utf8");
|
|
721
|
+
if (content.split("\n").some((line) => line.trim() === envFileName)) return;
|
|
722
|
+
const newContent = content.endsWith("\n") ? `${content}${envFileName}\n` : `${content}\n${envFileName}\n`;
|
|
723
|
+
fs.writeFileSync(gitignorePath, newContent, "utf8");
|
|
724
|
+
} else fs.writeFileSync(gitignorePath, `${envFileName}\n`, "utf8");
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Parse a .env file's content and return the set of defined key names.
|
|
728
|
+
*/
|
|
729
|
+
function parseEnvKeys(content) {
|
|
730
|
+
const keys = /* @__PURE__ */ new Set();
|
|
731
|
+
for (const line of content.split("\n")) {
|
|
732
|
+
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
733
|
+
if (match) keys.add(match[1]);
|
|
734
|
+
}
|
|
735
|
+
return keys;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Merge key-value pairs into existing .env content.
|
|
739
|
+
* Updates existing keys in-place, appends new keys at the end.
|
|
740
|
+
*/
|
|
741
|
+
function mergeEnvValues(content, values) {
|
|
742
|
+
let result = content;
|
|
743
|
+
const updatedKeys = /* @__PURE__ */ new Set();
|
|
744
|
+
for (const [key, value] of Object.entries(values)) {
|
|
745
|
+
const regex = new RegExp(`^(\\s*${key}\\s*=).*$`, "m");
|
|
746
|
+
if (regex.test(result)) {
|
|
747
|
+
result = result.replace(regex, `$1${value}`);
|
|
748
|
+
updatedKeys.add(key);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const newKeys = Object.entries(values).filter(([key]) => !updatedKeys.has(key));
|
|
752
|
+
if (newKeys.length > 0) {
|
|
753
|
+
if (result.length > 0 && !result.endsWith("\n")) result += "\n";
|
|
754
|
+
for (const [key, value] of newKeys) result += `${key}=${value}\n`;
|
|
755
|
+
}
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Swap sensitive text answers for opaque vault refs before they return to
|
|
760
|
+
* the agent — the raw value never enters the LLM conversation. Cancelled
|
|
761
|
+
* answers pass through as the sentinel, unvaulted.
|
|
762
|
+
*/
|
|
763
|
+
function vaultSensitiveAnswers(questions, answers, vault) {
|
|
764
|
+
const sensitiveById = new Map(questions.filter((q) => q.sensitive).map((q) => [q.id, q.prompt]));
|
|
765
|
+
const sanitised = {};
|
|
766
|
+
for (const [id, answer] of Object.entries(answers)) {
|
|
767
|
+
const label = sensitiveById.get(id);
|
|
768
|
+
if (label !== void 0 && typeof answer === "string" && answer !== "__cancelled__") {
|
|
769
|
+
const ref = vault.put(answer, {
|
|
770
|
+
label,
|
|
771
|
+
source: "wizard_ask"
|
|
772
|
+
});
|
|
773
|
+
sanitised[id] = { secretRef: ref };
|
|
774
|
+
logToFile(`wizard_ask: vaulted answer for "${id}" as ${ref}`);
|
|
775
|
+
} else sanitised[id] = answer;
|
|
776
|
+
}
|
|
777
|
+
return sanitised;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Resolve `{secretRef}` entries host-side, so the value is written but never
|
|
781
|
+
* returned to the agent. Callers format their own error envelope from the
|
|
782
|
+
* failing key/ref.
|
|
783
|
+
*/
|
|
784
|
+
function resolveEnvSecretRefs(values, vault) {
|
|
785
|
+
const resolved = {};
|
|
786
|
+
const refKeys = [];
|
|
787
|
+
for (const [key, val] of Object.entries(values)) {
|
|
788
|
+
if (typeof val === "string") {
|
|
789
|
+
resolved[key] = val;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const secret = vault.get(val.secretRef);
|
|
793
|
+
if (secret === void 0) return {
|
|
794
|
+
ok: false,
|
|
795
|
+
key,
|
|
796
|
+
secretRef: val.secretRef
|
|
797
|
+
};
|
|
798
|
+
resolved[key] = secret;
|
|
799
|
+
refKeys.push(key);
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
ok: true,
|
|
803
|
+
values: resolved,
|
|
804
|
+
refKeys
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const AUDIT_STATUSES = [
|
|
808
|
+
"pending",
|
|
809
|
+
"pass",
|
|
810
|
+
"error",
|
|
811
|
+
"warning",
|
|
812
|
+
"suggestion"
|
|
813
|
+
];
|
|
814
|
+
/** Atomically write the audit ledger. Thin typed wrapper over writeJsonAtomic. */
|
|
815
|
+
function writeLedgerAtomic(targetPath, checks) {
|
|
816
|
+
writeJsonAtomic(targetPath, checks);
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Apply a batch of patches to the ledger by id. Returns the new array and the
|
|
820
|
+
* list of update ids that didn't match any existing check.
|
|
821
|
+
*/
|
|
822
|
+
function applyAuditUpdates(current, updates) {
|
|
823
|
+
const byId = new Map(current.map((c) => [c.id, c]));
|
|
824
|
+
const unknown = [];
|
|
825
|
+
for (const u of updates) {
|
|
826
|
+
const existing = byId.get(u.id);
|
|
827
|
+
if (!existing) {
|
|
828
|
+
unknown.push(u.id);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
byId.set(u.id, {
|
|
832
|
+
...existing,
|
|
833
|
+
status: u.status,
|
|
834
|
+
...u.file !== void 0 ? { file: u.file } : {},
|
|
835
|
+
...u.details !== void 0 ? { details: u.details } : {}
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
next: current.map((c) => byId.get(c.id) ?? c),
|
|
840
|
+
unknown
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Append new checks to a seeded ledger. Duplicate ids are reported without
|
|
845
|
+
* mutating the current ledger, including duplicates inside the additions.
|
|
846
|
+
*/
|
|
847
|
+
function applyAuditAdditions(current, additions) {
|
|
848
|
+
const existingIds = new Set(current.map((c) => c.id));
|
|
849
|
+
const additionIds = /* @__PURE__ */ new Set();
|
|
850
|
+
const duplicates = [];
|
|
851
|
+
for (const check of additions) {
|
|
852
|
+
if (existingIds.has(check.id) || additionIds.has(check.id)) {
|
|
853
|
+
duplicates.push(check.id);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
additionIds.add(check.id);
|
|
857
|
+
}
|
|
858
|
+
if (duplicates.length > 0) return {
|
|
859
|
+
next: current,
|
|
860
|
+
duplicates
|
|
861
|
+
};
|
|
862
|
+
return {
|
|
863
|
+
next: [...current, ...additions],
|
|
864
|
+
duplicates: []
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function readLedger(targetPath) {
|
|
868
|
+
if (!fs.existsSync(targetPath)) return [];
|
|
869
|
+
try {
|
|
870
|
+
return coerceAuditChecks(JSON.parse(fs.readFileSync(targetPath, "utf8")));
|
|
871
|
+
} catch {
|
|
872
|
+
return [];
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function appendAuditChecksToLedger(targetPath, additions) {
|
|
876
|
+
if (!fs.existsSync(targetPath)) return {
|
|
877
|
+
ok: false,
|
|
878
|
+
reason: "missing-ledger"
|
|
879
|
+
};
|
|
880
|
+
const { next, duplicates } = applyAuditAdditions(readLedger(targetPath), additions);
|
|
881
|
+
if (duplicates.length > 0) return {
|
|
882
|
+
ok: false,
|
|
883
|
+
reason: "duplicate-ids",
|
|
884
|
+
ids: duplicates
|
|
885
|
+
};
|
|
886
|
+
writeLedgerAtomic(targetPath, next);
|
|
887
|
+
return {
|
|
888
|
+
ok: true,
|
|
889
|
+
added: additions.length
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
const SERVER_NAME = "wizard-tools";
|
|
893
|
+
/** Tool names exposed by the wizard-tools server, keyed for selective use. */
|
|
894
|
+
const WIZARD_TOOL_NAMES = {
|
|
895
|
+
checkEnvKeys: `mcp__${SERVER_NAME}__check_env_keys`,
|
|
896
|
+
setEnvValues: `mcp__${SERVER_NAME}__set_env_values`,
|
|
897
|
+
detectPackageManager: `mcp__${SERVER_NAME}__detect_package_manager`,
|
|
898
|
+
loadSkillMenu: `mcp__${SERVER_NAME}__load_skill_menu`,
|
|
899
|
+
installSkill: `mcp__${SERVER_NAME}__install_skill`,
|
|
900
|
+
auditSeedChecks: `mcp__${SERVER_NAME}__audit_seed_checks`,
|
|
901
|
+
auditAddChecks: `mcp__${SERVER_NAME}__audit_add_checks`,
|
|
902
|
+
auditResolveChecks: `mcp__${SERVER_NAME}__audit_resolve_checks`,
|
|
903
|
+
wizardAsk: `mcp__${SERVER_NAME}__wizard_ask`,
|
|
904
|
+
enqueueTask: `mcp__${SERVER_NAME}__enqueue_task`,
|
|
905
|
+
completeTask: `mcp__${SERVER_NAME}__complete_task`,
|
|
906
|
+
readHandoffs: `mcp__${SERVER_NAME}__read_handoffs`
|
|
907
|
+
};
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region src/lib/secret-vault.ts
|
|
910
|
+
/**
|
|
911
|
+
* Session-scoped secret vault.
|
|
912
|
+
*
|
|
913
|
+
* Tools that handle sensitive values (personal API keys pasted by the user
|
|
914
|
+
* or minted by the wizard, OAuth tokens, etc.) store them here and return
|
|
915
|
+
* an opaque `secret:<uuid>` ref to the agent. The agent passes the ref
|
|
916
|
+
* around to subsequent tools — `set_env_values` resolves it host-side
|
|
917
|
+
* before writing — but the raw value never enters the LLM conversation.
|
|
918
|
+
*
|
|
919
|
+
* The vault is created once per `createWizardToolsServer` call and lives
|
|
920
|
+
* for the duration of a single wizard run. There is no persistence and
|
|
921
|
+
* no cross-session sharing; refs minted in one run cannot be resolved in
|
|
922
|
+
* another.
|
|
923
|
+
*/
|
|
924
|
+
const REF_PREFIX = "secret:";
|
|
925
|
+
function createSecretVault() {
|
|
926
|
+
const store = /* @__PURE__ */ new Map();
|
|
927
|
+
return {
|
|
928
|
+
put(value, meta) {
|
|
929
|
+
const ref = `${REF_PREFIX}${randomUUID()}`;
|
|
930
|
+
store.set(ref, {
|
|
931
|
+
value,
|
|
932
|
+
meta: {
|
|
933
|
+
ref,
|
|
934
|
+
label: meta.label,
|
|
935
|
+
source: meta.source,
|
|
936
|
+
createdAt: Date.now()
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
return ref;
|
|
940
|
+
},
|
|
941
|
+
get(ref) {
|
|
942
|
+
return store.get(ref)?.value;
|
|
943
|
+
},
|
|
944
|
+
has(ref) {
|
|
945
|
+
return store.has(ref);
|
|
946
|
+
},
|
|
947
|
+
list() {
|
|
948
|
+
return [...store.values()].map((s) => s.meta);
|
|
949
|
+
},
|
|
950
|
+
clear() {
|
|
951
|
+
store.clear();
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
//#endregion
|
|
956
|
+
//#region src/lib/wizard-tools/mcp.ts
|
|
957
|
+
/**
|
|
958
|
+
* MCP facade over `./tools` — the unified in-process server the anthropic
|
|
959
|
+
* harness mounts. Declarations (zod schemas, descriptions) and the MCP result
|
|
960
|
+
* envelope live here; all behavior is imported from the shared core.
|
|
961
|
+
*
|
|
962
|
+
* Provides: check_env_keys, set_env_values, detect_package_manager,
|
|
963
|
+
* load_skill_menu / install_skill, audit_* ledger tools, wizard_ask, and the
|
|
964
|
+
* orchestrator queue tools. Secret values never leave the machine.
|
|
965
|
+
*/
|
|
966
|
+
const auditCheckSchema = z.object({
|
|
967
|
+
id: z.string().min(1),
|
|
968
|
+
area: z.string().min(1),
|
|
969
|
+
label: z.string().min(1),
|
|
970
|
+
status: z.enum(AUDIT_STATUSES),
|
|
971
|
+
file: z.string().optional(),
|
|
972
|
+
details: z.string().optional()
|
|
973
|
+
});
|
|
974
|
+
const auditUpdateSchema = z.object({
|
|
975
|
+
id: z.string().min(1),
|
|
976
|
+
status: z.enum(AUDIT_STATUSES),
|
|
977
|
+
file: z.string().optional(),
|
|
978
|
+
details: z.string().optional()
|
|
979
|
+
});
|
|
980
|
+
let _sdkModule$1 = null;
|
|
981
|
+
async function getSDKModule$1() {
|
|
982
|
+
if (!_sdkModule$1) _sdkModule$1 = await import("@anthropic-ai/claude-agent-sdk");
|
|
983
|
+
return _sdkModule$1;
|
|
984
|
+
}
|
|
985
|
+
/** Default per-run cap on wizard_ask calls when no override is provided. */
|
|
986
|
+
/**
|
|
987
|
+
* Create the unified in-process MCP server with all wizard tools.
|
|
988
|
+
* Must be called asynchronously because the SDK is an ESM module loaded via dynamic import.
|
|
989
|
+
*/
|
|
990
|
+
async function createWizardToolsServer(options) {
|
|
991
|
+
const { workingDirectory, detectPackageManager, skillsBaseUrl, askBridge, askMaxQuestions = 10, secretVault = createSecretVault(), orchestrator } = options;
|
|
992
|
+
const { tool, createSdkMcpServer } = await getSDKModule$1();
|
|
993
|
+
let askCallCount = 0;
|
|
994
|
+
let askAdjacencyNudged = false;
|
|
995
|
+
let cachedSkillMenu = {};
|
|
996
|
+
let categoryNames = ["integration"];
|
|
997
|
+
const menu = await fetchSkillMenu(skillsBaseUrl);
|
|
998
|
+
if (menu) cachedSkillMenu = menu.categories;
|
|
999
|
+
const keys = Object.keys(cachedSkillMenu);
|
|
1000
|
+
if (keys.length > 0) categoryNames = keys;
|
|
1001
|
+
const checkEnvKeys = tool("check_env_keys", "Check which environment variable keys are present or missing in a .env file. Never reveals values.", {
|
|
1002
|
+
filePath: z.string().describe(ENV_FILE_PATH_DESCRIPTION),
|
|
1003
|
+
keys: z.array(z.string()).describe("Environment variable key names to check")
|
|
1004
|
+
}, (args) => {
|
|
1005
|
+
const resolved = resolveEnvPath(workingDirectory, args.filePath);
|
|
1006
|
+
logToFile(`check_env_keys: ${resolved}, keys: ${args.keys.join(", ")}`);
|
|
1007
|
+
const existingKeys = fs.existsSync(resolved) ? parseEnvKeys(fs.readFileSync(resolved, "utf8")) : /* @__PURE__ */ new Set();
|
|
1008
|
+
const results = {};
|
|
1009
|
+
for (const key of args.keys) results[key] = existingKeys.has(key) ? "present" : "missing";
|
|
1010
|
+
return { content: [{
|
|
1011
|
+
type: "text",
|
|
1012
|
+
text: JSON.stringify(results, null, 2)
|
|
1013
|
+
}] };
|
|
1014
|
+
});
|
|
1015
|
+
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.", {
|
|
1016
|
+
filePath: z.string().describe(ENV_FILE_PATH_DESCRIPTION),
|
|
1017
|
+
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)")
|
|
1018
|
+
}, (args) => {
|
|
1019
|
+
const forbidden = Object.keys(args.values).find((k) => k.toUpperCase() === "POSTHOG_KEY");
|
|
1020
|
+
if (forbidden) return {
|
|
1021
|
+
content: [{
|
|
1022
|
+
type: "text",
|
|
1023
|
+
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).`
|
|
1024
|
+
}],
|
|
1025
|
+
isError: true
|
|
1026
|
+
};
|
|
1027
|
+
const resolution = resolveEnvSecretRefs(args.values, secretVault);
|
|
1028
|
+
if (!resolution.ok) return {
|
|
1029
|
+
content: [{
|
|
1030
|
+
type: "text",
|
|
1031
|
+
text: `Error: secret reference "${resolution.secretRef}" for key "${resolution.key}" is not known to the vault. The ref may have expired, been minted in a different run, or been mistyped.`
|
|
1032
|
+
}],
|
|
1033
|
+
isError: true
|
|
1034
|
+
};
|
|
1035
|
+
const { values: resolvedValues, refKeys: resolvedRefKeys } = resolution;
|
|
1036
|
+
const resolved = resolveEnvPath(workingDirectory, args.filePath);
|
|
1037
|
+
logToFile(`set_env_values: ${resolved}, keys: ${Object.keys(resolvedValues).join(", ")}${resolvedRefKeys.length > 0 ? ` (secret refs: ${resolvedRefKeys.join(", ")})` : ""}`);
|
|
1038
|
+
const content = mergeEnvValues(fs.existsSync(resolved) ? fs.readFileSync(resolved, "utf8") : "", resolvedValues);
|
|
1039
|
+
const dir = path.dirname(resolved);
|
|
1040
|
+
if (!fs.existsSync(dir)) {
|
|
1041
|
+
analytics.wizardCapture("set_env_values parent dir missing", { platform: process.platform });
|
|
1042
|
+
return {
|
|
1043
|
+
content: [{
|
|
1044
|
+
type: "text",
|
|
1045
|
+
text: `Error: parent directory does not exist: "${path.dirname(args.filePath)}". filePath is resolved against the wizard working directory — pass ".env" for a file there, or "<subproject>/.env" for an existing nested project.`
|
|
1046
|
+
}],
|
|
1047
|
+
isError: true
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
fs.writeFileSync(resolved, content, "utf8");
|
|
1051
|
+
ensureGitignoreCoverage(workingDirectory, path.basename(resolved));
|
|
1052
|
+
return { content: [{
|
|
1053
|
+
type: "text",
|
|
1054
|
+
text: `Updated ${Object.keys(args.values).length} key(s) in ${args.filePath}`
|
|
1055
|
+
}] };
|
|
1056
|
+
});
|
|
1057
|
+
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 () => {
|
|
1058
|
+
logToFile(`detect_package_manager: scanning ${workingDirectory}`);
|
|
1059
|
+
const result = await detectPackageManager(workingDirectory);
|
|
1060
|
+
logToFile(`detect_package_manager: detected ${result.detected.length} package manager(s)`);
|
|
1061
|
+
return { content: [{
|
|
1062
|
+
type: "text",
|
|
1063
|
+
text: JSON.stringify(result, null, 2)
|
|
1064
|
+
}] };
|
|
1065
|
+
});
|
|
1066
|
+
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) => {
|
|
1067
|
+
const skills = cachedSkillMenu[args.category];
|
|
1068
|
+
if (!skills || skills.length === 0) return {
|
|
1069
|
+
content: [{
|
|
1070
|
+
type: "text",
|
|
1071
|
+
text: `No skills found for category "${args.category}".`
|
|
1072
|
+
}],
|
|
1073
|
+
isError: true
|
|
1074
|
+
};
|
|
1075
|
+
const menuText = skills.map((s) => `- ${s.id}: ${s.name}`).join("\n");
|
|
1076
|
+
logToFile(`load_skill_menu: returning ${skills.length} skills for "${args.category}"`);
|
|
1077
|
+
return { content: [{
|
|
1078
|
+
type: "text",
|
|
1079
|
+
text: menuText
|
|
1080
|
+
}] };
|
|
1081
|
+
});
|
|
1082
|
+
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) => {
|
|
1083
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(args.skillId)) return {
|
|
1084
|
+
content: [{
|
|
1085
|
+
type: "text",
|
|
1086
|
+
text: "Error: skillId must be lowercase alphanumeric with hyphens."
|
|
1087
|
+
}],
|
|
1088
|
+
isError: true
|
|
1089
|
+
};
|
|
1090
|
+
const skill = Object.values(cachedSkillMenu).flat().find((s) => s.id === args.skillId);
|
|
1091
|
+
if (!skill) return {
|
|
1092
|
+
content: [{
|
|
1093
|
+
type: "text",
|
|
1094
|
+
text: `Error: skill "${args.skillId}" not found. Use load_skill_menu to see available skills.`
|
|
1095
|
+
}],
|
|
1096
|
+
isError: true
|
|
1097
|
+
};
|
|
1098
|
+
const result = await downloadSkill(skill, workingDirectory);
|
|
1099
|
+
if (result.success) return { content: [{
|
|
1100
|
+
type: "text",
|
|
1101
|
+
text: `Skill installed to .claude/skills/${args.skillId}/`
|
|
1102
|
+
}] };
|
|
1103
|
+
else {
|
|
1104
|
+
analytics.captureException(/* @__PURE__ */ new Error("Skill install failed: download-failed"), {
|
|
1105
|
+
source: "install_skill_tool",
|
|
1106
|
+
skill_id: args.skillId,
|
|
1107
|
+
error_detail: String(result.error).slice(0, 500),
|
|
1108
|
+
platform: process.platform
|
|
1109
|
+
});
|
|
1110
|
+
return {
|
|
1111
|
+
content: [{
|
|
1112
|
+
type: "text",
|
|
1113
|
+
text: `Error installing skill: ${result.error}`
|
|
1114
|
+
}],
|
|
1115
|
+
isError: true
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
const auditLedgerPath = path.join(workingDirectory, AUDIT_CHECKS_FILE);
|
|
1120
|
+
const auditMutex = makeMutex();
|
|
1121
|
+
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) => {
|
|
1122
|
+
return auditMutex(() => {
|
|
1123
|
+
writeLedgerAtomic(auditLedgerPath, args.checks);
|
|
1124
|
+
logToFile(`audit_seed_checks: wrote ${args.checks.length} entries`);
|
|
1125
|
+
return { content: [{
|
|
1126
|
+
type: "text",
|
|
1127
|
+
text: `Seeded ${args.checks.length} audit checks.`
|
|
1128
|
+
}] };
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
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) => {
|
|
1132
|
+
return auditMutex(() => {
|
|
1133
|
+
const result = appendAuditChecksToLedger(auditLedgerPath, args.checks);
|
|
1134
|
+
if (!result.ok) {
|
|
1135
|
+
if (result.reason === "missing-ledger") return {
|
|
1136
|
+
content: [{
|
|
1137
|
+
type: "text",
|
|
1138
|
+
text: "Error: audit ledger does not exist. Run audit_seed_checks first."
|
|
1139
|
+
}],
|
|
1140
|
+
isError: true
|
|
1141
|
+
};
|
|
1142
|
+
return {
|
|
1143
|
+
content: [{
|
|
1144
|
+
type: "text",
|
|
1145
|
+
text: `Error: duplicate check id(s): ${result.ids.join(", ")}. Check ids must be unique.`
|
|
1146
|
+
}],
|
|
1147
|
+
isError: true
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
logToFile(`audit_add_checks: added ${result.added} entries`);
|
|
1151
|
+
return { content: [{
|
|
1152
|
+
type: "text",
|
|
1153
|
+
text: `Added ${result.added} audit check(s).`
|
|
1154
|
+
}] };
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
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) => {
|
|
1158
|
+
return auditMutex(() => {
|
|
1159
|
+
const { next, unknown } = applyAuditUpdates(readLedger(auditLedgerPath), args.updates);
|
|
1160
|
+
if (unknown.length > 0) return {
|
|
1161
|
+
content: [{
|
|
1162
|
+
type: "text",
|
|
1163
|
+
text: `Error: unknown check id(s): ${unknown.join(", ")}. Run audit_seed_checks first or check the id.`
|
|
1164
|
+
}],
|
|
1165
|
+
isError: true
|
|
1166
|
+
};
|
|
1167
|
+
writeLedgerAtomic(auditLedgerPath, next);
|
|
1168
|
+
logToFile(`audit_resolve_checks: applied ${args.updates.length} update(s)`);
|
|
1169
|
+
return { content: [{
|
|
1170
|
+
type: "text",
|
|
1171
|
+
text: `Resolved ${args.updates.length} check(s).`
|
|
1172
|
+
}] };
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
const askQuestionSchema = z.object({
|
|
1176
|
+
id: z.string().min(1).describe("Stable key for the answer in the response map"),
|
|
1177
|
+
prompt: z.string().min(1).describe("Question text shown to the user"),
|
|
1178
|
+
kind: z.enum([
|
|
1179
|
+
"single",
|
|
1180
|
+
"multi",
|
|
1181
|
+
"text"
|
|
1182
|
+
]).describe("'single' = pick one option, 'multi' = pick any, 'text' = free-form single-line answer"),
|
|
1183
|
+
options: z.array(z.object({
|
|
1184
|
+
label: z.string(),
|
|
1185
|
+
value: z.string(),
|
|
1186
|
+
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.")
|
|
1187
|
+
})).optional().describe("Required for kind=single|multi; ignored for kind=text"),
|
|
1188
|
+
required: z.boolean().optional().describe("Defaults to true"),
|
|
1189
|
+
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.")
|
|
1190
|
+
});
|
|
1191
|
+
return createSdkMcpServer({
|
|
1192
|
+
name: SERVER_NAME,
|
|
1193
|
+
version: "1.0.0",
|
|
1194
|
+
tools: [
|
|
1195
|
+
checkEnvKeys,
|
|
1196
|
+
setEnvValues,
|
|
1197
|
+
detectPM,
|
|
1198
|
+
loadSkillMenu,
|
|
1199
|
+
installSkill,
|
|
1200
|
+
auditSeedChecks,
|
|
1201
|
+
auditAddChecks,
|
|
1202
|
+
auditResolveChecks,
|
|
1203
|
+
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) => {
|
|
1204
|
+
if (!askBridge) return {
|
|
1205
|
+
content: [{
|
|
1206
|
+
type: "text",
|
|
1207
|
+
text: "Error: wizard_ask is not available in this environment (CI / non-interactive). Proceed with sensible defaults or emit [ABORT] requirements-incomplete."
|
|
1208
|
+
}],
|
|
1209
|
+
isError: true
|
|
1210
|
+
};
|
|
1211
|
+
const capDecision = evaluateAskCap(askCallCount, askMaxQuestions, askAdjacencyNudged);
|
|
1212
|
+
if (capDecision.kind === "capped") {
|
|
1213
|
+
if (capDecision.reason === "adjacency") askAdjacencyNudged = true;
|
|
1214
|
+
analytics.wizardCapture("wizard_ask capped", {
|
|
1215
|
+
reason: capDecision.reason,
|
|
1216
|
+
call_count: askCallCount,
|
|
1217
|
+
max_questions: askMaxQuestions
|
|
1218
|
+
});
|
|
1219
|
+
return {
|
|
1220
|
+
content: [{
|
|
1221
|
+
type: "text",
|
|
1222
|
+
text: capDecision.message
|
|
1223
|
+
}],
|
|
1224
|
+
isError: true
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
for (const q of args.questions) {
|
|
1228
|
+
if ((q.kind === "single" || q.kind === "multi") && (!q.options || q.options.length === 0)) return {
|
|
1229
|
+
content: [{
|
|
1230
|
+
type: "text",
|
|
1231
|
+
text: `Error: question "${q.id}" has kind="${q.kind}" but no options. Provide at least one { label, value } option, or change kind to "text".`
|
|
1232
|
+
}],
|
|
1233
|
+
isError: true
|
|
1234
|
+
};
|
|
1235
|
+
if (q.sensitive && q.kind !== "text") return {
|
|
1236
|
+
content: [{
|
|
1237
|
+
type: "text",
|
|
1238
|
+
text: `Error: question "${q.id}" sets sensitive=true but kind="${q.kind}". Only kind="text" answers can be vaulted as secrets.`
|
|
1239
|
+
}],
|
|
1240
|
+
isError: true
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1244
|
+
for (const q of args.questions) {
|
|
1245
|
+
if (ids.has(q.id)) return {
|
|
1246
|
+
content: [{
|
|
1247
|
+
type: "text",
|
|
1248
|
+
text: `Error: duplicate question id "${q.id}". Each question must have a unique id.`
|
|
1249
|
+
}],
|
|
1250
|
+
isError: true
|
|
1251
|
+
};
|
|
1252
|
+
ids.add(q.id);
|
|
1253
|
+
}
|
|
1254
|
+
askCallCount += 1;
|
|
1255
|
+
try {
|
|
1256
|
+
const answers = await askBridge.request({ questions: args.questions });
|
|
1257
|
+
if (isFullyCancelled(answers)) askCallCount -= 1;
|
|
1258
|
+
const sanitised = vaultSensitiveAnswers(args.questions, answers, secretVault);
|
|
1259
|
+
logToFile(`wizard_ask: resolved ${Object.keys(answers).length} answer(s) for ${args.questions.length} question(s)`);
|
|
1260
|
+
return { content: [{
|
|
1261
|
+
type: "text",
|
|
1262
|
+
text: JSON.stringify({ answers: sanitised }, null, 2)
|
|
1263
|
+
}] };
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
askCallCount -= 1;
|
|
1266
|
+
logToFile(`wizard_ask: error: ${err?.message ?? err}`);
|
|
1267
|
+
return {
|
|
1268
|
+
content: [{
|
|
1269
|
+
type: "text",
|
|
1270
|
+
text: `Error: wizard_ask failed: ${err?.message ?? String(err)}`
|
|
1271
|
+
}],
|
|
1272
|
+
isError: true
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
}),
|
|
1276
|
+
...orchestrator ? buildOrchestratorTools(tool, orchestrator) : []
|
|
1277
|
+
]
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
//#endregion
|
|
456
1281
|
//#region src/lib/agent/triage-provider.ts
|
|
457
1282
|
/**
|
|
458
1283
|
* LLM provider for warlock security-scan triage.
|
|
@@ -1947,6 +2772,6 @@ function handleSDKMessage(message, options, spinner, signals, receivedSuccessRes
|
|
|
1947
2772
|
}
|
|
1948
2773
|
}
|
|
1949
2774
|
//#endregion
|
|
1950
|
-
export { wizardCanUseTool as a, classifySettingsConflicts as c, sanitizeAgentSubprocessEnv as d, AgentOutputSignals as f, createTriageLLMProvider as h, runAgent as i, recoverOrphanedSettingsBackups as l, REMARK_INSTRUCTION as m, buildRunTags as n, backupAndFixClaudeSettings as o, AgentSignals as p, initializeAgent as r, checkAllSettingsConflicts as s, buildAgentEnv as t, restoreClaudeSettings as u };
|
|
2775
|
+
export { isFullyCancelled as A, mergeEnvValues as C, vaultSensitiveAnswers as D, resolveEnvSecretRefs as E, fetchWithRetry as O, installSkillById as S, resolveEnvPath as T, ENV_FILE_PATH_DESCRIPTION as _, wizardCanUseTool as a, evaluateAskCap as b, classifySettingsConflicts as c, sanitizeAgentSubprocessEnv as d, AgentOutputSignals as f, createSecretVault as g, createTriageLLMProvider as h, runAgent as i, createWizardAskBridge as k, recoverOrphanedSettingsBackups as l, REMARK_INSTRUCTION as m, buildRunTags as n, backupAndFixClaudeSettings as o, AgentSignals as p, initializeAgent as r, checkAllSettingsConflicts as s, buildAgentEnv as t, restoreClaudeSettings as u, WIZARD_TOOL_NAMES as v, parseEnvKeys as w, fetchSkillMenu as x, downloadSkill as y };
|
|
1951
2776
|
|
|
1952
|
-
//# sourceMappingURL=agent-interface-
|
|
2777
|
+
//# sourceMappingURL=agent-interface-B8roB2UP.js.map
|