chainlesschain 0.37.9 → 0.37.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -19
- package/bin/chainlesschain.js +4 -0
- package/package.json +1 -1
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/did.js +376 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/import.js +259 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +155 -4
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +187 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +398 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/index.js +49 -1
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/plugin-manager.js +312 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/repl/agent-repl.js +142 -12
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth / RBAC commands
|
|
3
|
+
* chainlesschain auth roles|grant|revoke|check|permissions|users
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { logger } from "../lib/logger.js";
|
|
8
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
9
|
+
import {
|
|
10
|
+
getRoles,
|
|
11
|
+
createRole,
|
|
12
|
+
deleteRole,
|
|
13
|
+
grantRole,
|
|
14
|
+
revokeRole,
|
|
15
|
+
grantPermission,
|
|
16
|
+
revokePermission,
|
|
17
|
+
getUserPermissions,
|
|
18
|
+
checkPermission,
|
|
19
|
+
listUserRoles,
|
|
20
|
+
PERMISSION_SCOPES,
|
|
21
|
+
} from "../lib/permission-engine.js";
|
|
22
|
+
|
|
23
|
+
export function registerAuthCommand(program) {
|
|
24
|
+
const auth = program
|
|
25
|
+
.command("auth")
|
|
26
|
+
.description("RBAC permission management");
|
|
27
|
+
|
|
28
|
+
// auth roles
|
|
29
|
+
auth
|
|
30
|
+
.command("roles", { isDefault: true })
|
|
31
|
+
.description("List all roles")
|
|
32
|
+
.option("--json", "Output as JSON")
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
try {
|
|
35
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
36
|
+
if (!ctx.db) {
|
|
37
|
+
logger.error("Database not available");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const db = ctx.db.getDatabase();
|
|
41
|
+
const roles = getRoles(db);
|
|
42
|
+
|
|
43
|
+
if (options.json) {
|
|
44
|
+
console.log(JSON.stringify(roles, null, 2));
|
|
45
|
+
} else if (roles.length === 0) {
|
|
46
|
+
logger.info("No roles defined");
|
|
47
|
+
} else {
|
|
48
|
+
logger.log(chalk.bold(`Roles (${roles.length}):\n`));
|
|
49
|
+
for (const role of roles) {
|
|
50
|
+
const tag = role.isBuiltin
|
|
51
|
+
? chalk.gray(" [built-in]")
|
|
52
|
+
: chalk.blue(" [custom]");
|
|
53
|
+
logger.log(
|
|
54
|
+
` ${chalk.cyan(role.name.padEnd(15))}${tag} ${role.description || ""}`,
|
|
55
|
+
);
|
|
56
|
+
logger.log(
|
|
57
|
+
` ${chalk.gray("permissions:")} ${role.permissions.join(", ")}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await shutdown();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.error(`Failed: ${err.message}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// auth create-role
|
|
70
|
+
auth
|
|
71
|
+
.command("create-role")
|
|
72
|
+
.description("Create a custom role")
|
|
73
|
+
.argument("<name>", "Role name")
|
|
74
|
+
.option("-d, --description <desc>", "Role description")
|
|
75
|
+
.option("-p, --permissions <perms>", "Comma-separated permissions")
|
|
76
|
+
.action(async (name, options) => {
|
|
77
|
+
try {
|
|
78
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
79
|
+
if (!ctx.db) {
|
|
80
|
+
logger.error("Database not available");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
const db = ctx.db.getDatabase();
|
|
84
|
+
|
|
85
|
+
const perms = options.permissions
|
|
86
|
+
? options.permissions.split(",").map((s) => s.trim())
|
|
87
|
+
: [];
|
|
88
|
+
const role = createRole(db, name, options.description, perms);
|
|
89
|
+
|
|
90
|
+
logger.success(`Role created: ${role.name}`);
|
|
91
|
+
if (role.permissions.length > 0) {
|
|
92
|
+
logger.log(
|
|
93
|
+
` ${chalk.bold("Permissions:")} ${role.permissions.join(", ")}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await shutdown();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logger.error(`Failed: ${err.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// auth delete-role
|
|
105
|
+
auth
|
|
106
|
+
.command("delete-role")
|
|
107
|
+
.description("Delete a custom role")
|
|
108
|
+
.argument("<name>", "Role name")
|
|
109
|
+
.option("--force", "Skip confirmation")
|
|
110
|
+
.action(async (name, options) => {
|
|
111
|
+
try {
|
|
112
|
+
if (!options.force) {
|
|
113
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
114
|
+
const ok = await confirm({
|
|
115
|
+
message: `Delete role "${name}"? All grants for this role will be removed.`,
|
|
116
|
+
});
|
|
117
|
+
if (!ok) {
|
|
118
|
+
logger.info("Cancelled");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
124
|
+
if (!ctx.db) {
|
|
125
|
+
logger.error("Database not available");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const db = ctx.db.getDatabase();
|
|
129
|
+
const ok = deleteRole(db, name);
|
|
130
|
+
|
|
131
|
+
if (ok) {
|
|
132
|
+
logger.success(`Role deleted: ${name}`);
|
|
133
|
+
} else {
|
|
134
|
+
logger.error(`Role not found or is built-in: ${name}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await shutdown();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logger.error(`Failed: ${err.message}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// auth grant
|
|
145
|
+
auth
|
|
146
|
+
.command("grant")
|
|
147
|
+
.description("Grant a role to a user")
|
|
148
|
+
.argument("<user-did>", "User DID")
|
|
149
|
+
.argument("<role>", "Role name")
|
|
150
|
+
.option("--expires <date>", "Expiration date (ISO 8601)")
|
|
151
|
+
.action(async (userDid, role, options) => {
|
|
152
|
+
try {
|
|
153
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
154
|
+
if (!ctx.db) {
|
|
155
|
+
logger.error("Database not available");
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const db = ctx.db.getDatabase();
|
|
159
|
+
const grant = grantRole(db, userDid, role, null, options.expires);
|
|
160
|
+
|
|
161
|
+
logger.success(`Granted role "${role}" to ${userDid}`);
|
|
162
|
+
if (grant.expiresAt) {
|
|
163
|
+
logger.log(` ${chalk.bold("Expires:")} ${grant.expiresAt}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await shutdown();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
logger.error(`Failed: ${err.message}`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// auth revoke
|
|
174
|
+
auth
|
|
175
|
+
.command("revoke")
|
|
176
|
+
.description("Revoke a role from a user")
|
|
177
|
+
.argument("<user-did>", "User DID")
|
|
178
|
+
.argument("<role>", "Role name")
|
|
179
|
+
.action(async (userDid, role) => {
|
|
180
|
+
try {
|
|
181
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
182
|
+
if (!ctx.db) {
|
|
183
|
+
logger.error("Database not available");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
const db = ctx.db.getDatabase();
|
|
187
|
+
const ok = revokeRole(db, userDid, role);
|
|
188
|
+
|
|
189
|
+
if (ok) {
|
|
190
|
+
logger.success(`Revoked role "${role}" from ${userDid}`);
|
|
191
|
+
} else {
|
|
192
|
+
logger.error("Grant not found");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await shutdown();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
logger.error(`Failed: ${err.message}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// auth grant-permission (direct permission)
|
|
203
|
+
auth
|
|
204
|
+
.command("grant-permission")
|
|
205
|
+
.description("Grant a direct permission to a user")
|
|
206
|
+
.argument("<user-did>", "User DID")
|
|
207
|
+
.argument("<permission>", "Permission scope (e.g., note:read)")
|
|
208
|
+
.action(async (userDid, permission) => {
|
|
209
|
+
try {
|
|
210
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
211
|
+
if (!ctx.db) {
|
|
212
|
+
logger.error("Database not available");
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
const db = ctx.db.getDatabase();
|
|
216
|
+
grantPermission(db, userDid, permission);
|
|
217
|
+
logger.success(`Granted permission "${permission}" to ${userDid}`);
|
|
218
|
+
|
|
219
|
+
await shutdown();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
logger.error(`Failed: ${err.message}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// auth revoke-permission
|
|
227
|
+
auth
|
|
228
|
+
.command("revoke-permission")
|
|
229
|
+
.description("Revoke a direct permission from a user")
|
|
230
|
+
.argument("<user-did>", "User DID")
|
|
231
|
+
.argument("<permission>", "Permission scope")
|
|
232
|
+
.action(async (userDid, permission) => {
|
|
233
|
+
try {
|
|
234
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
235
|
+
if (!ctx.db) {
|
|
236
|
+
logger.error("Database not available");
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const db = ctx.db.getDatabase();
|
|
240
|
+
const ok = revokePermission(db, userDid, permission);
|
|
241
|
+
|
|
242
|
+
if (ok) {
|
|
243
|
+
logger.success(`Revoked permission "${permission}" from ${userDid}`);
|
|
244
|
+
} else {
|
|
245
|
+
logger.error("Permission grant not found");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await shutdown();
|
|
249
|
+
} catch (err) {
|
|
250
|
+
logger.error(`Failed: ${err.message}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// auth check
|
|
256
|
+
auth
|
|
257
|
+
.command("check")
|
|
258
|
+
.description("Check if a user has a specific permission")
|
|
259
|
+
.argument("<user-did>", "User DID")
|
|
260
|
+
.argument("<permission>", "Permission to check")
|
|
261
|
+
.option("--json", "Output as JSON")
|
|
262
|
+
.action(async (userDid, permission, options) => {
|
|
263
|
+
try {
|
|
264
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
265
|
+
if (!ctx.db) {
|
|
266
|
+
logger.error("Database not available");
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
const db = ctx.db.getDatabase();
|
|
270
|
+
const allowed = checkPermission(db, userDid, permission);
|
|
271
|
+
|
|
272
|
+
if (options.json) {
|
|
273
|
+
console.log(
|
|
274
|
+
JSON.stringify({ userDid, permission, allowed }, null, 2),
|
|
275
|
+
);
|
|
276
|
+
} else if (allowed) {
|
|
277
|
+
logger.success(
|
|
278
|
+
`${chalk.green("ALLOWED")} — ${userDid} has permission: ${permission}`,
|
|
279
|
+
);
|
|
280
|
+
} else {
|
|
281
|
+
logger.log(
|
|
282
|
+
`${chalk.red("DENIED")} — ${userDid} does not have permission: ${permission}`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await shutdown();
|
|
287
|
+
} catch (err) {
|
|
288
|
+
logger.error(`Failed: ${err.message}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// auth permissions
|
|
294
|
+
auth
|
|
295
|
+
.command("permissions")
|
|
296
|
+
.description("Show all permissions for a user")
|
|
297
|
+
.argument("<user-did>", "User DID")
|
|
298
|
+
.option("--json", "Output as JSON")
|
|
299
|
+
.action(async (userDid, options) => {
|
|
300
|
+
try {
|
|
301
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
302
|
+
if (!ctx.db) {
|
|
303
|
+
logger.error("Database not available");
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
const db = ctx.db.getDatabase();
|
|
307
|
+
const perms = getUserPermissions(db, userDid);
|
|
308
|
+
|
|
309
|
+
if (options.json) {
|
|
310
|
+
console.log(JSON.stringify(perms, null, 2));
|
|
311
|
+
} else {
|
|
312
|
+
logger.log(chalk.bold(`Permissions for ${chalk.cyan(userDid)}:\n`));
|
|
313
|
+
if (perms.isAdmin) {
|
|
314
|
+
logger.log(` ${chalk.green("ADMIN")} — Full access (wildcard *)`);
|
|
315
|
+
}
|
|
316
|
+
if (perms.roles.length > 0) {
|
|
317
|
+
logger.log(` ${chalk.bold("Roles:")} ${perms.roles.join(", ")}`);
|
|
318
|
+
}
|
|
319
|
+
if (perms.directPermissions.length > 0) {
|
|
320
|
+
logger.log(
|
|
321
|
+
` ${chalk.bold("Direct:")} ${perms.directPermissions.join(", ")}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (perms.effectivePermissions.length > 0) {
|
|
325
|
+
logger.log(
|
|
326
|
+
` ${chalk.bold("Effective:")} ${perms.effectivePermissions.join(", ")}`,
|
|
327
|
+
);
|
|
328
|
+
} else {
|
|
329
|
+
logger.log(` ${chalk.gray("No permissions assigned")}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
await shutdown();
|
|
334
|
+
} catch (err) {
|
|
335
|
+
logger.error(`Failed: ${err.message}`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// auth users
|
|
341
|
+
auth
|
|
342
|
+
.command("users")
|
|
343
|
+
.description("List all users with role assignments")
|
|
344
|
+
.option("--json", "Output as JSON")
|
|
345
|
+
.action(async (options) => {
|
|
346
|
+
try {
|
|
347
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
348
|
+
if (!ctx.db) {
|
|
349
|
+
logger.error("Database not available");
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
const db = ctx.db.getDatabase();
|
|
353
|
+
const users = listUserRoles(db);
|
|
354
|
+
|
|
355
|
+
if (options.json) {
|
|
356
|
+
console.log(JSON.stringify(users, null, 2));
|
|
357
|
+
} else if (users.length === 0) {
|
|
358
|
+
logger.info("No role assignments yet");
|
|
359
|
+
} else {
|
|
360
|
+
logger.log(chalk.bold(`Users with roles (${users.length}):\n`));
|
|
361
|
+
for (const u of users) {
|
|
362
|
+
logger.log(` ${chalk.cyan(u.userDid)}`);
|
|
363
|
+
logger.log(` ${chalk.gray("roles:")} ${u.roles.join(", ")}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
await shutdown();
|
|
368
|
+
} catch (err) {
|
|
369
|
+
logger.error(`Failed: ${err.message}`);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// auth scopes
|
|
375
|
+
auth
|
|
376
|
+
.command("scopes")
|
|
377
|
+
.description("List all available permission scopes")
|
|
378
|
+
.action(async () => {
|
|
379
|
+
logger.log(chalk.bold("Available Permission Scopes:\n"));
|
|
380
|
+
for (const scope of PERMISSION_SCOPES) {
|
|
381
|
+
const [resource, action] = scope.split(":");
|
|
382
|
+
logger.log(
|
|
383
|
+
` ${chalk.cyan(resource.padEnd(12))}:${chalk.white(action)}`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser automation commands
|
|
3
|
+
* chainlesschain browse <url> | browse scrape <url> | browse screenshot <url>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import { logger } from "../lib/logger.js";
|
|
9
|
+
import {
|
|
10
|
+
fetchPage,
|
|
11
|
+
extractText,
|
|
12
|
+
extractTitle,
|
|
13
|
+
extractMeta,
|
|
14
|
+
querySelectorAll,
|
|
15
|
+
extractLinks,
|
|
16
|
+
takeScreenshot,
|
|
17
|
+
} from "../lib/browser-automation.js";
|
|
18
|
+
|
|
19
|
+
export function registerBrowseCommand(program) {
|
|
20
|
+
const browse = program
|
|
21
|
+
.command("browse")
|
|
22
|
+
.description("Headless browser automation and web scraping");
|
|
23
|
+
|
|
24
|
+
// browse fetch — fetch and display page content
|
|
25
|
+
browse
|
|
26
|
+
.command("fetch")
|
|
27
|
+
.description("Fetch a URL and display text content")
|
|
28
|
+
.argument("<url>", "URL to fetch")
|
|
29
|
+
.option("--html", "Show raw HTML instead of text")
|
|
30
|
+
.option("--links", "Extract links only")
|
|
31
|
+
.option("--json", "Output as JSON")
|
|
32
|
+
.action(async (url, options) => {
|
|
33
|
+
try {
|
|
34
|
+
const spinner = ora(`Fetching ${url}...`).start();
|
|
35
|
+
const result = await fetchPage(url);
|
|
36
|
+
spinner.stop();
|
|
37
|
+
|
|
38
|
+
const title = extractTitle(result.html);
|
|
39
|
+
const text = extractText(result.html);
|
|
40
|
+
const description = extractMeta(result.html);
|
|
41
|
+
|
|
42
|
+
if (options.json) {
|
|
43
|
+
const output = {
|
|
44
|
+
url: result.url,
|
|
45
|
+
status: result.status,
|
|
46
|
+
title,
|
|
47
|
+
description,
|
|
48
|
+
size: result.size,
|
|
49
|
+
};
|
|
50
|
+
if (options.html) output.html = result.html;
|
|
51
|
+
else if (options.links)
|
|
52
|
+
output.links = extractLinks(result.html, result.url);
|
|
53
|
+
else output.text = text;
|
|
54
|
+
console.log(JSON.stringify(output, null, 2));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (options.links) {
|
|
59
|
+
const links = extractLinks(result.html, result.url);
|
|
60
|
+
logger.log(
|
|
61
|
+
chalk.bold(`Links from ${title || url} (${links.length}):\n`),
|
|
62
|
+
);
|
|
63
|
+
for (const link of links.slice(0, 50)) {
|
|
64
|
+
logger.log(
|
|
65
|
+
` ${chalk.cyan(link.text.substring(0, 60).padEnd(62))} ${chalk.gray(link.href)}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (links.length > 50)
|
|
69
|
+
logger.log(chalk.gray(` ... and ${links.length - 50} more`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (options.html) {
|
|
74
|
+
console.log(result.html);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
logger.log(chalk.bold(title || url));
|
|
79
|
+
if (description) logger.log(chalk.gray(description));
|
|
80
|
+
logger.log(
|
|
81
|
+
chalk.gray(`${result.size} bytes | ${result.contentType}\n`),
|
|
82
|
+
);
|
|
83
|
+
logger.log(text.substring(0, 5000));
|
|
84
|
+
if (text.length > 5000) {
|
|
85
|
+
logger.log(
|
|
86
|
+
chalk.gray(`\n... truncated (${text.length} chars total)`),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
logger.error(`Fetch failed: ${err.message}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// browse scrape — scrape elements matching a CSS selector
|
|
96
|
+
browse
|
|
97
|
+
.command("scrape")
|
|
98
|
+
.description("Scrape elements from a URL using CSS selector")
|
|
99
|
+
.argument("<url>", "URL to scrape")
|
|
100
|
+
.requiredOption("-s, --selector <css>", "CSS selector (tag, .class, #id)")
|
|
101
|
+
.option("-n, --limit <n>", "Max results", "20")
|
|
102
|
+
.option("--json", "Output as JSON")
|
|
103
|
+
.action(async (url, options) => {
|
|
104
|
+
try {
|
|
105
|
+
const spinner = ora(`Scraping ${url}...`).start();
|
|
106
|
+
const result = await fetchPage(url);
|
|
107
|
+
const elements = querySelectorAll(result.html, options.selector);
|
|
108
|
+
spinner.stop();
|
|
109
|
+
|
|
110
|
+
const limit = parseInt(options.limit) || 20;
|
|
111
|
+
const limited = elements.slice(0, limit);
|
|
112
|
+
|
|
113
|
+
if (options.json) {
|
|
114
|
+
console.log(
|
|
115
|
+
JSON.stringify(
|
|
116
|
+
{
|
|
117
|
+
url: result.url,
|
|
118
|
+
selector: options.selector,
|
|
119
|
+
count: elements.length,
|
|
120
|
+
results: limited.map((e) => ({ text: e.text })),
|
|
121
|
+
},
|
|
122
|
+
null,
|
|
123
|
+
2,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (elements.length === 0) {
|
|
130
|
+
logger.info(`No elements matching "${options.selector}"`);
|
|
131
|
+
} else {
|
|
132
|
+
logger.log(
|
|
133
|
+
chalk.bold(
|
|
134
|
+
`Scraped ${elements.length} elements matching "${options.selector}":\n`,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
for (let i = 0; i < limited.length; i++) {
|
|
138
|
+
const text = limited[i].text.substring(0, 200).replace(/\n/g, " ");
|
|
139
|
+
logger.log(` ${chalk.gray(`[${i + 1}]`)} ${text}`);
|
|
140
|
+
}
|
|
141
|
+
if (elements.length > limit) {
|
|
142
|
+
logger.log(chalk.gray(` ... ${elements.length - limit} more`));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
logger.error(`Scrape failed: ${err.message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// browse screenshot — take a screenshot (requires playwright)
|
|
152
|
+
browse
|
|
153
|
+
.command("screenshot")
|
|
154
|
+
.description("Take a screenshot of a URL (requires playwright)")
|
|
155
|
+
.argument("<url>", "URL to screenshot")
|
|
156
|
+
.option("-o, --output <path>", "Output file path", "screenshot.png")
|
|
157
|
+
.option("--width <n>", "Viewport width", "1280")
|
|
158
|
+
.option("--height <n>", "Viewport height", "720")
|
|
159
|
+
.option("--full-page", "Capture full page")
|
|
160
|
+
.option("--json", "Output as JSON")
|
|
161
|
+
.action(async (url, options) => {
|
|
162
|
+
try {
|
|
163
|
+
const spinner = ora(`Taking screenshot of ${url}...`).start();
|
|
164
|
+
const result = await takeScreenshot(url, options.output, {
|
|
165
|
+
width: parseInt(options.width),
|
|
166
|
+
height: parseInt(options.height),
|
|
167
|
+
fullPage: !!options.fullPage,
|
|
168
|
+
});
|
|
169
|
+
spinner.stop();
|
|
170
|
+
|
|
171
|
+
if (options.json) {
|
|
172
|
+
console.log(JSON.stringify(result, null, 2));
|
|
173
|
+
} else if (result.success) {
|
|
174
|
+
logger.success(`Screenshot saved: ${chalk.cyan(result.path)}`);
|
|
175
|
+
} else {
|
|
176
|
+
logger.error(result.error);
|
|
177
|
+
logger.info("Install playwright: npm install -g playwright");
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
logger.error(`Screenshot failed: ${err.message}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|