chainlesschain 0.37.9 → 0.37.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +309 -19
  2. package/bin/chainlesschain.js +4 -0
  3. package/package.json +1 -1
  4. package/src/commands/a2a.js +374 -0
  5. package/src/commands/audit.js +286 -0
  6. package/src/commands/auth.js +387 -0
  7. package/src/commands/bi.js +240 -0
  8. package/src/commands/browse.js +184 -0
  9. package/src/commands/cowork.js +317 -0
  10. package/src/commands/did.js +376 -0
  11. package/src/commands/economy.js +375 -0
  12. package/src/commands/encrypt.js +233 -0
  13. package/src/commands/evolution.js +398 -0
  14. package/src/commands/export.js +125 -0
  15. package/src/commands/git.js +215 -0
  16. package/src/commands/hmemory.js +273 -0
  17. package/src/commands/hook.js +260 -0
  18. package/src/commands/import.js +259 -0
  19. package/src/commands/init.js +184 -0
  20. package/src/commands/instinct.js +202 -0
  21. package/src/commands/llm.js +155 -4
  22. package/src/commands/lowcode.js +320 -0
  23. package/src/commands/mcp.js +302 -0
  24. package/src/commands/memory.js +282 -0
  25. package/src/commands/note.js +187 -0
  26. package/src/commands/org.js +505 -0
  27. package/src/commands/p2p.js +274 -0
  28. package/src/commands/plugin.js +451 -0
  29. package/src/commands/sandbox.js +366 -0
  30. package/src/commands/search.js +237 -0
  31. package/src/commands/session.js +238 -0
  32. package/src/commands/skill.js +254 -201
  33. package/src/commands/sync.js +249 -0
  34. package/src/commands/tokens.js +214 -0
  35. package/src/commands/wallet.js +416 -0
  36. package/src/commands/workflow.js +359 -0
  37. package/src/commands/zkp.js +277 -0
  38. package/src/index.js +93 -1
  39. package/src/lib/a2a-protocol.js +371 -0
  40. package/src/lib/agent-coordinator.js +273 -0
  41. package/src/lib/agent-economy.js +369 -0
  42. package/src/lib/app-builder.js +377 -0
  43. package/src/lib/audit-logger.js +364 -0
  44. package/src/lib/bi-engine.js +299 -0
  45. package/src/lib/bm25-search.js +322 -0
  46. package/src/lib/browser-automation.js +216 -0
  47. package/src/lib/cowork/ab-comparator-cli.js +180 -0
  48. package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
  49. package/src/lib/cowork/debate-review-cli.js +144 -0
  50. package/src/lib/cowork/decision-kb-cli.js +153 -0
  51. package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
  52. package/src/lib/cowork-adapter.js +106 -0
  53. package/src/lib/crypto-manager.js +246 -0
  54. package/src/lib/did-manager.js +270 -0
  55. package/src/lib/ensure-utf8.js +59 -0
  56. package/src/lib/evolution-system.js +508 -0
  57. package/src/lib/git-integration.js +220 -0
  58. package/src/lib/hierarchical-memory.js +471 -0
  59. package/src/lib/hook-manager.js +387 -0
  60. package/src/lib/instinct-manager.js +190 -0
  61. package/src/lib/knowledge-exporter.js +302 -0
  62. package/src/lib/knowledge-importer.js +293 -0
  63. package/src/lib/llm-providers.js +325 -0
  64. package/src/lib/mcp-client.js +413 -0
  65. package/src/lib/memory-manager.js +211 -0
  66. package/src/lib/note-versioning.js +244 -0
  67. package/src/lib/org-manager.js +424 -0
  68. package/src/lib/p2p-manager.js +317 -0
  69. package/src/lib/pdf-parser.js +96 -0
  70. package/src/lib/permission-engine.js +374 -0
  71. package/src/lib/plan-mode.js +333 -0
  72. package/src/lib/plugin-manager.js +430 -0
  73. package/src/lib/project-detector.js +53 -0
  74. package/src/lib/response-cache.js +156 -0
  75. package/src/lib/sandbox-v2.js +503 -0
  76. package/src/lib/service-container.js +183 -0
  77. package/src/lib/session-manager.js +189 -0
  78. package/src/lib/skill-loader.js +274 -0
  79. package/src/lib/sync-manager.js +347 -0
  80. package/src/lib/token-tracker.js +200 -0
  81. package/src/lib/wallet-manager.js +348 -0
  82. package/src/lib/workflow-engine.js +503 -0
  83. package/src/lib/zkp-engine.js +241 -0
  84. package/src/repl/agent-repl.js +259 -124
