chainlesschain 0.47.7 → 0.47.9
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/package.json +10 -8
- package/src/commands/activitypub.js +533 -0
- package/src/commands/compliance.js +597 -6
- package/src/commands/matrix.js +283 -0
- package/src/commands/mcp.js +344 -0
- package/src/commands/nostr.js +196 -7
- package/src/commands/social.js +265 -0
- package/src/index.js +2 -0
- package/src/lib/activitypub-bridge.js +623 -0
- package/src/lib/compliance-framework-reporter.js +600 -0
- package/src/lib/matrix-bridge.js +252 -0
- package/src/lib/mcp-registry.js +347 -0
- package/src/lib/mcp-scaffold.js +385 -0
- package/src/lib/nostr-bridge.js +214 -38
- package/src/lib/social-graph.js +408 -0
- package/src/lib/stix-parser.js +167 -0
- package/src/lib/threat-intel.js +268 -0
- package/src/lib/topic-classifier.js +400 -0
- package/src/lib/ueba.js +403 -0
- package/src/repl/agent-repl.js +23 -0
package/src/commands/nostr.js
CHANGED
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
getEvents,
|
|
15
15
|
generateKeypair,
|
|
16
16
|
mapDid,
|
|
17
|
+
publishDirectMessage,
|
|
18
|
+
decryptDirectMessage,
|
|
19
|
+
publishDeletion,
|
|
20
|
+
publishReaction,
|
|
17
21
|
} from "../lib/nostr-bridge.js";
|
|
18
22
|
|
|
19
23
|
export function registerNostrCommand(program) {
|
|
@@ -80,9 +84,13 @@ export function registerNostrCommand(program) {
|
|
|
80
84
|
|
|
81
85
|
nostr
|
|
82
86
|
.command("publish <content>")
|
|
83
|
-
.description("Publish a text note event")
|
|
87
|
+
.description("Publish a text note event (signed if --privkey provided)")
|
|
84
88
|
.option("-k, --kind <n>", "Event kind", "1")
|
|
85
|
-
.option("-p, --pubkey <key>", "Author public key")
|
|
89
|
+
.option("-p, --pubkey <key>", "Author public key (unsigned path)")
|
|
90
|
+
.option(
|
|
91
|
+
"--privkey <hex>",
|
|
92
|
+
"Sign event with this 32-byte private key (hex). Pubkey is derived.",
|
|
93
|
+
)
|
|
86
94
|
.action(async (content, options) => {
|
|
87
95
|
try {
|
|
88
96
|
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
@@ -98,12 +106,17 @@ export function registerNostrCommand(program) {
|
|
|
98
106
|
parseInt(options.kind),
|
|
99
107
|
content,
|
|
100
108
|
options.pubkey,
|
|
109
|
+
[],
|
|
110
|
+
options.privkey,
|
|
101
111
|
);
|
|
102
112
|
logger.success("Event published");
|
|
103
113
|
logger.log(
|
|
104
|
-
` ${chalk.bold("ID:")}
|
|
114
|
+
` ${chalk.bold("ID:")} ${chalk.cyan(result.event.id.slice(0, 16))}...`,
|
|
115
|
+
);
|
|
116
|
+
logger.log(
|
|
117
|
+
` ${chalk.bold("Signed:")} ${result.event.sig ? chalk.green("yes (BIP-340)") : chalk.yellow("no — will be rejected by real relays")}`,
|
|
105
118
|
);
|
|
106
|
-
logger.log(` ${chalk.bold("Sent:")}
|
|
119
|
+
logger.log(` ${chalk.bold("Sent:")} ${result.sentCount} relay(s)`);
|
|
107
120
|
await shutdown();
|
|
108
121
|
} catch (err) {
|
|
109
122
|
logger.error(`Failed: ${err.message}`);
|
|
@@ -158,11 +171,12 @@ export function registerNostrCommand(program) {
|
|
|
158
171
|
if (options.json) {
|
|
159
172
|
console.log(JSON.stringify(kp, null, 2));
|
|
160
173
|
} else {
|
|
161
|
-
logger.success("Keypair generated");
|
|
162
|
-
logger.log(` ${chalk.bold("
|
|
174
|
+
logger.success("Keypair generated (BIP-340 schnorr / NIP-19)");
|
|
175
|
+
logger.log(` ${chalk.bold("npub:")} ${chalk.cyan(kp.npub)}`);
|
|
163
176
|
logger.log(
|
|
164
|
-
` ${chalk.bold("
|
|
177
|
+
` ${chalk.bold("nsec:")} ${chalk.yellow(kp.nsec.slice(0, 20))}… ${chalk.dim("(keep private!)")}`,
|
|
165
178
|
);
|
|
179
|
+
logger.log(` ${chalk.bold("pubHex:")} ${kp.publicKey}`);
|
|
166
180
|
}
|
|
167
181
|
} catch (err) {
|
|
168
182
|
logger.error(`Failed: ${err.message}`);
|
|
@@ -182,4 +196,179 @@ export function registerNostrCommand(program) {
|
|
|
182
196
|
process.exit(1);
|
|
183
197
|
}
|
|
184
198
|
});
|
|
199
|
+
|
|
200
|
+
// ── NIP-04: Encrypted Direct Message ──────────────────────────────
|
|
201
|
+
|
|
202
|
+
nostr
|
|
203
|
+
.command("dm <recipientPubkey> <plaintext>")
|
|
204
|
+
.description("Send a NIP-04 encrypted direct message (kind=4)")
|
|
205
|
+
.requiredOption("--sender-priv <hex>", "Sender's 32-byte private key (hex)")
|
|
206
|
+
.requiredOption(
|
|
207
|
+
"--sender-pub <hex>",
|
|
208
|
+
"Sender's 32-byte x-only public key (hex)",
|
|
209
|
+
)
|
|
210
|
+
.option("--json", "Output as JSON")
|
|
211
|
+
.action(async (recipientPubkey, plaintext, options) => {
|
|
212
|
+
try {
|
|
213
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
214
|
+
if (!ctx.db) {
|
|
215
|
+
logger.error("Database not available");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const db = ctx.db.getDatabase();
|
|
219
|
+
ensureNostrTables(db);
|
|
220
|
+
|
|
221
|
+
const result = publishDirectMessage(db, {
|
|
222
|
+
senderPrivkey: options.senderPriv,
|
|
223
|
+
senderPubkey: options.senderPub,
|
|
224
|
+
recipientPubkey,
|
|
225
|
+
plaintext,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (options.json) {
|
|
229
|
+
console.log(JSON.stringify(result, null, 2));
|
|
230
|
+
} else {
|
|
231
|
+
logger.success("Encrypted DM sent");
|
|
232
|
+
logger.log(
|
|
233
|
+
` ${chalk.bold("ID:")} ${chalk.cyan(result.event.id.slice(0, 16))}...`,
|
|
234
|
+
);
|
|
235
|
+
logger.log(` ${chalk.bold("Sent:")} ${result.sentCount} relay(s)`);
|
|
236
|
+
}
|
|
237
|
+
await shutdown();
|
|
238
|
+
} catch (err) {
|
|
239
|
+
logger.error(`Failed: ${err.message}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
nostr
|
|
245
|
+
.command("dm-decrypt <eventId>")
|
|
246
|
+
.description("Decrypt a stored NIP-04 direct message by event id")
|
|
247
|
+
.requiredOption(
|
|
248
|
+
"--recipient-priv <hex>",
|
|
249
|
+
"Recipient's 32-byte private key (hex)",
|
|
250
|
+
)
|
|
251
|
+
.option("--json", "Output as JSON")
|
|
252
|
+
.action(async (eventId, options) => {
|
|
253
|
+
try {
|
|
254
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
255
|
+
if (!ctx.db) {
|
|
256
|
+
logger.error("Database not available");
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
const db = ctx.db.getDatabase();
|
|
260
|
+
ensureNostrTables(db);
|
|
261
|
+
|
|
262
|
+
const events = getEvents({ kinds: [4], limit: 1000 });
|
|
263
|
+
const event = events.find((e) => e.id === eventId);
|
|
264
|
+
if (!event) {
|
|
265
|
+
logger.error(`Event not found: ${eventId}`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const plaintext = decryptDirectMessage({
|
|
270
|
+
event,
|
|
271
|
+
recipientPrivkey: options.recipientPriv,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (options.json) {
|
|
275
|
+
console.log(JSON.stringify({ success: true, plaintext }, null, 2));
|
|
276
|
+
} else {
|
|
277
|
+
logger.success("Decrypted");
|
|
278
|
+
logger.log(
|
|
279
|
+
` ${chalk.bold("From:")} ${chalk.cyan(event.pubkey.slice(0, 16))}...`,
|
|
280
|
+
);
|
|
281
|
+
logger.log(` ${chalk.bold("Message:")} ${plaintext}`);
|
|
282
|
+
}
|
|
283
|
+
await shutdown();
|
|
284
|
+
} catch (err) {
|
|
285
|
+
logger.error(`Failed: ${err.message}`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ── NIP-09: Event Deletion Request ─────────────────────────────────
|
|
291
|
+
|
|
292
|
+
nostr
|
|
293
|
+
.command("delete <eventIds...>")
|
|
294
|
+
.description("Publish a NIP-09 deletion request (kind=5)")
|
|
295
|
+
.option("-r, --reason <text>", "Reason for deletion", "")
|
|
296
|
+
.option("-p, --pubkey <key>", "Author public key")
|
|
297
|
+
.option("--json", "Output as JSON")
|
|
298
|
+
.action(async (eventIds, options) => {
|
|
299
|
+
try {
|
|
300
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
301
|
+
if (!ctx.db) {
|
|
302
|
+
logger.error("Database not available");
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
const db = ctx.db.getDatabase();
|
|
306
|
+
ensureNostrTables(db);
|
|
307
|
+
|
|
308
|
+
const result = publishDeletion(db, {
|
|
309
|
+
eventIds,
|
|
310
|
+
reason: options.reason,
|
|
311
|
+
pubkey: options.pubkey,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (options.json) {
|
|
315
|
+
console.log(JSON.stringify(result, null, 2));
|
|
316
|
+
} else {
|
|
317
|
+
logger.success(
|
|
318
|
+
`Deletion request published for ${eventIds.length} event(s)`,
|
|
319
|
+
);
|
|
320
|
+
logger.log(
|
|
321
|
+
` ${chalk.bold("ID:")} ${chalk.cyan(result.event.id.slice(0, 16))}...`,
|
|
322
|
+
);
|
|
323
|
+
logger.log(` ${chalk.bold("Sent:")} ${result.sentCount} relay(s)`);
|
|
324
|
+
}
|
|
325
|
+
await shutdown();
|
|
326
|
+
} catch (err) {
|
|
327
|
+
logger.error(`Failed: ${err.message}`);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ── NIP-25: Reactions ──────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
nostr
|
|
335
|
+
.command("react <targetEventId> <targetPubkey>")
|
|
336
|
+
.description(
|
|
337
|
+
'Publish a NIP-25 reaction (kind=7). content "+" | "-" | emoji',
|
|
338
|
+
)
|
|
339
|
+
.option("-c, --content <symbol>", "Reaction symbol", "+")
|
|
340
|
+
.option("-p, --pubkey <key>", "Author public key")
|
|
341
|
+
.option("--json", "Output as JSON")
|
|
342
|
+
.action(async (targetEventId, targetPubkey, options) => {
|
|
343
|
+
try {
|
|
344
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
345
|
+
if (!ctx.db) {
|
|
346
|
+
logger.error("Database not available");
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
const db = ctx.db.getDatabase();
|
|
350
|
+
ensureNostrTables(db);
|
|
351
|
+
|
|
352
|
+
const result = publishReaction(db, {
|
|
353
|
+
targetEventId,
|
|
354
|
+
targetPubkey,
|
|
355
|
+
content: options.content,
|
|
356
|
+
pubkey: options.pubkey,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (options.json) {
|
|
360
|
+
console.log(JSON.stringify(result, null, 2));
|
|
361
|
+
} else {
|
|
362
|
+
logger.success(`Reaction "${options.content}" published`);
|
|
363
|
+
logger.log(
|
|
364
|
+
` ${chalk.bold("ID:")} ${chalk.cyan(result.event.id.slice(0, 16))}...`,
|
|
365
|
+
);
|
|
366
|
+
logger.log(` ${chalk.bold("Sent:")} ${result.sentCount} relay(s)`);
|
|
367
|
+
}
|
|
368
|
+
await shutdown();
|
|
369
|
+
} catch (err) {
|
|
370
|
+
logger.error(`Failed: ${err.message}`);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
185
374
|
}
|
package/src/commands/social.js
CHANGED
|
@@ -24,6 +24,17 @@ import {
|
|
|
24
24
|
getChatThreads,
|
|
25
25
|
getSocialStats,
|
|
26
26
|
} from "../lib/social-manager.js";
|
|
27
|
+
import { classifyTopic, detectLanguage } from "../lib/topic-classifier.js";
|
|
28
|
+
import {
|
|
29
|
+
ensureGraphTables,
|
|
30
|
+
addEdge as graphAddEdge,
|
|
31
|
+
removeEdge as graphRemoveEdge,
|
|
32
|
+
getNeighbors as graphGetNeighbors,
|
|
33
|
+
getGraphSnapshot,
|
|
34
|
+
loadFromDb as graphLoadFromDb,
|
|
35
|
+
subscribe as graphSubscribe,
|
|
36
|
+
EDGE_TYPES,
|
|
37
|
+
} from "../lib/social-graph.js";
|
|
27
38
|
|
|
28
39
|
export function registerSocialCommand(program) {
|
|
29
40
|
const social = program
|
|
@@ -477,4 +488,258 @@ export function registerSocialCommand(program) {
|
|
|
477
488
|
process.exit(1);
|
|
478
489
|
}
|
|
479
490
|
});
|
|
491
|
+
|
|
492
|
+
// ── Analyze (topic classification, language-aware) ──────────
|
|
493
|
+
|
|
494
|
+
social
|
|
495
|
+
.command("analyze <text>")
|
|
496
|
+
.description("Classify text into topics (language-aware, multilingual)")
|
|
497
|
+
.option("-k, --top-k <n>", "Top-K topics to return", "3")
|
|
498
|
+
.option("--lang <code>", "Override detected language (zh|ja|en|other)")
|
|
499
|
+
.option("--min-score <n>", "Drop topics with rawScore <= this", "0")
|
|
500
|
+
.option("--json", "Output as JSON")
|
|
501
|
+
.action((text, options) => {
|
|
502
|
+
try {
|
|
503
|
+
const topK = Math.max(1, parseInt(options.topK, 10) || 3);
|
|
504
|
+
const minScore = Number(options.minScore) || 0;
|
|
505
|
+
const result = classifyTopic(text, {
|
|
506
|
+
topK,
|
|
507
|
+
lang: options.lang,
|
|
508
|
+
minScore,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (options.json) {
|
|
512
|
+
console.log(JSON.stringify(result, null, 2));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
logger.log(
|
|
517
|
+
` ${chalk.bold("Language:")} ${chalk.cyan(result.language)}`,
|
|
518
|
+
);
|
|
519
|
+
logger.log(` ${chalk.bold("Tokens:")} ${result.tokens.length}`);
|
|
520
|
+
if (result.topics.length === 0) {
|
|
521
|
+
logger.log(` ${chalk.dim("(no topic matched)")}`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
logger.log(` ${chalk.bold("Top topics:")}`);
|
|
525
|
+
for (const t of result.topics) {
|
|
526
|
+
const pct = (t.score * 100).toFixed(1);
|
|
527
|
+
logger.log(
|
|
528
|
+
` ${chalk.cyan(t.topic.padEnd(16))} ${pct}% ` +
|
|
529
|
+
`${chalk.dim(`(raw=${t.rawScore}, hits=${t.hits})`)}`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
} catch (err) {
|
|
533
|
+
logger.error(`Failed: ${err.message}`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Language-only quick helper.
|
|
539
|
+
social
|
|
540
|
+
.command("detect-lang <text>")
|
|
541
|
+
.description("Detect dominant language of text (zh|ja|en|other)")
|
|
542
|
+
.option("--json", "Output as JSON")
|
|
543
|
+
.action((text, options) => {
|
|
544
|
+
const language = detectLanguage(text);
|
|
545
|
+
if (options.json) {
|
|
546
|
+
console.log(JSON.stringify({ language }));
|
|
547
|
+
} else {
|
|
548
|
+
logger.log(chalk.cyan(language));
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// ── Social Graph (realtime) ─────────────────────────────────
|
|
553
|
+
|
|
554
|
+
const graph = social
|
|
555
|
+
.command("graph")
|
|
556
|
+
.description("Social graph — typed edges, neighbors, live event stream");
|
|
557
|
+
|
|
558
|
+
graph
|
|
559
|
+
.command("add-edge <source> <target>")
|
|
560
|
+
.description(`Add a directed edge (types: ${EDGE_TYPES.join("|")})`)
|
|
561
|
+
.option("-t, --type <type>", "Edge type", "follow")
|
|
562
|
+
.option("-w, --weight <n>", "Edge weight", "1.0")
|
|
563
|
+
.option("-m, --metadata <json>", "JSON-encoded metadata")
|
|
564
|
+
.option("--json", "Output as JSON")
|
|
565
|
+
.action(async (source, target, options) => {
|
|
566
|
+
try {
|
|
567
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
568
|
+
if (!ctx.db) {
|
|
569
|
+
logger.error("Database not available");
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
const db = ctx.db.getDatabase();
|
|
573
|
+
ensureGraphTables(db);
|
|
574
|
+
graphLoadFromDb(db);
|
|
575
|
+
|
|
576
|
+
const metadata = options.metadata ? JSON.parse(options.metadata) : null;
|
|
577
|
+
const result = graphAddEdge(db, source, target, options.type, {
|
|
578
|
+
weight: Number(options.weight) || 1.0,
|
|
579
|
+
metadata,
|
|
580
|
+
});
|
|
581
|
+
if (options.json) {
|
|
582
|
+
console.log(JSON.stringify(result, null, 2));
|
|
583
|
+
} else {
|
|
584
|
+
const verb = result.created ? "added" : "updated";
|
|
585
|
+
logger.success(
|
|
586
|
+
`Edge ${verb}: ${chalk.cyan(source)} --${options.type}→ ${chalk.cyan(target)}`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
await shutdown();
|
|
590
|
+
} catch (err) {
|
|
591
|
+
logger.error(`Failed: ${err.message}`);
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
graph
|
|
597
|
+
.command("remove-edge <source> <target>")
|
|
598
|
+
.description("Remove a directed edge")
|
|
599
|
+
.option("-t, --type <type>", "Edge type", "follow")
|
|
600
|
+
.option("--json", "Output as JSON")
|
|
601
|
+
.action(async (source, target, options) => {
|
|
602
|
+
try {
|
|
603
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
604
|
+
if (!ctx.db) {
|
|
605
|
+
logger.error("Database not available");
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
const db = ctx.db.getDatabase();
|
|
609
|
+
ensureGraphTables(db);
|
|
610
|
+
graphLoadFromDb(db);
|
|
611
|
+
|
|
612
|
+
const result = graphRemoveEdge(db, source, target, options.type);
|
|
613
|
+
if (options.json) {
|
|
614
|
+
console.log(JSON.stringify(result, null, 2));
|
|
615
|
+
} else if (result.removed) {
|
|
616
|
+
logger.success(
|
|
617
|
+
`Edge removed: ${chalk.cyan(source)} --${options.type}→ ${chalk.cyan(target)}`,
|
|
618
|
+
);
|
|
619
|
+
} else {
|
|
620
|
+
logger.warn("Edge not found");
|
|
621
|
+
}
|
|
622
|
+
await shutdown();
|
|
623
|
+
} catch (err) {
|
|
624
|
+
logger.error(`Failed: ${err.message}`);
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
graph
|
|
630
|
+
.command("neighbors <did>")
|
|
631
|
+
.description("List neighbors of a DID")
|
|
632
|
+
.option("-d, --direction <dir>", "Direction: out | in | both", "both")
|
|
633
|
+
.option("-t, --type <type>", "Filter by edge type")
|
|
634
|
+
.option("--json", "Output as JSON")
|
|
635
|
+
.action(async (did, options) => {
|
|
636
|
+
try {
|
|
637
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
638
|
+
if (!ctx.db) {
|
|
639
|
+
logger.error("Database not available");
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
const db = ctx.db.getDatabase();
|
|
643
|
+
ensureGraphTables(db);
|
|
644
|
+
graphLoadFromDb(db);
|
|
645
|
+
|
|
646
|
+
const neighbors = graphGetNeighbors(did, {
|
|
647
|
+
direction: options.direction,
|
|
648
|
+
edgeType: options.type,
|
|
649
|
+
});
|
|
650
|
+
if (options.json) {
|
|
651
|
+
console.log(JSON.stringify({ did, neighbors }, null, 2));
|
|
652
|
+
} else if (neighbors.length === 0) {
|
|
653
|
+
logger.log(chalk.dim("(no neighbors)"));
|
|
654
|
+
} else {
|
|
655
|
+
for (const n of neighbors) logger.log(` ${chalk.cyan(n)}`);
|
|
656
|
+
}
|
|
657
|
+
await shutdown();
|
|
658
|
+
} catch (err) {
|
|
659
|
+
logger.error(`Failed: ${err.message}`);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
graph
|
|
665
|
+
.command("snapshot")
|
|
666
|
+
.description("Dump the full graph (nodes + edges + stats)")
|
|
667
|
+
.option("-t, --type <type>", "Filter by edge type")
|
|
668
|
+
.option("--json", "Output as JSON (default: yes — snapshot is raw)")
|
|
669
|
+
.action(async (options) => {
|
|
670
|
+
try {
|
|
671
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
672
|
+
if (!ctx.db) {
|
|
673
|
+
logger.error("Database not available");
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
const db = ctx.db.getDatabase();
|
|
677
|
+
ensureGraphTables(db);
|
|
678
|
+
graphLoadFromDb(db);
|
|
679
|
+
|
|
680
|
+
const snapshot = getGraphSnapshot({ edgeType: options.type });
|
|
681
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
682
|
+
await shutdown();
|
|
683
|
+
} catch (err) {
|
|
684
|
+
logger.error(`Failed: ${err.message}`);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
graph
|
|
690
|
+
.command("watch")
|
|
691
|
+
.description("Stream graph change events as NDJSON on stdout")
|
|
692
|
+
.option("-e, --events <list>", "Comma-separated event types (default: all)")
|
|
693
|
+
.option("--once", "Emit the first event then exit")
|
|
694
|
+
.action(async (options) => {
|
|
695
|
+
try {
|
|
696
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
697
|
+
if (!ctx.db) {
|
|
698
|
+
logger.error("Database not available");
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
const db = ctx.db.getDatabase();
|
|
702
|
+
ensureGraphTables(db);
|
|
703
|
+
graphLoadFromDb(db);
|
|
704
|
+
|
|
705
|
+
const events = options.events
|
|
706
|
+
? options.events
|
|
707
|
+
.split(",")
|
|
708
|
+
.map((s) => s.trim())
|
|
709
|
+
.filter(Boolean)
|
|
710
|
+
: null;
|
|
711
|
+
|
|
712
|
+
// Announce subscription on stdout so pipelines know the stream is live.
|
|
713
|
+
process.stdout.write(
|
|
714
|
+
JSON.stringify({
|
|
715
|
+
type: "watch.started",
|
|
716
|
+
events: events || "all",
|
|
717
|
+
at: new Date().toISOString(),
|
|
718
|
+
}) + "\n",
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
let unsubscribe = null;
|
|
722
|
+
const stop = async () => {
|
|
723
|
+
if (unsubscribe) unsubscribe();
|
|
724
|
+
await shutdown();
|
|
725
|
+
process.exit(0);
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
unsubscribe = graphSubscribe(
|
|
729
|
+
(evt) => {
|
|
730
|
+
process.stdout.write(
|
|
731
|
+
JSON.stringify({ ...evt, at: new Date().toISOString() }) + "\n",
|
|
732
|
+
);
|
|
733
|
+
if (options.once) void stop();
|
|
734
|
+
},
|
|
735
|
+
events ? { events } : undefined,
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
process.on("SIGINT", stop);
|
|
739
|
+
process.on("SIGTERM", stop);
|
|
740
|
+
} catch (err) {
|
|
741
|
+
logger.error(`Failed: ${err.message}`);
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
480
745
|
}
|
package/src/index.js
CHANGED
|
@@ -67,6 +67,7 @@ import { registerPqcCommand } from "./commands/pqc.js";
|
|
|
67
67
|
// Phase 8: Communication Bridges
|
|
68
68
|
import { registerNostrCommand } from "./commands/nostr.js";
|
|
69
69
|
import { registerMatrixCommand } from "./commands/matrix.js";
|
|
70
|
+
import { registerActivityPubCommand } from "./commands/activitypub.js";
|
|
70
71
|
import { registerScimCommand } from "./commands/scim.js";
|
|
71
72
|
|
|
72
73
|
// Phase 8: Infrastructure & Hardening
|
|
@@ -193,6 +194,7 @@ export function createProgram() {
|
|
|
193
194
|
// Phase 8: Communication Bridges
|
|
194
195
|
registerNostrCommand(program);
|
|
195
196
|
registerMatrixCommand(program);
|
|
197
|
+
registerActivityPubCommand(program);
|
|
196
198
|
registerScimCommand(program);
|
|
197
199
|
|
|
198
200
|
// Phase 8: Infrastructure & Hardening
|