surf-cli 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +61 -4
  2. package/dist/content/accessibility-tree.js +11 -0
  3. package/dist/content/accessibility-tree.js.map +1 -0
  4. package/dist/content/visual-indicator.js +111 -0
  5. package/dist/content/visual-indicator.js.map +1 -0
  6. package/dist/manifest.json +11 -2
  7. package/dist/options/options.js +3 -3
  8. package/dist/options/options.js.map +1 -1
  9. package/dist/service-worker/index.js +61 -261
  10. package/dist/service-worker/index.js.map +1 -1
  11. package/native/activity-journal.cjs +55 -0
  12. package/native/chatgpt-client-response.cjs +336 -0
  13. package/native/chatgpt-client-selection.cjs +119 -0
  14. package/native/chatgpt-client-ui.cjs +481 -0
  15. package/native/chatgpt-client.cjs +254 -664
  16. package/native/cli.cjs +100 -273
  17. package/native/do-executor.cjs +52 -475
  18. package/native/do-parser.cjs +8 -249
  19. package/native/host-helpers.cjs +32 -15
  20. package/native/host-sessions.cjs +6 -1
  21. package/native/host.cjs +228 -6
  22. package/native/network-export.cjs +20 -17
  23. package/native/network-store.cjs +38 -58
  24. package/native/oracle-cli.cjs +434 -0
  25. package/native/oracle-context.cjs +311 -0
  26. package/native/oracle-host.cjs +301 -0
  27. package/native/oracle-jobs.cjs +253 -0
  28. package/native/playbook-authoring.cjs +44 -0
  29. package/native/playbook-cli.cjs +157 -0
  30. package/native/playbook-client.cjs +259 -0
  31. package/native/playbook-receipts.cjs +109 -0
  32. package/native/playbook-records.cjs +208 -0
  33. package/native/playbook-runtime.cjs +177 -0
  34. package/native/playbooks.cjs +235 -0
  35. package/native/private-state.cjs +156 -0
  36. package/native/redaction.cjs +104 -0
  37. package/native/workflow-definition.cjs +369 -0
  38. package/native/workflow-runtime.cjs +225 -0
  39. package/package.json +2 -1
  40. package/playbooks/page/ops/read.json +22 -0
  41. package/playbooks/page/playbook.json +7 -0
  42. package/skills/surf/SKILL.md +72 -1
  43. package/dist/content/index.js +0 -116
  44. package/dist/content/index.js.map +0 -1
