@weftlabs/cli 0.25.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/dist/cli.js ADDED
@@ -0,0 +1,1104 @@
1
+ // src/credentials.ts
2
+ import { randomUUID } from "crypto";
3
+ import {
4
+ chmod,
5
+ mkdir,
6
+ open,
7
+ readFile,
8
+ rename,
9
+ rm,
10
+ writeFile
11
+ } from "fs/promises";
12
+ import { dirname } from "path";
13
+ import { setTimeout as delay } from "timers/promises";
14
+ var CREDENTIALS_FILE_MODE = 384;
15
+ var CREDENTIALS_DIR_MODE = 448;
16
+ var LOCK_RETRY_MS = 50;
17
+ var LOCK_TIMEOUT_MS = 1e4;
18
+ function isObject(value) {
19
+ return typeof value === "object" && value !== null;
20
+ }
21
+ function isBootstrapCredentials(value) {
22
+ if (value.type === "bootstrap" && typeof value.version === "number" && typeof value.base_url === "string" && typeof value.bootstrap_id === "string" && typeof value.temporary_api_key === "string" && typeof value.device_code === "string" && typeof value.client_id === "string" && typeof value.expiry === "string" && typeof value.polling_interval === "number") {
23
+ return value;
24
+ }
25
+ return void 0;
26
+ }
27
+ function isOAuthCredentials(value) {
28
+ if (value.type === "oauth" && typeof value.version === "number" && typeof value.base_url === "string" && typeof value.client_id === "string" && typeof value.access_token === "string" && typeof value.refresh_token === "string" && typeof value.token_type === "string" && typeof value.scope === "string" && typeof value.expiry === "string") {
29
+ return value;
30
+ }
31
+ return void 0;
32
+ }
33
+ function resolveCredentialsPath(env) {
34
+ if (env.WEFT_CREDENTIALS_FILE) return env.WEFT_CREDENTIALS_FILE;
35
+ if (env.XDG_CONFIG_HOME)
36
+ return `${env.XDG_CONFIG_HOME}/weft/credentials.json`;
37
+ const home = env.HOME || env.USERPROFILE;
38
+ if (!home) return void 0;
39
+ return `${home}/.config/weft/credentials.json`;
40
+ }
41
+ async function readStoredCredentials(env) {
42
+ const path = resolveCredentialsPath(env);
43
+ if (!path) return void 0;
44
+ try {
45
+ const raw = await readFile(path, "utf8");
46
+ const parsed = JSON.parse(raw);
47
+ if (!isObject(parsed)) throw new Error("Stored credentials malformed");
48
+ return isBootstrapCredentials(parsed) ?? isOAuthCredentials(parsed);
49
+ } catch (error) {
50
+ if (error.code === "ENOENT") return void 0;
51
+ throw error;
52
+ }
53
+ }
54
+ async function writeStoredCredentials(env, credentials) {
55
+ const path = resolveCredentialsPath(env);
56
+ if (!path) {
57
+ throw new Error("HOME, USERPROFILE, or WEFT_CREDENTIALS_FILE is required");
58
+ }
59
+ const parent = dirname(path);
60
+ await mkdir(parent, { recursive: true, mode: CREDENTIALS_DIR_MODE });
61
+ await chmod(parent, CREDENTIALS_DIR_MODE);
62
+ const tempPath = `${path}.${randomUUID()}.tmp`;
63
+ try {
64
+ await writeFile(tempPath, JSON.stringify(credentials), {
65
+ mode: CREDENTIALS_FILE_MODE
66
+ });
67
+ await rename(tempPath, path);
68
+ await chmod(path, CREDENTIALS_FILE_MODE);
69
+ } catch (error) {
70
+ await rm(tempPath, { force: true });
71
+ throw error;
72
+ }
73
+ }
74
+ async function withCredentialsLock(env, action) {
75
+ const path = resolveCredentialsPath(env);
76
+ if (!path) {
77
+ throw new Error("HOME, USERPROFILE, or WEFT_CREDENTIALS_FILE is required");
78
+ }
79
+ const lockPath = `${path}.lock`;
80
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
81
+ await mkdir(dirname(path), { recursive: true, mode: CREDENTIALS_DIR_MODE });
82
+ for (; ; ) {
83
+ try {
84
+ const lock = await open(lockPath, "wx", CREDENTIALS_FILE_MODE);
85
+ await lock.close();
86
+ break;
87
+ } catch (error) {
88
+ if (error.code !== "EEXIST") throw error;
89
+ if (Date.now() >= deadline) {
90
+ throw new Error(
91
+ `Timed out waiting for the credential lock; if no Weft CLI process is running, remove ${lockPath}`
92
+ );
93
+ }
94
+ await delay(LOCK_RETRY_MS);
95
+ }
96
+ }
97
+ try {
98
+ return await action();
99
+ } finally {
100
+ await rm(lockPath, { force: true });
101
+ }
102
+ }
103
+
104
+ // src/cli.ts
105
+ import { spawn } from "child_process";
106
+ import { randomUUID as randomUUID2 } from "crypto";
107
+ import { existsSync } from "fs";
108
+ import { dirname as dirname2, join as join2 } from "path";
109
+
110
+ // src/results.ts
111
+ import { chmod as chmod2, lstat, mkdir as mkdir2, mkdtemp, open as open2, realpath } from "fs/promises";
112
+ import { homedir } from "os";
113
+ import { join, resolve } from "path";
114
+ async function reserveResultDirectory(env) {
115
+ const root = resolve(
116
+ env.WEFT_RESULTS_DIR || join(env.HOME || env.USERPROFILE || homedir(), ".weft-results")
117
+ );
118
+ await mkdir2(root, { recursive: true, mode: 448 });
119
+ const stat = await lstat(root);
120
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
121
+ throw new Error(
122
+ "The result storage root must be a directory, not a symlink"
123
+ );
124
+ }
125
+ if (process.platform !== "win32" && ((stat.mode & 18) !== 0 || stat.uid !== process.getuid?.())) {
126
+ throw new Error(
127
+ "The result storage root must be owned by this user and not writable by others"
128
+ );
129
+ }
130
+ const directory = await mkdtemp(join(await realpath(root), "result-"));
131
+ await chmod2(directory, 448);
132
+ return directory;
133
+ }
134
+ async function writeResultFile(path, contents) {
135
+ const file = await open2(path, "wx", 384);
136
+ try {
137
+ await file.chmod(384);
138
+ await file.writeFile(contents);
139
+ await file.sync();
140
+ } finally {
141
+ await file.close();
142
+ }
143
+ }
144
+ function readableBody(bytes, headers) {
145
+ const header = (name) => Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
146
+ const contentType = header("content-type")?.toLowerCase() ?? "";
147
+ const mediaType = contentType.split(";", 1)[0].trim();
148
+ const charset = /(?:^|;)\s*charset\s*=\s*"?([^;"\s]+)/.exec(contentType)?.[1];
149
+ const encoding = header("content-encoding")?.trim().toLowerCase();
150
+ const file = {
151
+ media_type: mediaType || null,
152
+ body_encoding: "file"
153
+ };
154
+ const textual = mediaType.startsWith("text/") || /^application\/(?:json|xml|javascript|x-ndjson|x-www-form-urlencoded|[^;]+\+(?:json|xml))$/.test(
155
+ mediaType
156
+ );
157
+ if (encoding && encoding !== "identity" || charset && !["utf-8", "utf8", "us-ascii"].includes(charset) || !textual && !(bytes.length === 0 && !mediaType))
158
+ return file;
159
+ try {
160
+ const body = new TextDecoder("utf-8", {
161
+ fatal: true,
162
+ ignoreBOM: true
163
+ }).decode(bytes);
164
+ return { media_type: mediaType || null, body_encoding: "utf-8", body };
165
+ } catch {
166
+ return file;
167
+ }
168
+ }
169
+
170
+ // src/cli.ts
171
+ import {
172
+ ResponseError,
173
+ WeftClient,
174
+ WeftError
175
+ } from "@weftlabs/sdk";
176
+ var EXIT_SUCCESS = 0;
177
+ var EXIT_USAGE = 2;
178
+ var EXIT_AUTH = 3;
179
+ var EXIT_API = 4;
180
+ var EXIT_INTERNAL = 5;
181
+ var COMMANDS = [
182
+ "bootstrap",
183
+ "auth",
184
+ "skill",
185
+ "me",
186
+ "balance",
187
+ "search",
188
+ "fetch",
189
+ "purchases"
190
+ ];
191
+ var WEFT_API_BASE = "https://weft.network";
192
+ var OAUTH_SCOPE = "balance fetch search";
193
+ var OAUTH_CLIENT_NAME = "Weft CLI";
194
+ var OAUTH_HOST_NAME = "Weft CLI";
195
+ var OAUTH_REFRESH_SKEW_MS = 6e4;
196
+ var PROCESS_OUTPUT_LIMIT = 8192;
197
+ var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
198
+ var CORE_SKILL_NAMES = /* @__PURE__ */ new Set(["weft", "weft-setup"]);
199
+ var COMMAND_HELP = {
200
+ bootstrap: {
201
+ description: "Bootstrap an agent account",
202
+ usage: "weft bootstrap --email <email> --agent-name <name> --reason <text>",
203
+ options: [
204
+ "--email <email>",
205
+ "--agent-name <name>",
206
+ "--reason <text>",
207
+ "[--base-url <url>]"
208
+ ]
209
+ },
210
+ auth: {
211
+ description: "Check bootstrap status and exchange device token",
212
+ usage: "weft auth status",
213
+ options: ["status"]
214
+ },
215
+ skill: {
216
+ description: "Install the core Weft Skill or one optional workflow Skill",
217
+ usage: "weft skill install [name --agent <agent> [--global]]",
218
+ options: ["install", "[name]", "--agent <agent>", "--global"]
219
+ },
220
+ me: {
221
+ description: "Return the authenticated principal",
222
+ usage: "weft me",
223
+ options: []
224
+ },
225
+ balance: {
226
+ description: "Return wallet balance and spending policy",
227
+ usage: "weft balance",
228
+ options: []
229
+ },
230
+ search: {
231
+ description: "Search for payable resources",
232
+ usage: "weft search <query> [--max-results <1..50>]",
233
+ options: ["--max-results <1..50>"]
234
+ },
235
+ fetch: {
236
+ description: "Fetch a URL within an explicit spending limit",
237
+ usage: "weft fetch <url> --max-cost-usd <amount> [--method <method>] [--idempotency-key <key>] [--raw]",
238
+ options: [
239
+ "--max-cost-usd <amount>",
240
+ "--method <method>",
241
+ "--idempotency-key <key>",
242
+ "--raw"
243
+ ]
244
+ },
245
+ purchases: {
246
+ description: "List purchases or return one purchase by ID",
247
+ usage: "weft purchases [id] [--page <number>] [--per-page <1..100>]",
248
+ options: ["--page <number>", "--per-page <1..100>"]
249
+ }
250
+ };
251
+ async function loadSkillInstaller() {
252
+ const specifier = new URL("../scripts/install-skill.mjs", import.meta.url).href;
253
+ const module = await import(specifier);
254
+ return module.installSkill;
255
+ }
256
+ async function installSkillInBackground() {
257
+ try {
258
+ const installSkill = await loadSkillInstaller();
259
+ await installSkill({ silent: true });
260
+ } catch {
261
+ }
262
+ }
263
+ var GLOBAL_OPTIONS = ["--api-key-stdin", "--base-url <url>", "--help", "-h"];
264
+ var CliError = class extends Error {
265
+ constructor(exitCode, code, message, details) {
266
+ super(message);
267
+ this.exitCode = exitCode;
268
+ this.code = code;
269
+ this.details = details;
270
+ }
271
+ };
272
+ function takeValue(args, index, option) {
273
+ const value = args[index + 1];
274
+ if (value == null || value.startsWith("--")) {
275
+ throw new CliError(
276
+ EXIT_USAGE,
277
+ "INVALID_ARGUMENT",
278
+ `${option} requires a value`
279
+ );
280
+ }
281
+ return [value, index + 1];
282
+ }
283
+ function appendBounded(current, chunk) {
284
+ return `${current}${chunk.toString()}`.slice(-PROCESS_OUTPUT_LIMIT);
285
+ }
286
+ function defaultRunProcess(command, args) {
287
+ return new Promise((resolve2) => {
288
+ const child = spawn(command, args, {
289
+ shell: false,
290
+ stdio: ["ignore", "pipe", "pipe"]
291
+ });
292
+ let stdout = "";
293
+ let stderr = "";
294
+ child.stdout.on("data", (chunk) => {
295
+ stdout = appendBounded(stdout, chunk);
296
+ });
297
+ child.stderr.on("data", (chunk) => {
298
+ stderr = appendBounded(stderr, chunk);
299
+ });
300
+ child.on("error", (error) => {
301
+ resolve2({
302
+ exitCode: 1,
303
+ stdout,
304
+ stderr: appendBounded(stderr, error.message)
305
+ });
306
+ });
307
+ child.on("close", (exitCode) => {
308
+ resolve2({ exitCode: exitCode ?? 1, stdout, stderr });
309
+ });
310
+ });
311
+ }
312
+ function resolveNpxCliPath(env, nodeExecutable) {
313
+ const executableDirectory = dirname2(nodeExecutable);
314
+ const candidates = [
315
+ ...env.npm_execpath ? [join2(dirname2(env.npm_execpath), "npx-cli.js")] : [],
316
+ join2(executableDirectory, "node_modules", "npm", "bin", "npx-cli.js"),
317
+ join2(
318
+ executableDirectory,
319
+ "..",
320
+ "lib",
321
+ "node_modules",
322
+ "npm",
323
+ "bin",
324
+ "npx-cli.js"
325
+ )
326
+ ];
327
+ return candidates.find((candidate) => existsSync(candidate));
328
+ }
329
+ function npxProcessInvocation(processArgs, dependencies, env) {
330
+ if ((dependencies.platform ?? process.platform) !== "win32") {
331
+ return { executable: "npx", arguments: processArgs };
332
+ }
333
+ const nodeExecutable = dependencies.nodeExecutable ?? process.execPath;
334
+ const npxCliPath = dependencies.npxCliPath ?? resolveNpxCliPath(env, nodeExecutable);
335
+ if (!npxCliPath) {
336
+ throw new CliError(
337
+ EXIT_INTERNAL,
338
+ "NPX_NOT_FOUND",
339
+ "Could not find the npm npx program. Install npm, then run the command again."
340
+ );
341
+ }
342
+ return {
343
+ executable: nodeExecutable,
344
+ arguments: [npxCliPath, ...processArgs]
345
+ };
346
+ }
347
+ function rejectUnsafeCredentialArgument(args) {
348
+ if (args.some((arg) => arg === "--api-key" || arg.startsWith("--api-key="))) {
349
+ throw new CliError(
350
+ EXIT_USAGE,
351
+ "UNSAFE_CREDENTIAL_ARGUMENT",
352
+ "API keys are accepted only through WEFT_API_KEY or --api-key-stdin"
353
+ );
354
+ }
355
+ }
356
+ function requestedHelp(args) {
357
+ const isHelp = (value) => value === "--help" || value === "-h";
358
+ if (args.length === 1 && isHelp(args[0])) return "all";
359
+ if (args.length === 2 && COMMANDS.includes(args[0]) && isHelp(args[1])) {
360
+ return args[0];
361
+ }
362
+ return void 0;
363
+ }
364
+ function helpData(command) {
365
+ if (command !== "all") {
366
+ return {
367
+ command,
368
+ ...COMMAND_HELP[command],
369
+ global_options: GLOBAL_OPTIONS
370
+ };
371
+ }
372
+ return {
373
+ usage: "weft <command> [options]",
374
+ commands: COMMANDS.map((name) => ({ name, ...COMMAND_HELP[name] })),
375
+ global_options: GLOBAL_OPTIONS,
376
+ authentication: ["WEFT_API_KEY", "--api-key-stdin"],
377
+ exit_codes: { success: 0, usage: 2, auth: 3, api: 4, internal: 5 }
378
+ };
379
+ }
380
+ function parseArgs(args) {
381
+ let command;
382
+ let apiKeyStdin = false;
383
+ let baseUrl;
384
+ const positionals = [];
385
+ const options = /* @__PURE__ */ new Map();
386
+ for (let index = 0; index < args.length; index += 1) {
387
+ const arg = args[index];
388
+ if (arg === "--api-key-stdin") {
389
+ apiKeyStdin = true;
390
+ } else if (arg === "--base-url") {
391
+ [baseUrl, index] = takeValue(args, index, arg);
392
+ } else if (arg === "--global" || arg === "--raw") {
393
+ options.set(arg.slice(2), true);
394
+ } else if (arg.startsWith("--")) {
395
+ const [value, nextIndex] = takeValue(args, index, arg);
396
+ options.set(arg.slice(2), value);
397
+ index = nextIndex;
398
+ } else if (!command) {
399
+ if (!COMMANDS.includes(arg)) {
400
+ throw new CliError(
401
+ EXIT_USAGE,
402
+ "UNKNOWN_COMMAND",
403
+ `Unknown command: ${arg}`
404
+ );
405
+ }
406
+ command = arg;
407
+ } else {
408
+ positionals.push(arg);
409
+ }
410
+ }
411
+ if (!command) {
412
+ throw new CliError(
413
+ EXIT_USAGE,
414
+ "COMMAND_REQUIRED",
415
+ "Command required: bootstrap, auth, me, balance, search, fetch, or purchases"
416
+ );
417
+ }
418
+ return { command, apiKeyStdin, baseUrl, positionals, options };
419
+ }
420
+ function positiveInteger(value, option, maximum) {
421
+ if (value == null) return void 0;
422
+ const parsed = Number(value);
423
+ if (!Number.isInteger(parsed) || parsed <= 0) {
424
+ throw new CliError(
425
+ EXIT_USAGE,
426
+ "INVALID_ARGUMENT",
427
+ `${option} must be a positive integer`
428
+ );
429
+ }
430
+ if (maximum != null && parsed > maximum) {
431
+ throw new CliError(
432
+ EXIT_USAGE,
433
+ "INVALID_ARGUMENT",
434
+ `${option} must be at most ${maximum}`
435
+ );
436
+ }
437
+ return parsed;
438
+ }
439
+ function requiredOption(options, name) {
440
+ const value = options.get(name);
441
+ if (typeof value !== "string" || !value.trim()) {
442
+ throw new CliError(EXIT_USAGE, "MISSING_ARGUMENT", `--${name} is required`);
443
+ }
444
+ return value;
445
+ }
446
+ function baseApiUrl(baseUrl, env) {
447
+ const raw = baseUrl ?? env.WEFT_BASE_URL ?? WEFT_API_BASE;
448
+ return raw.endsWith("/") ? raw.slice(0, -1) : raw;
449
+ }
450
+ function resolveApiKeyFromStored(credentials) {
451
+ if (!credentials) return void 0;
452
+ if (credentials.type === "oauth" && credentials.access_token.trim()) {
453
+ return credentials.access_token;
454
+ }
455
+ if (credentials.type === "bootstrap" && credentials.temporary_api_key.trim()) {
456
+ return credentials.temporary_api_key;
457
+ }
458
+ return void 0;
459
+ }
460
+ function oauthNeedsRefresh(credentials) {
461
+ const expiry = Date.parse(credentials.expiry);
462
+ return !Number.isFinite(expiry) || expiry <= Date.now() + OAUTH_REFRESH_SKEW_MS;
463
+ }
464
+ async function refreshOAuthCredentials(fetchApi, env, stored) {
465
+ const token = await requestJson(
466
+ fetchApi,
467
+ `${stored.base_url}/oauth/token`,
468
+ {
469
+ method: "POST",
470
+ headers: { "content-type": "application/x-www-form-urlencoded" },
471
+ body: new URLSearchParams({
472
+ grant_type: "refresh_token",
473
+ refresh_token: stored.refresh_token,
474
+ client_id: stored.client_id
475
+ }).toString()
476
+ },
477
+ true
478
+ );
479
+ const refreshed = {
480
+ ...stored,
481
+ access_token: token.access_token,
482
+ refresh_token: token.refresh_token,
483
+ token_type: token.token_type,
484
+ scope: token.scope,
485
+ expiry: new Date(Date.now() + token.expires_in * 1e3).toISOString()
486
+ };
487
+ await writeStoredCredentials(env, refreshed);
488
+ return refreshed;
489
+ }
490
+ function noSecretFields(data) {
491
+ return {
492
+ ...data,
493
+ temporary_api_key: void 0,
494
+ device_code: void 0,
495
+ access_token: void 0,
496
+ refresh_token: void 0,
497
+ client_secret: void 0
498
+ };
499
+ }
500
+ async function requestJson(fetchApi, url, init, redactError = false) {
501
+ const response = await fetchApi(url, init);
502
+ if (!response.ok) {
503
+ if (redactError) {
504
+ throw new CliError(
505
+ response.status === 401 || response.status === 403 ? EXIT_AUTH : response.status >= 500 ? EXIT_INTERNAL : EXIT_API,
506
+ "OAUTH_TOKEN_ERROR",
507
+ `OAuth token request failed with HTTP ${response.status}`
508
+ );
509
+ }
510
+ throw new ResponseError(response);
511
+ }
512
+ return await response.json();
513
+ }
514
+ function withCredentials(headers, apiKey) {
515
+ return {
516
+ ...headers,
517
+ authorization: `Bearer ${apiKey}`
518
+ };
519
+ }
520
+ function ensureOnly(options, allowed) {
521
+ for (const name of options.keys()) {
522
+ if (!allowed.includes(name)) {
523
+ throw new CliError(
524
+ EXIT_USAGE,
525
+ "UNKNOWN_OPTION",
526
+ `Unknown option: --${name}`
527
+ );
528
+ }
529
+ }
530
+ }
531
+ async function responseDetails(error) {
532
+ try {
533
+ return await error.response.json();
534
+ } catch {
535
+ return void 0;
536
+ }
537
+ }
538
+ async function normalizeError(error) {
539
+ if (error instanceof CliError) return error;
540
+ if (error instanceof WeftError) {
541
+ const exitCode = error.status === 401 || error.status === 403 ? EXIT_AUTH : error.status >= 500 || error.status === 0 ? EXIT_INTERNAL : EXIT_API;
542
+ return new CliError(exitCode, error.code, error.message, error.details);
543
+ }
544
+ if (error instanceof ResponseError) {
545
+ const details = await responseDetails(error);
546
+ const body = details;
547
+ const nested = typeof body?.error === "object" ? body.error : void 0;
548
+ const code = nested?.code ?? (typeof body?.error === "string" ? body.error : `HTTP_${error.response.status}`);
549
+ const message2 = nested?.message ?? body?.error_description ?? `Weft API returned HTTP ${error.response.status}`;
550
+ const exitCode = error.response.status === 401 || error.response.status === 403 ? EXIT_AUTH : error.response.status >= 500 ? EXIT_INTERNAL : EXIT_API;
551
+ return new CliError(exitCode, code, message2, details);
552
+ }
553
+ const message = error instanceof Error ? error.message : String(error);
554
+ return new CliError(EXIT_INTERNAL, "INTERNAL_ERROR", message);
555
+ }
556
+ async function defaultReadStdin() {
557
+ const chunks = [];
558
+ for await (const chunk of process.stdin) {
559
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
560
+ }
561
+ return Buffer.concat(chunks).toString("utf8");
562
+ }
563
+ async function defaultWriteOut(value) {
564
+ await new Promise((resolve2, reject) => {
565
+ const onError = (error) => reject(error);
566
+ process.stdout.once("error", onError);
567
+ process.stdout.write(value, (error) => {
568
+ if (error) {
569
+ reject(error);
570
+ } else {
571
+ process.stdout.removeListener("error", onError);
572
+ resolve2();
573
+ }
574
+ });
575
+ });
576
+ }
577
+ async function runCli(args, dependencies = {}) {
578
+ const outputWriter = dependencies.writeOut ?? defaultWriteOut;
579
+ const writeErr = dependencies.writeErr ?? ((value) => process.stderr.write(value));
580
+ let command = "unknown";
581
+ let idempotencyKey;
582
+ let schemaVersion = "1";
583
+ let meta;
584
+ let deliveryReceipt;
585
+ const fetchApi = dependencies.fetchApi ?? fetch;
586
+ const writeOut = async (value) => {
587
+ try {
588
+ await outputWriter(value);
589
+ } catch {
590
+ throw new CliError(
591
+ EXIT_INTERNAL,
592
+ command === "fetch" ? "RESULT_OUTPUT_FAILED" : "OUTPUT_FAILED",
593
+ "Cannot write stdout. Use the saved result paths below if present. Otherwise keep this receipt and retry the same fetch with --idempotency-key and the same key.",
594
+ deliveryReceipt === void 0 ? void 0 : { receipt: deliveryReceipt }
595
+ );
596
+ }
597
+ };
598
+ try {
599
+ rejectUnsafeCredentialArgument(args);
600
+ const help = requestedHelp(args);
601
+ if (help !== void 0) {
602
+ command = "help";
603
+ await writeOut(
604
+ `${JSON.stringify({
605
+ schema_version: "1",
606
+ ok: true,
607
+ command,
608
+ data: helpData(help)
609
+ })}
610
+ `
611
+ );
612
+ return EXIT_SUCCESS;
613
+ }
614
+ const parsed = parseArgs(args);
615
+ command = parsed.command;
616
+ const env = dependencies.env ?? process.env;
617
+ const baseUrl = baseApiUrl(parsed.baseUrl, env);
618
+ if (parsed.command === "skill") {
619
+ if (parsed.positionals[0] !== "install" || parsed.positionals.length > 2) {
620
+ throw new CliError(
621
+ EXIT_USAGE,
622
+ "INVALID_ARGUMENT",
623
+ "skill requires 'install' and accepts at most one Skill name"
624
+ );
625
+ }
626
+ const skillName = parsed.positionals[1];
627
+ if (skillName) {
628
+ ensureOnly(parsed.options, ["agent", "global"]);
629
+ const agent = requiredOption(parsed.options, "agent");
630
+ if (!SKILL_NAME_PATTERN.test(skillName)) {
631
+ throw new CliError(
632
+ EXIT_USAGE,
633
+ "INVALID_ARGUMENT",
634
+ "Skill name must contain only lowercase letters, numbers, and single hyphens"
635
+ );
636
+ }
637
+ if (CORE_SKILL_NAMES.has(skillName)) {
638
+ throw new CliError(
639
+ EXIT_USAGE,
640
+ "INVALID_ARGUMENT",
641
+ `Use 'weft skill install' without a name to install the core ${skillName} Skill`
642
+ );
643
+ }
644
+ if (!SKILL_NAME_PATTERN.test(agent)) {
645
+ throw new CliError(
646
+ EXIT_USAGE,
647
+ "INVALID_ARGUMENT",
648
+ "Agent name must contain only lowercase letters, numbers, and single hyphens"
649
+ );
650
+ }
651
+ const processArgs = [
652
+ "--yes",
653
+ "skills",
654
+ "add",
655
+ "weftlabs/skills",
656
+ "--skill",
657
+ skillName,
658
+ "--agent",
659
+ agent,
660
+ "--yes",
661
+ ...parsed.options.get("global") === true ? ["--global"] : []
662
+ ];
663
+ const invocation = npxProcessInvocation(processArgs, dependencies, env);
664
+ const result2 = await (dependencies.runProcess ?? defaultRunProcess)(
665
+ invocation.executable,
666
+ invocation.arguments
667
+ );
668
+ if (result2.exitCode !== 0) {
669
+ const remediation = `npx ${processArgs.join(" ")}`;
670
+ throw new CliError(
671
+ EXIT_INTERNAL,
672
+ "SKILL_INSTALL_FAILED",
673
+ `Could not install ${skillName}. Run '${remediation}' directly for more detail.`,
674
+ {
675
+ executable: invocation.executable,
676
+ arguments: invocation.arguments,
677
+ exit_code: result2.exitCode,
678
+ stderr: result2.stderr
679
+ }
680
+ );
681
+ }
682
+ await writeOut(
683
+ `${JSON.stringify({
684
+ schema_version: "1",
685
+ ok: true,
686
+ command,
687
+ data: {
688
+ status: "installed",
689
+ skill: skillName,
690
+ agent,
691
+ scope: parsed.options.get("global") === true ? "global" : "project"
692
+ }
693
+ })}
694
+ `
695
+ );
696
+ return EXIT_SUCCESS;
697
+ }
698
+ ensureOnly(parsed.options, []);
699
+ const installSkill = await loadSkillInstaller();
700
+ const result = await installSkill({ force: true });
701
+ await writeOut(
702
+ `${JSON.stringify({
703
+ schema_version: "1",
704
+ ok: true,
705
+ command,
706
+ data: {
707
+ status: result.status,
708
+ installed: result.installed,
709
+ hosts: result.hosts,
710
+ ...result.reason === void 0 ? {} : { reason: result.reason },
711
+ ...result.warnings.length === 0 ? {} : { warnings: result.warnings }
712
+ }
713
+ })}
714
+ `
715
+ );
716
+ return EXIT_SUCCESS;
717
+ }
718
+ await installSkillInBackground();
719
+ if (parsed.command === "bootstrap") {
720
+ ensureOnly(parsed.options, ["email", "agent-name", "reason"]);
721
+ if (parsed.positionals.length !== 0) {
722
+ throw new CliError(
723
+ EXIT_USAGE,
724
+ "INVALID_ARGUMENT",
725
+ "bootstrap does not accept positional arguments"
726
+ );
727
+ }
728
+ const email = requiredOption(parsed.options, "email");
729
+ const agentName = requiredOption(parsed.options, "agent-name");
730
+ const reason = requiredOption(parsed.options, "reason");
731
+ const register = await requestJson(
732
+ fetchApi,
733
+ `${baseUrl}/oauth/register`,
734
+ {
735
+ method: "POST",
736
+ headers: {
737
+ "content-type": "application/json"
738
+ },
739
+ body: JSON.stringify({
740
+ client_name: OAUTH_CLIENT_NAME,
741
+ redirect_uris: ["http://localhost/callback"],
742
+ grant_types: [
743
+ "urn:ietf:params:oauth:grant-type:device_code",
744
+ "refresh_token"
745
+ ],
746
+ token_endpoint_auth_method: "none",
747
+ scope: OAUTH_SCOPE
748
+ })
749
+ }
750
+ );
751
+ const clientId = register.client_id;
752
+ const bootstrap = await requestJson(
753
+ fetchApi,
754
+ `${baseUrl}/api/v1/account_bootstraps`,
755
+ {
756
+ method: "POST",
757
+ headers: {
758
+ "content-type": "application/json"
759
+ },
760
+ body: JSON.stringify({
761
+ email,
762
+ agent_name: agentName,
763
+ host_name: OAUTH_HOST_NAME,
764
+ reason,
765
+ oauth_client_id: clientId,
766
+ requested_scopes: ["balance", "fetch", "search"]
767
+ })
768
+ }
769
+ );
770
+ const stored = {
771
+ version: 1,
772
+ type: "bootstrap",
773
+ base_url: baseUrl,
774
+ bootstrap_id: bootstrap.data.id,
775
+ temporary_api_key: bootstrap.data.temporary_api_key,
776
+ device_code: bootstrap.data.device_code,
777
+ client_id: clientId,
778
+ expiry: bootstrap.data.expires_at,
779
+ polling_interval: bootstrap.data.approval.interval
780
+ };
781
+ await writeStoredCredentials(env, stored);
782
+ await writeOut(
783
+ `${JSON.stringify({
784
+ schema_version: "1",
785
+ ok: true,
786
+ command,
787
+ data: noSecretFields({
788
+ ...bootstrap.data,
789
+ client_id: clientId,
790
+ base_url: baseUrl,
791
+ polling_interval: bootstrap.data.approval.interval
792
+ })
793
+ })}
794
+ `
795
+ );
796
+ return EXIT_SUCCESS;
797
+ }
798
+ if (parsed.command === "auth") {
799
+ ensureOnly(parsed.options, []);
800
+ if (parsed.positionals.length !== 1 || parsed.positionals[0] !== "status") {
801
+ throw new CliError(
802
+ EXIT_USAGE,
803
+ "INVALID_ARGUMENT",
804
+ "auth requires 'status'"
805
+ );
806
+ }
807
+ const stored = await readStoredCredentials(env);
808
+ if (stored?.type === "oauth") {
809
+ await writeOut(
810
+ `${JSON.stringify({
811
+ schema_version: "1",
812
+ ok: true,
813
+ command,
814
+ data: {
815
+ status: "consumed",
816
+ authentication: "oauth",
817
+ scope: stored.scope
818
+ }
819
+ })}
820
+ `
821
+ );
822
+ return EXIT_SUCCESS;
823
+ }
824
+ if (!stored) {
825
+ throw new CliError(
826
+ EXIT_AUTH,
827
+ "BOOTSTRAP_REQUIRED",
828
+ "Run weft bootstrap before auth status"
829
+ );
830
+ }
831
+ const statusBaseUrl = stored.base_url;
832
+ const status = await requestJson(
833
+ fetchApi,
834
+ `${statusBaseUrl}/api/v1/account_bootstraps/${stored.bootstrap_id}`,
835
+ {
836
+ method: "GET",
837
+ headers: withCredentials({}, stored.temporary_api_key)
838
+ }
839
+ );
840
+ const statusValue = status.data.status;
841
+ if (statusValue !== "claimed" && statusValue !== "consumed") {
842
+ await writeOut(
843
+ `${JSON.stringify({
844
+ schema_version: "1",
845
+ ok: true,
846
+ command,
847
+ data: { status: statusValue }
848
+ })}
849
+ `
850
+ );
851
+ return EXIT_SUCCESS;
852
+ }
853
+ const token = await requestJson(
854
+ fetchApi,
855
+ `${statusBaseUrl}/oauth/token`,
856
+ {
857
+ method: "POST",
858
+ headers: {
859
+ "content-type": "application/x-www-form-urlencoded"
860
+ },
861
+ body: new URLSearchParams({
862
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
863
+ device_code: stored.device_code,
864
+ client_id: stored.client_id,
865
+ name: OAUTH_CLIENT_NAME
866
+ }).toString()
867
+ },
868
+ true
869
+ );
870
+ const oauth = {
871
+ version: 1,
872
+ type: "oauth",
873
+ base_url: statusBaseUrl,
874
+ client_id: stored.client_id,
875
+ access_token: token.access_token,
876
+ refresh_token: token.refresh_token,
877
+ token_type: token.token_type,
878
+ scope: token.scope,
879
+ expiry: new Date(Date.now() + token.expires_in * 1e3).toISOString()
880
+ };
881
+ await writeStoredCredentials(env, oauth);
882
+ await writeOut(
883
+ `${JSON.stringify({
884
+ schema_version: "1",
885
+ ok: true,
886
+ command,
887
+ data: {
888
+ status: "consumed",
889
+ authentication: "oauth",
890
+ scope: token.scope
891
+ }
892
+ })}
893
+ `
894
+ );
895
+ return EXIT_SUCCESS;
896
+ }
897
+ const rawKey = parsed.apiKeyStdin ? await (dependencies.readStdin ?? defaultReadStdin)() : env.WEFT_API_KEY;
898
+ let apiKey = rawKey?.trim();
899
+ let authenticatedBaseUrl = baseUrl;
900
+ if (parsed.apiKeyStdin && !apiKey) {
901
+ throw new CliError(
902
+ EXIT_AUTH,
903
+ "API_KEY_REQUIRED",
904
+ "--api-key-stdin requires a non-empty API key"
905
+ );
906
+ }
907
+ if (!apiKey && !parsed.apiKeyStdin) {
908
+ let stored = await readStoredCredentials(env);
909
+ if (stored?.type === "oauth" && oauthNeedsRefresh(stored)) {
910
+ stored = await withCredentialsLock(env, async () => {
911
+ const current = await readStoredCredentials(env);
912
+ if (current?.type === "oauth" && oauthNeedsRefresh(current)) {
913
+ return refreshOAuthCredentials(fetchApi, env, current);
914
+ }
915
+ return current;
916
+ });
917
+ }
918
+ apiKey = resolveApiKeyFromStored(stored);
919
+ if (stored) authenticatedBaseUrl = stored.base_url;
920
+ }
921
+ if (!apiKey) {
922
+ throw new CliError(
923
+ EXIT_AUTH,
924
+ "API_KEY_REQUIRED",
925
+ "Set WEFT_API_KEY, pass --api-key-stdin, or store credentials"
926
+ );
927
+ }
928
+ const client = new WeftClient({
929
+ apiKey,
930
+ baseUrl: authenticatedBaseUrl,
931
+ fetchApi
932
+ });
933
+ let data;
934
+ if (parsed.command === "me") {
935
+ ensureOnly(parsed.options, []);
936
+ data = await client.me();
937
+ } else if (parsed.command === "balance") {
938
+ ensureOnly(parsed.options, []);
939
+ data = await client.balance();
940
+ } else if (parsed.command === "search") {
941
+ ensureOnly(parsed.options, ["max-results"]);
942
+ if (parsed.positionals.length !== 1) {
943
+ throw new CliError(
944
+ EXIT_USAGE,
945
+ "INVALID_ARGUMENT",
946
+ "search requires exactly one query"
947
+ );
948
+ }
949
+ data = await client.search({
950
+ query: parsed.positionals[0],
951
+ maxResults: positiveInteger(
952
+ parsed.options.get("max-results"),
953
+ "--max-results",
954
+ 50
955
+ )
956
+ });
957
+ } else if (parsed.command === "fetch") {
958
+ ensureOnly(parsed.options, [
959
+ "max-cost-usd",
960
+ "method",
961
+ "idempotency-key",
962
+ "raw"
963
+ ]);
964
+ const raw = parsed.options.has("raw");
965
+ schemaVersion = raw ? "1" : "2";
966
+ if (parsed.positionals.length !== 1) {
967
+ throw new CliError(
968
+ EXIT_USAGE,
969
+ "INVALID_ARGUMENT",
970
+ "fetch requires exactly one URL"
971
+ );
972
+ }
973
+ const maxCostUsd = requiredOption(parsed.options, "max-cost-usd");
974
+ if (!/^\d+(?:\.\d+)?$/.test(maxCostUsd) || Number(maxCostUsd) <= 0) {
975
+ throw new CliError(
976
+ EXIT_USAGE,
977
+ "INVALID_ARGUMENT",
978
+ "--max-cost-usd must be a positive decimal"
979
+ );
980
+ }
981
+ const method = parsed.options.get("method");
982
+ const request = {
983
+ url: parsed.positionals[0],
984
+ maxCostUsd,
985
+ method: typeof method === "string" ? method.toUpperCase() : void 0
986
+ };
987
+ const explicitKey = parsed.options.get("idempotency-key");
988
+ idempotencyKey = typeof explicitKey === "string" ? explicitKey : (dependencies.generateIdempotencyKey ?? randomUUID2)();
989
+ let resultDirectory;
990
+ if (!raw) {
991
+ try {
992
+ resultDirectory = await reserveResultDirectory(env);
993
+ } catch {
994
+ throw new CliError(
995
+ EXIT_INTERNAL,
996
+ "RESULT_STORAGE_UNAVAILABLE",
997
+ "Cannot reserve local result storage. Set WEFT_RESULTS_DIR to a directory you own that is writable only by you, with no symlink at that path, then retry. No fetch was sent."
998
+ );
999
+ }
1000
+ }
1001
+ const response = await client.fetch(request, { idempotencyKey });
1002
+ meta = { idempotency_key: idempotencyKey };
1003
+ data = response;
1004
+ const { bodyBase64, ...receipt } = response;
1005
+ deliveryReceipt = receipt;
1006
+ if (resultDirectory) {
1007
+ const savedPath = join2(resultDirectory, "body");
1008
+ const receiptPath = join2(resultDirectory, "receipt.json");
1009
+ const bytes = typeof bodyBase64 === "string" ? Buffer.from(bodyBase64, "base64") : void 0;
1010
+ if (!bytes || bytes.toString("base64") !== bodyBase64) {
1011
+ throw new CliError(
1012
+ EXIT_INTERNAL,
1013
+ "RESULT_DECODE_FAILED",
1014
+ "Fetch returned invalid Base64. No body was saved. Keep this receipt and retry the same request with --idempotency-key and the same key; if it persists, report the artifact ID to support.",
1015
+ { receipt, file_complete: false }
1016
+ );
1017
+ }
1018
+ try {
1019
+ meta = {
1020
+ ...meta,
1021
+ saved_path: savedPath,
1022
+ receipt_path: receiptPath,
1023
+ byte_count: bytes.length
1024
+ };
1025
+ await writeResultFile(
1026
+ receiptPath,
1027
+ `${JSON.stringify({ schema_version: "2", meta, data: receipt })}
1028
+ `
1029
+ );
1030
+ await writeResultFile(savedPath, bytes);
1031
+ data = { ...receipt, ...readableBody(bytes, response.headers) };
1032
+ } catch {
1033
+ throw new CliError(
1034
+ EXIT_INTERNAL,
1035
+ "RESULT_STORAGE_FAILED",
1036
+ "Fetch returned, but local result delivery failed. Keep this receipt. Check local storage, then retry the same request with --idempotency-key and the key below. Do not use a new key.",
1037
+ {
1038
+ receipt,
1039
+ saved_path: savedPath,
1040
+ receipt_path: receiptPath,
1041
+ file_complete: false
1042
+ }
1043
+ );
1044
+ }
1045
+ }
1046
+ } else {
1047
+ ensureOnly(parsed.options, ["page", "per-page"]);
1048
+ if (parsed.positionals.length > 1) {
1049
+ throw new CliError(
1050
+ EXIT_USAGE,
1051
+ "INVALID_ARGUMENT",
1052
+ "purchases accepts at most one purchase ID"
1053
+ );
1054
+ }
1055
+ data = parsed.positionals[0] ? await client.purchase(
1056
+ positiveInteger(parsed.positionals[0], "purchase ID")
1057
+ ) : await client.purchases({
1058
+ page: positiveInteger(parsed.options.get("page"), "--page"),
1059
+ perPage: positiveInteger(
1060
+ parsed.options.get("per-page"),
1061
+ "--per-page",
1062
+ 100
1063
+ )
1064
+ });
1065
+ }
1066
+ await writeOut(
1067
+ `${JSON.stringify({
1068
+ schema_version: schemaVersion,
1069
+ ok: true,
1070
+ command,
1071
+ ...meta === void 0 ? {} : { meta },
1072
+ data
1073
+ })}
1074
+ `
1075
+ );
1076
+ return EXIT_SUCCESS;
1077
+ } catch (error) {
1078
+ const normalized = await normalizeError(error);
1079
+ writeErr(
1080
+ `${JSON.stringify({
1081
+ schema_version: schemaVersion,
1082
+ ok: false,
1083
+ command,
1084
+ ...idempotencyKey === void 0 ? {} : { meta: { ...meta, idempotency_key: idempotencyKey } },
1085
+ error: {
1086
+ code: normalized.code,
1087
+ message: normalized.message,
1088
+ ...normalized.details === void 0 ? {} : { details: normalized.details }
1089
+ }
1090
+ })}
1091
+ `
1092
+ );
1093
+ return normalized.exitCode;
1094
+ }
1095
+ }
1096
+ export {
1097
+ EXIT_API,
1098
+ EXIT_AUTH,
1099
+ EXIT_INTERNAL,
1100
+ EXIT_SUCCESS,
1101
+ EXIT_USAGE,
1102
+ runCli
1103
+ };
1104
+ //# sourceMappingURL=cli.js.map