@posthog/wizard 2.36.0 → 2.38.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 +6 -3
- package/dist/{add-mcp-server-to-clients-D5e3JbUC.js → add-mcp-server-to-clients-CeoyDLC-.js} +5 -5
- package/dist/{add-mcp-server-to-clients-D5e3JbUC.js.map → add-mcp-server-to-clients-CeoyDLC-.js.map} +1 -1
- package/dist/{agent-interface-CPn9pZ8j.js → agent-interface-BAodys0H.js} +66 -1055
- package/dist/agent-interface-BAodys0H.js.map +1 -0
- package/dist/{agent-runner-Cic1uZZ8.js → agent-runner-BMuJP8fb.js} +2574 -1715
- package/dist/agent-runner-BMuJP8fb.js.map +1 -0
- package/dist/{analytics-Be_wRgxD.js → analytics-CvIK_07d.js} +3 -2
- package/dist/{analytics-Be_wRgxD.js.map → analytics-CvIK_07d.js.map} +1 -1
- package/dist/{api-C_R_ybiA.js → api-CyA47qug.js} +3 -3
- package/dist/{api-C_R_ybiA.js.map → api-CyA47qug.js.map} +1 -1
- package/dist/bin.js +82 -54
- package/dist/bin.js.map +1 -1
- package/dist/{ci-install-B0BHTkGI.js → ci-install-C_o0Q7Fg.js} +4 -4
- package/dist/{ci-install-B0BHTkGI.js.map → ci-install-C_o0Q7Fg.js.map} +1 -1
- package/dist/{debug-DJS2KPGb.js → debug-BhGq9rn9.js} +1 -1
- package/dist/{debug-BpWrz3hX.js → debug-CgdI7Rd-.js} +55 -3
- package/dist/debug-CgdI7Rd-.js.map +1 -0
- package/dist/{defaults-UswXMiTx.js → defaults-b2Bo9Wid.js} +1 -1
- package/dist/{defaults-UswXMiTx.js.map → defaults-b2Bo9Wid.js.map} +1 -1
- package/dist/{env-api-key-Db-mNPK2.js → env-api-key-Bezg_uXQ.js} +1 -1
- package/dist/{env-api-key-Db-mNPK2.js.map → env-api-key-Bezg_uXQ.js.map} +1 -1
- package/dist/{environment-Dc6VTWyK.js → environment-v22_Genm.js} +3 -3
- package/dist/{environment-Dc6VTWyK.js.map → environment-v22_Genm.js.map} +1 -1
- package/dist/{file-utils-Buj9-umf.js → file-utils-Gvc2Ysa_.js} +2 -2
- package/dist/{file-utils-Buj9-umf.js.map → file-utils-Gvc2Ysa_.js.map} +1 -1
- package/dist/{headless-ui-BplqZRcO.js → headless-ui-BtBcuxOj.js} +2 -2
- package/dist/{headless-ui-BplqZRcO.js.map → headless-ui-BtBcuxOj.js.map} +1 -1
- package/dist/{host-resolution-BD6TcZG0.js → host-resolution-CUjt6efz.js} +3 -3
- package/dist/{host-resolution-BD6TcZG0.js.map → host-resolution-CUjt6efz.js.map} +1 -1
- package/dist/{interactive-BspTioP_.js → interactive-CRdgBMxk.js} +2 -2
- package/dist/{interactive-BspTioP_.js.map → interactive-CRdgBMxk.js.map} +1 -1
- package/dist/mcp-XWzcKTeK.js +108 -0
- package/dist/mcp-XWzcKTeK.js.map +1 -0
- package/dist/{mcp-prompt-streaming-BCNzyL12.js → mcp-prompt-streaming-DeC85Icy.js} +5 -5
- package/dist/mcp-prompt-streaming-DeC85Icy.js.map +1 -0
- package/dist/{non-interactive-CFikUAzO.js → non-interactive-B-Ku4EOx.js} +2 -2
- package/dist/{non-interactive-CFikUAzO.js.map → non-interactive-B-Ku4EOx.js.map} +1 -1
- package/dist/{package-manager-L7gsAbE0.js → package-manager-Dj6dTKlK.js} +2 -2
- package/dist/{package-manager-L7gsAbE0.js.map → package-manager-Dj6dTKlK.js.map} +1 -1
- package/dist/{playground-Bl2iPxdt.js → playground-CmKFAlCE.js} +783 -7
- package/dist/playground-CmKFAlCE.js.map +1 -0
- package/dist/{posthog-BrLFkaji.js → posthog-Dnl6wsYc.js} +1 -1
- package/dist/{posthog-BrLFkaji.js.map → posthog-Dnl6wsYc.js.map} +1 -1
- package/dist/{posthog-integration-B6pTHVcn.js → posthog-integration-CcJWXPDa.js} +18 -17
- package/dist/posthog-integration-CcJWXPDa.js.map +1 -0
- package/dist/{provisioning-C9ts56R5.js → provisioning-BIuAPRcH.js} +4 -4
- package/dist/{provisioning-C9ts56R5.js.map → provisioning-BIuAPRcH.js.map} +1 -1
- package/dist/{registry-DFazVUSE.js → registry-ByrVTIE9.js} +4 -4
- package/dist/{registry-DFazVUSE.js.map → registry-ByrVTIE9.js.map} +1 -1
- package/dist/security-uSP4HiC0.js +431 -0
- package/dist/security-uSP4HiC0.js.map +1 -0
- package/dist/{setup-utils-BxdW2R0r.js → setup-utils-DhmigSLO.js} +202 -22
- package/dist/setup-utils-DhmigSLO.js.map +1 -0
- package/dist/{start-tui-oGWfxdTd.js → start-tui-DKHnPg5a.js} +63 -38
- package/dist/start-tui-DKHnPg5a.js.map +1 -0
- package/dist/{steps-O0v5UU1G.js → steps-BBzIrOMY.js} +7 -7
- package/dist/{steps-O0v5UU1G.js.map → steps-BBzIrOMY.js.map} +1 -1
- package/dist/{store-Di9OvMql.js → store-BV1a-8Wp.js} +68 -6
- package/dist/store-BV1a-8Wp.js.map +1 -0
- package/dist/subagent-fP1QmYBX.js +108 -0
- package/dist/subagent-fP1QmYBX.js.map +1 -0
- package/dist/{task-stream-DkbGqxZv.js → task-stream-C6s7qUCE.js} +3 -3
- package/dist/{task-stream-DkbGqxZv.js.map → task-stream-C6s7qUCE.js.map} +1 -1
- package/dist/tasks-YFkqfEio.js +112 -0
- package/dist/tasks-YFkqfEio.js.map +1 -0
- package/dist/{telemetry-D5WQ4-Ic.js → telemetry-wOdXbZLp.js} +3 -3
- package/dist/{telemetry-D5WQ4-Ic.js.map → telemetry-wOdXbZLp.js.map} +1 -1
- package/dist/{terminal-Bd-k7pCi.js → terminal-IAHPQoAT.js} +143 -793
- package/dist/terminal-IAHPQoAT.js.map +1 -0
- package/dist/tools-DdzmiBKF.js +122 -0
- package/dist/tools-DdzmiBKF.js.map +1 -0
- package/dist/{urls-DpTCPWLD.js → urls-BR7jQgQp.js} +2 -2
- package/dist/{urls-DpTCPWLD.js.map → urls-BR7jQgQp.js.map} +1 -1
- package/dist/{wizard-abort-t41wO4Bh.js → wizard-abort-bulz-_og.js} +4 -4
- package/dist/{wizard-abort-t41wO4Bh.js.map → wizard-abort-bulz-_og.js.map} +1 -1
- package/dist/{wizard-abort-C1t9M-JN.js → wizard-abort-lbH6n2WG.js} +1 -1
- package/dist/{wizard-session-xzqycI7i.js → wizard-session-B1ZM4OBv.js} +1 -1
- package/dist/{wizard-session-CqihOtk3.js → wizard-session-DWeUEncV.js} +4 -1
- package/dist/wizard-session-DWeUEncV.js.map +1 -0
- package/dist/wizard-tools-CGrE1O_b.js +1107 -0
- package/dist/wizard-tools-CGrE1O_b.js.map +1 -0
- package/dist/wizard-ui-WZ48rUgr.js.map +1 -1
- package/package.json +7 -2
- package/dist/agent-interface-CPn9pZ8j.js.map +0 -1
- package/dist/agent-runner-Cic1uZZ8.js.map +0 -1
- package/dist/debug-BpWrz3hX.js.map +0 -1
- package/dist/mcp-prompt-streaming-BCNzyL12.js.map +0 -1
- package/dist/playground-Bl2iPxdt.js.map +0 -1
- package/dist/posthog-integration-B6pTHVcn.js.map +0 -1
- package/dist/setup-utils-BxdW2R0r.js.map +0 -1
- package/dist/start-tui-oGWfxdTd.js.map +0 -1
- package/dist/store-Di9OvMql.js.map +0 -1
- package/dist/terminal-Bd-k7pCi.js.map +0 -1
- package/dist/wizard-session-CqihOtk3.js.map +0 -1
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import { n as __require } from "./rolldown-runtime-B_-DWIq7.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as analytics } from "./analytics-
|
|
4
|
-
import { t as HostResolution } from "./host-resolution-
|
|
5
|
-
import { n as ADDITIONAL_FEATURE_PROMPTS } from "./wizard-session-
|
|
6
|
-
import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-
|
|
2
|
+
import { G as POSTHOG_PROPERTY_HEADER_PREFIX, H as POSTHOG_FLAG_HEADER_PREFIX, a as getLogFilePath, ct as WIZARD_USER_AGENT, mt as runtimeEnv, nt as WIZARD_ORCHESTRATOR_FLAG_KEY, o as initLogFile, ot as WIZARD_REMARK_EVENT_NAME, p as getUI, r as debug, s as logToFile, u as WIZARD_YARA_REPORT_FILE } from "./debug-CgdI7Rd-.js";
|
|
3
|
+
import { t as analytics } from "./analytics-CvIK_07d.js";
|
|
4
|
+
import { t as HostResolution } from "./host-resolution-CUjt6efz.js";
|
|
5
|
+
import { n as ADDITIONAL_FEATURE_PROMPTS } from "./wizard-session-DWeUEncV.js";
|
|
6
|
+
import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-bulz-_og.js";
|
|
7
|
+
import { h as AUDIT_REPORT_FILE, n as createWizardToolsServer, t as WIZARD_TOOL_NAMES } from "./wizard-tools-CGrE1O_b.js";
|
|
7
8
|
import { createRequire } from "node:module";
|
|
8
9
|
import * as fs$1 from "fs";
|
|
9
10
|
import fs from "fs";
|
|
10
|
-
import * as path$1 from "path";
|
|
11
11
|
import path from "path";
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
13
13
|
import axios from "axios";
|
|
14
|
-
import { z } from "zod";
|
|
15
14
|
import fg from "fast-glob";
|
|
16
15
|
import * as os from "os";
|
|
17
|
-
import { execFileSync } from "child_process";
|
|
18
|
-
import { randomUUID } from "crypto";
|
|
19
16
|
//#region src/utils/custom-headers.ts
|
|
20
17
|
/**
|
|
21
18
|
* Builds a list of custom headers for ANTHROPIC_CUSTOM_HEADERS.
|
|
@@ -180,1038 +177,6 @@ const LINTING_TOOLS = [
|
|
|
180
177
|
"yamllint"
|
|
181
178
|
];
|
|
182
179
|
//#endregion
|
|
183
|
-
//#region src/utils/atomic-ledger.ts
|
|
184
|
-
/**
|
|
185
|
-
* Small shared primitives for on-disk ledgers: an atomic JSON writer and a
|
|
186
|
-
* single-chain async mutex. Used by the audit tools and by the orchestrator
|
|
187
|
-
* queue. Lifted here so both share one implementation.
|
|
188
|
-
*/
|
|
189
|
-
/**
|
|
190
|
-
* Atomically write JSON: write to a `.tmp` file then rename over the target. The
|
|
191
|
-
* rename bumps the file's mtime in one step, which is what a file watcher polls.
|
|
192
|
-
*/
|
|
193
|
-
function writeJsonAtomic(targetPath, data) {
|
|
194
|
-
const tmpPath = `${targetPath}.tmp`;
|
|
195
|
-
fs$1.writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf8");
|
|
196
|
-
fs$1.renameSync(tmpPath, targetPath);
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* A single async mutex. Serializes read-modify-write cycles so concurrent callers
|
|
200
|
-
* (parallel task agents, audit tool calls) never interleave a mutation.
|
|
201
|
-
*/
|
|
202
|
-
function makeMutex() {
|
|
203
|
-
let chain = Promise.resolve();
|
|
204
|
-
return async function run(fn) {
|
|
205
|
-
const next = chain.then(() => fn());
|
|
206
|
-
chain = next.catch(() => void 0);
|
|
207
|
-
return next;
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
//#endregion
|
|
211
|
-
//#region src/lib/programs/audit/types.ts
|
|
212
|
-
/** Single source of truth for status glyph + color across audit views. */
|
|
213
|
-
const AUDIT_SEVERITY_STYLE = {
|
|
214
|
-
pending: {
|
|
215
|
-
glyph: "◌",
|
|
216
|
-
color: "gray"
|
|
217
|
-
},
|
|
218
|
-
pass: {
|
|
219
|
-
glyph: "✔",
|
|
220
|
-
color: "green"
|
|
221
|
-
},
|
|
222
|
-
error: {
|
|
223
|
-
glyph: "✘",
|
|
224
|
-
color: "red"
|
|
225
|
-
},
|
|
226
|
-
warning: {
|
|
227
|
-
glyph: "⚠",
|
|
228
|
-
color: "yellow"
|
|
229
|
-
},
|
|
230
|
-
suggestion: {
|
|
231
|
-
glyph: "•",
|
|
232
|
-
color: "cyan"
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
const AUDIT_CHECKS_FILE = ".posthog-audit-checks.json";
|
|
236
|
-
const AUDIT_REPORT_FILE = "posthog-audit-report.md";
|
|
237
|
-
const AUDIT_CHECKS_KEY = "auditChecks";
|
|
238
|
-
function getAuditChecks(session) {
|
|
239
|
-
const raw = session.frameworkContext[AUDIT_CHECKS_KEY];
|
|
240
|
-
return Array.isArray(raw) ? raw : [];
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Read the audit checks ledger off disk. Validation lives at write time —
|
|
244
|
-
* every writer (`audit_seed_checks` / `audit_add_checks` / `audit_resolve_checks`
|
|
245
|
-
* MCP tools, `seedAuditLedger`) zod-parses entries before the atomic write,
|
|
246
|
-
* so by the time the file watcher fires we trust the shape and only guard
|
|
247
|
-
* against the file not being a JSON array (corrupted / hand-edited / not yet
|
|
248
|
-
* seeded).
|
|
249
|
-
*/
|
|
250
|
-
function coerceAuditChecks(parsed) {
|
|
251
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
252
|
-
}
|
|
253
|
-
//#endregion
|
|
254
|
-
//#region src/lib/secret-vault.ts
|
|
255
|
-
/**
|
|
256
|
-
* Session-scoped secret vault.
|
|
257
|
-
*
|
|
258
|
-
* Tools that handle sensitive values (personal API keys pasted by the user
|
|
259
|
-
* or minted by the wizard, OAuth tokens, etc.) store them here and return
|
|
260
|
-
* an opaque `secret:<uuid>` ref to the agent. The agent passes the ref
|
|
261
|
-
* around to subsequent tools — `set_env_values` resolves it host-side
|
|
262
|
-
* before writing — but the raw value never enters the LLM conversation.
|
|
263
|
-
*
|
|
264
|
-
* The vault is created once per `createWizardToolsServer` call and lives
|
|
265
|
-
* for the duration of a single wizard run. There is no persistence and
|
|
266
|
-
* no cross-session sharing; refs minted in one run cannot be resolved in
|
|
267
|
-
* another.
|
|
268
|
-
*/
|
|
269
|
-
const REF_PREFIX = "secret:";
|
|
270
|
-
function createSecretVault() {
|
|
271
|
-
const store = /* @__PURE__ */ new Map();
|
|
272
|
-
return {
|
|
273
|
-
put(value, meta) {
|
|
274
|
-
const ref = `${REF_PREFIX}${randomUUID()}`;
|
|
275
|
-
store.set(ref, {
|
|
276
|
-
value,
|
|
277
|
-
meta: {
|
|
278
|
-
ref,
|
|
279
|
-
label: meta.label,
|
|
280
|
-
source: meta.source,
|
|
281
|
-
createdAt: Date.now()
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
return ref;
|
|
285
|
-
},
|
|
286
|
-
get(ref) {
|
|
287
|
-
return store.get(ref)?.value;
|
|
288
|
-
},
|
|
289
|
-
has(ref) {
|
|
290
|
-
return store.has(ref);
|
|
291
|
-
},
|
|
292
|
-
list() {
|
|
293
|
-
return [...store.values()].map((s) => s.meta);
|
|
294
|
-
},
|
|
295
|
-
clear() {
|
|
296
|
-
store.clear();
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
//#endregion
|
|
301
|
-
//#region src/lib/agent/runner/orchestrator/queue.ts
|
|
302
|
-
/**
|
|
303
|
-
* The orchestrator task queue.
|
|
304
|
-
*
|
|
305
|
-
* In memory, synchronous, single-owner: one Node process drives the run, so
|
|
306
|
-
* there is no locking. The queue imposes no execution policy — `nextRunnable`
|
|
307
|
-
* returns every pending task whose dependencies are satisfied, and how many of
|
|
308
|
-
* those run at once is decided by the task graph, not the queue.
|
|
309
|
-
*
|
|
310
|
-
* Every transition rewrites `<installDir>/.posthog-wizard-cache/queue.json`, a
|
|
311
|
-
* small file holding the whole queue, handoffs included. It is the run's log
|
|
312
|
-
* and the report's source. The whole cache folder is run-scoped and wiped when
|
|
313
|
-
* the run ends.
|
|
314
|
-
*/
|
|
315
|
-
const TaskStatus = {
|
|
316
|
-
Pending: "pending",
|
|
317
|
-
Running: "running",
|
|
318
|
-
Done: "done",
|
|
319
|
-
Skipped: "skipped",
|
|
320
|
-
Failed: "failed"
|
|
321
|
-
};
|
|
322
|
-
const QUEUE_DIR_NAME = ".posthog-wizard-cache";
|
|
323
|
-
const DEFAULT_MAX_ATTEMPTS = 2;
|
|
324
|
-
function nowIso() {
|
|
325
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
326
|
-
}
|
|
327
|
-
/** Dropped in the cache folder so an orphaned copy explains itself. */
|
|
328
|
-
const DELETE_ME_FILE = ".DELETE-ME.md";
|
|
329
|
-
const DELETE_ME_BODY = `# Safe to delete
|
|
330
|
-
|
|
331
|
-
This folder contains run artifacts from the PostHog Wizard. This should have
|
|
332
|
-
been deleted if the Wizard has finished running. If this wasn't deleted for
|
|
333
|
-
some reason, you can safely delete the entire \`${QUEUE_DIR_NAME}/\` folder.
|
|
334
|
-
`;
|
|
335
|
-
var QueueStore = class {
|
|
336
|
-
tasks = [];
|
|
337
|
-
onTransition;
|
|
338
|
-
runId;
|
|
339
|
-
queuePath;
|
|
340
|
-
constructor(installDir, runId, opts) {
|
|
341
|
-
this.onTransition = opts?.onTransition;
|
|
342
|
-
this.runId = runId;
|
|
343
|
-
const dir = path$1.join(installDir, QUEUE_DIR_NAME);
|
|
344
|
-
this.queuePath = path$1.join(dir, "queue.json");
|
|
345
|
-
fs$1.mkdirSync(dir, { recursive: true });
|
|
346
|
-
fs$1.writeFileSync(path$1.join(dir, DELETE_ME_FILE), DELETE_ME_BODY);
|
|
347
|
-
}
|
|
348
|
-
list() {
|
|
349
|
-
return this.tasks;
|
|
350
|
-
}
|
|
351
|
-
get(id) {
|
|
352
|
-
return this.tasks.find((t) => t.id === id);
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Every pending task whose dependencies are all satisfied (`done` or
|
|
356
|
-
* `skipped`). A skipped dependency does not block downstream work.
|
|
357
|
-
*/
|
|
358
|
-
nextRunnable() {
|
|
359
|
-
const doneIds = new Set(this.tasks.filter((t) => t.status === TaskStatus.Done || t.status === TaskStatus.Skipped).map((t) => t.id));
|
|
360
|
-
return this.tasks.filter((t) => t.status === TaskStatus.Pending && t.dependsOn.every((d) => doneIds.has(d)));
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* True when no task is running and none can be started. Either everything
|
|
364
|
-
* is terminal, or the only pending tasks are blocked by a failed dependency.
|
|
365
|
-
*/
|
|
366
|
-
isDrained() {
|
|
367
|
-
if (this.tasks.some((t) => t.status === TaskStatus.Running)) return false;
|
|
368
|
-
return this.nextRunnable().length === 0;
|
|
369
|
-
}
|
|
370
|
-
summary() {
|
|
371
|
-
const counts = {
|
|
372
|
-
[TaskStatus.Pending]: 0,
|
|
373
|
-
[TaskStatus.Running]: 0,
|
|
374
|
-
[TaskStatus.Done]: 0,
|
|
375
|
-
[TaskStatus.Skipped]: 0,
|
|
376
|
-
[TaskStatus.Failed]: 0
|
|
377
|
-
};
|
|
378
|
-
for (const t of this.tasks) counts[t.status] += 1;
|
|
379
|
-
return {
|
|
380
|
-
...counts,
|
|
381
|
-
total: this.tasks.length
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
readHandoff(id) {
|
|
385
|
-
return this.get(id)?.handoff ?? null;
|
|
386
|
-
}
|
|
387
|
-
/** Handoffs of completed tasks of a given type, oldest first. */
|
|
388
|
-
readHandoffsByType(type) {
|
|
389
|
-
return this.tasks.filter((t) => t.type === type && t.handoff).map((t) => t.handoff);
|
|
390
|
-
}
|
|
391
|
-
enqueue(input) {
|
|
392
|
-
const task = {
|
|
393
|
-
id: randomUUID(),
|
|
394
|
-
type: input.type,
|
|
395
|
-
label: input.label,
|
|
396
|
-
status: TaskStatus.Pending,
|
|
397
|
-
dependsOn: input.dependsOn ?? [],
|
|
398
|
-
inputs: input.inputs ?? {},
|
|
399
|
-
model: input.model,
|
|
400
|
-
attempts: 0,
|
|
401
|
-
maxAttempts: input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
402
|
-
enqueuedBy: input.enqueuedBy ?? "orchestrator",
|
|
403
|
-
createdAt: nowIso()
|
|
404
|
-
};
|
|
405
|
-
this.tasks.push(task);
|
|
406
|
-
this.reflect();
|
|
407
|
-
this.notify("enqueue", task);
|
|
408
|
-
return task;
|
|
409
|
-
}
|
|
410
|
-
start(id) {
|
|
411
|
-
const t = this.require(id);
|
|
412
|
-
t.status = TaskStatus.Running;
|
|
413
|
-
t.startedAt = nowIso();
|
|
414
|
-
t.attempts += 1;
|
|
415
|
-
this.reflect();
|
|
416
|
-
this.notify("start", t);
|
|
417
|
-
return t;
|
|
418
|
-
}
|
|
419
|
-
complete(id, handoff) {
|
|
420
|
-
return this.finish(id, TaskStatus.Done, handoff);
|
|
421
|
-
}
|
|
422
|
-
/** Terminal: the agent could not do the task. Not done, not failed. */
|
|
423
|
-
skip(id, handoff) {
|
|
424
|
-
return this.finish(id, TaskStatus.Skipped, handoff);
|
|
425
|
-
}
|
|
426
|
-
fail(id, error, handoff) {
|
|
427
|
-
const t = this.require(id);
|
|
428
|
-
t.error = error;
|
|
429
|
-
return this.finish(id, TaskStatus.Failed, handoff);
|
|
430
|
-
}
|
|
431
|
-
/** Put a failed/running task back to pending for a retry within the run. */
|
|
432
|
-
requeue(id) {
|
|
433
|
-
const t = this.require(id);
|
|
434
|
-
t.status = TaskStatus.Pending;
|
|
435
|
-
t.startedAt = void 0;
|
|
436
|
-
t.finishedAt = void 0;
|
|
437
|
-
this.reflect();
|
|
438
|
-
this.notify("requeue", t);
|
|
439
|
-
return t;
|
|
440
|
-
}
|
|
441
|
-
finish(id, status, handoff) {
|
|
442
|
-
const t = this.require(id);
|
|
443
|
-
if (handoff) t.handoff = handoff;
|
|
444
|
-
t.status = status;
|
|
445
|
-
t.finishedAt = nowIso();
|
|
446
|
-
this.reflect();
|
|
447
|
-
this.notify(status === TaskStatus.Done ? "complete" : status === TaskStatus.Skipped ? "skip" : "fail", t);
|
|
448
|
-
return t;
|
|
449
|
-
}
|
|
450
|
-
reflect() {
|
|
451
|
-
const file = {
|
|
452
|
-
version: 1,
|
|
453
|
-
runId: this.runId,
|
|
454
|
-
tasks: this.tasks
|
|
455
|
-
};
|
|
456
|
-
writeJsonAtomic(this.queuePath, file);
|
|
457
|
-
}
|
|
458
|
-
notify(event, task) {
|
|
459
|
-
try {
|
|
460
|
-
this.onTransition?.(event, task);
|
|
461
|
-
} catch (error) {
|
|
462
|
-
analytics.captureException(error instanceof Error ? error : new Error(String(error)), {
|
|
463
|
-
step: "orchestrator_queue_listener",
|
|
464
|
-
event
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
require(id) {
|
|
469
|
-
const t = this.get(id);
|
|
470
|
-
if (!t) throw new Error(`No task ${id} in the queue`);
|
|
471
|
-
return t;
|
|
472
|
-
}
|
|
473
|
-
};
|
|
474
|
-
//#endregion
|
|
475
|
-
//#region src/lib/agent/runner/orchestrator/queue-tools.ts
|
|
476
|
-
/**
|
|
477
|
-
* Orchestrator MCP tools, registered into the existing `wizard-tools` server when
|
|
478
|
-
* a queue is present. They let the orchestrator agent and task agents grow the
|
|
479
|
-
* queue, report completion with a structured handoff, and read prior handoffs.
|
|
480
|
-
*
|
|
481
|
-
* The guard logic and the apply functions are plain, exported, and unit-tested.
|
|
482
|
-
* `buildOrchestratorTools` wraps them in the SDK `tool()` shape.
|
|
483
|
-
*/
|
|
484
|
-
function stableStringify(value) {
|
|
485
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
486
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
487
|
-
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
488
|
-
}
|
|
489
|
-
function dedupKey(type, inputs) {
|
|
490
|
-
return `${type}::${stableStringify(inputs)}`;
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* A backstop on total queue size. Tasks can enqueue tasks, so a misbehaving
|
|
494
|
-
* type could grow the queue without bound. Keeping the graph small is the job
|
|
495
|
-
* of good agent and skill design, not this number — it only stops a runaway.
|
|
496
|
-
* The real flow is ~9 tasks, so this sits well clear of it.
|
|
497
|
-
*/
|
|
498
|
-
const MAX_QUEUE_TASKS = 30;
|
|
499
|
-
/**
|
|
500
|
-
* Validate an enqueue. Structural checks only — a real type, real dependencies,
|
|
501
|
-
* not a literal duplicate, and not past the runaway backstop. How much runs,
|
|
502
|
-
* and in what shape, is the task graph's business, not a knob's.
|
|
503
|
-
*/
|
|
504
|
-
function checkEnqueueGuards(ctx, args) {
|
|
505
|
-
const tasks = ctx.store.list();
|
|
506
|
-
if (tasks.length >= MAX_QUEUE_TASKS) return {
|
|
507
|
-
ok: false,
|
|
508
|
-
guard: "queue-full",
|
|
509
|
-
message: `The queue already holds ${tasks.length} tasks (cap ${MAX_QUEUE_TASKS}). Refine the existing tasks rather than adding more.`
|
|
510
|
-
};
|
|
511
|
-
if (!ctx.validTypes.includes(args.type)) return {
|
|
512
|
-
ok: false,
|
|
513
|
-
guard: "unknown-type",
|
|
514
|
-
message: `Unknown task type "${args.type}". Valid types: ${ctx.validTypes.join(", ")}.`
|
|
515
|
-
};
|
|
516
|
-
for (const dep of args.dependsOn ?? []) if (!ctx.store.get(dep)) return {
|
|
517
|
-
ok: false,
|
|
518
|
-
guard: "unknown-dep",
|
|
519
|
-
message: `Dependency "${dep}" is not a known task id.`
|
|
520
|
-
};
|
|
521
|
-
const key = dedupKey(args.type, args.inputs ?? {});
|
|
522
|
-
if (tasks.some((t) => t.status !== TaskStatus.Failed && dedupKey(t.type, t.inputs) === key)) return {
|
|
523
|
-
ok: false,
|
|
524
|
-
guard: "dedup",
|
|
525
|
-
message: `A "${args.type}" task with these inputs already exists.`
|
|
526
|
-
};
|
|
527
|
-
return { ok: true };
|
|
528
|
-
}
|
|
529
|
-
function applyEnqueue(ctx, args) {
|
|
530
|
-
const guard = checkEnqueueGuards(ctx, args);
|
|
531
|
-
if (!guard.ok) return guard;
|
|
532
|
-
return {
|
|
533
|
-
ok: true,
|
|
534
|
-
task: ctx.store.enqueue({
|
|
535
|
-
type: args.type,
|
|
536
|
-
label: args.label,
|
|
537
|
-
inputs: args.inputs ?? {},
|
|
538
|
-
dependsOn: args.dependsOn ?? [],
|
|
539
|
-
model: args.model,
|
|
540
|
-
enqueuedBy: ctx.currentTaskId ?? "orchestrator"
|
|
541
|
-
})
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
function applyComplete(ctx, args) {
|
|
545
|
-
const id = ctx.currentTaskId;
|
|
546
|
-
if (!id) return {
|
|
547
|
-
ok: false,
|
|
548
|
-
message: "complete_task can only be called by a running task agent."
|
|
549
|
-
};
|
|
550
|
-
if (args.status === TaskStatus.Failed) ctx.store.fail(id, {
|
|
551
|
-
type: "self-reported",
|
|
552
|
-
message: args.handoff.forNextAgent
|
|
553
|
-
}, args.handoff);
|
|
554
|
-
else if (args.status === TaskStatus.Skipped) ctx.store.skip(id, args.handoff);
|
|
555
|
-
else ctx.store.complete(id, args.handoff);
|
|
556
|
-
return { ok: true };
|
|
557
|
-
}
|
|
558
|
-
function applyReadHandoffs(ctx, args) {
|
|
559
|
-
if (args.taskId) {
|
|
560
|
-
const h = ctx.store.readHandoff(args.taskId);
|
|
561
|
-
return h ? [h] : [];
|
|
562
|
-
}
|
|
563
|
-
if (args.type) return ctx.store.readHandoffsByType(args.type);
|
|
564
|
-
const currentId = ctx.currentTaskId;
|
|
565
|
-
const current = currentId ? ctx.store.get(currentId) : void 0;
|
|
566
|
-
if (!current) return [];
|
|
567
|
-
return current.dependsOn.map((depId) => ctx.store.readHandoff(depId)).filter((h) => h !== null);
|
|
568
|
-
}
|
|
569
|
-
const HANDOFF_SHAPE = {
|
|
570
|
-
goals: z.string().describe("What this task was asked to achieve."),
|
|
571
|
-
did: z.string().describe("What you actually did."),
|
|
572
|
-
forNextAgent: z.string().describe("What the next agent should know."),
|
|
573
|
-
filesTouched: z.array(z.string()).optional(),
|
|
574
|
-
conflict: z.string().optional().describe("A one-line summary of any conflict you could not cleanly resolve (e.g. a dependency or build conflict). Put full detail in your work; this line is surfaced to the user.")
|
|
575
|
-
};
|
|
576
|
-
function textResult(text, isError = false) {
|
|
577
|
-
return {
|
|
578
|
-
isError,
|
|
579
|
-
content: [{
|
|
580
|
-
type: "text",
|
|
581
|
-
text
|
|
582
|
-
}]
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Build the orchestrator tools in the SDK `tool()` shape. Called from
|
|
587
|
-
* createWizardToolsServer only when a queue context is present.
|
|
588
|
-
*/
|
|
589
|
-
function buildOrchestratorTools(tool, ctx) {
|
|
590
|
-
return [
|
|
591
|
-
tool("enqueue_task", "Add a task to the orchestrator queue. Use it to seed work and to enqueue follow-up work you discover. Keep tasks small and discrete.", {
|
|
592
|
-
type: z.string().describe(`The task type. One of: ${ctx.validTypes.join(", ")}.`),
|
|
593
|
-
label: z.string().optional().describe("A short label for the UI — the action in a few words (e.g. \"Add the PostHog SDK\", \"Initialize PostHog\"). Leave out file names, class names, and other specifics."),
|
|
594
|
-
inputs: z.record(z.unknown()).optional(),
|
|
595
|
-
dependsOn: z.array(z.string()).optional().describe("Task ids that must be done before this task runs."),
|
|
596
|
-
model: z.string().optional(),
|
|
597
|
-
reason: z.string().describe("One line on why this task is needed.")
|
|
598
|
-
}, ((args) => {
|
|
599
|
-
const res = applyEnqueue(ctx, args);
|
|
600
|
-
if (!res.ok) {
|
|
601
|
-
analytics.wizardCapture("orchestrator guard tripped", {
|
|
602
|
-
guard: res.guard,
|
|
603
|
-
type: args.type
|
|
604
|
-
});
|
|
605
|
-
return textResult(res.message, true);
|
|
606
|
-
}
|
|
607
|
-
return textResult(JSON.stringify({ id: res.task.id }));
|
|
608
|
-
})),
|
|
609
|
-
tool("complete_task", "Report the outcome of your task. Always call this exactly once when you finish, with a structured handoff for the next agent. Use status 'skipped' when the task does not apply to this project and you cannot do it (say why in the handoff) — not 'done'.", {
|
|
610
|
-
status: z.enum([
|
|
611
|
-
"done",
|
|
612
|
-
"failed",
|
|
613
|
-
"skipped"
|
|
614
|
-
]),
|
|
615
|
-
handoff: z.object(HANDOFF_SHAPE)
|
|
616
|
-
}, ((args) => {
|
|
617
|
-
const res = applyComplete(ctx, args);
|
|
618
|
-
if (!res.ok) return textResult(res.message, true);
|
|
619
|
-
return textResult("ok");
|
|
620
|
-
})),
|
|
621
|
-
tool("read_handoffs", "Read structured handoffs from earlier tasks. With no argument, returns the handoffs of your dependencies.", {
|
|
622
|
-
type: z.string().optional(),
|
|
623
|
-
taskId: z.string().optional()
|
|
624
|
-
}, ((args) => {
|
|
625
|
-
const handoffs = applyReadHandoffs(ctx, args);
|
|
626
|
-
return textResult(JSON.stringify(handoffs, null, 2));
|
|
627
|
-
}))
|
|
628
|
-
];
|
|
629
|
-
}
|
|
630
|
-
//#endregion
|
|
631
|
-
//#region src/lib/wizard-tools.ts
|
|
632
|
-
/**
|
|
633
|
-
* Unified in-process MCP server for the PostHog wizard.
|
|
634
|
-
*
|
|
635
|
-
* Provides tools that run locally (secret values never leave the machine):
|
|
636
|
-
* - check_env_keys: Check which env var keys exist in a .env file
|
|
637
|
-
* - set_env_values: Create/update env vars in a .env file
|
|
638
|
-
* - detect_package_manager: Detect the project's package manager(s)
|
|
639
|
-
* - load_skill_menu / install_skill: Skill installation
|
|
640
|
-
* - audit_seed_checks / audit_add_checks / audit_resolve_checks: Audit ledger ownership
|
|
641
|
-
*/
|
|
642
|
-
let _sdkModule$1 = null;
|
|
643
|
-
async function getSDKModule$1() {
|
|
644
|
-
if (!_sdkModule$1) _sdkModule$1 = await import("@anthropic-ai/claude-agent-sdk");
|
|
645
|
-
return _sdkModule$1;
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Fetch the skill menu from the skills server.
|
|
649
|
-
* Returns parsed data on success, `null` on failure.
|
|
650
|
-
*/
|
|
651
|
-
async function fetchSkillMenu(skillsBaseUrl) {
|
|
652
|
-
try {
|
|
653
|
-
const menuUrl = `${skillsBaseUrl}/skill-menu.json`;
|
|
654
|
-
logToFile(`fetchSkillMenu: fetching from ${menuUrl}`);
|
|
655
|
-
const resp = await fetch(menuUrl);
|
|
656
|
-
if (resp.ok) {
|
|
657
|
-
const data = await resp.json();
|
|
658
|
-
logToFile(`fetchSkillMenu: loaded (${Object.keys(data.categories).length} categories)`);
|
|
659
|
-
return data;
|
|
660
|
-
}
|
|
661
|
-
logToFile(`fetchSkillMenu: failed with HTTP ${resp.status}`);
|
|
662
|
-
return null;
|
|
663
|
-
} catch (err) {
|
|
664
|
-
logToFile(`fetchSkillMenu: error: ${err.message}`);
|
|
665
|
-
return null;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Download and extract a skill.
|
|
670
|
-
* By default installs to `<installDir>/.claude/skills/<id>/`.
|
|
671
|
-
* Pass `skillsRoot` to override the base directory (e.g. `.posthog/skills`).
|
|
672
|
-
*/
|
|
673
|
-
function downloadSkill(skillEntry, installDir, skillsRoot) {
|
|
674
|
-
const skillDir = skillsRoot ? path.join(installDir, skillsRoot, skillEntry.id) : path.join(installDir, ".claude", "skills", skillEntry.id);
|
|
675
|
-
const tmpFile = skillTmpPath(skillEntry.id);
|
|
676
|
-
try {
|
|
677
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
678
|
-
execFileSync("curl", [
|
|
679
|
-
"-sL",
|
|
680
|
-
skillEntry.downloadUrl,
|
|
681
|
-
"-o",
|
|
682
|
-
tmpFile
|
|
683
|
-
], { timeout: 3e4 });
|
|
684
|
-
execFileSync("unzip", [
|
|
685
|
-
"-o",
|
|
686
|
-
tmpFile,
|
|
687
|
-
"-d",
|
|
688
|
-
skillDir
|
|
689
|
-
], { timeout: 3e4 });
|
|
690
|
-
fs.writeFileSync(path.join(skillDir, ".posthog-wizard"), "");
|
|
691
|
-
try {
|
|
692
|
-
fs.unlinkSync(tmpFile);
|
|
693
|
-
} catch {}
|
|
694
|
-
logToFile(`downloadSkill: installed ${skillEntry.id} from ${skillEntry.downloadUrl}`);
|
|
695
|
-
return { success: true };
|
|
696
|
-
} catch (err) {
|
|
697
|
-
logToFile(`downloadSkill: error: ${err.message}`);
|
|
698
|
-
return {
|
|
699
|
-
success: false,
|
|
700
|
-
error: err.message
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* High-level "install a skill by ID" helper. Fetches the skill menu,
|
|
706
|
-
* finds the skill, downloads and extracts it. Programs should use this
|
|
707
|
-
* instead of composing fetchSkillMenu + downloadSkill themselves.
|
|
708
|
-
*/
|
|
709
|
-
async function installSkillById(skillId, installDir, skillsBaseUrl, skillsRoot) {
|
|
710
|
-
const menu = await fetchSkillMenu(skillsBaseUrl);
|
|
711
|
-
if (!menu) return { kind: "menu-fetch-failed" };
|
|
712
|
-
const skill = Object.values(menu.categories).flat().find((s) => s.id === skillId);
|
|
713
|
-
if (!skill) return {
|
|
714
|
-
kind: "skill-not-found",
|
|
715
|
-
skillId
|
|
716
|
-
};
|
|
717
|
-
const result = downloadSkill(skill, installDir, skillsRoot);
|
|
718
|
-
if (!result.success) return {
|
|
719
|
-
kind: "download-failed",
|
|
720
|
-
message: result.error ?? "unknown"
|
|
721
|
-
};
|
|
722
|
-
return {
|
|
723
|
-
kind: "ok",
|
|
724
|
-
path: skillsRoot ? `${skillsRoot}/${skillId}` : `.claude/skills/${skillId}`
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Pure decision function for the wizard_ask caps. Returns whether the
|
|
729
|
-
* upcoming call should proceed and, if not, the error message to surface
|
|
730
|
-
* to the agent. Extracted so the policy can be unit-tested without
|
|
731
|
-
* spinning up an MCP server.
|
|
732
|
-
*
|
|
733
|
-
* The adjacency nudge fires exactly once per run (the caller records it
|
|
734
|
-
* via `adjacencyNudged`) — flows that legitimately need several
|
|
735
|
-
* sequential, answer-dependent asks then proceed up to `maxQuestions`.
|
|
736
|
-
* Without the flag the rejected call would never advance the counter and
|
|
737
|
-
* every later call would be rejected, making caps above the threshold
|
|
738
|
-
* unreachable.
|
|
739
|
-
*/
|
|
740
|
-
function evaluateAskCap(callCount, maxQuestions, adjacencyNudged = false) {
|
|
741
|
-
if (callCount >= maxQuestions) return {
|
|
742
|
-
kind: "capped",
|
|
743
|
-
reason: "max_questions",
|
|
744
|
-
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.`
|
|
745
|
-
};
|
|
746
|
-
if (!adjacencyNudged && callCount >= 3) return {
|
|
747
|
-
kind: "capped",
|
|
748
|
-
reason: "adjacency",
|
|
749
|
-
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.`
|
|
750
|
-
};
|
|
751
|
-
return { kind: "ok" };
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* Resolve filePath relative to workingDirectory, rejecting path traversal.
|
|
755
|
-
*/
|
|
756
|
-
function resolveEnvPath(workingDirectory, filePath) {
|
|
757
|
-
const resolved = path.resolve(workingDirectory, filePath);
|
|
758
|
-
if (!resolved.startsWith(workingDirectory + path.sep) && resolved !== workingDirectory) throw new Error(`Path traversal rejected: "${filePath}" resolves outside working directory`);
|
|
759
|
-
return resolved;
|
|
760
|
-
}
|
|
761
|
-
/**
|
|
762
|
-
* Ensure the given env file basename is covered by .gitignore in the working directory.
|
|
763
|
-
* Creates .gitignore if it doesn't exist; appends the entry if missing.
|
|
764
|
-
*/
|
|
765
|
-
function ensureGitignoreCoverage(workingDirectory, envFileName) {
|
|
766
|
-
const gitignorePath = path.join(workingDirectory, ".gitignore");
|
|
767
|
-
if (fs.existsSync(gitignorePath)) {
|
|
768
|
-
const content = fs.readFileSync(gitignorePath, "utf8");
|
|
769
|
-
if (content.split("\n").some((line) => line.trim() === envFileName)) return;
|
|
770
|
-
const newContent = content.endsWith("\n") ? `${content}${envFileName}\n` : `${content}\n${envFileName}\n`;
|
|
771
|
-
fs.writeFileSync(gitignorePath, newContent, "utf8");
|
|
772
|
-
} else fs.writeFileSync(gitignorePath, `${envFileName}\n`, "utf8");
|
|
773
|
-
}
|
|
774
|
-
/**
|
|
775
|
-
* Parse a .env file's content and return the set of defined key names.
|
|
776
|
-
*/
|
|
777
|
-
function parseEnvKeys(content) {
|
|
778
|
-
const keys = /* @__PURE__ */ new Set();
|
|
779
|
-
for (const line of content.split("\n")) {
|
|
780
|
-
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
781
|
-
if (match) keys.add(match[1]);
|
|
782
|
-
}
|
|
783
|
-
return keys;
|
|
784
|
-
}
|
|
785
|
-
/**
|
|
786
|
-
* Merge key-value pairs into existing .env content.
|
|
787
|
-
* Updates existing keys in-place, appends new keys at the end.
|
|
788
|
-
*/
|
|
789
|
-
function mergeEnvValues(content, values) {
|
|
790
|
-
let result = content;
|
|
791
|
-
const updatedKeys = /* @__PURE__ */ new Set();
|
|
792
|
-
for (const [key, value] of Object.entries(values)) {
|
|
793
|
-
const regex = new RegExp(`^(\\s*${key}\\s*=).*$`, "m");
|
|
794
|
-
if (regex.test(result)) {
|
|
795
|
-
result = result.replace(regex, `$1${value}`);
|
|
796
|
-
updatedKeys.add(key);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
const newKeys = Object.entries(values).filter(([key]) => !updatedKeys.has(key));
|
|
800
|
-
if (newKeys.length > 0) {
|
|
801
|
-
if (result.length > 0 && !result.endsWith("\n")) result += "\n";
|
|
802
|
-
for (const [key, value] of newKeys) result += `${key}=${value}\n`;
|
|
803
|
-
}
|
|
804
|
-
return result;
|
|
805
|
-
}
|
|
806
|
-
const AUDIT_STATUSES = [
|
|
807
|
-
"pending",
|
|
808
|
-
"pass",
|
|
809
|
-
"error",
|
|
810
|
-
"warning",
|
|
811
|
-
"suggestion"
|
|
812
|
-
];
|
|
813
|
-
const auditCheckSchema = z.object({
|
|
814
|
-
id: z.string().min(1),
|
|
815
|
-
area: z.string().min(1),
|
|
816
|
-
label: z.string().min(1),
|
|
817
|
-
status: z.enum(AUDIT_STATUSES),
|
|
818
|
-
file: z.string().optional(),
|
|
819
|
-
details: z.string().optional()
|
|
820
|
-
});
|
|
821
|
-
const auditUpdateSchema = z.object({
|
|
822
|
-
id: z.string().min(1),
|
|
823
|
-
status: z.enum(AUDIT_STATUSES),
|
|
824
|
-
file: z.string().optional(),
|
|
825
|
-
details: z.string().optional()
|
|
826
|
-
});
|
|
827
|
-
/** Atomically write the audit ledger. Thin typed wrapper over writeJsonAtomic. */
|
|
828
|
-
function writeLedgerAtomic(targetPath, checks) {
|
|
829
|
-
writeJsonAtomic(targetPath, checks);
|
|
830
|
-
}
|
|
831
|
-
/**
|
|
832
|
-
* Apply a batch of patches to the ledger by id. Returns the new array and the
|
|
833
|
-
* list of update ids that didn't match any existing check.
|
|
834
|
-
*/
|
|
835
|
-
function applyAuditUpdates(current, updates) {
|
|
836
|
-
const byId = new Map(current.map((c) => [c.id, c]));
|
|
837
|
-
const unknown = [];
|
|
838
|
-
for (const u of updates) {
|
|
839
|
-
const existing = byId.get(u.id);
|
|
840
|
-
if (!existing) {
|
|
841
|
-
unknown.push(u.id);
|
|
842
|
-
continue;
|
|
843
|
-
}
|
|
844
|
-
byId.set(u.id, {
|
|
845
|
-
...existing,
|
|
846
|
-
status: u.status,
|
|
847
|
-
...u.file !== void 0 ? { file: u.file } : {},
|
|
848
|
-
...u.details !== void 0 ? { details: u.details } : {}
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
return {
|
|
852
|
-
next: current.map((c) => byId.get(c.id) ?? c),
|
|
853
|
-
unknown
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
/**
|
|
857
|
-
* Append new checks to a seeded ledger. Duplicate ids are reported without
|
|
858
|
-
* mutating the current ledger, including duplicates inside the additions.
|
|
859
|
-
*/
|
|
860
|
-
function applyAuditAdditions(current, additions) {
|
|
861
|
-
const existingIds = new Set(current.map((c) => c.id));
|
|
862
|
-
const additionIds = /* @__PURE__ */ new Set();
|
|
863
|
-
const duplicates = [];
|
|
864
|
-
for (const check of additions) {
|
|
865
|
-
if (existingIds.has(check.id) || additionIds.has(check.id)) {
|
|
866
|
-
duplicates.push(check.id);
|
|
867
|
-
continue;
|
|
868
|
-
}
|
|
869
|
-
additionIds.add(check.id);
|
|
870
|
-
}
|
|
871
|
-
if (duplicates.length > 0) return {
|
|
872
|
-
next: current,
|
|
873
|
-
duplicates
|
|
874
|
-
};
|
|
875
|
-
return {
|
|
876
|
-
next: [...current, ...additions],
|
|
877
|
-
duplicates: []
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
function readLedger(targetPath) {
|
|
881
|
-
if (!fs.existsSync(targetPath)) return [];
|
|
882
|
-
try {
|
|
883
|
-
return coerceAuditChecks(JSON.parse(fs.readFileSync(targetPath, "utf8")));
|
|
884
|
-
} catch {
|
|
885
|
-
return [];
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
function appendAuditChecksToLedger(targetPath, additions) {
|
|
889
|
-
if (!fs.existsSync(targetPath)) return {
|
|
890
|
-
ok: false,
|
|
891
|
-
reason: "missing-ledger"
|
|
892
|
-
};
|
|
893
|
-
const { next, duplicates } = applyAuditAdditions(readLedger(targetPath), additions);
|
|
894
|
-
if (duplicates.length > 0) return {
|
|
895
|
-
ok: false,
|
|
896
|
-
reason: "duplicate-ids",
|
|
897
|
-
ids: duplicates
|
|
898
|
-
};
|
|
899
|
-
writeLedgerAtomic(targetPath, next);
|
|
900
|
-
return {
|
|
901
|
-
ok: true,
|
|
902
|
-
added: additions.length
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
const SERVER_NAME = "wizard-tools";
|
|
906
|
-
/**
|
|
907
|
-
* Create the unified in-process MCP server with all wizard tools.
|
|
908
|
-
* Must be called asynchronously because the SDK is an ESM module loaded via dynamic import.
|
|
909
|
-
*/
|
|
910
|
-
async function createWizardToolsServer(options) {
|
|
911
|
-
const { workingDirectory, detectPackageManager, skillsBaseUrl, askBridge, askMaxQuestions = 10, secretVault = createSecretVault(), orchestrator } = options;
|
|
912
|
-
const { tool, createSdkMcpServer } = await getSDKModule$1();
|
|
913
|
-
let askCallCount = 0;
|
|
914
|
-
let askAdjacencyNudged = false;
|
|
915
|
-
let cachedSkillMenu = {};
|
|
916
|
-
let categoryNames = ["integration"];
|
|
917
|
-
const menu = await fetchSkillMenu(skillsBaseUrl);
|
|
918
|
-
if (menu) cachedSkillMenu = menu.categories;
|
|
919
|
-
const keys = Object.keys(cachedSkillMenu);
|
|
920
|
-
if (keys.length > 0) categoryNames = keys;
|
|
921
|
-
const checkEnvKeys = tool("check_env_keys", "Check which environment variable keys are present or missing in a .env file. Never reveals values.", {
|
|
922
|
-
filePath: z.string().describe("Path to the .env file, relative to the project root"),
|
|
923
|
-
keys: z.array(z.string()).describe("Environment variable key names to check")
|
|
924
|
-
}, (args) => {
|
|
925
|
-
const resolved = resolveEnvPath(workingDirectory, args.filePath);
|
|
926
|
-
logToFile(`check_env_keys: ${resolved}, keys: ${args.keys.join(", ")}`);
|
|
927
|
-
const existingKeys = fs.existsSync(resolved) ? parseEnvKeys(fs.readFileSync(resolved, "utf8")) : /* @__PURE__ */ new Set();
|
|
928
|
-
const results = {};
|
|
929
|
-
for (const key of args.keys) results[key] = existingKeys.has(key) ? "present" : "missing";
|
|
930
|
-
return { content: [{
|
|
931
|
-
type: "text",
|
|
932
|
-
text: JSON.stringify(results, null, 2)
|
|
933
|
-
}] };
|
|
934
|
-
});
|
|
935
|
-
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.", {
|
|
936
|
-
filePath: z.string().describe("Path to the .env file, relative to the project root"),
|
|
937
|
-
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)")
|
|
938
|
-
}, (args) => {
|
|
939
|
-
const forbidden = Object.keys(args.values).find((k) => k.toUpperCase() === "POSTHOG_KEY");
|
|
940
|
-
if (forbidden) return {
|
|
941
|
-
content: [{
|
|
942
|
-
type: "text",
|
|
943
|
-
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).`
|
|
944
|
-
}],
|
|
945
|
-
isError: true
|
|
946
|
-
};
|
|
947
|
-
const resolvedValues = {};
|
|
948
|
-
const resolvedRefKeys = [];
|
|
949
|
-
for (const [key, val] of Object.entries(args.values)) if (typeof val === "string") resolvedValues[key] = val;
|
|
950
|
-
else {
|
|
951
|
-
const secret = secretVault.get(val.secretRef);
|
|
952
|
-
if (secret === void 0) return {
|
|
953
|
-
content: [{
|
|
954
|
-
type: "text",
|
|
955
|
-
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.`
|
|
956
|
-
}],
|
|
957
|
-
isError: true
|
|
958
|
-
};
|
|
959
|
-
resolvedValues[key] = secret;
|
|
960
|
-
resolvedRefKeys.push(key);
|
|
961
|
-
}
|
|
962
|
-
const resolved = resolveEnvPath(workingDirectory, args.filePath);
|
|
963
|
-
logToFile(`set_env_values: ${resolved}, keys: ${Object.keys(resolvedValues).join(", ")}${resolvedRefKeys.length > 0 ? ` (secret refs: ${resolvedRefKeys.join(", ")})` : ""}`);
|
|
964
|
-
const content = mergeEnvValues(fs.existsSync(resolved) ? fs.readFileSync(resolved, "utf8") : "", resolvedValues);
|
|
965
|
-
const dir = path.dirname(resolved);
|
|
966
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
967
|
-
fs.writeFileSync(resolved, content, "utf8");
|
|
968
|
-
ensureGitignoreCoverage(workingDirectory, path.basename(resolved));
|
|
969
|
-
return { content: [{
|
|
970
|
-
type: "text",
|
|
971
|
-
text: `Updated ${Object.keys(args.values).length} key(s) in ${args.filePath}`
|
|
972
|
-
}] };
|
|
973
|
-
});
|
|
974
|
-
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 () => {
|
|
975
|
-
logToFile(`detect_package_manager: scanning ${workingDirectory}`);
|
|
976
|
-
const result = await detectPackageManager(workingDirectory);
|
|
977
|
-
logToFile(`detect_package_manager: detected ${result.detected.length} package manager(s)`);
|
|
978
|
-
return { content: [{
|
|
979
|
-
type: "text",
|
|
980
|
-
text: JSON.stringify(result, null, 2)
|
|
981
|
-
}] };
|
|
982
|
-
});
|
|
983
|
-
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) => {
|
|
984
|
-
const skills = cachedSkillMenu[args.category];
|
|
985
|
-
if (!skills || skills.length === 0) return {
|
|
986
|
-
content: [{
|
|
987
|
-
type: "text",
|
|
988
|
-
text: `No skills found for category "${args.category}".`
|
|
989
|
-
}],
|
|
990
|
-
isError: true
|
|
991
|
-
};
|
|
992
|
-
const menuText = skills.map((s) => `- ${s.id}: ${s.name}`).join("\n");
|
|
993
|
-
logToFile(`load_skill_menu: returning ${skills.length} skills for "${args.category}"`);
|
|
994
|
-
return { content: [{
|
|
995
|
-
type: "text",
|
|
996
|
-
text: menuText
|
|
997
|
-
}] };
|
|
998
|
-
});
|
|
999
|
-
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\")") }, (args) => {
|
|
1000
|
-
if (!/^[a-z0-9][a-z0-9_-]*$/.test(args.skillId)) return {
|
|
1001
|
-
content: [{
|
|
1002
|
-
type: "text",
|
|
1003
|
-
text: "Error: skillId must be lowercase alphanumeric with hyphens."
|
|
1004
|
-
}],
|
|
1005
|
-
isError: true
|
|
1006
|
-
};
|
|
1007
|
-
const skill = Object.values(cachedSkillMenu).flat().find((s) => s.id === args.skillId);
|
|
1008
|
-
if (!skill) return {
|
|
1009
|
-
content: [{
|
|
1010
|
-
type: "text",
|
|
1011
|
-
text: `Error: skill "${args.skillId}" not found. Use load_skill_menu to see available skills.`
|
|
1012
|
-
}],
|
|
1013
|
-
isError: true
|
|
1014
|
-
};
|
|
1015
|
-
const result = downloadSkill(skill, workingDirectory);
|
|
1016
|
-
if (result.success) return { content: [{
|
|
1017
|
-
type: "text",
|
|
1018
|
-
text: `Skill installed to .claude/skills/${args.skillId}/`
|
|
1019
|
-
}] };
|
|
1020
|
-
else return {
|
|
1021
|
-
content: [{
|
|
1022
|
-
type: "text",
|
|
1023
|
-
text: `Error installing skill: ${result.error}`
|
|
1024
|
-
}],
|
|
1025
|
-
isError: true
|
|
1026
|
-
};
|
|
1027
|
-
});
|
|
1028
|
-
const auditLedgerPath = path.join(workingDirectory, AUDIT_CHECKS_FILE);
|
|
1029
|
-
const auditMutex = makeMutex();
|
|
1030
|
-
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) => {
|
|
1031
|
-
return auditMutex(() => {
|
|
1032
|
-
writeLedgerAtomic(auditLedgerPath, args.checks);
|
|
1033
|
-
logToFile(`audit_seed_checks: wrote ${args.checks.length} entries`);
|
|
1034
|
-
return { content: [{
|
|
1035
|
-
type: "text",
|
|
1036
|
-
text: `Seeded ${args.checks.length} audit checks.`
|
|
1037
|
-
}] };
|
|
1038
|
-
});
|
|
1039
|
-
});
|
|
1040
|
-
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) => {
|
|
1041
|
-
return auditMutex(() => {
|
|
1042
|
-
const result = appendAuditChecksToLedger(auditLedgerPath, args.checks);
|
|
1043
|
-
if (!result.ok) {
|
|
1044
|
-
if (result.reason === "missing-ledger") return {
|
|
1045
|
-
content: [{
|
|
1046
|
-
type: "text",
|
|
1047
|
-
text: "Error: audit ledger does not exist. Run audit_seed_checks first."
|
|
1048
|
-
}],
|
|
1049
|
-
isError: true
|
|
1050
|
-
};
|
|
1051
|
-
return {
|
|
1052
|
-
content: [{
|
|
1053
|
-
type: "text",
|
|
1054
|
-
text: `Error: duplicate check id(s): ${result.ids.join(", ")}. Check ids must be unique.`
|
|
1055
|
-
}],
|
|
1056
|
-
isError: true
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
logToFile(`audit_add_checks: added ${result.added} entries`);
|
|
1060
|
-
return { content: [{
|
|
1061
|
-
type: "text",
|
|
1062
|
-
text: `Added ${result.added} audit check(s).`
|
|
1063
|
-
}] };
|
|
1064
|
-
});
|
|
1065
|
-
});
|
|
1066
|
-
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) => {
|
|
1067
|
-
return auditMutex(() => {
|
|
1068
|
-
const { next, unknown } = applyAuditUpdates(readLedger(auditLedgerPath), args.updates);
|
|
1069
|
-
if (unknown.length > 0) return {
|
|
1070
|
-
content: [{
|
|
1071
|
-
type: "text",
|
|
1072
|
-
text: `Error: unknown check id(s): ${unknown.join(", ")}. Run audit_seed_checks first or check the id.`
|
|
1073
|
-
}],
|
|
1074
|
-
isError: true
|
|
1075
|
-
};
|
|
1076
|
-
writeLedgerAtomic(auditLedgerPath, next);
|
|
1077
|
-
logToFile(`audit_resolve_checks: applied ${args.updates.length} update(s)`);
|
|
1078
|
-
return { content: [{
|
|
1079
|
-
type: "text",
|
|
1080
|
-
text: `Resolved ${args.updates.length} check(s).`
|
|
1081
|
-
}] };
|
|
1082
|
-
});
|
|
1083
|
-
});
|
|
1084
|
-
const askQuestionSchema = z.object({
|
|
1085
|
-
id: z.string().min(1).describe("Stable key for the answer in the response map"),
|
|
1086
|
-
prompt: z.string().min(1).describe("Question text shown to the user"),
|
|
1087
|
-
kind: z.enum([
|
|
1088
|
-
"single",
|
|
1089
|
-
"multi",
|
|
1090
|
-
"text"
|
|
1091
|
-
]).describe("'single' = pick one option, 'multi' = pick any, 'text' = free-form single-line answer"),
|
|
1092
|
-
options: z.array(z.object({
|
|
1093
|
-
label: z.string(),
|
|
1094
|
-
value: z.string(),
|
|
1095
|
-
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.")
|
|
1096
|
-
})).optional().describe("Required for kind=single|multi; ignored for kind=text"),
|
|
1097
|
-
required: z.boolean().optional().describe("Defaults to true"),
|
|
1098
|
-
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.")
|
|
1099
|
-
});
|
|
1100
|
-
return createSdkMcpServer({
|
|
1101
|
-
name: SERVER_NAME,
|
|
1102
|
-
version: "1.0.0",
|
|
1103
|
-
tools: [
|
|
1104
|
-
checkEnvKeys,
|
|
1105
|
-
setEnvValues,
|
|
1106
|
-
detectPM,
|
|
1107
|
-
loadSkillMenu,
|
|
1108
|
-
installSkill,
|
|
1109
|
-
auditSeedChecks,
|
|
1110
|
-
auditAddChecks,
|
|
1111
|
-
auditResolveChecks,
|
|
1112
|
-
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 — do not call this multiple times in a row.", { questions: z.array(askQuestionSchema).min(1).max(8) }, async (args) => {
|
|
1113
|
-
if (!askBridge) return {
|
|
1114
|
-
content: [{
|
|
1115
|
-
type: "text",
|
|
1116
|
-
text: "Error: wizard_ask is not available in this environment (CI / non-interactive). Proceed with sensible defaults or emit [ABORT] requirements-incomplete."
|
|
1117
|
-
}],
|
|
1118
|
-
isError: true
|
|
1119
|
-
};
|
|
1120
|
-
const capDecision = evaluateAskCap(askCallCount, askMaxQuestions, askAdjacencyNudged);
|
|
1121
|
-
if (capDecision.kind === "capped") {
|
|
1122
|
-
if (capDecision.reason === "adjacency") askAdjacencyNudged = true;
|
|
1123
|
-
analytics.wizardCapture("wizard_ask capped", {
|
|
1124
|
-
reason: capDecision.reason,
|
|
1125
|
-
call_count: askCallCount,
|
|
1126
|
-
max_questions: askMaxQuestions
|
|
1127
|
-
});
|
|
1128
|
-
return {
|
|
1129
|
-
content: [{
|
|
1130
|
-
type: "text",
|
|
1131
|
-
text: capDecision.message
|
|
1132
|
-
}],
|
|
1133
|
-
isError: true
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
for (const q of args.questions) {
|
|
1137
|
-
if ((q.kind === "single" || q.kind === "multi") && (!q.options || q.options.length === 0)) return {
|
|
1138
|
-
content: [{
|
|
1139
|
-
type: "text",
|
|
1140
|
-
text: `Error: question "${q.id}" has kind="${q.kind}" but no options. Provide at least one { label, value } option, or change kind to "text".`
|
|
1141
|
-
}],
|
|
1142
|
-
isError: true
|
|
1143
|
-
};
|
|
1144
|
-
if (q.sensitive && q.kind !== "text") return {
|
|
1145
|
-
content: [{
|
|
1146
|
-
type: "text",
|
|
1147
|
-
text: `Error: question "${q.id}" sets sensitive=true but kind="${q.kind}". Only kind="text" answers can be vaulted as secrets.`
|
|
1148
|
-
}],
|
|
1149
|
-
isError: true
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1153
|
-
for (const q of args.questions) {
|
|
1154
|
-
if (ids.has(q.id)) return {
|
|
1155
|
-
content: [{
|
|
1156
|
-
type: "text",
|
|
1157
|
-
text: `Error: duplicate question id "${q.id}". Each question must have a unique id.`
|
|
1158
|
-
}],
|
|
1159
|
-
isError: true
|
|
1160
|
-
};
|
|
1161
|
-
ids.add(q.id);
|
|
1162
|
-
}
|
|
1163
|
-
askCallCount += 1;
|
|
1164
|
-
try {
|
|
1165
|
-
const answers = await askBridge.request({ questions: args.questions });
|
|
1166
|
-
const sensitiveById = new Map(args.questions.filter((q) => q.sensitive).map((q) => [q.id, q.prompt]));
|
|
1167
|
-
const sanitised = {};
|
|
1168
|
-
for (const [id, answer] of Object.entries(answers)) {
|
|
1169
|
-
const label = sensitiveById.get(id);
|
|
1170
|
-
if (label !== void 0 && typeof answer === "string" && answer !== "__cancelled__") {
|
|
1171
|
-
const ref = secretVault.put(answer, {
|
|
1172
|
-
label,
|
|
1173
|
-
source: "wizard_ask"
|
|
1174
|
-
});
|
|
1175
|
-
sanitised[id] = { secretRef: ref };
|
|
1176
|
-
logToFile(`wizard_ask: vaulted answer for "${id}" as ${ref}`);
|
|
1177
|
-
} else sanitised[id] = answer;
|
|
1178
|
-
}
|
|
1179
|
-
logToFile(`wizard_ask: resolved ${Object.keys(answers).length} answer(s) for ${args.questions.length} question(s)`);
|
|
1180
|
-
return { content: [{
|
|
1181
|
-
type: "text",
|
|
1182
|
-
text: JSON.stringify({ answers: sanitised }, null, 2)
|
|
1183
|
-
}] };
|
|
1184
|
-
} catch (err) {
|
|
1185
|
-
logToFile(`wizard_ask: error: ${err?.message ?? err}`);
|
|
1186
|
-
return {
|
|
1187
|
-
content: [{
|
|
1188
|
-
type: "text",
|
|
1189
|
-
text: `Error: wizard_ask failed: ${err?.message ?? String(err)}`
|
|
1190
|
-
}],
|
|
1191
|
-
isError: true
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
}),
|
|
1195
|
-
...orchestrator ? buildOrchestratorTools(tool, orchestrator) : []
|
|
1196
|
-
]
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
/** Tool names exposed by the wizard-tools server, keyed for selective use. */
|
|
1200
|
-
const WIZARD_TOOL_NAMES = {
|
|
1201
|
-
checkEnvKeys: `mcp__${SERVER_NAME}__check_env_keys`,
|
|
1202
|
-
setEnvValues: `mcp__${SERVER_NAME}__set_env_values`,
|
|
1203
|
-
detectPackageManager: `mcp__${SERVER_NAME}__detect_package_manager`,
|
|
1204
|
-
loadSkillMenu: `mcp__${SERVER_NAME}__load_skill_menu`,
|
|
1205
|
-
installSkill: `mcp__${SERVER_NAME}__install_skill`,
|
|
1206
|
-
auditSeedChecks: `mcp__${SERVER_NAME}__audit_seed_checks`,
|
|
1207
|
-
auditAddChecks: `mcp__${SERVER_NAME}__audit_add_checks`,
|
|
1208
|
-
auditResolveChecks: `mcp__${SERVER_NAME}__audit_resolve_checks`,
|
|
1209
|
-
wizardAsk: `mcp__${SERVER_NAME}__wizard_ask`,
|
|
1210
|
-
enqueueTask: `mcp__${SERVER_NAME}__enqueue_task`,
|
|
1211
|
-
completeTask: `mcp__${SERVER_NAME}__complete_task`,
|
|
1212
|
-
readHandoffs: `mcp__${SERVER_NAME}__read_handoffs`
|
|
1213
|
-
};
|
|
1214
|
-
//#endregion
|
|
1215
180
|
//#region src/lib/skill-install.ts
|
|
1216
181
|
/**
|
|
1217
182
|
* Check if command is a PostHog skill installation from MCP.
|
|
@@ -1323,6 +288,23 @@ function recordMatch(phase, tool, match, action) {
|
|
|
1323
288
|
triageReason: triage?.reason
|
|
1324
289
|
});
|
|
1325
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Scan bookkeeping for harnesses that run a scanner outside these hooks (pi):
|
|
293
|
+
* counts the scan and routes violations through `recordMatch`, so they reach
|
|
294
|
+
* the run log, per-match telemetry, and the end-of-run scan report.
|
|
295
|
+
*/
|
|
296
|
+
function recordExternalScan(phase, tool, violations, action) {
|
|
297
|
+
recordScan();
|
|
298
|
+
for (const v of violations) recordMatch(phase, tool, {
|
|
299
|
+
rule: v.rule,
|
|
300
|
+
metadata: {
|
|
301
|
+
severity: v.severity,
|
|
302
|
+
category: v.category,
|
|
303
|
+
description: v.description
|
|
304
|
+
},
|
|
305
|
+
matchedStrings: []
|
|
306
|
+
}, action);
|
|
307
|
+
}
|
|
1326
308
|
/** Format the scan report summary. Returns null if no scans occurred */
|
|
1327
309
|
function formatScanReport() {
|
|
1328
310
|
if (scanCount === 0) return null;
|
|
@@ -1854,10 +836,10 @@ const WIZARD_COMMANDMENTS = [
|
|
|
1854
836
|
"Never hallucinate a PostHog project token, host, or any other secret. Always use the real values that have been configured for this project (for example via environment variables).",
|
|
1855
837
|
"Never write API keys, access tokens, or other secrets directly into source code. Always reference environment variables instead, and rely on the wizard-tools MCP server (check_env_keys / set_env_values) to create or update .env files.",
|
|
1856
838
|
"Always use the detect_package_manager tool from the wizard-tools MCP server to determine the package manager. Do not guess based on lockfiles or hard-code npm, yarn, pnpm, bun, pip, etc.",
|
|
1857
|
-
"When installing packages, start the installation as a background task and then continue with other work. Do not block waiting for installs to finish unless explicitly instructed.",
|
|
1858
839
|
"Before writing to any file, you MUST read that exact file immediately beforehand using the Read tool, even if you have already read it earlier in the run. This avoids tool failures and stale edits.",
|
|
1859
840
|
"Treat feature flags, custom properties, and event names as part of an analytics contract. Prefer reusing existing names and patterns in the project. When you must introduce new ones, make them clear, descriptive, and consistent with existing conventions, and avoid scattering the same flag or property across many unrelated callsites.",
|
|
1860
841
|
"Keep PostHog data capture at its defaults unless the user explicitly asks otherwise. Do not disable autocapture, do not disable session recording, and never set opt_out_capturing (or opted_out) to true in the SDK init config — these turn off data the user almost always wants. Note: posthog.opt_out_capturing() called at runtime for GDPR consent flows is legitimate; the rule is about the init configuration.",
|
|
842
|
+
"Never put personally identifiable information — emails, full or partial names, phone numbers, physical or IP addresses, or other user-entered PII — in capture() event properties. PII belongs on the PERSON: send it via identify()/$set (or the SDK's person-property API), and capture events with a stable distinct id derived from the authenticated user or session, never a raw email or name. This holds for every SDK, client and server.",
|
|
1861
843
|
"Prefer minimal, targeted edits that achieve the requested behavior while preserving existing structure and style. Avoid large refactors, broad reformatting, or unrelated changes unless explicitly requested.",
|
|
1862
844
|
"Do not spawn subagents unless explicitly instructed to do so.",
|
|
1863
845
|
"Create tasks as soon as you understand the work you are going to carry out. Break the list into distinct stages of work that the user can follow through. Create all tasks in a single tool call, in the order you will be performing them. Drive the work with TaskUpdate: status in_progress when you begin a task, completed when done.",
|
|
@@ -1908,6 +890,13 @@ const AgentSignals = {
|
|
|
1908
890
|
DASHBOARD_URL: "[DASHBOARD_URL]",
|
|
1909
891
|
NOTEBOOK_URL: "[NOTEBOOK_URL]"
|
|
1910
892
|
};
|
|
893
|
+
/**
|
|
894
|
+
* End-of-run reflection ask, shared by both harnesses so remarks are
|
|
895
|
+
* comparable. The format clause comes FIRST and nothing trails the marker —
|
|
896
|
+
* literal models (gpt-5-mini) echo whatever text follows it. The parser also
|
|
897
|
+
* drops remarks that quote this instruction (see AgentOutputSignals.remark).
|
|
898
|
+
*/
|
|
899
|
+
const REMARK_INSTRUCTION = `Reply with a single line that starts with ${AgentSignals.WIZARD_REMARK} and no other lines. In that line, state briefly what information or guidance would have been useful to have in the integration prompt or documentation for this run — specifically anything that would have prevented tool failures, erroneous edits, or other wasted turns.`;
|
|
1911
900
|
//#endregion
|
|
1912
901
|
//#region src/lib/agent/output-signals.ts
|
|
1913
902
|
/**
|
|
@@ -1986,7 +975,9 @@ var AgentOutputSignals = class {
|
|
|
1986
975
|
/** Text after the single `[WIZARD-REMARK]` marker, trimmed, or undefined. */
|
|
1987
976
|
remark() {
|
|
1988
977
|
const re = new RegExp(`${OUTPUT_SIGNALS.WIZARD_REMARK.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*(.+?)(?:\\n|$)`, "s");
|
|
1989
|
-
|
|
978
|
+
const text = this.text.match(re)?.[1]?.trim() || void 0;
|
|
979
|
+
if (text && REMARK_INSTRUCTION.includes(text)) return void 0;
|
|
980
|
+
return text;
|
|
1990
981
|
}
|
|
1991
982
|
};
|
|
1992
983
|
//#endregion
|
|
@@ -2492,7 +1483,7 @@ function createStopHook(featureQueue, signals, requestRemark = true) {
|
|
|
2492
1483
|
logToFile("Stop hook: requesting reflection");
|
|
2493
1484
|
return {
|
|
2494
1485
|
decision: "block",
|
|
2495
|
-
reason:
|
|
1486
|
+
reason: REMARK_INSTRUCTION
|
|
2496
1487
|
};
|
|
2497
1488
|
}
|
|
2498
1489
|
logToFile("Stop hook: allowing stop");
|
|
@@ -2527,14 +1518,6 @@ function isWarlockDisabled(flags = {}) {
|
|
|
2527
1518
|
return flags["wizard-warlock-disabled"] === "true" || runtimeEnv("POSTHOG_WIZARD_WARLOCK_DISABLED") === "true";
|
|
2528
1519
|
}
|
|
2529
1520
|
/**
|
|
2530
|
-
* Whether this run uses the experimental task-queue orchestrator. Gated by the
|
|
2531
|
-
* boolean `wizard-orchestrator` feature flag, targeted to the user in the wizard's
|
|
2532
|
-
* analytics project.
|
|
2533
|
-
*/
|
|
2534
|
-
function isOrchestratorEnabled(flags = {}) {
|
|
2535
|
-
return flags[WIZARD_ORCHESTRATOR_FLAG_KEY] === "true";
|
|
2536
|
-
}
|
|
2537
|
-
/**
|
|
2538
1521
|
* Build env for the SDK subprocess: process.env plus ANTHROPIC_CUSTOM_HEADERS, which always
|
|
2539
1522
|
* includes `x-posthog-use-bedrock-fallback: true` so the LLM gateway falls back to Bedrock on
|
|
2540
1523
|
* Anthropic 5xx, plus any wizard metadata/flags.
|
|
@@ -2598,6 +1581,7 @@ function matchesAllowedPrefix(command) {
|
|
|
2598
1581
|
if (parts.length === 0 || !PACKAGE_MANAGERS.includes(parts[0])) return false;
|
|
2599
1582
|
let scriptIndex = 1;
|
|
2600
1583
|
if (parts[scriptIndex] === "run" || parts[scriptIndex] === "exec") scriptIndex++;
|
|
1584
|
+
if (parts[scriptIndex] === "i") return true;
|
|
2601
1585
|
const scriptPart = parts.slice(scriptIndex).join(" ");
|
|
2602
1586
|
return SAFE_SCRIPTS.some((safe) => scriptPart.startsWith(safe)) || LINTING_TOOLS.some((tool) => scriptPart.startsWith(tool));
|
|
2603
1587
|
}
|
|
@@ -2869,6 +1853,8 @@ async function runAgent(agentConfig, prompt, options, spinner, config, middlewar
|
|
|
2869
1853
|
cache_read_input_tokens: usage?.cache_read_input_tokens,
|
|
2870
1854
|
...config?.analyticsProperties
|
|
2871
1855
|
});
|
|
1856
|
+
const totalCostUsd = Number(lastResultMessage?.total_cost_usd ?? 0);
|
|
1857
|
+
if (totalCostUsd > 0) getUI().setFinalTokenCostUsd(totalCostUsd);
|
|
2872
1858
|
try {
|
|
2873
1859
|
middleware?.finalize(lastResultMessage, durationMs);
|
|
2874
1860
|
} catch (e) {
|
|
@@ -3015,7 +2001,7 @@ async function runAgent(agentConfig, prompt, options, spinner, config, middlewar
|
|
|
3015
2001
|
}
|
|
3016
2002
|
loggedInitialContext = true;
|
|
3017
2003
|
}
|
|
3018
|
-
handleSDKMessage(message, options, spinner, signals, receivedSuccessResult, tasks,
|
|
2004
|
+
handleSDKMessage(message, options, spinner, signals, receivedSuccessResult, tasks, (agentConfig.wizardFlags ?? {})[WIZARD_ORCHESTRATOR_FLAG_KEY] === "true", emitStepEvents);
|
|
3019
2005
|
if (abortCases.length > 0 && !abortReason && message.type === "assistant") {
|
|
3020
2006
|
const content = message.message?.content;
|
|
3021
2007
|
if (Array.isArray(content)) {
|
|
@@ -3275,6 +2261,29 @@ function extractTaskIdFromResult(content) {
|
|
|
3275
2261
|
}
|
|
3276
2262
|
}
|
|
3277
2263
|
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Extracts a `TokenUsageDelta` for the hidden Ctrl+T token/cost HUD from an
|
|
2266
|
+
* SDK assistant message, or `null` when the message carries no usage (e.g.
|
|
2267
|
+
* a duplicate/retried turn). `message.message` is a BetaMessage (the raw
|
|
2268
|
+
* Anthropic API response), which carries `model` alongside `usage` — read
|
|
2269
|
+
* per turn, not from the run's nominal model, since a subagent dispatched
|
|
2270
|
+
* via the Agent tool can genuinely run on a different model than the main
|
|
2271
|
+
* session.
|
|
2272
|
+
*/
|
|
2273
|
+
function extractTokenUsageDelta(message) {
|
|
2274
|
+
const apiMessage = message.message;
|
|
2275
|
+
const usage = apiMessage?.usage;
|
|
2276
|
+
if (!usage) return null;
|
|
2277
|
+
return {
|
|
2278
|
+
inputTokens: Number(usage.input_tokens ?? 0),
|
|
2279
|
+
outputTokens: Number(usage.output_tokens ?? 0),
|
|
2280
|
+
cacheReadTokens: Number(usage.cache_read_input_tokens ?? 0),
|
|
2281
|
+
cacheCreationTokens: Number(usage.cache_creation_input_tokens ?? 0),
|
|
2282
|
+
cacheCreation5m: Number(usage.cache_creation?.ephemeral_5m_input_tokens ?? 0),
|
|
2283
|
+
cacheCreation1h: Number(usage.cache_creation?.ephemeral_1h_input_tokens ?? 0),
|
|
2284
|
+
model: apiMessage?.model
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
3278
2287
|
function handleSDKMessage(message, options, spinner, signals, receivedSuccessResult = false, tasks, suppressTaskRender = false, emitStepEvents = false) {
|
|
3279
2288
|
const STATUS_RANK = {
|
|
3280
2289
|
completed: 0,
|
|
@@ -3290,6 +2299,8 @@ function handleSDKMessage(message, options, spinner, signals, receivedSuccessRes
|
|
|
3290
2299
|
if (options.debug) debug(`SDK Message type: ${message.type}`);
|
|
3291
2300
|
switch (message.type) {
|
|
3292
2301
|
case "assistant": {
|
|
2302
|
+
const tokenUsageDelta = extractTokenUsageDelta(message);
|
|
2303
|
+
if (tokenUsageDelta) getUI().addTokenUsage(tokenUsageDelta);
|
|
3293
2304
|
const content = message.message?.content;
|
|
3294
2305
|
if (Array.isArray(content)) for (const block of content) {
|
|
3295
2306
|
if (block.type === "text" && typeof block.text === "string") {
|
|
@@ -3370,6 +2381,6 @@ function handleSDKMessage(message, options, spinner, signals, receivedSuccessRes
|
|
|
3370
2381
|
}
|
|
3371
2382
|
}
|
|
3372
2383
|
//#endregion
|
|
3373
|
-
export {
|
|
2384
|
+
export { SETUP_REPORT_FILE as C, EVENT_PLAN_FILE as S, formatScanReport as _, wizardCanUseTool as a, recordExternalScan as b, classifySettingsConflicts as c, sanitizeAgentSubprocessEnv as d, AgentOutputSignals as f, flushScanReport as g, getWizardCommandments 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, formatYaraAbortMessage as v, writeScanReport as x, isWizardDocumentationPath as y };
|
|
3374
2385
|
|
|
3375
|
-
//# sourceMappingURL=agent-interface-
|
|
2386
|
+
//# sourceMappingURL=agent-interface-BAodys0H.js.map
|