@@ -6,6 +6,15 @@
6
6
  import chalk from "chalk";
7
7
  import { logger } from "../lib/logger.js";
8
8
  import { bootstrap, shutdown } from "../runtime/bootstrap.js";
9
+ import {
10
+ ensureVersionsTable,
11
+ saveVersion,
12
+ getHistory,
13
+ getVersion,
14
+ simpleDiff,
15
+ formatDiff,
16
+ revertToVersion,
17
+ } from "../lib/note-versioning.js";
9
18
 
10
19
  /**
11
20
  * Ensure the notes table exists in the database
@@ -299,4 +308,182 @@ export function registerNoteCommand(program) {
299
308
  process.exit(1);
300
309
  }
301
310
  });
311
+
312
+ // note history
313
+ note
314
+ .command("history")
315
+ .description("Show version history for a note")
316
+ .argument("<id>", "Note ID (or prefix)")
317
+ .option("--json", "Output as JSON")
318
+ .action(async (id, options) => {
319
+ try {
320
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
321
+ if (!ctx.db) {
322
+ logger.error("Database not available");
323
+ process.exit(1);
324
+ }
325
+
326
+ const rawDb = ctx.db.getDatabase();
327
+ ensureNotesTable(rawDb);
328
+ ensureVersionsTable(rawDb);
329
+
330
+ // Find the note
331
+ const noteRow = rawDb
332
+ .prepare(
333
+ "SELECT id FROM notes WHERE id LIKE ? AND deleted_at IS NULL",
334
+ )
335
+ .get(`${id}%`);
336
+
337
+ if (!noteRow) {
338
+ logger.error(`Note not found: ${id}`);
339
+ process.exit(1);
340
+ }
341
+
342
+ const history = getHistory(rawDb, noteRow.id);
343
+
344
+ if (options.json) {
345
+ console.log(JSON.stringify(history, null, 2));
346
+ } else if (history.length === 0) {
347
+ logger.info("No version history found");
348
+ } else {
349
+ logger.log(
350
+ chalk.bold(`Version history (${history.length} versions):\n`),
351
+ );
352
+ for (const v of history) {
353
+ logger.log(
354
+ ` ${chalk.cyan(`v${v.version}`)} ${chalk.white(v.title)} ${chalk.gray(v.change_type)} ${chalk.gray(v.created_at || "")}`,
355
+ );
356
+ }
357
+ }
358
+
359
+ await shutdown();
360
+ } catch (err) {
361
+ logger.error(`Failed to get history: ${err.message}`);
362
+ process.exit(1);
363
+ }
364
+ });
365
+
366
+ // note diff
367
+ note
368
+ .command("diff")
369
+ .description("Show diff between two versions of a note")
370
+ .argument("<id>", "Note ID (or prefix)")
371
+ .argument("<v1>", "First version number")
372
+ .argument("<v2>", "Second version number")
373
+ .option("--json", "Output as JSON")
374
+ .action(async (id, v1, v2, options) => {
375
+ try {
376
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
377
+ if (!ctx.db) {
378
+ logger.error("Database not available");
379
+ process.exit(1);
380
+ }
381
+
382
+ const rawDb = ctx.db.getDatabase();
383
+ ensureNotesTable(rawDb);
384
+ ensureVersionsTable(rawDb);
385
+
386
+ const noteRow = rawDb
387
+ .prepare(
388
+ "SELECT id FROM notes WHERE id LIKE ? AND deleted_at IS NULL",
389
+ )
390
+ .get(`${id}%`);
391
+
392
+ if (!noteRow) {
393
+ logger.error(`Note not found: ${id}`);
394
+ process.exit(1);
395
+ }
396
+
397
+ const ver1 = getVersion(rawDb, noteRow.id, parseInt(v1));
398
+ const ver2 = getVersion(rawDb, noteRow.id, parseInt(v2));
399
+
400
+ if (!ver1) {
401
+ logger.error(`Version ${v1} not found`);
402
+ process.exit(1);
403
+ }
404
+ if (!ver2) {
405
+ logger.error(`Version ${v2} not found`);
406
+ process.exit(1);
407
+ }
408
+
409
+ const diff = simpleDiff(ver1.content, ver2.content);
410
+
411
+ if (options.json) {
412
+ console.log(
413
+ JSON.stringify(
414
+ { v1: parseInt(v1), v2: parseInt(v2), diff },
415
+ null,
416
+ 2,
417
+ ),
418
+ );
419
+ } else {
420
+ logger.log(chalk.bold(`Diff: v${v1} → v${v2}\n`));
421
+ logger.log(formatDiff(diff));
422
+ }
423
+
424
+ await shutdown();
425
+ } catch (err) {
426
+ logger.error(`Failed to compute diff: ${err.message}`);
427
+ process.exit(1);
428
+ }
429
+ });
430
+
431
+ // note revert
432
+ note
433
+ .command("revert")
434
+ .description("Revert a note to a specific version")
435
+ .argument("<id>", "Note ID (or prefix)")
436
+ .argument("<version>", "Version number to revert to")
437
+ .option("--force", "Skip confirmation")
438
+ .action(async (id, version, options) => {
439
+ try {
440
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
441
+ if (!ctx.db) {
442
+ logger.error("Database not available");
443
+ process.exit(1);
444
+ }
445
+
446
+ const rawDb = ctx.db.getDatabase();
447
+ ensureNotesTable(rawDb);
448
+ ensureVersionsTable(rawDb);
449
+
450
+ const noteRow = rawDb
451
+ .prepare(
452
+ "SELECT id, title FROM notes WHERE id LIKE ? AND deleted_at IS NULL",
453
+ )
454
+ .get(`${id}%`);
455
+
456
+ if (!noteRow) {
457
+ logger.error(`Note not found: ${id}`);
458
+ process.exit(1);
459
+ }
460
+
461
+ if (!options.force) {
462
+ const { confirm } = await import("@inquirer/prompts");
463
+ const ok = await confirm({
464
+ message: `Revert note "${noteRow.title}" to version ${version}?`,
465
+ });
466
+ if (!ok) {
467
+ logger.info("Cancelled");
468
+ return;
469
+ }
470
+ }
471
+
472
+ const result = revertToVersion(rawDb, noteRow.id, parseInt(version));
473
+
474
+ if (!result) {
475
+ logger.error(`Version ${version} not found or note unavailable`);
476
+ process.exit(1);
477
+ }
478
+
479
+ logger.success(
480
+ `Note reverted to v${result.reverted_to} → new version v${result.new_version}`,
481
+ );
482
+
483
+ await shutdown();
484
+ } catch (err) {
485
+ logger.error(`Failed to revert: ${err.message}`);
486
+ process.exit(1);
487
+ }
488
+ });
302
489
  }
@@ -0,0 +1,505 @@
1
+ /**
2
+ * Organization & team commands
3
+ * chainlesschain org create|list|show|update|delete|invite|members|team|approval
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
+ createOrg,
11
+ getOrg,
12
+ listOrgs,
13
+ updateOrg,
14
+ deleteOrg,
15
+ inviteMember,
16
+ acceptInvite,
17
+ getMembers,
18
+ updateMemberRole,
19
+ removeMember,
20
+ createTeam,
21
+ listTeams,
22
+ addTeamMember,
23
+ getTeamMembers,
24
+ removeTeamMember,
25
+ deleteTeam,
26
+ submitApproval,
27
+ approveRequest,
28
+ rejectRequest,
29
+ getApprovals,
30
+ getOrgSummary,
31
+ } from "../lib/org-manager.js";
32
+
33
+ export function registerOrgCommand(program) {
34
+ const org = program
35
+ .command("org")
36
+ .description("Organization, team, and approval management");
37
+
38
+ // org create
39
+ org
40
+ .command("create")
41
+ .description("Create a new organization")
42
+ .argument("<name>", "Organization name")
43
+ .option("--owner <id>", "Owner user ID", "cli-user")
44
+ .option("--description <desc>", "Organization description")
45
+ .option("--json", "Output as JSON")
46
+ .action(async (name, options) => {
47
+ try {
48
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
49
+ if (!ctx.db) {
50
+ logger.error("Database not available");
51
+ process.exit(1);
52
+ }
53
+ const db = ctx.db.getDatabase();
54
+ const result = createOrg(db, name, options.owner, options.description);
55
+
56
+ if (options.json) {
57
+ console.log(JSON.stringify(result, null, 2));
58
+ } else {
59
+ logger.success("Organization created");
60
+ logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(result.id)}`);
61
+ logger.log(` ${chalk.bold("Name:")} ${result.name}`);
62
+ logger.log(` ${chalk.bold("Owner:")} ${result.ownerId}`);
63
+ }
64
+
65
+ await shutdown();
66
+ } catch (err) {
67
+ logger.error(`Failed: ${err.message}`);
68
+ process.exit(1);
69
+ }
70
+ });
71
+
72
+ // org list
73
+ org
74
+ .command("list", { isDefault: true })
75
+ .description("List all organizations")
76
+ .option("--json", "Output as JSON")
77
+ .action(async (options) => {
78
+ try {
79
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
80
+ if (!ctx.db) {
81
+ logger.error("Database not available");
82
+ process.exit(1);
83
+ }
84
+ const db = ctx.db.getDatabase();
85
+ const orgs = listOrgs(db);
86
+
87
+ if (options.json) {
88
+ console.log(JSON.stringify(orgs, null, 2));
89
+ } else if (orgs.length === 0) {
90
+ logger.info(
91
+ 'No organizations. Create one with "chainlesschain org create <name>"',
92
+ );
93
+ } else {
94
+ logger.log(chalk.bold(`Organizations (${orgs.length}):\n`));
95
+ for (const o of orgs) {
96
+ const status =
97
+ o.status === "active"
98
+ ? chalk.green("active")
99
+ : chalk.gray(o.status);
100
+ logger.log(` ${chalk.cyan(o.id)} - ${o.name} [${status}]`);
101
+ }
102
+ }
103
+
104
+ await shutdown();
105
+ } catch (err) {
106
+ logger.error(`Failed: ${err.message}`);
107
+ process.exit(1);
108
+ }
109
+ });
110
+
111
+ // org show
112
+ org
113
+ .command("show")
114
+ .description("Show organization details")
115
+ .argument("<org-id>", "Organization ID")
116
+ .option("--json", "Output as JSON")
117
+ .action(async (orgId, options) => {
118
+ try {
119
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
120
+ if (!ctx.db) {
121
+ logger.error("Database not available");
122
+ process.exit(1);
123
+ }
124
+ const db = ctx.db.getDatabase();
125
+ const orgData = getOrg(db, orgId);
126
+
127
+ if (!orgData) {
128
+ logger.error(`Organization not found: ${orgId}`);
129
+ process.exit(1);
130
+ }
131
+
132
+ const summary = getOrgSummary(db, orgId);
133
+
134
+ if (options.json) {
135
+ console.log(JSON.stringify({ ...orgData, ...summary }, null, 2));
136
+ } else {
137
+ logger.log(chalk.bold("Organization:\n"));
138
+ logger.log(
139
+ ` ${chalk.bold("ID:")} ${chalk.cyan(orgData.id)}`,
140
+ );
141
+ logger.log(` ${chalk.bold("Name:")} ${orgData.name}`);
142
+ logger.log(
143
+ ` ${chalk.bold("Description:")} ${orgData.description || chalk.gray("(none)")}`,
144
+ );
145
+ logger.log(` ${chalk.bold("Owner:")} ${orgData.owner_id}`);
146
+ logger.log(` ${chalk.bold("Members:")} ${summary.memberCount}`);
147
+ logger.log(` ${chalk.bold("Teams:")} ${summary.teamCount}`);
148
+ logger.log(
149
+ ` ${chalk.bold("Pending:")} ${summary.pendingApprovals}`,
150
+ );
151
+ }
152
+
153
+ await shutdown();
154
+ } catch (err) {
155
+ logger.error(`Failed: ${err.message}`);
156
+ process.exit(1);
157
+ }
158
+ });
159
+
160
+ // org delete
161
+ org
162
+ .command("delete")
163
+ .description("Delete an organization")
164
+ .argument("<org-id>", "Organization ID")
165
+ .option("--force", "Skip confirmation")
166
+ .action(async (orgId, options) => {
167
+ try {
168
+ if (!options.force) {
169
+ const { confirm } = await import("@inquirer/prompts");
170
+ const ok = await confirm({
171
+ message: "Delete this organization and all its data?",
172
+ });
173
+ if (!ok) {
174
+ logger.info("Cancelled");
175
+ return;
176
+ }
177
+ }
178
+
179
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
180
+ if (!ctx.db) {
181
+ logger.error("Database not available");
182
+ process.exit(1);
183
+ }
184
+ const db = ctx.db.getDatabase();
185
+ const ok = deleteOrg(db, orgId);
186
+
187
+ if (ok) {
188
+ logger.success("Organization deleted");
189
+ } else {
190
+ logger.error(`Organization not found: ${orgId}`);
191
+ }
192
+
193
+ await shutdown();
194
+ } catch (err) {
195
+ logger.error(`Failed: ${err.message}`);
196
+ process.exit(1);
197
+ }
198
+ });
199
+
200
+ // org invite
201
+ org
202
+ .command("invite")
203
+ .description("Invite a member to an organization")
204
+ .argument("<org-id>", "Organization ID")
205
+ .argument("<user-id>", "User ID to invite")
206
+ .option("--name <name>", "Display name")
207
+ .option("--role <role>", "Role (admin/member/viewer)", "member")
208
+ .action(async (orgId, userId, options) => {
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
+ const result = inviteMember(
217
+ db,
218
+ orgId,
219
+ userId,
220
+ options.name,
221
+ options.role,
222
+ );
223
+ logger.success(`Invited ${userId} as ${result.role}`);
224
+ await shutdown();
225
+ } catch (err) {
226
+ logger.error(`Failed: ${err.message}`);
227
+ process.exit(1);
228
+ }
229
+ });
230
+
231
+ // org members
232
+ org
233
+ .command("members")
234
+ .description("List organization members")
235
+ .argument("<org-id>", "Organization ID")
236
+ .option("--json", "Output as JSON")
237
+ .action(async (orgId, options) => {
238
+ try {
239
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
240
+ if (!ctx.db) {
241
+ logger.error("Database not available");
242
+ process.exit(1);
243
+ }
244
+ const db = ctx.db.getDatabase();
245
+ const members = getMembers(db, orgId);
246
+
247
+ if (options.json) {
248
+ console.log(JSON.stringify(members, null, 2));
249
+ } else if (members.length === 0) {
250
+ logger.info("No members");
251
+ } else {
252
+ logger.log(chalk.bold(`Members (${members.length}):\n`));
253
+ for (const m of members) {
254
+ const role =
255
+ m.role === "admin" ? chalk.yellow(m.role) : chalk.gray(m.role);
256
+ const status =
257
+ m.status === "active"
258
+ ? chalk.green("active")
259
+ : chalk.gray(m.status);
260
+ logger.log(` ${m.user_id} [${role}] ${status}`);
261
+ }
262
+ }
263
+
264
+ await shutdown();
265
+ } catch (err) {
266
+ logger.error(`Failed: ${err.message}`);
267
+ process.exit(1);
268
+ }
269
+ });
270
+
271
+ // org team create
272
+ org
273
+ .command("team-create")
274
+ .description("Create a team")
275
+ .argument("<org-id>", "Organization ID")
276
+ .argument("<team-name>", "Team name")
277
+ .option("--description <desc>", "Team description")
278
+ .option("--lead <id>", "Team lead user ID")
279
+ .option("--json", "Output as JSON")
280
+ .action(async (orgId, teamName, options) => {
281
+ try {
282
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
283
+ if (!ctx.db) {
284
+ logger.error("Database not available");
285
+ process.exit(1);
286
+ }
287
+ const db = ctx.db.getDatabase();
288
+ const team = createTeam(
289
+ db,
290
+ orgId,
291
+ teamName,
292
+ options.description,
293
+ options.lead,
294
+ );
295
+
296
+ if (options.json) {
297
+ console.log(JSON.stringify(team, null, 2));
298
+ } else {
299
+ logger.success("Team created");
300
+ logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(team.id)}`);
301
+ logger.log(` ${chalk.bold("Name:")} ${team.name}`);
302
+ }
303
+
304
+ await shutdown();
305
+ } catch (err) {
306
+ logger.error(`Failed: ${err.message}`);
307
+ process.exit(1);
308
+ }
309
+ });
310
+
311
+ // org teams
312
+ org
313
+ .command("teams")
314
+ .description("List teams in an organization")
315
+ .argument("<org-id>", "Organization ID")
316
+ .option("--json", "Output as JSON")
317
+ .action(async (orgId, options) => {
318
+ try {
319
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
320
+ if (!ctx.db) {
321
+ logger.error("Database not available");
322
+ process.exit(1);
323
+ }
324
+ const db = ctx.db.getDatabase();
325
+ const teams = listTeams(db, orgId);
326
+
327
+ if (options.json) {
328
+ console.log(JSON.stringify(teams, null, 2));
329
+ } else if (teams.length === 0) {
330
+ logger.info("No teams");
331
+ } else {
332
+ logger.log(chalk.bold(`Teams (${teams.length}):\n`));
333
+ for (const t of teams) {
334
+ logger.log(` ${chalk.cyan(t.id)} - ${t.name}`);
335
+ if (t.description) logger.log(` ${chalk.gray(t.description)}`);
336
+ }
337
+ }
338
+
339
+ await shutdown();
340
+ } catch (err) {
341
+ logger.error(`Failed: ${err.message}`);
342
+ process.exit(1);
343
+ }
344
+ });
345
+
346
+ // org approval submit
347
+ org
348
+ .command("approval-submit")
349
+ .description("Submit an approval request")
350
+ .argument("<org-id>", "Organization ID")
351
+ .argument("<title>", "Request title")
352
+ .option("--type <type>", "Request type", "general")
353
+ .option("--description <desc>", "Request description")
354
+ .option("--requester <id>", "Requester ID", "cli-user")
355
+ .option("--json", "Output as JSON")
356
+ .action(async (orgId, title, options) => {
357
+ try {
358
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
359
+ if (!ctx.db) {
360
+ logger.error("Database not available");
361
+ process.exit(1);
362
+ }
363
+ const db = ctx.db.getDatabase();
364
+ const result = submitApproval(
365
+ db,
366
+ orgId,
367
+ options.requester,
368
+ options.type,
369
+ title,
370
+ options.description,
371
+ );
372
+
373
+ if (options.json) {
374
+ console.log(JSON.stringify(result, null, 2));
375
+ } else {
376
+ logger.success("Approval request submitted");
377
+ logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(result.id)}`);
378
+ logger.log(
379
+ ` ${chalk.bold("Status:")} ${chalk.yellow(result.status)}`,
380
+ );
381
+ }
382
+
383
+ await shutdown();
384
+ } catch (err) {
385
+ logger.error(`Failed: ${err.message}`);
386
+ process.exit(1);
387
+ }
388
+ });
389
+
390
+ // org approval list
391
+ org
392
+ .command("approvals")
393
+ .description("List approval requests")
394
+ .option("--org <id>", "Filter by organization")
395
+ .option("--status <status>", "Filter by status (pending/approved/rejected)")
396
+ .option("--json", "Output as JSON")
397
+ .action(async (options) => {
398
+ try {
399
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
400
+ if (!ctx.db) {
401
+ logger.error("Database not available");
402
+ process.exit(1);
403
+ }
404
+ const db = ctx.db.getDatabase();
405
+ const approvals = getApprovals(db, {
406
+ orgId: options.org,
407
+ status: options.status,
408
+ });
409
+
410
+ if (options.json) {
411
+ console.log(JSON.stringify(approvals, null, 2));
412
+ } else if (approvals.length === 0) {
413
+ logger.info("No approval requests");
414
+ } else {
415
+ logger.log(chalk.bold(`Approvals (${approvals.length}):\n`));
416
+ for (const a of approvals) {
417
+ const statusColor =
418
+ a.status === "approved"
419
+ ? chalk.green
420
+ : a.status === "rejected"
421
+ ? chalk.red
422
+ : chalk.yellow;
423
+ logger.log(
424
+ ` ${chalk.cyan(a.id)} ${a.title} [${statusColor(a.status)}]`,
425
+ );
426
+ }
427
+ }
428
+
429
+ await shutdown();
430
+ } catch (err) {
431
+ logger.error(`Failed: ${err.message}`);
432
+ process.exit(1);
433
+ }
434
+ });
435
+
436
+ // org approval approve
437
+ org
438
+ .command("approve")
439
+ .description("Approve a request")
440
+ .argument("<approval-id>", "Approval request ID")
441
+ .option("--approver <id>", "Approver ID", "cli-user")
442
+ .option("--note <note>", "Decision note")
443
+ .action(async (approvalId, options) => {
444
+ try {
445
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
446
+ if (!ctx.db) {
447
+ logger.error("Database not available");
448
+ process.exit(1);
449
+ }
450
+ const db = ctx.db.getDatabase();
451
+ const ok = approveRequest(
452
+ db,
453
+ approvalId,
454
+ options.approver,
455
+ options.note,
456
+ );
457
+
458
+ if (ok) {
459
+ logger.success("Request approved");
460
+ } else {
461
+ logger.error(`Request not found: ${approvalId}`);
462
+ }
463
+
464
+ await shutdown();
465
+ } catch (err) {
466
+ logger.error(`Failed: ${err.message}`);
467
+ process.exit(1);
468
+ }
469
+ });
470
+
471
+ // org approval reject
472
+ org
473
+ .command("reject")
474
+ .description("Reject a request")
475
+ .argument("<approval-id>", "Approval request ID")
476
+ .option("--approver <id>", "Approver ID", "cli-user")
477
+ .option("--note <note>", "Decision note")
478
+ .action(async (approvalId, options) => {
479
+ try {
480
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
481
+ if (!ctx.db) {
482
+ logger.error("Database not available");
483
+ process.exit(1);
484
+ }
485
+ const db = ctx.db.getDatabase();
486
+ const ok = rejectRequest(
487
+ db,
488
+ approvalId,
489
+ options.approver,
490
+ options.note,
491
+ );
492
+
493
+ if (ok) {
494
+ logger.success("Request rejected");
495
+ } else {
496
+ logger.error(`Request not found: ${approvalId}`);
497
+ }
498
+
499
+ await shutdown();
500
+ } catch (err) {
501
+ logger.error(`Failed: ${err.message}`);
502
+ process.exit(1);
503
+ }
504
+ });
505
+ }