@zeroxyz/cli 0.0.37 → 0.0.39
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/index.js +807 -221
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { homedir as
|
|
5
|
-
import { join as
|
|
4
|
+
import { homedir as homedir8 } from "os";
|
|
5
|
+
import { join as join9 } from "path";
|
|
6
6
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "@zeroxyz/cli",
|
|
10
|
-
version: "0.0.
|
|
10
|
+
version: "0.0.39",
|
|
11
11
|
type: "module",
|
|
12
12
|
bin: {
|
|
13
13
|
zero: "dist/index.js",
|
|
@@ -61,12 +61,124 @@ var package_default = {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
// src/app.ts
|
|
64
|
-
import { Command as
|
|
64
|
+
import { Command as Command13 } from "commander";
|
|
65
|
+
|
|
66
|
+
// src/commands/auth-command.ts
|
|
67
|
+
import { homedir } from "os";
|
|
68
|
+
import { join } from "path";
|
|
69
|
+
import { Command } from "commander";
|
|
70
|
+
import open from "open";
|
|
71
|
+
|
|
72
|
+
// src/util/secure-config.ts
|
|
73
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
74
|
+
var SECURE_DIR_MODE = 448;
|
|
75
|
+
var SECURE_FILE_MODE = 384;
|
|
76
|
+
var ensureSecureDir = (path) => {
|
|
77
|
+
mkdirSync(path, { recursive: true, mode: SECURE_DIR_MODE });
|
|
78
|
+
chmodSync(path, SECURE_DIR_MODE);
|
|
79
|
+
};
|
|
80
|
+
var writeSecureFile = (path, contents) => {
|
|
81
|
+
writeFileSync(path, contents, { mode: SECURE_FILE_MODE });
|
|
82
|
+
chmodSync(path, SECURE_FILE_MODE);
|
|
83
|
+
};
|
|
84
|
+
var readConfig = (path) => {
|
|
85
|
+
try {
|
|
86
|
+
const raw = readFileSync(path, "utf8");
|
|
87
|
+
return JSON.parse(raw);
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/commands/auth-command.ts
|
|
94
|
+
var getConfigPath = () => join(homedir(), ".zero", "config.json");
|
|
95
|
+
var authLoginCommand = (appContext) => new Command("login").description("Sign in to Zero").option("--no-open", "Do not open the browser automatically").action(async (opts) => {
|
|
96
|
+
const { apiService } = appContext.services;
|
|
97
|
+
const start = await apiService.startDeviceLogin();
|
|
98
|
+
const url = `${start.verificationUri}?code=${start.userCode}`;
|
|
99
|
+
process.stdout.write(
|
|
100
|
+
`Open this URL to authorize:
|
|
101
|
+
${url}
|
|
102
|
+
User code: ${start.userCode}
|
|
103
|
+
`
|
|
104
|
+
);
|
|
105
|
+
if (opts.open) {
|
|
106
|
+
await open(url).catch(() => {
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
while (Date.now() < start.expiresAt) {
|
|
110
|
+
await new Promise(
|
|
111
|
+
(r) => setTimeout(r, start.pollInterval * 1e3)
|
|
112
|
+
);
|
|
113
|
+
const result = await apiService.pollDeviceLogin(start.deviceCode);
|
|
114
|
+
if (result.status === "pending") {
|
|
115
|
+
process.stdout.write(".");
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (result.status === "expired") {
|
|
119
|
+
process.stderr.write(
|
|
120
|
+
"\nDevice code expired. Run `zero auth login` again.\n"
|
|
121
|
+
);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const path = getConfigPath();
|
|
126
|
+
const current = readConfig(path);
|
|
127
|
+
const next = {
|
|
128
|
+
...current,
|
|
129
|
+
session: {
|
|
130
|
+
userId: result.user.id,
|
|
131
|
+
authMethod: "workos",
|
|
132
|
+
accessToken: result.accessToken,
|
|
133
|
+
refreshToken: result.refreshToken
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
writeSecureFile(path, JSON.stringify(next, null, 2));
|
|
137
|
+
process.stdout.write(
|
|
138
|
+
`
|
|
139
|
+
Signed in as ${result.user.email ?? result.user.id}
|
|
140
|
+
`
|
|
141
|
+
);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
process.stderr.write(
|
|
145
|
+
"\nDevice code expired. Run `zero auth login` again.\n"
|
|
146
|
+
);
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
});
|
|
149
|
+
var authLogoutCommand = (appContext) => new Command("logout").description("Sign out of Zero").action(async () => {
|
|
150
|
+
const { apiService } = appContext.services;
|
|
151
|
+
const path = getConfigPath();
|
|
152
|
+
const config = readConfig(path);
|
|
153
|
+
if (config.session) {
|
|
154
|
+
await apiService.logout(config.session.refreshToken);
|
|
155
|
+
}
|
|
156
|
+
const { session: _drop, ...rest } = config;
|
|
157
|
+
writeSecureFile(path, JSON.stringify(rest, null, 2));
|
|
158
|
+
process.stdout.write("Signed out.\n");
|
|
159
|
+
});
|
|
160
|
+
var authWhoamiCommand = (appContext) => new Command("whoami").description("Print the current Zero identity").action(async () => {
|
|
161
|
+
const { apiService } = appContext.services;
|
|
162
|
+
const me = await apiService.getMe();
|
|
163
|
+
process.stdout.write(
|
|
164
|
+
`${me.email ?? me.id}
|
|
165
|
+
uid: ${me.id}
|
|
166
|
+
authMethod: ${me.authMethod}
|
|
167
|
+
`
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
var authCommand = (appContext) => {
|
|
171
|
+
const cmd = new Command("auth").description("Authentication commands");
|
|
172
|
+
cmd.addCommand(authLoginCommand(appContext));
|
|
173
|
+
cmd.addCommand(authLogoutCommand(appContext));
|
|
174
|
+
cmd.addCommand(authWhoamiCommand(appContext), { hidden: true });
|
|
175
|
+
return cmd;
|
|
176
|
+
};
|
|
65
177
|
|
|
66
178
|
// src/commands/bug-report-command.ts
|
|
67
179
|
import { createHash as createHash2 } from "crypto";
|
|
68
|
-
import { readFileSync } from "fs";
|
|
69
|
-
import { Command } from "commander";
|
|
180
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
181
|
+
import { Command as Command2 } from "commander";
|
|
70
182
|
import { z as z2 } from "zod";
|
|
71
183
|
|
|
72
184
|
// src/services/api-service.ts
|
|
@@ -115,6 +227,7 @@ var capabilityResponseSchema = z.object({
|
|
|
115
227
|
responseSchema: z.record(z.string(), z.unknown()).nullable(),
|
|
116
228
|
example: z.object({ request: z.unknown(), response: z.unknown() }).nullable(),
|
|
117
229
|
tags: z.array(z.string()).nullable(),
|
|
230
|
+
exampleAgentPrompt: z.string().nullable().optional(),
|
|
118
231
|
displayCostAmount: z.string(),
|
|
119
232
|
displayCostAsset: z.string(),
|
|
120
233
|
reviewCount: z.number(),
|
|
@@ -224,18 +337,48 @@ var listRunsResponseSchema = z.object({
|
|
|
224
337
|
runs: z.array(runListItemSchema),
|
|
225
338
|
nextCursor: z.string().nullable()
|
|
226
339
|
});
|
|
340
|
+
var userDtoSchema = z.object({
|
|
341
|
+
id: z.string(),
|
|
342
|
+
email: z.string().nullable(),
|
|
343
|
+
authMethod: z.string(),
|
|
344
|
+
createdAt: z.union([z.string(), z.date()]).optional(),
|
|
345
|
+
lastLoginAt: z.union([z.string(), z.date()]).nullable().optional()
|
|
346
|
+
});
|
|
347
|
+
var deviceStartResultSchema = z.object({
|
|
348
|
+
deviceCode: z.string(),
|
|
349
|
+
userCode: z.string(),
|
|
350
|
+
verificationUri: z.string(),
|
|
351
|
+
pollInterval: z.number(),
|
|
352
|
+
expiresAt: z.number()
|
|
353
|
+
});
|
|
354
|
+
var devicePollResponseSchema = z.union([
|
|
355
|
+
z.object({ error: z.literal("authorization_pending") }),
|
|
356
|
+
z.object({ error: z.literal("expired_token") }),
|
|
357
|
+
z.object({
|
|
358
|
+
accessToken: z.string(),
|
|
359
|
+
refreshToken: z.string(),
|
|
360
|
+
expiresIn: z.number(),
|
|
361
|
+
user: userDtoSchema
|
|
362
|
+
})
|
|
363
|
+
]);
|
|
227
364
|
var buildCanonicalMessage = (method, path, body, timestamp, nonce) => {
|
|
228
365
|
const bodyHash = createHash("sha256").update(body ?? "").digest("hex");
|
|
229
366
|
return `${method}:${path}:${bodyHash}:${timestamp}:${nonce}`;
|
|
230
367
|
};
|
|
231
|
-
var ApiService = class {
|
|
232
|
-
constructor(baseUrl, account) {
|
|
368
|
+
var ApiService = class _ApiService {
|
|
369
|
+
constructor(baseUrl, account, credentials = { kind: "none" }, onSessionRefreshed = async () => {
|
|
370
|
+
}) {
|
|
233
371
|
this.baseUrl = baseUrl;
|
|
234
372
|
this.account = account;
|
|
235
373
|
this.walletAddress = this.account?.address ?? null;
|
|
374
|
+
this.credentials = credentials;
|
|
375
|
+
this.onSessionRefreshed = onSessionRefreshed;
|
|
236
376
|
}
|
|
237
377
|
walletAddress;
|
|
238
378
|
account;
|
|
379
|
+
credentials;
|
|
380
|
+
onSessionRefreshed;
|
|
381
|
+
withAccount = (account) => new _ApiService(this.baseUrl, account);
|
|
239
382
|
signRequest = async (method, path, body) => {
|
|
240
383
|
if (!this.account) throw new Error("No private key configured");
|
|
241
384
|
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
@@ -249,21 +392,59 @@ var ApiService = class {
|
|
|
249
392
|
"x-zero-signature": signature
|
|
250
393
|
};
|
|
251
394
|
};
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
395
|
+
// Session credentials take precedence over EIP-191. We never combine them
|
|
396
|
+
// in a single request: if the user has signed in, the JWT is the identity
|
|
397
|
+
// the API trusts, and the wallet signing path stays out of the picture.
|
|
398
|
+
buildHeaders = async (method, path, bodyStr) => {
|
|
399
|
+
const base2 = {
|
|
256
400
|
"content-type": "application/json"
|
|
257
401
|
};
|
|
402
|
+
if (this.credentials.kind === "session") {
|
|
403
|
+
base2.authorization = `Bearer ${this.credentials.accessToken}`;
|
|
404
|
+
return base2;
|
|
405
|
+
}
|
|
258
406
|
if (this.account) {
|
|
259
407
|
const walletHeaders = await this.signRequest(method, path, bodyStr);
|
|
260
|
-
|
|
408
|
+
return { ...base2, ...walletHeaders };
|
|
261
409
|
}
|
|
262
|
-
|
|
410
|
+
return base2;
|
|
411
|
+
};
|
|
412
|
+
// Rotates the in-memory tokens and fires onSessionRefreshed so the
|
|
413
|
+
// caller can persist them. Returns false on any refresh failure — the
|
|
414
|
+
// outer request then propagates the original 401 to the caller.
|
|
415
|
+
tryRefreshSession = async () => {
|
|
416
|
+
if (this.credentials.kind !== "session") return false;
|
|
417
|
+
const { refreshToken } = this.credentials;
|
|
418
|
+
const resp = await fetch(`${this.baseUrl}/v1/auth/refresh`, {
|
|
419
|
+
method: "POST",
|
|
420
|
+
headers: { "content-type": "application/json" },
|
|
421
|
+
body: JSON.stringify({ refreshToken })
|
|
422
|
+
});
|
|
423
|
+
if (!resp.ok) return false;
|
|
424
|
+
const body = await resp.json();
|
|
425
|
+
this.credentials = {
|
|
426
|
+
...this.credentials,
|
|
427
|
+
accessToken: body.accessToken,
|
|
428
|
+
refreshToken: body.refreshToken
|
|
429
|
+
};
|
|
430
|
+
await this.onSessionRefreshed(body);
|
|
431
|
+
return true;
|
|
432
|
+
};
|
|
433
|
+
request = async (method, path, body) => {
|
|
434
|
+
const url = `${this.baseUrl}${path}`;
|
|
435
|
+
const bodyStr = body ? JSON.stringify(body) : void 0;
|
|
436
|
+
const makeInit = async () => ({
|
|
263
437
|
method,
|
|
264
|
-
headers,
|
|
438
|
+
headers: await this.buildHeaders(method, path, bodyStr),
|
|
265
439
|
body: bodyStr
|
|
266
440
|
});
|
|
441
|
+
let response = await fetch(url, await makeInit());
|
|
442
|
+
if (response.status === 401 && this.credentials.kind === "session") {
|
|
443
|
+
const refreshed = await this.tryRefreshSession();
|
|
444
|
+
if (refreshed) {
|
|
445
|
+
response = await fetch(url, await makeInit());
|
|
446
|
+
}
|
|
447
|
+
}
|
|
267
448
|
if (!response.ok) {
|
|
268
449
|
const errorBody = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
269
450
|
throw new Error(
|
|
@@ -272,6 +453,44 @@ var ApiService = class {
|
|
|
272
453
|
}
|
|
273
454
|
return response.json();
|
|
274
455
|
};
|
|
456
|
+
startDeviceLogin = async () => {
|
|
457
|
+
const resp = await fetch(`${this.baseUrl}/v1/auth/device/start`, {
|
|
458
|
+
method: "POST"
|
|
459
|
+
});
|
|
460
|
+
if (!resp.ok) throw new Error(`device_start_${resp.status}`);
|
|
461
|
+
return deviceStartResultSchema.parse(await resp.json());
|
|
462
|
+
};
|
|
463
|
+
pollDeviceLogin = async (deviceCode) => {
|
|
464
|
+
const resp = await fetch(`${this.baseUrl}/v1/auth/device/poll`, {
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers: { "content-type": "application/json" },
|
|
467
|
+
body: JSON.stringify({ deviceCode })
|
|
468
|
+
});
|
|
469
|
+
if (!resp.ok) throw new Error(`device_poll_${resp.status}`);
|
|
470
|
+
const parsed = devicePollResponseSchema.parse(await resp.json());
|
|
471
|
+
if ("error" in parsed) {
|
|
472
|
+
return parsed.error === "authorization_pending" ? { status: "pending" } : { status: "expired" };
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
status: "ok",
|
|
476
|
+
accessToken: parsed.accessToken,
|
|
477
|
+
refreshToken: parsed.refreshToken,
|
|
478
|
+
expiresIn: parsed.expiresIn,
|
|
479
|
+
user: parsed.user
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
logout = async (refreshToken) => {
|
|
483
|
+
await fetch(`${this.baseUrl}/v1/auth/logout`, {
|
|
484
|
+
method: "POST",
|
|
485
|
+
headers: { "content-type": "application/json" },
|
|
486
|
+
body: JSON.stringify({ refreshToken })
|
|
487
|
+
}).catch(() => {
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
getMe = async () => {
|
|
491
|
+
const json = await this.request("GET", "/v1/users/me");
|
|
492
|
+
return userDtoSchema.parse(json);
|
|
493
|
+
};
|
|
275
494
|
search = async (options) => {
|
|
276
495
|
const json = await this.request("POST", "/v1/search", options);
|
|
277
496
|
return searchResponseSchema.parse(json);
|
|
@@ -349,7 +568,7 @@ var autoIdempotencyKey = (description, contextSeed) => {
|
|
|
349
568
|
const seed = `${description}|${contextSeed ?? ""}|${bucket}`;
|
|
350
569
|
return `auto-${createHash2("sha256").update(seed).digest("hex").slice(0, 16)}`;
|
|
351
570
|
};
|
|
352
|
-
var bugReportCommand = (appContext) => new
|
|
571
|
+
var bugReportCommand = (appContext) => new Command2("bug-report").description(
|
|
353
572
|
"Report a Zero platform bug \u2014 bad search ranking, wrong indexed URL, CLI bugs, billing issues. The CLI auto-attaches your most recent context (last search + last run), auto-derives a title, and lets the server classify the category. For 'this capability returned a bad result', use `zero review` instead."
|
|
354
573
|
).addHelpText(
|
|
355
574
|
"after",
|
|
@@ -426,7 +645,7 @@ Categories the classifier picks from:
|
|
|
426
645
|
return;
|
|
427
646
|
}
|
|
428
647
|
if (options.fromFile) {
|
|
429
|
-
const contents =
|
|
648
|
+
const contents = readFileSync2(options.fromFile, "utf8");
|
|
430
649
|
const lines = contents.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
431
650
|
let ok = 0;
|
|
432
651
|
let failed = 0;
|
|
@@ -563,21 +782,21 @@ Bulk bug-report complete: ${ok} ok, ${failed} failed`
|
|
|
563
782
|
);
|
|
564
783
|
|
|
565
784
|
// src/commands/config-command.ts
|
|
566
|
-
import { existsSync, mkdirSync, readFileSync as
|
|
567
|
-
import { homedir } from "os";
|
|
568
|
-
import { join } from "path";
|
|
569
|
-
import { Command as
|
|
785
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
786
|
+
import { homedir as homedir2 } from "os";
|
|
787
|
+
import { join as join2 } from "path";
|
|
788
|
+
import { Command as Command3 } from "commander";
|
|
570
789
|
var VALID_KEYS = ["lowBalanceWarning", "auth", "telemetry"];
|
|
571
790
|
var loadConfig = (configPath) => {
|
|
572
791
|
try {
|
|
573
792
|
if (!existsSync(configPath)) return {};
|
|
574
|
-
return JSON.parse(
|
|
793
|
+
return JSON.parse(readFileSync3(configPath, "utf8"));
|
|
575
794
|
} catch {
|
|
576
795
|
return {};
|
|
577
796
|
}
|
|
578
797
|
};
|
|
579
|
-
var configCommand = (_appContext) => new
|
|
580
|
-
const configPath =
|
|
798
|
+
var configCommand = (_appContext) => new Command3("config").description("View or update CLI configuration").option("--set <keyValue>", "Set a config value (key=value)").action((options) => {
|
|
799
|
+
const configPath = join2(homedir2(), ".zero", "config.json");
|
|
581
800
|
if (!options.set) {
|
|
582
801
|
const config2 = loadConfig(configPath);
|
|
583
802
|
console.log(JSON.stringify(config2, null, 2));
|
|
@@ -607,16 +826,16 @@ var configCommand = (_appContext) => new Command2("config").description("View or
|
|
|
607
826
|
}
|
|
608
827
|
const config = loadConfig(configPath);
|
|
609
828
|
config[key] = value;
|
|
610
|
-
|
|
611
|
-
|
|
829
|
+
mkdirSync2(join2(homedir2(), ".zero"), { recursive: true });
|
|
830
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
612
831
|
console.log(`Set ${key} = ${JSON.stringify(value)}`);
|
|
613
832
|
});
|
|
614
833
|
|
|
615
834
|
// src/commands/fetch-command.ts
|
|
616
835
|
import { createHash as createHash3 } from "crypto";
|
|
617
|
-
import { readFileSync as
|
|
836
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
618
837
|
import { resolve as resolvePath } from "path";
|
|
619
|
-
import { Command as
|
|
838
|
+
import { Command as Command4 } from "commander";
|
|
620
839
|
import { formatUnits as formatUnits2 } from "viem";
|
|
621
840
|
|
|
622
841
|
// src/services/payment-service.ts
|
|
@@ -1120,8 +1339,9 @@ var PaymentService = class {
|
|
|
1120
1339
|
return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
|
|
1121
1340
|
}
|
|
1122
1341
|
};
|
|
1123
|
-
getBalanceRaw = async (chain) => {
|
|
1124
|
-
|
|
1342
|
+
getBalanceRaw = async (chain, address) => {
|
|
1343
|
+
const target = address ?? this.account?.address;
|
|
1344
|
+
if (!target) return 0n;
|
|
1125
1345
|
const { viemChain, token } = this.resolveChainConfig(chain);
|
|
1126
1346
|
const client = createPublicClient({
|
|
1127
1347
|
chain: viemChain,
|
|
@@ -1131,7 +1351,7 @@ var PaymentService = class {
|
|
|
1131
1351
|
address: token,
|
|
1132
1352
|
abi: ERC20_BALANCE_ABI,
|
|
1133
1353
|
functionName: "balanceOf",
|
|
1134
|
-
args: [
|
|
1354
|
+
args: [target]
|
|
1135
1355
|
});
|
|
1136
1356
|
return balance;
|
|
1137
1357
|
};
|
|
@@ -1151,6 +1371,13 @@ var PaymentService = class {
|
|
|
1151
1371
|
]);
|
|
1152
1372
|
return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
|
|
1153
1373
|
};
|
|
1374
|
+
getTotalBalanceForAddress = async (address) => {
|
|
1375
|
+
const [baseRaw, tempoRaw] = await Promise.all([
|
|
1376
|
+
this.getBalanceRaw("base", address),
|
|
1377
|
+
this.getBalanceRaw("tempo", address)
|
|
1378
|
+
]);
|
|
1379
|
+
return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
|
|
1380
|
+
};
|
|
1154
1381
|
};
|
|
1155
1382
|
|
|
1156
1383
|
// src/util/infer-schema.ts
|
|
@@ -1237,30 +1464,69 @@ var isJsonContentType = (contentType) => {
|
|
|
1237
1464
|
const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
1238
1465
|
return ct === "application/json" || ct.endsWith("+json");
|
|
1239
1466
|
};
|
|
1240
|
-
var
|
|
1241
|
-
var
|
|
1467
|
+
var MAX_INLINE_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
|
|
1468
|
+
var MAX_FILE_REQUEST_BODY_BYTES = 500 * 1024 * 1024;
|
|
1469
|
+
var UPSTREAM_ERROR_FIELDS = [
|
|
1470
|
+
"error",
|
|
1471
|
+
"message",
|
|
1472
|
+
"detail",
|
|
1473
|
+
"reason",
|
|
1474
|
+
"error_description"
|
|
1475
|
+
];
|
|
1476
|
+
var UPSTREAM_ERROR_MAX_LEN = 200;
|
|
1477
|
+
var clampUpstreamMessage = (value) => {
|
|
1478
|
+
const trimmed = value.trim();
|
|
1479
|
+
if (!trimmed) return "";
|
|
1480
|
+
return trimmed.length > UPSTREAM_ERROR_MAX_LEN ? `${trimmed.slice(0, UPSTREAM_ERROR_MAX_LEN)}\u2026` : trimmed;
|
|
1481
|
+
};
|
|
1482
|
+
var extractUpstreamErrorMessage = (body) => {
|
|
1483
|
+
const parsed = tryParseJson(body);
|
|
1484
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1485
|
+
const clipped = clampUpstreamMessage(body);
|
|
1486
|
+
return clipped || void 0;
|
|
1487
|
+
}
|
|
1488
|
+
const record = parsed;
|
|
1489
|
+
for (const field of UPSTREAM_ERROR_FIELDS) {
|
|
1490
|
+
const value = record[field];
|
|
1491
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1492
|
+
return clampUpstreamMessage(value);
|
|
1493
|
+
}
|
|
1494
|
+
if (value && typeof value === "object") {
|
|
1495
|
+
const nested = value.message;
|
|
1496
|
+
if (typeof nested === "string" && nested.length > 0) {
|
|
1497
|
+
return clampUpstreamMessage(nested);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return void 0;
|
|
1502
|
+
};
|
|
1503
|
+
var resolveRequestBody = (rawData, readStdin2) => {
|
|
1242
1504
|
const fromFile = (spec) => {
|
|
1243
|
-
if (spec === "@-") return
|
|
1244
|
-
return
|
|
1505
|
+
if (spec === "@-") return readFileSync4(0);
|
|
1506
|
+
return readFileSync4(resolvePath(spec.slice(1)));
|
|
1245
1507
|
};
|
|
1246
|
-
if (
|
|
1508
|
+
if (readStdin2 && rawData !== void 0) {
|
|
1247
1509
|
throw new Error(
|
|
1248
1510
|
"Conflicting body sources: use either --data-stdin or -d, not both."
|
|
1249
1511
|
);
|
|
1250
1512
|
}
|
|
1251
1513
|
let body;
|
|
1252
|
-
|
|
1253
|
-
|
|
1514
|
+
let cap;
|
|
1515
|
+
if (readStdin2) {
|
|
1516
|
+
body = readFileSync4(0);
|
|
1517
|
+
cap = MAX_FILE_REQUEST_BODY_BYTES;
|
|
1254
1518
|
} else if (rawData?.startsWith("@")) {
|
|
1255
1519
|
body = fromFile(rawData);
|
|
1520
|
+
cap = MAX_FILE_REQUEST_BODY_BYTES;
|
|
1256
1521
|
} else {
|
|
1257
1522
|
body = rawData;
|
|
1523
|
+
cap = MAX_INLINE_REQUEST_BODY_BYTES;
|
|
1258
1524
|
}
|
|
1259
1525
|
if (body !== void 0) {
|
|
1260
1526
|
const bytes = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body, "utf8");
|
|
1261
|
-
if (bytes >
|
|
1527
|
+
if (bytes > cap) {
|
|
1262
1528
|
throw new Error(
|
|
1263
|
-
`Request body is ${bytes} bytes \u2014 exceeds the ${
|
|
1529
|
+
`Request body is ${bytes} bytes \u2014 exceeds the ${cap} byte local cap. This is a CLI-side guard, not a protocol limit. Compress the payload or contact us if you need it raised.`
|
|
1264
1530
|
);
|
|
1265
1531
|
}
|
|
1266
1532
|
}
|
|
@@ -1300,7 +1566,7 @@ var detectPaymentRequirement = async (response) => {
|
|
|
1300
1566
|
}
|
|
1301
1567
|
return { protocol: "unknown", raw: {} };
|
|
1302
1568
|
};
|
|
1303
|
-
var fetchCommand = (appContext) => new
|
|
1569
|
+
var fetchCommand = (appContext) => new Command4("fetch").description(
|
|
1304
1570
|
"Fetch a capability URL, handling 402 challenges automatically"
|
|
1305
1571
|
).argument(
|
|
1306
1572
|
"[url]",
|
|
@@ -1371,14 +1637,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1371
1637
|
capName = cap.name;
|
|
1372
1638
|
}
|
|
1373
1639
|
const searchResult = await apiService.search({ query: capName });
|
|
1374
|
-
const
|
|
1640
|
+
const matchedEntry = searchResult.capabilities.find(
|
|
1375
1641
|
(c) => c.slug === options.capability || c.id === options.capability
|
|
1376
1642
|
);
|
|
1643
|
+
const slugFoundInResults = Boolean(matchedEntry);
|
|
1377
1644
|
stateService.saveLastSearch({
|
|
1378
1645
|
searchId: searchResult.searchId,
|
|
1379
1646
|
capabilities: searchResult.capabilities.map((c) => ({
|
|
1380
1647
|
position: c.position,
|
|
1381
1648
|
id: c.id,
|
|
1649
|
+
slug: c.slug,
|
|
1382
1650
|
url: c.url,
|
|
1383
1651
|
urlTemplate: c.urlTemplate ?? null,
|
|
1384
1652
|
displayCostAmount: c.cost.amount
|
|
@@ -1396,7 +1664,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1396
1664
|
slugFoundInResults
|
|
1397
1665
|
});
|
|
1398
1666
|
analyticsService.capture("capability_viewed", {
|
|
1667
|
+
// Existing field preserved as the raw --capability
|
|
1668
|
+
// input for back-compat.
|
|
1399
1669
|
capabilityId: options.capability,
|
|
1670
|
+
// New canonical identifier fields hydrated from the
|
|
1671
|
+
// resolved search result (when the slug was found).
|
|
1672
|
+
capabilityUid: matchedEntry?.id,
|
|
1673
|
+
capabilitySlug: matchedEntry?.slug,
|
|
1400
1674
|
fromLastSearch: false,
|
|
1401
1675
|
searchId: searchResult.searchId,
|
|
1402
1676
|
triggeredBy: "slug_handoff"
|
|
@@ -1413,7 +1687,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1413
1687
|
} catch (err) {
|
|
1414
1688
|
const message = err instanceof Error ? err.message : "Failed to read request body";
|
|
1415
1689
|
analyticsService.capture("fetch_error", {
|
|
1416
|
-
cliErrorClass: /exceeds the .* byte
|
|
1690
|
+
cliErrorClass: /exceeds the .* byte local cap/.test(message) ? "payload_too_large" : "schema_validation_failed",
|
|
1417
1691
|
url: redactUrl(resolvedUrl),
|
|
1418
1692
|
error: truncateError(message)
|
|
1419
1693
|
});
|
|
@@ -1455,6 +1729,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1455
1729
|
});
|
|
1456
1730
|
const matchCtx = explicitCapabilityCtx ?? urlCtx;
|
|
1457
1731
|
const capabilityId = options.capability ?? matchCtx?.capabilityId ?? null;
|
|
1732
|
+
const capabilityUid = matchCtx?.capabilityUid ?? null;
|
|
1733
|
+
const capabilitySlug = matchCtx?.capabilitySlug ?? null;
|
|
1458
1734
|
const searchId = matchCtx?.searchId;
|
|
1459
1735
|
const resultRank = matchCtx?.resultRank;
|
|
1460
1736
|
const matchedDisplayCostAmount = matchCtx?.displayCostAmount;
|
|
@@ -1604,10 +1880,12 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1604
1880
|
const isFailure = !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
|
|
1605
1881
|
let errorSnippetHash;
|
|
1606
1882
|
let errorSnippetLength;
|
|
1883
|
+
let upstreamErrorMessage;
|
|
1607
1884
|
if (isFailure && body && !bodyIsBinary) {
|
|
1608
1885
|
const snippet = body.slice(0, 500);
|
|
1609
1886
|
errorSnippetHash = createHash3("sha256").update(snippet).digest("hex");
|
|
1610
1887
|
errorSnippetLength = snippet.length;
|
|
1888
|
+
upstreamErrorMessage = extractUpstreamErrorMessage(body);
|
|
1611
1889
|
}
|
|
1612
1890
|
let runId = null;
|
|
1613
1891
|
if (capabilityId && apiService.walletAddress) {
|
|
@@ -1651,7 +1929,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1651
1929
|
);
|
|
1652
1930
|
}
|
|
1653
1931
|
}
|
|
1654
|
-
const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
|
|
1932
|
+
const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status === 402 && paymentMeta ? "payment_rejected" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
|
|
1655
1933
|
analyticsService.capture("fetch_executed", {
|
|
1656
1934
|
url: redactUrl(resolvedUrl),
|
|
1657
1935
|
status,
|
|
@@ -1660,6 +1938,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1660
1938
|
hasPayment: !!paymentMeta,
|
|
1661
1939
|
paymentProtocol: paymentMeta?.protocol,
|
|
1662
1940
|
paymentAmount: paymentMeta?.amount,
|
|
1941
|
+
// Canonical join keys — capabilitySlug is the dashboard
|
|
1942
|
+
// breakdown dimension; capabilityUid is the stable join
|
|
1943
|
+
// key with the capabilities table. capabilityId is the
|
|
1944
|
+
// back-compat alias (same value as capabilityUid when
|
|
1945
|
+
// resolved, else the raw --capability input).
|
|
1946
|
+
capabilityUid: capabilityUid ?? void 0,
|
|
1947
|
+
capabilitySlug: capabilitySlug ?? void 0,
|
|
1663
1948
|
capabilityId: capabilityId ?? void 0,
|
|
1664
1949
|
searchId: searchId ?? void 0,
|
|
1665
1950
|
resultRank: resultRank ?? void 0,
|
|
@@ -1669,15 +1954,21 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1669
1954
|
});
|
|
1670
1955
|
const isFetchFailure = Boolean(fetchError) || !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
|
|
1671
1956
|
if (isFetchFailure) {
|
|
1672
|
-
const cliErrorClass = fetchError || !finalResponse ? "network" : !apiService.walletAddress ? "auth_missing" : "unknown";
|
|
1957
|
+
const cliErrorClass = fetchError || !finalResponse ? "network" : !apiService.walletAddress ? "auth_missing" : status === 402 && paymentMeta ? "payment_rejected" : typeof status === "number" && status >= 500 ? "upstream_5xx" : typeof status === "number" && status >= 400 ? "upstream_4xx" : "unknown";
|
|
1673
1958
|
analyticsService.capture("fetch_error", {
|
|
1674
1959
|
cliErrorClass,
|
|
1960
|
+
capabilityUid: capabilityUid ?? void 0,
|
|
1961
|
+
capabilitySlug: capabilitySlug ?? void 0,
|
|
1675
1962
|
capabilityId: capabilityId ?? void 0,
|
|
1676
1963
|
searchId: searchId ?? void 0,
|
|
1677
1964
|
resultRank: resultRank ?? void 0,
|
|
1678
1965
|
url: redactUrl(resolvedUrl),
|
|
1966
|
+
status: typeof status === "number" ? status : void 0,
|
|
1967
|
+
upstreamErrorMessage,
|
|
1968
|
+
errorSnippetHash,
|
|
1969
|
+
errorSnippetLength,
|
|
1679
1970
|
error: truncateError(
|
|
1680
|
-
fetchError?.message ?? skipReasons.join("; ")
|
|
1971
|
+
fetchError?.message ?? upstreamErrorMessage ?? skipReasons.join("; ")
|
|
1681
1972
|
),
|
|
1682
1973
|
skippedRun: !runId && skipReasons.length > 0
|
|
1683
1974
|
});
|
|
@@ -1685,6 +1976,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1685
1976
|
if (fetchError && !options.json) {
|
|
1686
1977
|
console.error(` Fetch failed: ${fetchError.message}`);
|
|
1687
1978
|
}
|
|
1979
|
+
if (finalResponse && !fetchError && !options.json && typeof status === "number" && (status < 200 || status >= 300)) {
|
|
1980
|
+
const messageSuffix = upstreamErrorMessage ? ` \u2014 ${upstreamErrorMessage}` : "";
|
|
1981
|
+
if (status === 402 && paymentMeta) {
|
|
1982
|
+
console.error(
|
|
1983
|
+
` Upstream returned 402 after payment was sent \u2014 the seller's facilitator rejected the credential${messageSuffix}`
|
|
1984
|
+
);
|
|
1985
|
+
} else {
|
|
1986
|
+
console.error(` Upstream returned ${status}${messageSuffix}`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1688
1989
|
if (options.json) {
|
|
1689
1990
|
const responseStatus = finalResponse?.status ?? null;
|
|
1690
1991
|
const ok = responseStatus !== null && responseStatus >= 200 && responseStatus < 300;
|
|
@@ -1764,7 +2065,20 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1764
2065
|
);
|
|
1765
2066
|
|
|
1766
2067
|
// src/commands/get-command.ts
|
|
1767
|
-
import { Command as
|
|
2068
|
+
import { Command as Command5 } from "commander";
|
|
2069
|
+
|
|
2070
|
+
// src/util/format-price.ts
|
|
2071
|
+
var centsToDollars = (cents) => {
|
|
2072
|
+
const value = Number.parseFloat(cents) / 100;
|
|
2073
|
+
if (!Number.isFinite(value) || value === 0) return "0.00";
|
|
2074
|
+
if (value < 1e-4) return value.toFixed(6);
|
|
2075
|
+
if (value < 1e-3) return value.toFixed(5);
|
|
2076
|
+
if (value < 0.01) return value.toFixed(4);
|
|
2077
|
+
if (value < 1) return value.toFixed(3);
|
|
2078
|
+
return value.toFixed(2);
|
|
2079
|
+
};
|
|
2080
|
+
|
|
2081
|
+
// src/commands/get-command.ts
|
|
1768
2082
|
var formatRelativeTimestamp = (iso) => {
|
|
1769
2083
|
if (!iso) return "never";
|
|
1770
2084
|
const then = new Date(iso).getTime();
|
|
@@ -1781,12 +2095,6 @@ var formatRelativeTimestamp = (iso) => {
|
|
|
1781
2095
|
if (diffMo < 12) return `${diffMo}mo ago`;
|
|
1782
2096
|
return `${Math.round(diffMo / 12)}y ago`;
|
|
1783
2097
|
};
|
|
1784
|
-
var centsToDollars = (cents) => {
|
|
1785
|
-
const value = Number.parseFloat(cents) / 100;
|
|
1786
|
-
if (value < 0.01) return value.toFixed(4);
|
|
1787
|
-
if (value < 1) return value.toFixed(3);
|
|
1788
|
-
return value.toFixed(2);
|
|
1789
|
-
};
|
|
1790
2098
|
var formatCost = (capability) => {
|
|
1791
2099
|
const lines = [];
|
|
1792
2100
|
const observed = capability.priceObserved;
|
|
@@ -1796,6 +2104,10 @@ var formatCost = (capability) => {
|
|
|
1796
2104
|
const median = observed.medianCents ? centsToDollars(observed.medianCents) : null;
|
|
1797
2105
|
const detail = median ? `median $${median}, n=${observed.sampleCount}` : `n=${observed.sampleCount}`;
|
|
1798
2106
|
lines.push(` Cost: $${min}\u2013$${max}/call (${detail})`);
|
|
2107
|
+
} else if (capability.displayCostAmount === "0") {
|
|
2108
|
+
lines.push(` Cost: Free`);
|
|
2109
|
+
} else if (capability.displayCostAmount === "unknown") {
|
|
2110
|
+
lines.push(` Cost: variable pricing`);
|
|
1799
2111
|
} else {
|
|
1800
2112
|
lines.push(` Cost: $${capability.displayCostAmount}/call`);
|
|
1801
2113
|
}
|
|
@@ -1903,10 +2215,16 @@ var formatCapability = (capability) => {
|
|
|
1903
2215
|
lines.push(
|
|
1904
2216
|
` Last successful run: ${formatRelativeTimestamp(capability.lastSuccessfullyRanAt)}`
|
|
1905
2217
|
);
|
|
2218
|
+
if (capability.exampleAgentPrompt?.trim()) {
|
|
2219
|
+
lines.push(" Example prompt:");
|
|
2220
|
+
for (const promptLine of capability.exampleAgentPrompt.split("\n")) {
|
|
2221
|
+
lines.push(` ${promptLine}`);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
1906
2224
|
lines.push(...buildTryItExample(capability));
|
|
1907
2225
|
return lines.join("\n");
|
|
1908
2226
|
};
|
|
1909
|
-
var getCommand = (appContext) => new
|
|
2227
|
+
var getCommand = (appContext) => new Command5("get").description(
|
|
1910
2228
|
"Get details for a capability by position from last search, or by slug"
|
|
1911
2229
|
).argument(
|
|
1912
2230
|
"<identifier>",
|
|
@@ -1955,7 +2273,15 @@ var getCommand = (appContext) => new Command4("get").description(
|
|
|
1955
2273
|
console.log(JSON.stringify(capability, null, 2));
|
|
1956
2274
|
}
|
|
1957
2275
|
analyticsService.capture("capability_viewed", {
|
|
2276
|
+
// Existing field — raw user input (uid, slug, or position
|
|
2277
|
+
// resolved to uid via the last-search cache). Preserved
|
|
2278
|
+
// for back-compat with existing dashboards.
|
|
1958
2279
|
capabilityId,
|
|
2280
|
+
// New canonical identifier fields read off the API response,
|
|
2281
|
+
// so they are always populated regardless of how the user
|
|
2282
|
+
// referenced the capability.
|
|
2283
|
+
capabilityUid: capability.uid,
|
|
2284
|
+
capabilitySlug: capability.slug,
|
|
1959
2285
|
fromLastSearch: isPosition,
|
|
1960
2286
|
...isPosition ? { position } : {},
|
|
1961
2287
|
...searchId ? { searchId } : {}
|
|
@@ -1973,15 +2299,15 @@ import {
|
|
|
1973
2299
|
existsSync as existsSync2,
|
|
1974
2300
|
mkdirSync as mkdirSync3,
|
|
1975
2301
|
readdirSync,
|
|
1976
|
-
readFileSync as
|
|
2302
|
+
readFileSync as readFileSync5,
|
|
1977
2303
|
rmSync,
|
|
1978
2304
|
statSync,
|
|
1979
2305
|
writeFileSync as writeFileSync3
|
|
1980
2306
|
} from "fs";
|
|
1981
|
-
import { homedir as
|
|
1982
|
-
import { dirname, join as
|
|
2307
|
+
import { homedir as homedir3 } from "os";
|
|
2308
|
+
import { dirname, join as join3, relative } from "path";
|
|
1983
2309
|
import { fileURLToPath } from "url";
|
|
1984
|
-
import { Command as
|
|
2310
|
+
import { Command as Command6 } from "commander";
|
|
1985
2311
|
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
1986
2312
|
|
|
1987
2313
|
// src/util/install-banner.ts
|
|
@@ -2089,7 +2415,12 @@ var printReadyFooter = () => {
|
|
|
2089
2415
|
const lines = [
|
|
2090
2416
|
"",
|
|
2091
2417
|
` ${color.boldGreen("Zero is ready!")} Zero works best with an AI agent.`,
|
|
2092
|
-
|
|
2418
|
+
"",
|
|
2419
|
+
" First, claim your $5 welcome bonus \u2014 run this now, before opening your agent:",
|
|
2420
|
+
"",
|
|
2421
|
+
` ${color.cyan("zero welcome")}`,
|
|
2422
|
+
"",
|
|
2423
|
+
` Then open ${color.boldRed("Claude Code")}, Codex, Cursor, Blackbox, or your agent of choice`,
|
|
2093
2424
|
" and try this prompt to get started:",
|
|
2094
2425
|
"",
|
|
2095
2426
|
` ${color.cyan("What is zero and how do I use it?")}`,
|
|
@@ -2108,19 +2439,6 @@ var printReadyFooter = () => {
|
|
|
2108
2439
|
return lines.join("\n");
|
|
2109
2440
|
};
|
|
2110
2441
|
|
|
2111
|
-
// src/util/secure-config.ts
|
|
2112
|
-
import { chmodSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2113
|
-
var SECURE_DIR_MODE = 448;
|
|
2114
|
-
var SECURE_FILE_MODE = 384;
|
|
2115
|
-
var ensureSecureDir = (path) => {
|
|
2116
|
-
mkdirSync2(path, { recursive: true, mode: SECURE_DIR_MODE });
|
|
2117
|
-
chmodSync(path, SECURE_DIR_MODE);
|
|
2118
|
-
};
|
|
2119
|
-
var writeSecureFile = (path, contents) => {
|
|
2120
|
-
writeFileSync2(path, contents, { mode: SECURE_FILE_MODE });
|
|
2121
|
-
chmodSync(path, SECURE_FILE_MODE);
|
|
2122
|
-
};
|
|
2123
|
-
|
|
2124
2442
|
// src/commands/init-command.ts
|
|
2125
2443
|
var AGENT_TOOLS = [
|
|
2126
2444
|
{ name: "Claude Code", detectDir: ".claude", skillsDir: ".claude/skills" },
|
|
@@ -2135,7 +2453,7 @@ var AGENT_TOOLS = [
|
|
|
2135
2453
|
var findResourceDir = (startDir, resourceName) => {
|
|
2136
2454
|
let current = startDir;
|
|
2137
2455
|
while (true) {
|
|
2138
|
-
const candidate =
|
|
2456
|
+
const candidate = join3(current, resourceName);
|
|
2139
2457
|
if (existsSync2(candidate)) {
|
|
2140
2458
|
return candidate;
|
|
2141
2459
|
}
|
|
@@ -2154,7 +2472,7 @@ var getCliModuleDir = () => {
|
|
|
2154
2472
|
}
|
|
2155
2473
|
return __dirname;
|
|
2156
2474
|
};
|
|
2157
|
-
var sha256File = (filePath) => createHash4("sha256").update(
|
|
2475
|
+
var sha256File = (filePath) => createHash4("sha256").update(readFileSync5(filePath)).digest("hex");
|
|
2158
2476
|
var verifyFileCopy = (src, dest) => {
|
|
2159
2477
|
if (!existsSync2(dest)) return false;
|
|
2160
2478
|
return sha256File(src) === sha256File(dest);
|
|
@@ -2162,7 +2480,7 @@ var verifyFileCopy = (src, dest) => {
|
|
|
2162
2480
|
var collectAllFiles = (dir) => {
|
|
2163
2481
|
const files = [];
|
|
2164
2482
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2165
|
-
const fullPath =
|
|
2483
|
+
const fullPath = join3(dir, entry.name);
|
|
2166
2484
|
if (entry.isDirectory()) {
|
|
2167
2485
|
files.push(...collectAllFiles(fullPath));
|
|
2168
2486
|
} else {
|
|
@@ -2172,7 +2490,7 @@ var collectAllFiles = (dir) => {
|
|
|
2172
2490
|
return files;
|
|
2173
2491
|
};
|
|
2174
2492
|
var copyFile = (src, dest) => {
|
|
2175
|
-
writeFileSync3(dest,
|
|
2493
|
+
writeFileSync3(dest, readFileSync5(src));
|
|
2176
2494
|
try {
|
|
2177
2495
|
chmodSync2(dest, statSync(src).mode);
|
|
2178
2496
|
} catch {
|
|
@@ -2181,8 +2499,8 @@ var copyFile = (src, dest) => {
|
|
|
2181
2499
|
var copyDirRecursive = (src, dest) => {
|
|
2182
2500
|
mkdirSync3(dest, { recursive: true });
|
|
2183
2501
|
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
2184
|
-
const srcPath =
|
|
2185
|
-
const destPath =
|
|
2502
|
+
const srcPath = join3(src, entry.name);
|
|
2503
|
+
const destPath = join3(dest, entry.name);
|
|
2186
2504
|
if (entry.isDirectory()) {
|
|
2187
2505
|
copyDirRecursive(srcPath, destPath);
|
|
2188
2506
|
} else {
|
|
@@ -2191,22 +2509,22 @@ var copyDirRecursive = (src, dest) => {
|
|
|
2191
2509
|
}
|
|
2192
2510
|
};
|
|
2193
2511
|
var installHook = (home, verbose = false) => {
|
|
2194
|
-
const claudeDir =
|
|
2512
|
+
const claudeDir = join3(home, ".claude");
|
|
2195
2513
|
if (!existsSync2(claudeDir)) {
|
|
2196
2514
|
if (verbose) {
|
|
2197
2515
|
stepInfo(`~/.claude not found \u2014 Claude Code not installed, skipping`);
|
|
2198
2516
|
}
|
|
2199
2517
|
return false;
|
|
2200
2518
|
}
|
|
2201
|
-
const zeroHooksDir =
|
|
2519
|
+
const zeroHooksDir = join3(home, ".zero", "hooks");
|
|
2202
2520
|
mkdirSync3(zeroHooksDir, { recursive: true });
|
|
2203
2521
|
if (verbose) stepInfo(`staged hook dir at ${zeroHooksDir}`);
|
|
2204
2522
|
const hookFiles = ["auto-approve-zero.sh", "zero-context.sh"];
|
|
2205
2523
|
const hookDests = {};
|
|
2206
2524
|
const hooksSourceDir = findResourceDir(getCliModuleDir(), "hooks");
|
|
2207
2525
|
for (const hookFile of hookFiles) {
|
|
2208
|
-
const hookSource =
|
|
2209
|
-
const hookDest =
|
|
2526
|
+
const hookSource = join3(hooksSourceDir, hookFile);
|
|
2527
|
+
const hookDest = join3(zeroHooksDir, hookFile);
|
|
2210
2528
|
copyFile(hookSource, hookDest);
|
|
2211
2529
|
chmodSync2(hookDest, 493);
|
|
2212
2530
|
if (!verifyFileCopy(hookSource, hookDest)) {
|
|
@@ -2217,14 +2535,14 @@ var installHook = (home, verbose = false) => {
|
|
|
2217
2535
|
hookDests[hookFile] = hookDest;
|
|
2218
2536
|
if (verbose) stepInfo(`copied ${hookFile} \u2192 ${hookDest} (verified)`);
|
|
2219
2537
|
}
|
|
2220
|
-
const settingsPath =
|
|
2538
|
+
const settingsPath = join3(claudeDir, "settings.json");
|
|
2221
2539
|
let settings = {};
|
|
2222
2540
|
let settingsExisted = false;
|
|
2223
2541
|
let settingsCorrupted = false;
|
|
2224
2542
|
if (existsSync2(settingsPath)) {
|
|
2225
2543
|
settingsExisted = true;
|
|
2226
2544
|
try {
|
|
2227
|
-
settings = JSON.parse(
|
|
2545
|
+
settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
2228
2546
|
} catch {
|
|
2229
2547
|
settingsCorrupted = true;
|
|
2230
2548
|
}
|
|
@@ -2322,7 +2640,7 @@ var CONFLICTING_SKILL_PATTERNS = ["zam"];
|
|
|
2322
2640
|
var findConflictingSkills = (home) => {
|
|
2323
2641
|
const found = [];
|
|
2324
2642
|
for (const tool of AGENT_TOOLS) {
|
|
2325
|
-
const toolSkillsPath =
|
|
2643
|
+
const toolSkillsPath = join3(home, tool.skillsDir);
|
|
2326
2644
|
if (!existsSync2(toolSkillsPath)) continue;
|
|
2327
2645
|
const entries = readdirSync(toolSkillsPath, { withFileTypes: true });
|
|
2328
2646
|
for (const entry of entries) {
|
|
@@ -2331,7 +2649,7 @@ var findConflictingSkills = (home) => {
|
|
|
2331
2649
|
if (CONFLICTING_SKILL_PATTERNS.some((p) => lower.includes(p))) {
|
|
2332
2650
|
found.push({
|
|
2333
2651
|
tool: tool.name,
|
|
2334
|
-
skillPath:
|
|
2652
|
+
skillPath: join3(toolSkillsPath, entry.name),
|
|
2335
2653
|
skillName: entry.name
|
|
2336
2654
|
});
|
|
2337
2655
|
}
|
|
@@ -2359,7 +2677,7 @@ var installSkills = (home, verbose = false) => {
|
|
|
2359
2677
|
const installed = [];
|
|
2360
2678
|
const errors = [];
|
|
2361
2679
|
for (const tool of AGENT_TOOLS) {
|
|
2362
|
-
const toolDetectPath =
|
|
2680
|
+
const toolDetectPath = join3(home, tool.detectDir);
|
|
2363
2681
|
if (!existsSync2(toolDetectPath)) {
|
|
2364
2682
|
if (verbose) {
|
|
2365
2683
|
stepInfo(
|
|
@@ -2374,16 +2692,16 @@ var installSkills = (home, verbose = false) => {
|
|
|
2374
2692
|
);
|
|
2375
2693
|
}
|
|
2376
2694
|
try {
|
|
2377
|
-
const toolSkillsPath =
|
|
2695
|
+
const toolSkillsPath = join3(home, tool.skillsDir);
|
|
2378
2696
|
mkdirSync3(toolSkillsPath, { recursive: true });
|
|
2379
2697
|
for (const skillDir of skillDirs) {
|
|
2380
|
-
const src =
|
|
2381
|
-
const dest =
|
|
2698
|
+
const src = join3(skillsSourceDir, skillDir);
|
|
2699
|
+
const dest = join3(toolSkillsPath, skillDir);
|
|
2382
2700
|
const existed = existsSync2(dest);
|
|
2383
2701
|
copyDirRecursive(src, dest);
|
|
2384
2702
|
for (const srcFile of collectAllFiles(src)) {
|
|
2385
2703
|
const relPath = relative(src, srcFile);
|
|
2386
|
-
const destFile =
|
|
2704
|
+
const destFile = join3(dest, relPath);
|
|
2387
2705
|
if (!verifyFileCopy(srcFile, destFile)) {
|
|
2388
2706
|
throw new Error(
|
|
2389
2707
|
`Integrity check failed: ${destFile} does not match source`
|
|
@@ -2414,15 +2732,15 @@ var runInit = async (appContext, options = {}) => {
|
|
|
2414
2732
|
let currentStep = "wallet";
|
|
2415
2733
|
try {
|
|
2416
2734
|
printZeroBanner();
|
|
2417
|
-
const home =
|
|
2418
|
-
const zeroDir =
|
|
2419
|
-
const configPath =
|
|
2735
|
+
const home = homedir3();
|
|
2736
|
+
const zeroDir = join3(home, ".zero");
|
|
2737
|
+
const configPath = join3(zeroDir, "config.json");
|
|
2420
2738
|
let walletCreated = false;
|
|
2421
2739
|
let walletAddress = null;
|
|
2422
2740
|
const walletExists = (() => {
|
|
2423
2741
|
if (!existsSync2(configPath)) return false;
|
|
2424
2742
|
try {
|
|
2425
|
-
const existing = JSON.parse(
|
|
2743
|
+
const existing = JSON.parse(readFileSync5(configPath, "utf8"));
|
|
2426
2744
|
return !!existing.privateKey;
|
|
2427
2745
|
} catch {
|
|
2428
2746
|
return false;
|
|
@@ -2432,7 +2750,7 @@ var runInit = async (appContext, options = {}) => {
|
|
|
2432
2750
|
const privateKey = generatePrivateKey();
|
|
2433
2751
|
const account = privateKeyToAccount(privateKey);
|
|
2434
2752
|
ensureSecureDir(zeroDir);
|
|
2435
|
-
const existing = existsSync2(configPath) ? JSON.parse(
|
|
2753
|
+
const existing = existsSync2(configPath) ? JSON.parse(readFileSync5(configPath, "utf8")) : {};
|
|
2436
2754
|
writeSecureFile(
|
|
2437
2755
|
configPath,
|
|
2438
2756
|
JSON.stringify(
|
|
@@ -2455,7 +2773,7 @@ var runInit = async (appContext, options = {}) => {
|
|
|
2455
2773
|
}
|
|
2456
2774
|
} else {
|
|
2457
2775
|
try {
|
|
2458
|
-
const existing = JSON.parse(
|
|
2776
|
+
const existing = JSON.parse(readFileSync5(configPath, "utf8"));
|
|
2459
2777
|
const account = privateKeyToAccount(existing.privateKey);
|
|
2460
2778
|
walletAddress = account.address;
|
|
2461
2779
|
} catch {
|
|
@@ -2476,7 +2794,7 @@ var runInit = async (appContext, options = {}) => {
|
|
|
2476
2794
|
let hookInstalled = false;
|
|
2477
2795
|
let hookError = null;
|
|
2478
2796
|
for (const tool of AGENT_TOOLS) {
|
|
2479
|
-
if (existsSync2(
|
|
2797
|
+
if (existsSync2(join3(home, tool.detectDir))) {
|
|
2480
2798
|
agentsDetected.push(tool.name);
|
|
2481
2799
|
}
|
|
2482
2800
|
}
|
|
@@ -2552,12 +2870,13 @@ To remove them, run: ${color.cyan("zero init cleanup")}`
|
|
|
2552
2870
|
sectionDivider();
|
|
2553
2871
|
console.error(printReadyFooter());
|
|
2554
2872
|
currentStep = "complete";
|
|
2873
|
+
if (walletAddress) {
|
|
2874
|
+
appContext.services.analyticsService.setWalletAddress(walletAddress);
|
|
2875
|
+
}
|
|
2555
2876
|
appContext.services.analyticsService.capture("wallet_initialized", {
|
|
2556
2877
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
2557
2878
|
wallet_created: walletCreated,
|
|
2558
2879
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
2559
|
-
wallet_address: walletAddress,
|
|
2560
|
-
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
2561
2880
|
agents_detected: agentsDetected,
|
|
2562
2881
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
2563
2882
|
agents_detected_count: agentsDetected.length,
|
|
@@ -2585,14 +2904,14 @@ To remove them, run: ${color.cyan("zero init cleanup")}`
|
|
|
2585
2904
|
throw err;
|
|
2586
2905
|
}
|
|
2587
2906
|
};
|
|
2588
|
-
var initCommand = (appContext) => new
|
|
2907
|
+
var initCommand = (appContext) => new Command6("init").description("Initialize Zero CLI for usage").option("--force", "Overwrite existing configuration").option(
|
|
2589
2908
|
"-v, --verbose",
|
|
2590
2909
|
"Explain why each install step was taken or skipped"
|
|
2591
2910
|
).action(async (options) => {
|
|
2592
2911
|
await runInit(appContext, options);
|
|
2593
2912
|
}).addCommand(
|
|
2594
|
-
new
|
|
2595
|
-
const home =
|
|
2913
|
+
new Command6("cleanup").description("Remove deprecated skills (zam) that conflict with Zero").action(() => {
|
|
2914
|
+
const home = homedir3();
|
|
2596
2915
|
const removed = removeConflictingSkills(home);
|
|
2597
2916
|
if (removed.length === 0) {
|
|
2598
2917
|
console.error("No conflicting skills found. Nothing to remove.");
|
|
@@ -2611,8 +2930,8 @@ ${removedList}`);
|
|
|
2611
2930
|
);
|
|
2612
2931
|
|
|
2613
2932
|
// src/commands/review-command.ts
|
|
2614
|
-
import { readFileSync as
|
|
2615
|
-
import { Command as
|
|
2933
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
2934
|
+
import { Command as Command7 } from "commander";
|
|
2616
2935
|
import { z as z3 } from "zod";
|
|
2617
2936
|
var bulkEntrySchema2 = z3.object({
|
|
2618
2937
|
runId: z3.string(),
|
|
@@ -2622,7 +2941,7 @@ var bulkEntrySchema2 = z3.object({
|
|
|
2622
2941
|
reliability: z3.number().int().min(1).max(5),
|
|
2623
2942
|
content: z3.string().optional()
|
|
2624
2943
|
});
|
|
2625
|
-
var reviewCommand = (appContext) => new
|
|
2944
|
+
var reviewCommand = (appContext) => new Command7("review").description("Submit a review for a capability run").addHelpText(
|
|
2626
2945
|
"after",
|
|
2627
2946
|
`
|
|
2628
2947
|
Tips for a great review:
|
|
@@ -2653,7 +2972,7 @@ Examples:
|
|
|
2653
2972
|
try {
|
|
2654
2973
|
const { analyticsService, apiService } = appContext.services;
|
|
2655
2974
|
if (options.fromFile) {
|
|
2656
|
-
const contents =
|
|
2975
|
+
const contents = readFileSync6(options.fromFile, "utf8");
|
|
2657
2976
|
const lines = contents.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
2658
2977
|
const parsed = [];
|
|
2659
2978
|
let parseFailures = 0;
|
|
@@ -2801,7 +3120,7 @@ Bulk review complete: ${ok} ok, ${failed} failed`);
|
|
|
2801
3120
|
);
|
|
2802
3121
|
|
|
2803
3122
|
// src/commands/runs-command.ts
|
|
2804
|
-
import { Command as
|
|
3123
|
+
import { Command as Command8 } from "commander";
|
|
2805
3124
|
var USD_ASSETS = /* @__PURE__ */ new Set(["USD", "USDC"]);
|
|
2806
3125
|
var formatCost2 = (cost) => {
|
|
2807
3126
|
if (!cost) return "free";
|
|
@@ -2816,7 +3135,7 @@ var formatPayment = (payment) => {
|
|
|
2816
3135
|
const mode = payment.mode ? ` (${payment.mode})` : "";
|
|
2817
3136
|
return `${payment.protocol}${chain}${mode}`;
|
|
2818
3137
|
};
|
|
2819
|
-
var runsCommand = (appContext) => new
|
|
3138
|
+
var runsCommand = (appContext) => new Command8("runs").description("List your recent capability runs").addHelpText(
|
|
2820
3139
|
"after",
|
|
2821
3140
|
`
|
|
2822
3141
|
View your recent capability runs \u2014 status, latency, cost, and payment info.
|
|
@@ -2874,7 +3193,7 @@ Examples:
|
|
|
2874
3193
|
);
|
|
2875
3194
|
|
|
2876
3195
|
// src/commands/search-command.ts
|
|
2877
|
-
import { Command as
|
|
3196
|
+
import { Command as Command9 } from "commander";
|
|
2878
3197
|
var DEFAULT_MAX_COST_USD = "30";
|
|
2879
3198
|
var formatReviewCount2 = (count) => {
|
|
2880
3199
|
if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
|
|
@@ -2901,16 +3220,17 @@ var formatSearchResults = (results) => {
|
|
|
2901
3220
|
const displayDescription = r.whatItDoes ?? r.description;
|
|
2902
3221
|
const ratingBadge = formatRatingBadge(r.rating);
|
|
2903
3222
|
const statusBadge = formatStatusBadge(r.displayStatus);
|
|
2904
|
-
|
|
3223
|
+
const costLabel = r.cost.amount === "0" ? "Free" : r.cost.amount === "unknown" ? "variable pricing" : `$${r.cost.amount}/call`;
|
|
3224
|
+
return ` ${r.position}. ${displayName} \u2014 ${costLabel} \u2014 ${ratingBadge}${statusBadge}
|
|
2905
3225
|
"${displayDescription}"`;
|
|
2906
3226
|
}).join("\n");
|
|
2907
3227
|
};
|
|
2908
|
-
var searchCommand = (appContext) => new
|
|
3228
|
+
var searchCommand = (appContext) => new Command9("search").description("Search for capabilities").argument("<query>", "Search query").option("--json", "Output raw JSON to stdout").option("--offset <n>", "Pagination offset", Number).option("--limit <n>", "Results per page", Number).option("--free", "Only show free capabilities").option(
|
|
2909
3229
|
"--max-cost <amount>",
|
|
2910
3230
|
`Maximum cost per call in USD (default: ${DEFAULT_MAX_COST_USD})`
|
|
2911
3231
|
).option("--protocol <protocol>", "Payment protocol (x402 or mpp)").option(
|
|
2912
3232
|
"--status <status>",
|
|
2913
|
-
"Filter by availability (healthy,
|
|
3233
|
+
"Filter by availability (healthy, unknown, down). Defaults to healthy when neither --status nor --all is set; pass --all to see everything."
|
|
2914
3234
|
).option("--all", "Disable default quality filtering").option(
|
|
2915
3235
|
"--source <source>",
|
|
2916
3236
|
"Only show results from this crawl source (e.g. mpp, bazaar)"
|
|
@@ -2937,14 +3257,20 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
|
|
|
2937
3257
|
effectiveMaxCost = DEFAULT_MAX_COST_USD;
|
|
2938
3258
|
appliedDefaultMaxCost = true;
|
|
2939
3259
|
}
|
|
2940
|
-
const validStatuses = ["healthy", "degraded", "down"];
|
|
3260
|
+
const validStatuses = ["healthy", "unknown", "degraded", "down"];
|
|
2941
3261
|
if (options.status && !validStatuses.includes(options.status)) {
|
|
2942
3262
|
console.error(
|
|
2943
|
-
`Invalid status "${options.status}". Must be one of:
|
|
3263
|
+
`Invalid status "${options.status}". Must be one of: ${validStatuses.join(", ")}.`
|
|
2944
3264
|
);
|
|
2945
3265
|
process.exitCode = 1;
|
|
2946
3266
|
return;
|
|
2947
3267
|
}
|
|
3268
|
+
let effectiveStatus = options.status;
|
|
3269
|
+
let appliedDefaultStatus = false;
|
|
3270
|
+
if (effectiveStatus === void 0 && !options.all) {
|
|
3271
|
+
effectiveStatus = "healthy";
|
|
3272
|
+
appliedDefaultStatus = true;
|
|
3273
|
+
}
|
|
2948
3274
|
const result = await apiService.search({
|
|
2949
3275
|
query,
|
|
2950
3276
|
offset: options.offset,
|
|
@@ -2952,7 +3278,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
|
|
|
2952
3278
|
freeOnly: options.free,
|
|
2953
3279
|
maxCost: effectiveMaxCost,
|
|
2954
3280
|
protocol: options.protocol,
|
|
2955
|
-
availabilityStatus:
|
|
3281
|
+
availabilityStatus: effectiveStatus,
|
|
2956
3282
|
includeAll: options.all,
|
|
2957
3283
|
source: options.source,
|
|
2958
3284
|
excludeSource: options.excludeSource
|
|
@@ -2970,10 +3296,11 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
|
|
|
2970
3296
|
maxCost: effectiveMaxCost,
|
|
2971
3297
|
maxCostDefaulted: appliedDefaultMaxCost,
|
|
2972
3298
|
protocol: options.protocol,
|
|
2973
|
-
availabilityStatus:
|
|
3299
|
+
availabilityStatus: effectiveStatus,
|
|
3300
|
+
availabilityStatusDefaulted: appliedDefaultStatus,
|
|
2974
3301
|
includeAll: options.all ?? false,
|
|
2975
|
-
|
|
2976
|
-
|
|
3302
|
+
listingSource: options.source,
|
|
3303
|
+
excludeListingSource: options.excludeSource,
|
|
2977
3304
|
json: options.json ?? false
|
|
2978
3305
|
});
|
|
2979
3306
|
if (options.json) {
|
|
@@ -2995,6 +3322,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
|
|
|
2995
3322
|
capabilities: result.capabilities.map((c) => ({
|
|
2996
3323
|
position: c.position,
|
|
2997
3324
|
id: c.id,
|
|
3325
|
+
slug: c.slug,
|
|
2998
3326
|
url: c.url,
|
|
2999
3327
|
urlTemplate: c.urlTemplate ?? null,
|
|
3000
3328
|
displayCostAmount: c.cost.amount
|
|
@@ -3020,9 +3348,9 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
|
|
|
3020
3348
|
);
|
|
3021
3349
|
|
|
3022
3350
|
// src/commands/terms-command.ts
|
|
3023
|
-
import { Command as
|
|
3351
|
+
import { Command as Command10 } from "commander";
|
|
3024
3352
|
var TERMS_URL = "https://zero.xyz/terms-of-service";
|
|
3025
|
-
var termsCommand = (_appContext) => new
|
|
3353
|
+
var termsCommand = (_appContext) => new Command10("terms").description("View the ZeroClick Terms of Service").action(async () => {
|
|
3026
3354
|
console.log(
|
|
3027
3355
|
`ZeroClick Agentic Capability Search \u2014 Terms of Service
|
|
3028
3356
|
|
|
@@ -3040,14 +3368,48 @@ Read the full terms at: ${TERMS_URL}
|
|
|
3040
3368
|
});
|
|
3041
3369
|
|
|
3042
3370
|
// src/commands/wallet-command.ts
|
|
3043
|
-
import { existsSync as existsSync3, readFileSync as
|
|
3044
|
-
import { homedir as
|
|
3045
|
-
import { join as
|
|
3046
|
-
import { Command as
|
|
3047
|
-
import
|
|
3048
|
-
import {
|
|
3049
|
-
|
|
3050
|
-
|
|
3371
|
+
import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
|
|
3372
|
+
import { homedir as homedir4 } from "os";
|
|
3373
|
+
import { join as join4 } from "path";
|
|
3374
|
+
import { Command as Command11 } from "commander";
|
|
3375
|
+
import open2 from "open";
|
|
3376
|
+
import { isAddress } from "viem";
|
|
3377
|
+
import { generatePrivateKey as generatePrivateKey2, privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
3378
|
+
|
|
3379
|
+
// src/util/stdin.ts
|
|
3380
|
+
var readStdin = async () => {
|
|
3381
|
+
const chunks = [];
|
|
3382
|
+
for await (const chunk of process.stdin) {
|
|
3383
|
+
chunks.push(chunk);
|
|
3384
|
+
}
|
|
3385
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
3386
|
+
};
|
|
3387
|
+
|
|
3388
|
+
// src/commands/wallet-command.ts
|
|
3389
|
+
var PRIVATE_KEY_PATTERN = /^0x[0-9a-fA-F]{64}$/;
|
|
3390
|
+
var parseProvider = (raw) => raw === "stripe" ? "stripe" : "coinbase";
|
|
3391
|
+
var walletBalanceCommand = (appContext) => new Command11("balance").description("Show wallet balance").option(
|
|
3392
|
+
"--address <address>",
|
|
3393
|
+
"Check balance for an arbitrary address (no key needed). Useful with `zero wallet generate` \u2014 verify funds arrived without setting the wallet as your default."
|
|
3394
|
+
).action(async (options) => {
|
|
3395
|
+
const { walletService, paymentService } = appContext.services;
|
|
3396
|
+
if (options.address) {
|
|
3397
|
+
if (!isAddress(options.address)) {
|
|
3398
|
+
console.error("Invalid address (expected 0x + 40 hex chars).");
|
|
3399
|
+
process.exitCode = 1;
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
3402
|
+
try {
|
|
3403
|
+
const balance2 = await paymentService.getTotalBalanceForAddress(
|
|
3404
|
+
options.address
|
|
3405
|
+
);
|
|
3406
|
+
console.log(`${balance2.amount} ${balance2.asset}`);
|
|
3407
|
+
} catch {
|
|
3408
|
+
console.error("Failed to fetch balance");
|
|
3409
|
+
process.exitCode = 1;
|
|
3410
|
+
}
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3051
3413
|
const balance = await walletService.getBalance();
|
|
3052
3414
|
if (balance === null) {
|
|
3053
3415
|
console.error("No wallet configured. Run `zero init` first.");
|
|
@@ -3061,16 +3423,76 @@ var walletBalanceCommand = (appContext) => new Command10("balance").description(
|
|
|
3061
3423
|
}
|
|
3062
3424
|
console.log(`${balance.amount} ${balance.asset}`);
|
|
3063
3425
|
});
|
|
3064
|
-
var
|
|
3426
|
+
var readPrivateKeyFromStdin = async () => {
|
|
3427
|
+
if (process.stdin.isTTY) {
|
|
3428
|
+
console.error(
|
|
3429
|
+
"Expected a private key on stdin. Example:\n zero wallet generate --json | jq -r .privateKey | zero wallet fund --key-stdin"
|
|
3430
|
+
);
|
|
3431
|
+
return null;
|
|
3432
|
+
}
|
|
3433
|
+
const raw = (await readStdin()).trim();
|
|
3434
|
+
if (!PRIVATE_KEY_PATTERN.test(raw)) {
|
|
3435
|
+
console.error("Invalid private key on stdin (expected 0x + 64 hex chars).");
|
|
3436
|
+
return null;
|
|
3437
|
+
}
|
|
3438
|
+
return raw;
|
|
3439
|
+
};
|
|
3440
|
+
var walletFundCommand = (appContext) => new Command11("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").option(
|
|
3065
3441
|
"--no-open",
|
|
3066
3442
|
"Print the funding URL instead of opening a browser (for agents \u2014 funding links are one-time use, hand the URL to the user)"
|
|
3067
3443
|
).option(
|
|
3068
3444
|
"--use <provider>",
|
|
3069
3445
|
"Onramp provider: coinbase or stripe",
|
|
3070
3446
|
"coinbase"
|
|
3447
|
+
).option(
|
|
3448
|
+
"--key-stdin",
|
|
3449
|
+
"Read a private key from stdin and mint a funding URL for that wallet instead of your configured one. Useful with `zero wallet generate`. Does not modify your config."
|
|
3071
3450
|
).action(
|
|
3072
3451
|
async (amount, options) => {
|
|
3073
|
-
const { analyticsService, walletService } = appContext.services;
|
|
3452
|
+
const { analyticsService, apiService, walletService } = appContext.services;
|
|
3453
|
+
const provider = parseProvider(options.use);
|
|
3454
|
+
if (options.keyStdin) {
|
|
3455
|
+
const key = await readPrivateKeyFromStdin();
|
|
3456
|
+
if (!key) {
|
|
3457
|
+
process.exitCode = 1;
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3460
|
+
const altAccount = privateKeyToAccount2(key);
|
|
3461
|
+
const altApi = apiService.withAccount(altAccount);
|
|
3462
|
+
if (options.manual) {
|
|
3463
|
+
console.log(`Send USDC (Base) to:
|
|
3464
|
+
${altAccount.address}`);
|
|
3465
|
+
analyticsService.capture("wallet_funded", {
|
|
3466
|
+
method: "manual",
|
|
3467
|
+
amount,
|
|
3468
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3469
|
+
alt_wallet: true
|
|
3470
|
+
});
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
const url2 = await altApi.getFundingUrl(amount, provider);
|
|
3474
|
+
if (!url2) {
|
|
3475
|
+
console.log(`Send USDC (Base) to:
|
|
3476
|
+
${altAccount.address}`);
|
|
3477
|
+
console.error(
|
|
3478
|
+
"Could not get funding URL. Send USDC (Base) manually."
|
|
3479
|
+
);
|
|
3480
|
+
process.exitCode = 1;
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
console.log(
|
|
3484
|
+
"Funding URL (one-time use \u2014 open it in a browser to fund):"
|
|
3485
|
+
);
|
|
3486
|
+
console.log(url2);
|
|
3487
|
+
console.log(`Wallet address: ${altAccount.address}`);
|
|
3488
|
+
analyticsService.capture("wallet_funded", {
|
|
3489
|
+
method: "url",
|
|
3490
|
+
amount,
|
|
3491
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3492
|
+
alt_wallet: true
|
|
3493
|
+
});
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3074
3496
|
const address = walletService.getAddress();
|
|
3075
3497
|
if (!address) {
|
|
3076
3498
|
console.error("No wallet configured. Run `zero init` first.");
|
|
@@ -3086,14 +3508,10 @@ ${address}`);
|
|
|
3086
3508
|
});
|
|
3087
3509
|
return;
|
|
3088
3510
|
}
|
|
3089
|
-
const
|
|
3090
|
-
const url = await appContext.services.apiService.getFundingUrl(
|
|
3091
|
-
amount,
|
|
3092
|
-
provider
|
|
3093
|
-
);
|
|
3511
|
+
const url = await apiService.getFundingUrl(amount, provider);
|
|
3094
3512
|
if (url) {
|
|
3095
3513
|
if (options.open) {
|
|
3096
|
-
await
|
|
3514
|
+
await open2(url);
|
|
3097
3515
|
console.log("Opened funding page in your browser.");
|
|
3098
3516
|
console.log(`If it didn't open, visit: ${url}`);
|
|
3099
3517
|
} else {
|
|
@@ -3116,7 +3534,7 @@ ${address}`);
|
|
|
3116
3534
|
}
|
|
3117
3535
|
}
|
|
3118
3536
|
);
|
|
3119
|
-
var walletAddressCommand = (appContext) => new
|
|
3537
|
+
var walletAddressCommand = (appContext) => new Command11("address").description("Show wallet address").action(() => {
|
|
3120
3538
|
const { walletService } = appContext.services;
|
|
3121
3539
|
const address = walletService.getAddress();
|
|
3122
3540
|
if (!address) {
|
|
@@ -3126,7 +3544,7 @@ var walletAddressCommand = (appContext) => new Command10("address").description(
|
|
|
3126
3544
|
}
|
|
3127
3545
|
console.log(address);
|
|
3128
3546
|
});
|
|
3129
|
-
var walletSetCommand = (appContext) => new
|
|
3547
|
+
var walletSetCommand = (appContext) => new Command11("set").description("Set wallet from an existing private key").argument("<privateKey>", "Hex-encoded private key (0x-prefixed)").option("--force", "Overwrite existing wallet without prompting").action(async (privateKey, options) => {
|
|
3130
3548
|
const { analyticsService } = appContext.services;
|
|
3131
3549
|
if (!privateKey.startsWith("0x")) {
|
|
3132
3550
|
console.error("Private key must be 0x-prefixed hex string.");
|
|
@@ -3141,11 +3559,11 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
|
|
|
3141
3559
|
process.exitCode = 1;
|
|
3142
3560
|
return;
|
|
3143
3561
|
}
|
|
3144
|
-
const zeroDir =
|
|
3145
|
-
const configPath =
|
|
3562
|
+
const zeroDir = join4(homedir4(), ".zero");
|
|
3563
|
+
const configPath = join4(zeroDir, "config.json");
|
|
3146
3564
|
if (!options.force && existsSync3(configPath)) {
|
|
3147
3565
|
try {
|
|
3148
|
-
const existing2 = JSON.parse(
|
|
3566
|
+
const existing2 = JSON.parse(readFileSync7(configPath, "utf8"));
|
|
3149
3567
|
if (existing2.privateKey) {
|
|
3150
3568
|
console.error(
|
|
3151
3569
|
"Wallet already configured. Use --force to overwrite."
|
|
@@ -3157,7 +3575,7 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
|
|
|
3157
3575
|
}
|
|
3158
3576
|
}
|
|
3159
3577
|
ensureSecureDir(zeroDir);
|
|
3160
|
-
const existing = existsSync3(configPath) ? JSON.parse(
|
|
3578
|
+
const existing = existsSync3(configPath) ? JSON.parse(readFileSync7(configPath, "utf8")) : {};
|
|
3161
3579
|
writeSecureFile(
|
|
3162
3580
|
configPath,
|
|
3163
3581
|
JSON.stringify(
|
|
@@ -3171,34 +3589,111 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
|
|
|
3171
3589
|
)
|
|
3172
3590
|
);
|
|
3173
3591
|
console.log(`Wallet set: ${account.address}`);
|
|
3592
|
+
analyticsService.setWalletAddress(account.address);
|
|
3174
3593
|
analyticsService.capture("wallet_set", {
|
|
3175
|
-
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3176
|
-
wallet_address: account.address,
|
|
3177
3594
|
force: options.force ?? false
|
|
3178
3595
|
});
|
|
3179
3596
|
});
|
|
3597
|
+
var walletGenerateCommand = (appContext) => new Command11("generate").description(
|
|
3598
|
+
"Generate a fresh wallet (address + private key) without touching your configured wallet"
|
|
3599
|
+
).option("--json", "Emit { address, privateKey } as JSON").option(
|
|
3600
|
+
"--fund",
|
|
3601
|
+
"Also mint a one-time onramp URL for the new wallet (does not auto-open)"
|
|
3602
|
+
).option("--amount <amount>", "With --fund: USDC amount to pre-fill").option(
|
|
3603
|
+
"--use <provider>",
|
|
3604
|
+
"With --fund: onramp provider (coinbase | stripe)",
|
|
3605
|
+
"coinbase"
|
|
3606
|
+
).action(
|
|
3607
|
+
async (options) => {
|
|
3608
|
+
const { analyticsService, apiService } = appContext.services;
|
|
3609
|
+
const privateKey = generatePrivateKey2();
|
|
3610
|
+
const account = privateKeyToAccount2(privateKey);
|
|
3611
|
+
let fundingUrl = null;
|
|
3612
|
+
if (options.fund) {
|
|
3613
|
+
const provider = parseProvider(options.use);
|
|
3614
|
+
fundingUrl = await apiService.withAccount(account).getFundingUrl(options.amount, provider);
|
|
3615
|
+
}
|
|
3616
|
+
if (options.json) {
|
|
3617
|
+
const payload = {
|
|
3618
|
+
address: account.address,
|
|
3619
|
+
privateKey
|
|
3620
|
+
};
|
|
3621
|
+
if (options.fund) {
|
|
3622
|
+
payload.fundingUrl = fundingUrl;
|
|
3623
|
+
}
|
|
3624
|
+
console.log(JSON.stringify(payload));
|
|
3625
|
+
} else {
|
|
3626
|
+
console.log("");
|
|
3627
|
+
console.log(
|
|
3628
|
+
"New wallet generated. Save these \u2014 they are NOT stored anywhere."
|
|
3629
|
+
);
|
|
3630
|
+
console.log("");
|
|
3631
|
+
console.log(
|
|
3632
|
+
" WARNING: Anyone with this private key can spend any funds sent"
|
|
3633
|
+
);
|
|
3634
|
+
console.log(
|
|
3635
|
+
" to the address. Don't paste it into chat, commit it to git, or"
|
|
3636
|
+
);
|
|
3637
|
+
console.log(" share your screen with it visible.");
|
|
3638
|
+
console.log("");
|
|
3639
|
+
console.log(` Address: ${account.address}`);
|
|
3640
|
+
console.log(` Private key: ${privateKey}`);
|
|
3641
|
+
console.log("");
|
|
3642
|
+
if (options.fund) {
|
|
3643
|
+
if (fundingUrl) {
|
|
3644
|
+
console.log("Funding URL (one-time use):");
|
|
3645
|
+
console.log(` ${fundingUrl}`);
|
|
3646
|
+
console.log("");
|
|
3647
|
+
} else {
|
|
3648
|
+
console.log(
|
|
3649
|
+
"Could not mint a funding URL \u2014 fund the address manually with USDC (Base)."
|
|
3650
|
+
);
|
|
3651
|
+
console.log("");
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
console.log("Next steps:");
|
|
3655
|
+
if (!options.fund) {
|
|
3656
|
+
console.log(" - Fund the address with USDC (Base) before use.");
|
|
3657
|
+
}
|
|
3658
|
+
console.log(
|
|
3659
|
+
` - Use it for a single command: ZERO_PRIVATE_KEY=${privateKey} zero <command>`
|
|
3660
|
+
);
|
|
3661
|
+
console.log(
|
|
3662
|
+
" - Or make it your default wallet: zero wallet set <privateKey> --force"
|
|
3663
|
+
);
|
|
3664
|
+
console.log("");
|
|
3665
|
+
}
|
|
3666
|
+
analyticsService.capture("wallet_generated", {
|
|
3667
|
+
format: options.json ? "json" : "text",
|
|
3668
|
+
funded: options.fund ?? false,
|
|
3669
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3670
|
+
funding_url_minted: options.fund ? fundingUrl !== null : false
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
);
|
|
3180
3674
|
var walletCommand = (appContext) => {
|
|
3181
|
-
const cmd = new
|
|
3675
|
+
const cmd = new Command11("wallet").description("Manage your wallet");
|
|
3182
3676
|
cmd.addCommand(walletBalanceCommand(appContext));
|
|
3183
3677
|
cmd.addCommand(walletFundCommand(appContext));
|
|
3184
3678
|
cmd.addCommand(walletAddressCommand(appContext));
|
|
3185
3679
|
cmd.addCommand(walletSetCommand(appContext));
|
|
3680
|
+
cmd.addCommand(walletGenerateCommand(appContext));
|
|
3186
3681
|
return cmd;
|
|
3187
3682
|
};
|
|
3188
3683
|
|
|
3189
3684
|
// src/commands/welcome-command.ts
|
|
3190
|
-
import { existsSync as existsSync4, readFileSync as
|
|
3191
|
-
import { homedir as
|
|
3192
|
-
import { join as
|
|
3193
|
-
import { Command as
|
|
3194
|
-
import
|
|
3685
|
+
import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
|
|
3686
|
+
import { homedir as homedir5 } from "os";
|
|
3687
|
+
import { join as join5 } from "path";
|
|
3688
|
+
import { Command as Command12 } from "commander";
|
|
3689
|
+
import open3 from "open";
|
|
3195
3690
|
import { getAddress } from "viem";
|
|
3196
3691
|
import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
3197
3692
|
var readPrivateKey = () => {
|
|
3198
|
-
const configPath =
|
|
3693
|
+
const configPath = join5(homedir5(), ".zero", "config.json");
|
|
3199
3694
|
if (!existsSync4(configPath)) return null;
|
|
3200
3695
|
try {
|
|
3201
|
-
const config = JSON.parse(
|
|
3696
|
+
const config = JSON.parse(readFileSync8(configPath, "utf8"));
|
|
3202
3697
|
if (typeof config.privateKey === "string") {
|
|
3203
3698
|
return config.privateKey;
|
|
3204
3699
|
}
|
|
@@ -3207,9 +3702,37 @@ var readPrivateKey = () => {
|
|
|
3207
3702
|
}
|
|
3208
3703
|
return null;
|
|
3209
3704
|
};
|
|
3210
|
-
var
|
|
3705
|
+
var linuxInstallHint = () => {
|
|
3706
|
+
return [
|
|
3707
|
+
" On Debian/Ubuntu: sudo apt install xdg-utils",
|
|
3708
|
+
" On RHEL/Fedora: sudo dnf install xdg-utils"
|
|
3709
|
+
].join("\n");
|
|
3710
|
+
};
|
|
3711
|
+
var printManualFallback = (url) => {
|
|
3712
|
+
const lines = [
|
|
3713
|
+
"",
|
|
3714
|
+
"\u26A0\uFE0F Couldn't auto-open your browser (this is common on WSL, SSH, or",
|
|
3715
|
+
" containers \u2014 no xdg-open installed).",
|
|
3716
|
+
"",
|
|
3717
|
+
"Copy and paste this URL into your browser to claim:",
|
|
3718
|
+
"",
|
|
3719
|
+
` ${url}`,
|
|
3720
|
+
""
|
|
3721
|
+
];
|
|
3722
|
+
if (process.platform === "linux") {
|
|
3723
|
+
lines.push(
|
|
3724
|
+
"Tip: install xdg-utils so 'zero welcome' can open the browser for you:",
|
|
3725
|
+
linuxInstallHint(),
|
|
3726
|
+
""
|
|
3727
|
+
);
|
|
3728
|
+
}
|
|
3729
|
+
console.log(lines.join("\n"));
|
|
3730
|
+
};
|
|
3731
|
+
var welcomeCommand = (appContext) => new Command12("welcome").description("Claim your $5 welcome bonus.").action(async () => {
|
|
3211
3732
|
const { analyticsService } = appContext.services;
|
|
3212
3733
|
analyticsService.capture("welcome_started", {});
|
|
3734
|
+
let walletAddress;
|
|
3735
|
+
let url;
|
|
3213
3736
|
try {
|
|
3214
3737
|
let privateKey = readPrivateKey();
|
|
3215
3738
|
if (!privateKey) {
|
|
@@ -3220,37 +3743,52 @@ var welcomeCommand = (appContext) => new Command11("welcome").description("Claim
|
|
|
3220
3743
|
}
|
|
3221
3744
|
}
|
|
3222
3745
|
const account = privateKeyToAccount3(privateKey);
|
|
3223
|
-
|
|
3746
|
+
walletAddress = getAddress(account.address);
|
|
3224
3747
|
const walletSignature = await account.signMessage({
|
|
3225
3748
|
message: walletAddress
|
|
3226
3749
|
});
|
|
3227
|
-
|
|
3750
|
+
url = new URL("/welcome", appContext.env.ZERO_WEB_URL);
|
|
3228
3751
|
url.searchParams.set("wallet", walletAddress);
|
|
3229
3752
|
url.searchParams.set("walletSignature", walletSignature);
|
|
3753
|
+
} catch (err) {
|
|
3754
|
+
analyticsService.capture("welcome_failed", {
|
|
3755
|
+
error: truncateError(
|
|
3756
|
+
err instanceof Error ? err.message : String(err)
|
|
3757
|
+
)
|
|
3758
|
+
});
|
|
3759
|
+
throw err;
|
|
3760
|
+
}
|
|
3761
|
+
const urlString = url.toString();
|
|
3762
|
+
analyticsService.setWalletAddress(walletAddress);
|
|
3763
|
+
try {
|
|
3764
|
+
await open3(urlString);
|
|
3230
3765
|
console.log(
|
|
3231
|
-
`Opening ${
|
|
3766
|
+
`Opening ${urlString}
|
|
3232
3767
|
|
|
3233
3768
|
If your browser didn't open, paste the URL above.`
|
|
3234
3769
|
);
|
|
3235
|
-
await open2(url.toString());
|
|
3236
3770
|
analyticsService.capture("welcome_link_opened", {
|
|
3237
3771
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3238
|
-
|
|
3772
|
+
open_method: "auto"
|
|
3239
3773
|
});
|
|
3240
3774
|
} catch (err) {
|
|
3241
|
-
|
|
3242
|
-
|
|
3775
|
+
printManualFallback(urlString);
|
|
3776
|
+
analyticsService.capture("welcome_link_opened", {
|
|
3777
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3778
|
+
open_method: "manual_copy",
|
|
3779
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3780
|
+
open_error: truncateError(
|
|
3243
3781
|
err instanceof Error ? err.message : String(err)
|
|
3244
|
-
)
|
|
3782
|
+
),
|
|
3783
|
+
platform: process.platform
|
|
3245
3784
|
});
|
|
3246
|
-
throw err;
|
|
3247
3785
|
}
|
|
3248
3786
|
});
|
|
3249
3787
|
|
|
3250
3788
|
// src/app.ts
|
|
3251
3789
|
var createApp = (appContext) => {
|
|
3252
3790
|
const { analyticsService } = appContext.services;
|
|
3253
|
-
const program = new
|
|
3791
|
+
const program = new Command13().name("zero").description("Zero CLI \u2014 Search engine for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", async (_thisCommand, actionCommand) => {
|
|
3254
3792
|
const agentFlag = actionCommand.opts().agent;
|
|
3255
3793
|
if (typeof agentFlag === "string" && agentFlag.trim().length > 0) {
|
|
3256
3794
|
analyticsService.setAgentHost(agentFlag.trim());
|
|
@@ -3274,6 +3812,7 @@ var createApp = (appContext) => {
|
|
|
3274
3812
|
program.addCommand(configCommand(appContext));
|
|
3275
3813
|
program.addCommand(termsCommand(appContext));
|
|
3276
3814
|
program.addCommand(welcomeCommand(appContext));
|
|
3815
|
+
program.addCommand(authCommand(appContext), { hidden: true });
|
|
3277
3816
|
return program;
|
|
3278
3817
|
};
|
|
3279
3818
|
|
|
@@ -3297,14 +3836,14 @@ var getEnv = () => {
|
|
|
3297
3836
|
|
|
3298
3837
|
// src/app/app-services.ts
|
|
3299
3838
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3300
|
-
import { existsSync as existsSync7
|
|
3301
|
-
import { homedir as
|
|
3302
|
-
import { join as
|
|
3839
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3840
|
+
import { homedir as homedir6 } from "os";
|
|
3841
|
+
import { join as join7 } from "path";
|
|
3303
3842
|
import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
|
|
3304
3843
|
|
|
3305
3844
|
// src/services/analytics-service.ts
|
|
3306
3845
|
import { randomUUID } from "crypto";
|
|
3307
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as
|
|
3846
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
3308
3847
|
import { dirname as dirname2 } from "path";
|
|
3309
3848
|
import { PostHog } from "posthog-node";
|
|
3310
3849
|
var POSTHOG_API_KEY = "phc_B2vLyNxAf2mnqvdPQajf4d4b2iXc35dep2ZrvebMJLuX";
|
|
@@ -3327,7 +3866,7 @@ var AnalyticsService = class {
|
|
|
3327
3866
|
let persistedAnonId;
|
|
3328
3867
|
try {
|
|
3329
3868
|
if (existsSync5(opts.configPath)) {
|
|
3330
|
-
const config = JSON.parse(
|
|
3869
|
+
const config = JSON.parse(readFileSync9(opts.configPath, "utf8"));
|
|
3331
3870
|
if (config.telemetry === false) {
|
|
3332
3871
|
telemetryEnabled = false;
|
|
3333
3872
|
}
|
|
@@ -3352,7 +3891,7 @@ var AnalyticsService = class {
|
|
|
3352
3891
|
try {
|
|
3353
3892
|
const dir = dirname2(opts.configPath);
|
|
3354
3893
|
mkdirSync4(dir, { recursive: true });
|
|
3355
|
-
const existing = existsSync5(opts.configPath) ? JSON.parse(
|
|
3894
|
+
const existing = existsSync5(opts.configPath) ? JSON.parse(readFileSync9(opts.configPath, "utf8")) : {};
|
|
3356
3895
|
writeFileSync4(
|
|
3357
3896
|
opts.configPath,
|
|
3358
3897
|
JSON.stringify({ ...existing, anonId: newAnonId }, null, 2)
|
|
@@ -3392,7 +3931,7 @@ var AnalyticsService = class {
|
|
|
3392
3931
|
if (anonId === walletAddress) return;
|
|
3393
3932
|
let aliasedTo;
|
|
3394
3933
|
try {
|
|
3395
|
-
const config = JSON.parse(
|
|
3934
|
+
const config = JSON.parse(readFileSync9(configPath, "utf8"));
|
|
3396
3935
|
if (typeof config.aliasedTo === "string") {
|
|
3397
3936
|
aliasedTo = config.aliasedTo;
|
|
3398
3937
|
}
|
|
@@ -3401,7 +3940,7 @@ var AnalyticsService = class {
|
|
|
3401
3940
|
if (aliasedTo === walletAddress) return;
|
|
3402
3941
|
this.posthog.alias({ distinctId: walletAddress, alias: anonId });
|
|
3403
3942
|
try {
|
|
3404
|
-
const config = existsSync5(configPath) ? JSON.parse(
|
|
3943
|
+
const config = existsSync5(configPath) ? JSON.parse(readFileSync9(configPath, "utf8")) : {};
|
|
3405
3944
|
writeFileSync4(
|
|
3406
3945
|
configPath,
|
|
3407
3946
|
JSON.stringify({ ...config, aliasedTo: walletAddress }, null, 2)
|
|
@@ -3414,12 +3953,30 @@ var AnalyticsService = class {
|
|
|
3414
3953
|
setAgentHost(next) {
|
|
3415
3954
|
this.agentHost = next;
|
|
3416
3955
|
}
|
|
3956
|
+
// Per-invocation override for the `wallet_address` super-prop. Used by
|
|
3957
|
+
// commands that mint or load a wallet mid-process (init, wallet set,
|
|
3958
|
+
// welcome) so subsequent captures carry the now-known address without
|
|
3959
|
+
// needing to pass it as a per-event prop (which would collide with the
|
|
3960
|
+
// super-prop — see ZERO-43). distinctId is intentionally NOT changed:
|
|
3961
|
+
// the anon→wallet identity merge happens on the next CLI invocation
|
|
3962
|
+
// via PostHog alias, when AnalyticsService is constructed with both
|
|
3963
|
+
// the persisted anonId and the new walletAddress.
|
|
3964
|
+
setWalletAddress(walletAddress) {
|
|
3965
|
+
this.walletAddress = walletAddress;
|
|
3966
|
+
}
|
|
3417
3967
|
capture(event, properties) {
|
|
3418
3968
|
if (!this.posthog) return;
|
|
3419
3969
|
this.posthog.capture({
|
|
3420
3970
|
distinctId: this.distinctId,
|
|
3421
3971
|
event,
|
|
3972
|
+
// Super-props are spread LAST so they always win against a same-
|
|
3973
|
+
// named per-event prop (see ZERO-43: a `source: undefined` on
|
|
3974
|
+
// `search_executed` was silently clobbering the `cli` surface stamp).
|
|
3975
|
+
// The reserved super-prop names are enforced by the events registry
|
|
3976
|
+
// test in `analytics/__tests__/events.test.ts` — any new event prop
|
|
3977
|
+
// that collides will fail CI.
|
|
3422
3978
|
properties: {
|
|
3979
|
+
...properties,
|
|
3423
3980
|
source: "cli",
|
|
3424
3981
|
// biome-ignore lint/style/useNamingConvention: snake_case is standard for analytics event properties
|
|
3425
3982
|
cli_version: this.cliVersion,
|
|
@@ -3429,14 +3986,14 @@ var AnalyticsService = class {
|
|
|
3429
3986
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3430
3987
|
request_id: this.requestId,
|
|
3431
3988
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3432
|
-
agent_host: this.agentHost
|
|
3433
|
-
...properties
|
|
3989
|
+
agent_host: this.agentHost
|
|
3434
3990
|
}
|
|
3435
3991
|
});
|
|
3436
3992
|
}
|
|
3437
3993
|
captureException(error, properties) {
|
|
3438
3994
|
if (!this.posthog) return;
|
|
3439
3995
|
this.posthog.captureException(error, this.distinctId, {
|
|
3996
|
+
...properties,
|
|
3440
3997
|
source: "cli",
|
|
3441
3998
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3442
3999
|
cli_version: this.cliVersion,
|
|
@@ -3446,8 +4003,7 @@ var AnalyticsService = class {
|
|
|
3446
4003
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3447
4004
|
request_id: this.requestId,
|
|
3448
4005
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3449
|
-
agent_host: this.agentHost
|
|
3450
|
-
...properties
|
|
4006
|
+
agent_host: this.agentHost
|
|
3451
4007
|
});
|
|
3452
4008
|
}
|
|
3453
4009
|
async shutdown() {
|
|
@@ -3460,14 +4016,14 @@ var AnalyticsService = class {
|
|
|
3460
4016
|
};
|
|
3461
4017
|
|
|
3462
4018
|
// src/services/state-service.ts
|
|
3463
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as
|
|
3464
|
-
import { join as
|
|
4019
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
4020
|
+
import { join as join6 } from "path";
|
|
3465
4021
|
var RECENT_SEARCH_LIMIT = 10;
|
|
3466
4022
|
var StateService = class {
|
|
3467
4023
|
constructor(zeroDir) {
|
|
3468
4024
|
this.zeroDir = zeroDir;
|
|
3469
|
-
this.lastSearchPath =
|
|
3470
|
-
this.recentSearchesPath =
|
|
4025
|
+
this.lastSearchPath = join6(zeroDir, "last_search.json");
|
|
4026
|
+
this.recentSearchesPath = join6(zeroDir, "recent_searches.json");
|
|
3471
4027
|
}
|
|
3472
4028
|
lastSearchPath;
|
|
3473
4029
|
recentSearchesPath;
|
|
@@ -3487,7 +4043,7 @@ var StateService = class {
|
|
|
3487
4043
|
loadLastSearch = () => {
|
|
3488
4044
|
try {
|
|
3489
4045
|
if (!existsSync6(this.lastSearchPath)) return null;
|
|
3490
|
-
const raw =
|
|
4046
|
+
const raw = readFileSync10(this.lastSearchPath, "utf8");
|
|
3491
4047
|
return JSON.parse(raw);
|
|
3492
4048
|
} catch {
|
|
3493
4049
|
return null;
|
|
@@ -3499,7 +4055,7 @@ var StateService = class {
|
|
|
3499
4055
|
const last = this.loadLastSearch();
|
|
3500
4056
|
return { searches: last ? [last] : [] };
|
|
3501
4057
|
}
|
|
3502
|
-
const raw =
|
|
4058
|
+
const raw = readFileSync10(this.recentSearchesPath, "utf8");
|
|
3503
4059
|
const parsed = JSON.parse(raw);
|
|
3504
4060
|
return { searches: parsed.searches ?? [] };
|
|
3505
4061
|
} catch {
|
|
@@ -3507,18 +4063,23 @@ var StateService = class {
|
|
|
3507
4063
|
}
|
|
3508
4064
|
};
|
|
3509
4065
|
// Walk recent searches newest-first, returning the first one whose
|
|
3510
|
-
// results contain `
|
|
3511
|
-
// "loadLastSearch" for attributing rank — handles the
|
|
3512
|
-
// case where the most recent search isn't the one being
|
|
3513
|
-
|
|
4066
|
+
// results contain `capabilityRef` (matches against uid OR slug).
|
|
4067
|
+
// Preferred over "loadLastSearch" for attributing rank — handles the
|
|
4068
|
+
// parallel-search case where the most recent search isn't the one being
|
|
4069
|
+
// fetched.
|
|
4070
|
+
findSearchContextByCapability = (capabilityRef) => {
|
|
3514
4071
|
const recent = this.loadRecentSearches();
|
|
3515
4072
|
for (const search of recent.searches) {
|
|
3516
|
-
const entry = search.capabilities.find(
|
|
4073
|
+
const entry = search.capabilities.find(
|
|
4074
|
+
(c) => c.id === capabilityRef || c.slug === capabilityRef
|
|
4075
|
+
);
|
|
3517
4076
|
if (entry) {
|
|
3518
4077
|
return {
|
|
3519
4078
|
searchId: search.searchId,
|
|
3520
4079
|
resultRank: entry.position,
|
|
3521
4080
|
capabilityId: entry.id,
|
|
4081
|
+
capabilityUid: entry.id,
|
|
4082
|
+
capabilitySlug: entry.slug ?? null,
|
|
3522
4083
|
url: entry.url,
|
|
3523
4084
|
displayCostAmount: entry.displayCostAmount
|
|
3524
4085
|
};
|
|
@@ -3543,6 +4104,8 @@ var StateService = class {
|
|
|
3543
4104
|
searchId: search.searchId,
|
|
3544
4105
|
resultRank: entry.position,
|
|
3545
4106
|
capabilityId: entry.id,
|
|
4107
|
+
capabilityUid: entry.id,
|
|
4108
|
+
capabilitySlug: entry.slug ?? null,
|
|
3546
4109
|
url: entry.url,
|
|
3547
4110
|
displayCostAmount: entry.displayCostAmount
|
|
3548
4111
|
};
|
|
@@ -3590,40 +4153,63 @@ var detectAgentHost = (env = process.env) => {
|
|
|
3590
4153
|
|
|
3591
4154
|
// src/app/app-services.ts
|
|
3592
4155
|
var CLI_VERSION = package_default.version;
|
|
3593
|
-
var
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
}
|
|
3605
|
-
} catch {
|
|
3606
|
-
}
|
|
4156
|
+
var resolveCredentials = (env, config) => {
|
|
4157
|
+
if (config.session) {
|
|
4158
|
+
return {
|
|
4159
|
+
credentials: {
|
|
4160
|
+
kind: "session",
|
|
4161
|
+
accessToken: config.session.accessToken,
|
|
4162
|
+
refreshToken: config.session.refreshToken,
|
|
4163
|
+
userId: config.session.userId
|
|
4164
|
+
},
|
|
4165
|
+
privateKey: null
|
|
4166
|
+
};
|
|
3607
4167
|
}
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
4168
|
+
if (env.ZERO_PRIVATE_KEY) {
|
|
4169
|
+
return {
|
|
4170
|
+
credentials: { kind: "none" },
|
|
4171
|
+
privateKey: env.ZERO_PRIVATE_KEY
|
|
4172
|
+
};
|
|
4173
|
+
}
|
|
4174
|
+
if (config.privateKey) {
|
|
4175
|
+
return {
|
|
4176
|
+
credentials: { kind: "none" },
|
|
4177
|
+
privateKey: config.privateKey
|
|
4178
|
+
};
|
|
3618
4179
|
}
|
|
3619
|
-
|
|
4180
|
+
return { credentials: { kind: "none" }, privateKey: null };
|
|
4181
|
+
};
|
|
4182
|
+
var buildOnSessionRefreshed = (configPath) => async (tokens) => {
|
|
4183
|
+
const current = readConfig(configPath);
|
|
4184
|
+
if (!current.session) return;
|
|
4185
|
+
const next = {
|
|
4186
|
+
...current,
|
|
4187
|
+
session: {
|
|
4188
|
+
...current.session,
|
|
4189
|
+
accessToken: tokens.accessToken,
|
|
4190
|
+
refreshToken: tokens.refreshToken
|
|
4191
|
+
}
|
|
4192
|
+
};
|
|
4193
|
+
writeSecureFile(configPath, JSON.stringify(next, null, 2));
|
|
4194
|
+
};
|
|
4195
|
+
var getServices = (env) => {
|
|
4196
|
+
const zeroDir = join7(homedir6(), ".zero");
|
|
4197
|
+
const configPath = join7(zeroDir, "config.json");
|
|
4198
|
+
const config = existsSync7(configPath) ? readConfig(configPath) : {};
|
|
4199
|
+
const { credentials, privateKey } = resolveCredentials(env, config);
|
|
4200
|
+
const account = privateKey ? privateKeyToAccount4(privateKey) : null;
|
|
4201
|
+
const lowBalanceWarning = typeof config.lowBalanceWarning === "number" ? config.lowBalanceWarning : 1;
|
|
4202
|
+
const apiService = new ApiService(
|
|
4203
|
+
env.ZERO_API_URL,
|
|
4204
|
+
account,
|
|
4205
|
+
credentials,
|
|
4206
|
+
buildOnSessionRefreshed(configPath)
|
|
4207
|
+
);
|
|
3620
4208
|
const paymentService = new PaymentService(account, { lowBalanceWarning });
|
|
3621
4209
|
const stateService = new StateService(zeroDir);
|
|
3622
4210
|
const walletService = new WalletService(
|
|
3623
4211
|
apiService.walletAddress,
|
|
3624
|
-
{
|
|
3625
|
-
lowBalanceWarning
|
|
3626
|
-
},
|
|
4212
|
+
{ lowBalanceWarning },
|
|
3627
4213
|
paymentService.getTotalBalance
|
|
3628
4214
|
);
|
|
3629
4215
|
const analyticsService = new AnalyticsService({
|
|
@@ -3665,8 +4251,8 @@ import {
|
|
|
3665
4251
|
readlinkSync,
|
|
3666
4252
|
writeFileSync as writeFileSync6
|
|
3667
4253
|
} from "fs";
|
|
3668
|
-
import { homedir as
|
|
3669
|
-
import { dirname as dirname3, join as
|
|
4254
|
+
import { homedir as homedir7 } from "os";
|
|
4255
|
+
import { dirname as dirname3, join as join8, resolve } from "path";
|
|
3670
4256
|
var CACHE_FILENAME = "update_check.json";
|
|
3671
4257
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org/@zeroxyz/cli/latest";
|
|
3672
4258
|
var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
|
|
@@ -3690,10 +4276,10 @@ var resolveExecPath = (execPath) => {
|
|
|
3690
4276
|
var detectInstallMethod = (opts = {}) => {
|
|
3691
4277
|
const execPath = opts.execPath ?? process.execPath;
|
|
3692
4278
|
const pkg = opts.pkg ?? process.pkg;
|
|
3693
|
-
const home = opts.home ??
|
|
4279
|
+
const home = opts.home ?? homedir7();
|
|
3694
4280
|
if (pkg) return "binary";
|
|
3695
4281
|
const resolved = resolveExecPath(execPath);
|
|
3696
|
-
const zeroBin =
|
|
4282
|
+
const zeroBin = join8(home, ".zero", "bin");
|
|
3697
4283
|
if (resolved.startsWith(zeroBin)) return "binary";
|
|
3698
4284
|
return "npm";
|
|
3699
4285
|
};
|
|
@@ -3718,7 +4304,7 @@ var compareVersions = (a, b) => {
|
|
|
3718
4304
|
if (pb.pre === null) return -1;
|
|
3719
4305
|
return pa.pre < pb.pre ? -1 : 1;
|
|
3720
4306
|
};
|
|
3721
|
-
var cachePath = (zeroDir) =>
|
|
4307
|
+
var cachePath = (zeroDir) => join8(zeroDir, CACHE_FILENAME);
|
|
3722
4308
|
var readCache = (zeroDir) => {
|
|
3723
4309
|
try {
|
|
3724
4310
|
const path = cachePath(zeroDir);
|
|
@@ -3812,7 +4398,7 @@ var main = async () => {
|
|
|
3812
4398
|
console.error("Failed to create app context");
|
|
3813
4399
|
process.exit(1);
|
|
3814
4400
|
}
|
|
3815
|
-
const zeroDir =
|
|
4401
|
+
const zeroDir = join9(homedir8(), ".zero");
|
|
3816
4402
|
maybePrintUpdateBanner(zeroDir, package_default.version);
|
|
3817
4403
|
const app = createApp(appContext);
|
|
3818
4404
|
let caughtError = null;
|