chainlesschain 0.47.8 → 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.
@@ -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:")} ${chalk.cyan(result.event.id.slice(0, 16))}...`,
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:")} ${result.sentCount} relay(s)`);
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("Public:")} ${chalk.cyan(kp.publicKey)}`);
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("Private:")} ${kp.privateKey.slice(0, 16)}...`,
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
  }
@@ -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