@@ -0,0 +1,253 @@
1
+ const crypto = require("crypto");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const {
5
+ assertNotSymlink,
6
+ atomicWriteFile,
7
+ atomicWriteJson,
8
+ ensurePrivateDir,
9
+ getPrivateStateRoot,
10
+ readPrivateFile,
11
+ readPrivateJson,
12
+ } = require("./private-state.cjs");
13
+
14
+ const JOB_ID_PATTERN = /^\d{8}-\d{6}-[0-9a-f]{4}$/;
15
+ const TERMINAL_STATES = new Set(["captured", "failed"]);
16
+ const TRANSITIONS = {
17
+ created: new Set(["dispatched", "failed"]),
18
+ dispatched: new Set(["awaiting", "failed"]),
19
+ awaiting: new Set(["captured", "failed"]),
20
+ };
21
+
22
+ function oracleRoot(root = getPrivateStateRoot()) {
23
+ return path.join(root, "oracle");
24
+ }
25
+
26
+ function jobDirectory(id, root = getPrivateStateRoot()) {
27
+ if (!JOB_ID_PATTERN.test(id)) throw codedError("not_found", `oracle job not found: ${id}`);
28
+ return path.join(oracleRoot(root), id);
29
+ }
30
+
31
+ function codedError(code, message, details = {}) {
32
+ const error = new Error(message);
33
+ error.code = code;
34
+ Object.assign(error, details);
35
+ return error;
36
+ }
37
+
38
+ function readJobs(root = getPrivateStateRoot()) {
39
+ const base = oracleRoot(root);
40
+ if (!fs.existsSync(base)) return [];
41
+ const stat = assertNotSymlink(base, false);
42
+ if (!stat.isDirectory()) throw new Error(`oracle state path is not a directory: ${base}`);
43
+ return fs.readdirSync(base)
44
+ .filter((id) => JOB_ID_PATTERN.test(id))
45
+ .sort((a, b) => b.localeCompare(a))
46
+ .map((id) => readPrivateJson(path.join(base, id, "job.json"), null, { root }))
47
+ .filter(Boolean);
48
+ }
49
+
50
+ function createJob({ prompt, contextManifest = {}, model = null, effortRequested = null, follow = null }) {
51
+ const root = getPrivateStateRoot();
52
+ const base = oracleRoot(root);
53
+ ensurePrivateDir(base, root);
54
+ const inFlight = readJobs(root).find((job) => !TERMINAL_STATES.has(job.state));
55
+ if (inFlight) {
56
+ throw codedError(
57
+ "capacity",
58
+ `oracle job capacity reached; in-flight job: ${inFlight.id}`,
59
+ { jobId: inFlight.id },
60
+ );
61
+ }
62
+
63
+ const now = new Date();
64
+ const compactTimestamp = now.toISOString().replace(/\D/g, "").slice(0, 14);
65
+ const timestamp = `${compactTimestamp.slice(0, 8)}-${compactTimestamp.slice(8)}`;
66
+ let id;
67
+ let directory;
68
+ for (;;) {
69
+ id = `${timestamp}-${crypto.randomBytes(2).toString("hex")}`;
70
+ directory = path.join(base, id);
71
+ try {
72
+ fs.mkdirSync(directory, { mode: 0o700 });
73
+ break;
74
+ } catch (error) {
75
+ if (error?.code !== "EEXIST") throw error;
76
+ }
77
+ }
78
+
79
+ try {
80
+ ensurePrivateDir(path.join(directory, "turns"), root);
81
+ atomicWriteFile(path.join(directory, "request.md"), prompt, { root, encoding: "utf8" });
82
+ atomicWriteJson(path.join(directory, "context-manifest.json"), contextManifest, { root });
83
+ const job = {
84
+ id,
85
+ state: "created",
86
+ model,
87
+ effortRequested,
88
+ effortVerified: null,
89
+ createdAt: now.toISOString(),
90
+ dispatchedAt: null,
91
+ awaitingAt: null,
92
+ capturedAt: null,
93
+ failedAt: null,
94
+ tabId: null,
95
+ conversationUrl: null,
96
+ promptEcho: null,
97
+ error: null,
98
+ turns: [],
99
+ ...(follow ? { follow } : {}),
100
+ };
101
+ atomicWriteJson(path.join(directory, "job.json"), job, { root });
102
+ return job;
103
+ } catch (error) {
104
+ fs.rmSync(directory, { recursive: true, force: true });
105
+ throw error;
106
+ }
107
+ }
108
+
109
+ function getJob(id) {
110
+ const root = getPrivateStateRoot();
111
+ const job = readPrivateJson(path.join(jobDirectory(id, root), "job.json"), null, { root });
112
+ if (!job) throw codedError("not_found", `oracle job not found: ${id}`);
113
+ return job;
114
+ }
115
+
116
+ function getResponse(id) {
117
+ const root = getPrivateStateRoot();
118
+ getJob(id);
119
+ return readPrivateFile(path.join(jobDirectory(id, root), "response.md"), {
120
+ root,
121
+ encoding: "utf8",
122
+ });
123
+ }
124
+
125
+ function transition(id, state, updates) {
126
+ const job = getJob(id);
127
+ if (!TRANSITIONS[job.state]?.has(state)) {
128
+ throw codedError(
129
+ "invalid_transition",
130
+ `oracle job ${id} cannot transition from ${job.state} to ${state}`,
131
+ );
132
+ }
133
+ const updated = { ...job, state, ...updates };
134
+ const root = getPrivateStateRoot();
135
+ atomicWriteJson(path.join(jobDirectory(id, root), "job.json"), updated, { root });
136
+ return updated;
137
+ }
138
+
139
+ function markDispatched(id, { tabId, promptEcho, modelVerified, effortVerified }) {
140
+ return transition(id, "dispatched", {
141
+ dispatchedAt: new Date().toISOString(),
142
+ tabId,
143
+ ...(promptEcho ? { promptEcho } : {}),
144
+ ...(modelVerified ? { model: modelVerified } : {}),
145
+ ...(effortVerified ? { effortVerified } : {}),
146
+ });
147
+ }
148
+
149
+ function markAwaiting(id, { conversationUrl, promptEcho }) {
150
+ return transition(id, "awaiting", {
151
+ awaitingAt: new Date().toISOString(),
152
+ conversationUrl,
153
+ promptEcho,
154
+ });
155
+ }
156
+
157
+ function markCaptured(id, { response }) {
158
+ const job = getJob(id);
159
+ if (!TRANSITIONS[job.state]?.has("captured")) {
160
+ throw codedError(
161
+ "invalid_transition",
162
+ `oracle job ${id} cannot transition from ${job.state} to captured`,
163
+ );
164
+ }
165
+ const root = getPrivateStateRoot();
166
+ atomicWriteFile(path.join(jobDirectory(id, root), "response.md"), response, {
167
+ root,
168
+ encoding: "utf8",
169
+ });
170
+ return transition(id, "captured", { capturedAt: new Date().toISOString() });
171
+ }
172
+
173
+ function markFailed(id, { code, message }) {
174
+ return transition(id, "failed", {
175
+ failedAt: new Date().toISOString(),
176
+ error: { code, message },
177
+ });
178
+ }
179
+
180
+ function updateTabId(id, tabId) {
181
+ const job = getJob(id);
182
+ if (TERMINAL_STATES.has(job.state)) {
183
+ throw codedError(
184
+ "invalid_transition",
185
+ `oracle job ${id} cannot transition from ${job.state} to update tab`,
186
+ );
187
+ }
188
+ const updated = { ...job, tabId };
189
+ const root = getPrivateStateRoot();
190
+ atomicWriteJson(path.join(jobDirectory(id, root), "job.json"), updated, { root });
191
+ return updated;
192
+ }
193
+
194
+ function appendTurn(id, turn) {
195
+ const job = getJob(id);
196
+ const storedTurn = {
197
+ prompt: turn.prompt,
198
+ dispatchedAt: turn.dispatchedAt ?? null,
199
+ capturedAt: turn.capturedAt ?? null,
200
+ };
201
+ const root = getPrivateStateRoot();
202
+ const directory = jobDirectory(id, root);
203
+ const turnName = `${String(job.turns.length + 1).padStart(4, "0")}.json`;
204
+ atomicWriteJson(path.join(directory, "turns", turnName), storedTurn, { root });
205
+ const updated = { ...job, turns: [...job.turns, storedTurn] };
206
+ atomicWriteJson(path.join(directory, "job.json"), updated, { root });
207
+ return updated;
208
+ }
209
+
210
+ function markTurnCaptured(id, { dispatchedAt, capturedAt }) {
211
+ const job = getJob(id);
212
+ const turnIndex = job.turns.findIndex((turn) => turn.dispatchedAt === dispatchedAt);
213
+ if (turnIndex === -1) {
214
+ throw codedError(
215
+ "invalid_transition",
216
+ `oracle job ${id} has no follow turn dispatched at ${dispatchedAt}`,
217
+ );
218
+ }
219
+ const turns = [...job.turns];
220
+ turns[turnIndex] = { ...turns[turnIndex], capturedAt };
221
+ const root = getPrivateStateRoot();
222
+ const directory = jobDirectory(id, root);
223
+ const turnName = `${String(turnIndex + 1).padStart(4, "0")}.json`;
224
+ atomicWriteJson(path.join(directory, "turns", turnName), turns[turnIndex], { root });
225
+ const updated = { ...job, turns };
226
+ atomicWriteJson(path.join(directory, "job.json"), updated, { root });
227
+ return updated;
228
+ }
229
+
230
+ function listJobs({ limit } = {}) {
231
+ const jobs = readJobs();
232
+ return limit === undefined ? jobs : jobs.slice(0, Math.max(0, limit));
233
+ }
234
+
235
+ function adoptOrphans() {
236
+ return listJobs({}).filter((job) => !TERMINAL_STATES.has(job.state));
237
+ }
238
+
239
+ module.exports = {
240
+ adoptOrphans,
241
+ appendTurn,
242
+ createJob,
243
+ getJob,
244
+ getResponse,
245
+ listJobs,
246
+ markAwaiting,
247
+ markCaptured,
248
+ markDispatched,
249
+ markFailed,
250
+ markTurnCaptured,
251
+ oracleRoot,
252
+ updateTabId,
253
+ };
@@ -0,0 +1,44 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { readRecent } = require("./activity-journal.cjs");
4
+ const { writeNetworkExport } = require("./network-export.cjs");
5
+ const { getPrivateStateRoot, readPrivateJson } = require("./private-state.cjs");
6
+ const { draftFromRecord, readRecord, recordsRoot } = require("./playbook-records.cjs");
7
+ const { savePlaybook, validateOp } = require("./playbooks.cjs");
8
+ const { commandMetadata, promoteRedactedStepArgs } = require("./workflow-definition.cjs");
9
+
10
+ function suggestions({ since = "1h", root } = {}) {
11
+ const events = readRecent({ since, root });
12
+ const counts = new Map();
13
+ for (const event of events.filter((entry) => entry.type === "tool.issued")) counts.set(event.command, (counts.get(event.command) || 0) + 1);
14
+ return [...counts.entries()].map(([command, count]) => ({ command, count })).sort((a, b) => b.count - a.count || a.command.localeCompare(b.command));
15
+ }
16
+
17
+ function saveFromRecent({ site, op: opId, since = "1h", scope = "user", cwd, home, root }) {
18
+ const events = readRecent({ since, root }).filter((event) => event.type === "tool.issued");
19
+ if (events.length === 0) throw new Error("no recent Surf activity to save");
20
+ if (events.some((event) => ["page-write", "unknown"].includes(commandMetadata(event.command).effect))) {
21
+ throw new Error("recent activity includes write-capable commands; use an explicit record and review its draft before saving");
22
+ }
23
+ const promoted = promoteRedactedStepArgs(events.map((event) => ({ tool: event.command, args: event.argsRedacted || {} })));
24
+ const op = { id: opId, description: "Drafted from recent Surf activity", effect: "read", args: promoted.args, run: [{ using: "workflow", steps: promoted.steps }], provenance: { recentSince: since } };
25
+ validateOp(op, { origins: [] });
26
+ return savePlaybook({ manifest: { id: site, name: site, version: "1.0.0", origins: [] }, op, scope, cwd, home });
27
+ }
28
+
29
+ function saveFromRecord({ recordId, scope = "user", cwd, home, root = getPrivateStateRoot() }) {
30
+ const record = readRecord(recordId, root);
31
+ if (!record) throw new Error(`record not found: ${recordId}`);
32
+ const draftPath = path.join(recordsRoot(root), recordId, "draft", "op.json");
33
+ const op = fs.existsSync(draftPath) ? readPrivateJson(draftPath, null, { root }) : draftFromRecord(recordId, root);
34
+ validateOp(op, { origins: record.origin ? [record.origin] : [] });
35
+ return savePlaybook({ manifest: { id: record.site, name: record.site, version: "1.0.0", origins: record.origin ? [record.origin] : [] }, op, scope, cwd, home });
36
+ }
37
+
38
+ function exportRecordHar(recordId, output, root = getPrivateStateRoot()) {
39
+ const trace = readPrivateJson(path.join(recordsRoot(root), recordId, "network", "trace.json"), null, { root });
40
+ if (!trace) throw new Error(`record ${recordId} has no network trace`);
41
+ return writeNetworkExport(path.resolve(output), trace.entries, "har");
42
+ }
43
+
44
+ module.exports = { exportRecordHar, saveFromRecent, saveFromRecord, suggestions };
@@ -0,0 +1,157 @@
1
+ const path = require("path");
2
+ const { openClientTransport } = require("./client-transport.cjs");
3
+ const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
4
+ const { exportRecordHar, saveFromRecent, saveFromRecord, suggestions } = require("./playbook-authoring.cjs");
5
+ const { deriveClient, exportClient, verifyClient } = require("./playbook-client.cjs");
6
+ const { exportPlaybookDirectory, importPlaybookDirectory, listPlaybooks, resolvePlaybook } = require("./playbooks.cjs");
7
+
8
+ function parseCommandArgs(argv) {
9
+ const positional = [];
10
+ const options = {};
11
+ for (let index = 0; index < argv.length; index++) {
12
+ const value = argv[index];
13
+ if (!value.startsWith("--")) {
14
+ positional.push(value);
15
+ continue;
16
+ }
17
+ const name = value.slice(2);
18
+ const next = argv[index + 1];
19
+ if (next !== undefined && !next.startsWith("--")) {
20
+ options[name] = /^-?\d+(?:\.\d+)?$/.test(next) ? Number(next) : next;
21
+ index++;
22
+ } else options[name] = true;
23
+ }
24
+ return { positional, options };
25
+ }
26
+
27
+ function unwrapResponse(response) {
28
+ if (response.error) throw new Error(response.error.content?.[0]?.text || JSON.stringify(response.error));
29
+ const text = response.result?.content?.[0]?.text;
30
+ if (text === undefined) return response.result;
31
+ try { return JSON.parse(text); } catch { return text; }
32
+ }
33
+
34
+ async function requestHost(endpoint, tool, args, options = {}) {
35
+ const transport = await openClientTransport(endpoint, { requestTimeoutMs: options.timeoutMs || 11 * 60 * 1000 });
36
+ try {
37
+ const request = { type: "tool_request", method: "execute_tool", params: { tool, args }, id: `playbook-${Date.now()}-${Math.random()}` };
38
+ if (options.tabId) request.tabId = options.tabId;
39
+ return unwrapResponse(await transport.request(request, options.timeoutMs || 11 * 60 * 1000));
40
+ } finally {
41
+ await transport.close();
42
+ }
43
+ }
44
+
45
+ function runSpec(argv) {
46
+ const direct = argv[0] === "use";
47
+ const offset = direct ? 1 : 2;
48
+ const parsed = parseCommandArgs(argv.slice(offset));
49
+ const [playbook, op] = parsed.positional;
50
+ if (!playbook || !op) throw new Error(direct ? "Usage: surf use <playbook> <op> [--arg value]" : "Usage: surf pb run <playbook> <op> [--arg value]");
51
+ const reserved = new Set(["json", "no-lock", "tab-id", "write", "repeat", "retry-attempt", "override-in-doubt", "pin-built-in"]);
52
+ const args = Object.fromEntries(Object.entries(parsed.options).filter(([name]) => !reserved.has(name)));
53
+ return { playbook, op, args, options: parsed.options };
54
+ }
55
+
56
+ function resolveRunTimeout(spec, cwd) {
57
+ const explicit = Number(spec.args.timeout);
58
+ if (Number.isFinite(explicit) && explicit > 0) return explicit;
59
+ try {
60
+ const playbook = resolvePlaybook(spec.playbook, { cwd, pinBuiltIn: spec.options["pin-built-in"] === true });
61
+ const op = playbook.ops.get(spec.op);
62
+ const value = Number(op?.args?.timeout?.default);
63
+ return Number.isFinite(value) && value > 0 ? value : undefined;
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ }
68
+
69
+ function playbookCommandNeedsBrowser(argv) {
70
+ if (argv[0] === "use") return true;
71
+ const subcommand = argv[1];
72
+ if (subcommand === "run") return true;
73
+ return subcommand === "record" && ["start", "stop", "discard"].includes(argv[2]);
74
+ }
75
+
76
+ async function handlePlaybookCli(argv, { endpoint, cwd = process.cwd() }) {
77
+ if (!["playbook", "pb", "use"].includes(argv[0])) return { handled: false };
78
+ if (argv[0] === "use" || argv[1] === "run") {
79
+ const spec = runSpec(argv);
80
+ const timeout = resolveRunTimeout(spec, cwd);
81
+ const args = {
82
+ playbook: spec.playbook,
83
+ op: spec.op,
84
+ args: spec.args,
85
+ projectDir: cwd,
86
+ ...(timeout ? { timeout } : {}),
87
+ write: spec.options.write === true,
88
+ repeat: spec.options.repeat === true,
89
+ retryAttempt: spec.options["retry-attempt"],
90
+ overrideInDoubt: spec.options["override-in-doubt"] === true,
91
+ pinBuiltIn: spec.options["pin-built-in"] === true,
92
+ };
93
+ const value = await requestHost(endpoint, "playbook.run", args, {
94
+ tabId: spec.options["tab-id"],
95
+ timeoutMs: resolveRequestDeadlineMs("playbook.run", args),
96
+ });
97
+ return { handled: true, value, json: spec.options.json === true };
98
+ }
99
+ const command = argv[1];
100
+ const parsed = parseCommandArgs(argv.slice(2));
101
+ if (!command || command === "help") return { handled: true, value: "Usage: surf playbook|pb <list|show|ops|run|record|suggest|save|client|trace|export|import>" };
102
+ if (endpoint?.kind === "remote" && ["list", "show", "ops"].includes(command)) throw new Error(`playbook ${command} is local-only with --remote because runs resolve on the browser host`);
103
+ if (command === "list") return { handled: true, value: listPlaybooks({ cwd }), json: parsed.options.json === true };
104
+ if (command === "show") {
105
+ const playbook = resolvePlaybook(parsed.positional[0], { cwd });
106
+ return { handled: true, value: { id: playbook.id, name: playbook.name, version: playbook.version, description: playbook.description, origins: playbook.origins, provenance: playbook.provenance, ops: [...playbook.ops.keys()] }, json: parsed.options.json === true };
107
+ }
108
+ if (command === "ops") {
109
+ const playbook = resolvePlaybook(parsed.positional[0], { cwd });
110
+ return { handled: true, value: [...playbook.ops.values()].map((op) => ({ id: op.id, description: op.description || "", effect: op.effect, strategies: op.run.map((strategy) => strategy.using) })), json: parsed.options.json === true };
111
+ }
112
+ if (command === "record") {
113
+ const action = parsed.positional[0];
114
+ const tool = `playbook.record.${action}`;
115
+ let args = {};
116
+ if (action === "start") args = { site: parsed.positional[1], op: parsed.options.op, watch: parsed.options.watch === true, network: parsed.options.network === true, includeInputValues: parsed.options["include-input-values"] === true };
117
+ else if (action === "mark") args = { label: parsed.positional.slice(1).join(" ") };
118
+ else if (action === "stop") args = { draft: parsed.options.draft === true };
119
+ else if (!["status", "pause", "resume", "discard"].includes(action)) throw new Error("Unknown record command");
120
+ const value = await requestHost(endpoint, tool, args, { tabId: parsed.options["tab-id"] });
121
+ return { handled: true, value, json: parsed.options.json === true };
122
+ }
123
+ if (command === "suggest") return { handled: true, value: suggestions({ since: parsed.options.since || "1h" }), json: parsed.options.json === true };
124
+ if (command === "save") {
125
+ let value;
126
+ if (parsed.options["from-record"]) value = saveFromRecord({ recordId: parsed.options["from-record"], scope: parsed.options.project ? "project" : "user", cwd });
127
+ else if (parsed.options["from-recent"] || parsed.positional[0]) value = saveFromRecent({ site: parsed.positional[0], op: parsed.options.op, since: parsed.options["from-recent"] === true ? "1h" : parsed.options["from-recent"] || "1h", scope: parsed.options.project ? "project" : "user", cwd });
128
+ else throw new Error("save requires --from-record <id> or <site> --op <name> --from-recent");
129
+ return { handled: true, value, json: parsed.options.json === true };
130
+ }
131
+ if (command === "client") {
132
+ const action = parsed.positional[0];
133
+ if (action === "derive") return { handled: true, value: deriveClient(parsed.positional[1], parsed.options.op, parsed.options.out, { recordId: parsed.options["from-record"], requestId: parsed.options["request-id"] }), json: parsed.options.json === true };
134
+ if (action === "export") {
135
+ const playbook = parsed.positional[1];
136
+ const resolved = resolvePlaybook(playbook, { cwd });
137
+ const op = parsed.options.op || [...resolved.ops.keys()][0];
138
+ return { handled: true, value: exportClient(playbook, op, parsed.options.out, { cwd }), json: parsed.options.json === true };
139
+ }
140
+ if (action === "verify") return { handled: true, value: await verifyClient(parsed.positional[1], { live: parsed.options.live === true ? true : undefined }), json: parsed.options.json === true };
141
+ throw new Error("Unknown client command");
142
+ }
143
+ if (command === "trace" && parsed.positional[0] === "export") {
144
+ if (!parsed.options["from-record"] || !parsed.options.har) throw new Error("trace export requires --from-record <id> --har <path>");
145
+ return { handled: true, value: exportRecordHar(parsed.options["from-record"], path.resolve(parsed.options.har)), json: parsed.options.json === true };
146
+ }
147
+ if (command === "export") return { handled: true, value: exportPlaybookDirectory(parsed.positional[0], { out: parsed.options.out, cwd }), json: parsed.options.json === true };
148
+ if (command === "import") return { handled: true, value: importPlaybookDirectory(parsed.positional[0], { scope: parsed.options.project ? "project" : "user", cwd }), json: parsed.options.json === true };
149
+ throw new Error(`Unknown playbook command: ${command}`);
150
+ }
151
+
152
+ function formatPlaybookOutput(value, json = false) {
153
+ if (json || typeof value !== "string") return JSON.stringify(value, null, 2);
154
+ return value;
155
+ }
156
+
157
+ module.exports = { formatPlaybookOutput, handlePlaybookCli, playbookCommandNeedsBrowser };