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.
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chainlesschain",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.9",
|
|
4
4
|
"description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,20 +49,22 @@
|
|
|
49
49
|
},
|
|
50
50
|
"homepage": "https://www.chainlesschain.com",
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"
|
|
52
|
+
"@chainlesschain/core-config": "0.1.0",
|
|
53
|
+
"@chainlesschain/core-db": "0.1.0",
|
|
54
|
+
"@chainlesschain/core-env": "0.1.0",
|
|
55
|
+
"@chainlesschain/core-infra": "0.1.0",
|
|
56
|
+
"@chainlesschain/session-core": "0.3.1",
|
|
57
|
+
"@chainlesschain/shared-logger": "0.1.0",
|
|
53
58
|
"@inquirer/prompts": "^7.2.0",
|
|
54
59
|
"chalk": "^5.4.1",
|
|
60
|
+
"commander": "^12.1.0",
|
|
55
61
|
"ora": "^8.1.1",
|
|
56
62
|
"semver": "^7.6.3",
|
|
57
|
-
"@chainlesschain/core-env": "0.1.0",
|
|
58
|
-
"@chainlesschain/shared-logger": "0.1.0",
|
|
59
|
-
"@chainlesschain/core-db": "0.1.0",
|
|
60
|
-
"@chainlesschain/core-config": "0.1.0",
|
|
61
|
-
"@chainlesschain/core-infra": "0.1.0",
|
|
62
|
-
"@chainlesschain/session-core": "0.3.0",
|
|
63
63
|
"ws": "^8.14.2"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
+
"@noble/curves": "^1.9.7",
|
|
67
|
+
"@scure/base": "^1.2.6",
|
|
66
68
|
"vitest": "^3.1.1"
|
|
67
69
|
}
|
|
68
70
|
}
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivityPub C2S commands
|
|
3
|
+
* chainlesschain activitypub actor|publish|follow|unfollow|outbox|inbox|...
|
|
4
|
+
*
|
|
5
|
+
* In-memory bridge that implements the W3C ActivityPub Client-to-Server
|
|
6
|
+
* surface (actors, outbox, inbox, follow graph). Network delivery is
|
|
7
|
+
* simulated — `activitypub inbox deliver` lets you push a JSON activity
|
|
8
|
+
* into a local actor's inbox.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import { logger } from "../lib/logger.js";
|
|
13
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
14
|
+
import {
|
|
15
|
+
ensureActivityPubTables,
|
|
16
|
+
createActor,
|
|
17
|
+
listActors,
|
|
18
|
+
getActor,
|
|
19
|
+
publishNote,
|
|
20
|
+
follow,
|
|
21
|
+
acceptFollow,
|
|
22
|
+
undoFollow,
|
|
23
|
+
like,
|
|
24
|
+
announce,
|
|
25
|
+
deliverToInbox,
|
|
26
|
+
getOutbox,
|
|
27
|
+
getInbox,
|
|
28
|
+
listFollowers,
|
|
29
|
+
listFollowing,
|
|
30
|
+
searchActors,
|
|
31
|
+
searchNotes,
|
|
32
|
+
} from "../lib/activitypub-bridge.js";
|
|
33
|
+
|
|
34
|
+
async function bootstrapDb(program) {
|
|
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
|
+
ensureActivityPubTables(db);
|
|
42
|
+
return db;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerActivityPubCommand(program) {
|
|
46
|
+
const ap = program
|
|
47
|
+
.command("activitypub")
|
|
48
|
+
.alias("ap")
|
|
49
|
+
.description(
|
|
50
|
+
"ActivityPub C2S bridge — actors, outbox/inbox, Follow/Create/Like/Announce",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// ── Actor management ──────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const actor = ap
|
|
56
|
+
.command("actor")
|
|
57
|
+
.description("Manage local and remote ActivityPub actors");
|
|
58
|
+
|
|
59
|
+
actor
|
|
60
|
+
.command("create <username>")
|
|
61
|
+
.description("Create a local Person actor")
|
|
62
|
+
.option("-n, --name <name>", "Display name (defaults to username)")
|
|
63
|
+
.option("-s, --summary <text>", "Profile summary / bio")
|
|
64
|
+
.option("--origin <url>", "Origin URL for the actor id")
|
|
65
|
+
.option("--json", "Output as JSON")
|
|
66
|
+
.action(async (username, options) => {
|
|
67
|
+
try {
|
|
68
|
+
const db = await bootstrapDb(program);
|
|
69
|
+
const result = createActor(db, {
|
|
70
|
+
username,
|
|
71
|
+
name: options.name,
|
|
72
|
+
summary: options.summary,
|
|
73
|
+
origin: options.origin,
|
|
74
|
+
});
|
|
75
|
+
if (options.json) {
|
|
76
|
+
console.log(JSON.stringify(result, null, 2));
|
|
77
|
+
} else {
|
|
78
|
+
logger.success(`Actor created: ${chalk.cyan(result.actor.id)}`);
|
|
79
|
+
logger.log(` ${chalk.bold("Name:")} ${result.actor.name || "-"}`);
|
|
80
|
+
logger.log(` ${chalk.bold("Inbox:")} ${result.actor.inbox}`);
|
|
81
|
+
logger.log(` ${chalk.bold("Outbox:")} ${result.actor.outbox}`);
|
|
82
|
+
}
|
|
83
|
+
await shutdown();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.error(`Failed: ${err.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
actor
|
|
91
|
+
.command("list")
|
|
92
|
+
.description("List known actors")
|
|
93
|
+
.option("--local", "Local actors only")
|
|
94
|
+
.option("--remote", "Remote actors only")
|
|
95
|
+
.option("--json", "Output as JSON")
|
|
96
|
+
.action(async (options) => {
|
|
97
|
+
try {
|
|
98
|
+
await bootstrapDb(program);
|
|
99
|
+
const filter =
|
|
100
|
+
options.local && !options.remote
|
|
101
|
+
? { local: true }
|
|
102
|
+
: options.remote && !options.local
|
|
103
|
+
? { local: false }
|
|
104
|
+
: {};
|
|
105
|
+
const actors = listActors(filter);
|
|
106
|
+
if (options.json) {
|
|
107
|
+
console.log(JSON.stringify(actors, null, 2));
|
|
108
|
+
} else if (actors.length === 0) {
|
|
109
|
+
logger.info("No actors.");
|
|
110
|
+
} else {
|
|
111
|
+
for (const a of actors) {
|
|
112
|
+
const scope = a.isLocal
|
|
113
|
+
? chalk.green("local")
|
|
114
|
+
: chalk.yellow("remote");
|
|
115
|
+
logger.log(` [${scope}] ${chalk.cyan(a.id)} — ${a.name || "-"}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await shutdown();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logger.error(`Failed: ${err.message}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
actor
|
|
126
|
+
.command("show <username>")
|
|
127
|
+
.description("Show an actor's profile (username or full URL)")
|
|
128
|
+
.option("--json", "Output as JSON")
|
|
129
|
+
.action(async (username, options) => {
|
|
130
|
+
try {
|
|
131
|
+
await bootstrapDb(program);
|
|
132
|
+
const found = getActor(username);
|
|
133
|
+
if (!found) {
|
|
134
|
+
logger.error(`Actor not found: ${username}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
if (options.json) {
|
|
138
|
+
console.log(JSON.stringify(found, null, 2));
|
|
139
|
+
} else {
|
|
140
|
+
logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(found.id)}`);
|
|
141
|
+
logger.log(` ${chalk.bold("Name:")} ${found.name || "-"}`);
|
|
142
|
+
logger.log(` ${chalk.bold("Summary:")} ${found.summary || "-"}`);
|
|
143
|
+
logger.log(` ${chalk.bold("Inbox:")} ${found.inbox}`);
|
|
144
|
+
logger.log(` ${chalk.bold("Outbox:")} ${found.outbox}`);
|
|
145
|
+
logger.log(` ${chalk.bold("Local:")} ${found.isLocal}`);
|
|
146
|
+
}
|
|
147
|
+
await shutdown();
|
|
148
|
+
} catch (err) {
|
|
149
|
+
logger.error(`Failed: ${err.message}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── Publishing ────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
ap.command("publish <content>")
|
|
157
|
+
.description("Publish a Create(Note) activity to the actor's outbox")
|
|
158
|
+
.requiredOption(
|
|
159
|
+
"-a, --actor <username>",
|
|
160
|
+
"Authoring actor (username or URL)",
|
|
161
|
+
)
|
|
162
|
+
.option("--to <target...>", "Primary audience URLs")
|
|
163
|
+
.option("--cc <target...>", "Secondary audience URLs")
|
|
164
|
+
.option("--reply-to <id>", "Reply to an existing Note id")
|
|
165
|
+
.option("--json", "Output as JSON")
|
|
166
|
+
.action(async (content, options) => {
|
|
167
|
+
try {
|
|
168
|
+
const db = await bootstrapDb(program);
|
|
169
|
+
const result = publishNote(db, {
|
|
170
|
+
actor: options.actor,
|
|
171
|
+
content,
|
|
172
|
+
to: options.to,
|
|
173
|
+
cc: options.cc,
|
|
174
|
+
inReplyTo: options.replyTo,
|
|
175
|
+
});
|
|
176
|
+
if (options.json) {
|
|
177
|
+
console.log(JSON.stringify(result, null, 2));
|
|
178
|
+
} else {
|
|
179
|
+
logger.success(`Note published by ${chalk.cyan(options.actor)}`);
|
|
180
|
+
logger.log(` ${chalk.bold("Activity:")} ${result.activity.id}`);
|
|
181
|
+
logger.log(
|
|
182
|
+
` ${chalk.bold("Note:")} ${result.activity.object.id}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
await shutdown();
|
|
186
|
+
} catch (err) {
|
|
187
|
+
logger.error(`Failed: ${err.message}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
ap.command("follow <target>")
|
|
193
|
+
.description("Publish a Follow activity (target: username or actor URL)")
|
|
194
|
+
.requiredOption("-a, --actor <username>", "Follower actor")
|
|
195
|
+
.option("--json", "Output as JSON")
|
|
196
|
+
.action(async (target, options) => {
|
|
197
|
+
try {
|
|
198
|
+
const db = await bootstrapDb(program);
|
|
199
|
+
const result = follow(db, { actor: options.actor, target });
|
|
200
|
+
if (options.json) {
|
|
201
|
+
console.log(JSON.stringify(result, null, 2));
|
|
202
|
+
} else {
|
|
203
|
+
logger.success(
|
|
204
|
+
`${chalk.cyan(options.actor)} → Follow(${chalk.cyan(target)})`,
|
|
205
|
+
);
|
|
206
|
+
logger.log(` ${chalk.bold("Activity:")} ${result.activity.id}`);
|
|
207
|
+
}
|
|
208
|
+
await shutdown();
|
|
209
|
+
} catch (err) {
|
|
210
|
+
logger.error(`Failed: ${err.message}`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
ap.command("accept <follow-activity-id>")
|
|
216
|
+
.description("Accept a pending Follow request")
|
|
217
|
+
.requiredOption(
|
|
218
|
+
"-a, --actor <username>",
|
|
219
|
+
"Target actor (the one being followed)",
|
|
220
|
+
)
|
|
221
|
+
.option("--json", "Output as JSON")
|
|
222
|
+
.action(async (followActivityId, options) => {
|
|
223
|
+
try {
|
|
224
|
+
const db = await bootstrapDb(program);
|
|
225
|
+
const result = acceptFollow(db, {
|
|
226
|
+
actor: options.actor,
|
|
227
|
+
followActivityId,
|
|
228
|
+
});
|
|
229
|
+
if (options.json) {
|
|
230
|
+
console.log(JSON.stringify(result, null, 2));
|
|
231
|
+
} else {
|
|
232
|
+
logger.success(`Follow accepted: ${chalk.cyan(result.activity.id)}`);
|
|
233
|
+
}
|
|
234
|
+
await shutdown();
|
|
235
|
+
} catch (err) {
|
|
236
|
+
logger.error(`Failed: ${err.message}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
ap.command("unfollow <target>")
|
|
242
|
+
.description("Publish an Undo(Follow) activity")
|
|
243
|
+
.requiredOption("-a, --actor <username>", "Follower actor")
|
|
244
|
+
.option("--json", "Output as JSON")
|
|
245
|
+
.action(async (target, options) => {
|
|
246
|
+
try {
|
|
247
|
+
const db = await bootstrapDb(program);
|
|
248
|
+
const result = undoFollow(db, { actor: options.actor, target });
|
|
249
|
+
if (options.json) {
|
|
250
|
+
console.log(JSON.stringify(result, null, 2));
|
|
251
|
+
} else {
|
|
252
|
+
logger.success(
|
|
253
|
+
`${chalk.cyan(options.actor)} → Undo(Follow(${chalk.cyan(target)}))`,
|
|
254
|
+
);
|
|
255
|
+
logger.log(` ${chalk.bold("Activity:")} ${result.activity.id}`);
|
|
256
|
+
}
|
|
257
|
+
await shutdown();
|
|
258
|
+
} catch (err) {
|
|
259
|
+
logger.error(`Failed: ${err.message}`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
ap.command("like <object-id>")
|
|
265
|
+
.description("Publish a Like activity for an object URL")
|
|
266
|
+
.requiredOption("-a, --actor <username>", "Actor")
|
|
267
|
+
.option("--json", "Output as JSON")
|
|
268
|
+
.action(async (objectId, options) => {
|
|
269
|
+
try {
|
|
270
|
+
const db = await bootstrapDb(program);
|
|
271
|
+
const result = like(db, { actor: options.actor, object: objectId });
|
|
272
|
+
if (options.json) {
|
|
273
|
+
console.log(JSON.stringify(result, null, 2));
|
|
274
|
+
} else {
|
|
275
|
+
logger.success(`Liked ${chalk.cyan(objectId)}`);
|
|
276
|
+
logger.log(` ${chalk.bold("Activity:")} ${result.activity.id}`);
|
|
277
|
+
}
|
|
278
|
+
await shutdown();
|
|
279
|
+
} catch (err) {
|
|
280
|
+
logger.error(`Failed: ${err.message}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
ap.command("announce <object-id>")
|
|
286
|
+
.description("Publish an Announce (boost) activity for an object URL")
|
|
287
|
+
.requiredOption("-a, --actor <username>", "Actor")
|
|
288
|
+
.option("--json", "Output as JSON")
|
|
289
|
+
.action(async (objectId, options) => {
|
|
290
|
+
try {
|
|
291
|
+
const db = await bootstrapDb(program);
|
|
292
|
+
const result = announce(db, {
|
|
293
|
+
actor: options.actor,
|
|
294
|
+
object: objectId,
|
|
295
|
+
});
|
|
296
|
+
if (options.json) {
|
|
297
|
+
console.log(JSON.stringify(result, null, 2));
|
|
298
|
+
} else {
|
|
299
|
+
logger.success(`Announced ${chalk.cyan(objectId)}`);
|
|
300
|
+
logger.log(` ${chalk.bold("Activity:")} ${result.activity.id}`);
|
|
301
|
+
}
|
|
302
|
+
await shutdown();
|
|
303
|
+
} catch (err) {
|
|
304
|
+
logger.error(`Failed: ${err.message}`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ── Outbox / Inbox reads ──────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
ap.command("outbox <username>")
|
|
312
|
+
.description("List an actor's outbox activities")
|
|
313
|
+
.option("-n, --limit <n>", "Max activities", "50")
|
|
314
|
+
.option(
|
|
315
|
+
"-t, --type <type...>",
|
|
316
|
+
"Filter by activity type (Create, Follow, ...)",
|
|
317
|
+
)
|
|
318
|
+
.option("--json", "Output as JSON")
|
|
319
|
+
.action(async (username, options) => {
|
|
320
|
+
try {
|
|
321
|
+
await bootstrapDb(program);
|
|
322
|
+
const items = getOutbox(username, {
|
|
323
|
+
limit: parseInt(options.limit),
|
|
324
|
+
types: options.type,
|
|
325
|
+
});
|
|
326
|
+
if (options.json) {
|
|
327
|
+
console.log(JSON.stringify(items, null, 2));
|
|
328
|
+
} else if (items.length === 0) {
|
|
329
|
+
logger.info("Outbox empty.");
|
|
330
|
+
} else {
|
|
331
|
+
for (const a of items) {
|
|
332
|
+
const body =
|
|
333
|
+
typeof a.object === "string"
|
|
334
|
+
? a.object
|
|
335
|
+
: a.object?.content || a.object?.id || "";
|
|
336
|
+
logger.log(
|
|
337
|
+
` ${chalk.cyan(a.type.padEnd(10))} ${a.published} ${body.slice(0, 80)}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
await shutdown();
|
|
342
|
+
} catch (err) {
|
|
343
|
+
logger.error(`Failed: ${err.message}`);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
ap.command("inbox <username>")
|
|
349
|
+
.description("List an actor's inbox activities")
|
|
350
|
+
.option("-n, --limit <n>", "Max activities", "50")
|
|
351
|
+
.option("-t, --type <type...>", "Filter by activity type")
|
|
352
|
+
.option("--json", "Output as JSON")
|
|
353
|
+
.action(async (username, options) => {
|
|
354
|
+
try {
|
|
355
|
+
await bootstrapDb(program);
|
|
356
|
+
const items = getInbox(username, {
|
|
357
|
+
limit: parseInt(options.limit),
|
|
358
|
+
types: options.type,
|
|
359
|
+
});
|
|
360
|
+
if (options.json) {
|
|
361
|
+
console.log(JSON.stringify(items, null, 2));
|
|
362
|
+
} else if (items.length === 0) {
|
|
363
|
+
logger.info("Inbox empty.");
|
|
364
|
+
} else {
|
|
365
|
+
for (const a of items) {
|
|
366
|
+
const body =
|
|
367
|
+
typeof a.object === "string"
|
|
368
|
+
? a.object
|
|
369
|
+
: a.object?.content || a.object?.id || "";
|
|
370
|
+
logger.log(
|
|
371
|
+
` ${chalk.cyan(a.type.padEnd(10))} from ${a.actor} ${body.slice(0, 80)}`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
await shutdown();
|
|
376
|
+
} catch (err) {
|
|
377
|
+
logger.error(`Failed: ${err.message}`);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
ap.command("deliver <username> <activity-json>")
|
|
383
|
+
.description(
|
|
384
|
+
"Simulate inbox delivery — push a JSON activity into a local actor's inbox",
|
|
385
|
+
)
|
|
386
|
+
.option("--json", "Output as JSON")
|
|
387
|
+
.action(async (username, activityJson, options) => {
|
|
388
|
+
try {
|
|
389
|
+
const db = await bootstrapDb(program);
|
|
390
|
+
let activity;
|
|
391
|
+
try {
|
|
392
|
+
activity = JSON.parse(activityJson);
|
|
393
|
+
} catch (e) {
|
|
394
|
+
throw new Error(`Invalid JSON activity: ${e.message}`);
|
|
395
|
+
}
|
|
396
|
+
const result = deliverToInbox(db, { actor: username, activity });
|
|
397
|
+
if (options.json) {
|
|
398
|
+
console.log(JSON.stringify(result, null, 2));
|
|
399
|
+
} else {
|
|
400
|
+
logger.success(
|
|
401
|
+
`Delivered ${result.activity.type} to ${chalk.cyan(username)}`,
|
|
402
|
+
);
|
|
403
|
+
logger.log(` ${chalk.bold("Activity:")} ${result.activity.id}`);
|
|
404
|
+
}
|
|
405
|
+
await shutdown();
|
|
406
|
+
} catch (err) {
|
|
407
|
+
logger.error(`Failed: ${err.message}`);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// ── Follow graph ──────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
ap.command("followers <username>")
|
|
415
|
+
.description("List followers of an actor")
|
|
416
|
+
.option("--state <state>", "Filter by state (pending | accepted)")
|
|
417
|
+
.option("--json", "Output as JSON")
|
|
418
|
+
.action(async (username, options) => {
|
|
419
|
+
try {
|
|
420
|
+
await bootstrapDb(program);
|
|
421
|
+
const rows = listFollowers(username, { state: options.state });
|
|
422
|
+
if (options.json) {
|
|
423
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
424
|
+
} else if (rows.length === 0) {
|
|
425
|
+
logger.info("No followers.");
|
|
426
|
+
} else {
|
|
427
|
+
for (const r of rows) {
|
|
428
|
+
logger.log(` ${chalk.cyan(r.id)} [${r.state}]`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
await shutdown();
|
|
432
|
+
} catch (err) {
|
|
433
|
+
logger.error(`Failed: ${err.message}`);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ── Fediverse search ──────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
ap.command("search <query>")
|
|
441
|
+
.description("Search actors and/or notes in the local Fediverse index")
|
|
442
|
+
.option("-t, --target <type>", "actors | notes | all", "all")
|
|
443
|
+
.option("-s, --scope <scope>", "local | remote | all", "all")
|
|
444
|
+
.option(
|
|
445
|
+
"-a, --author <username>",
|
|
446
|
+
"Filter notes by author (username or URL)",
|
|
447
|
+
)
|
|
448
|
+
.option("--since <iso>", "Only notes published since this ISO timestamp")
|
|
449
|
+
.option("--until <iso>", "Only notes published until this ISO timestamp")
|
|
450
|
+
.option("-n, --limit <n>", "Max results per target", "20")
|
|
451
|
+
.option("--json", "Output as JSON")
|
|
452
|
+
.action(async (query, options) => {
|
|
453
|
+
try {
|
|
454
|
+
await bootstrapDb(program);
|
|
455
|
+
const limit = parseInt(options.limit);
|
|
456
|
+
const target = options.target;
|
|
457
|
+
const result = {};
|
|
458
|
+
if (target === "actors" || target === "all") {
|
|
459
|
+
result.actors = searchActors(query, {
|
|
460
|
+
limit,
|
|
461
|
+
scope: options.scope,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
if (target === "notes" || target === "all") {
|
|
465
|
+
result.notes = searchNotes(query, {
|
|
466
|
+
limit,
|
|
467
|
+
author: options.author,
|
|
468
|
+
since: options.since,
|
|
469
|
+
until: options.until,
|
|
470
|
+
scope: options.scope,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (options.json) {
|
|
475
|
+
console.log(JSON.stringify(result, null, 2));
|
|
476
|
+
} else {
|
|
477
|
+
const actors = result.actors || [];
|
|
478
|
+
const notes = result.notes || [];
|
|
479
|
+
if (actors.length === 0 && notes.length === 0) {
|
|
480
|
+
logger.info("No matches.");
|
|
481
|
+
} else {
|
|
482
|
+
if (actors.length > 0) {
|
|
483
|
+
logger.log(chalk.bold("Actors:"));
|
|
484
|
+
for (const a of actors) {
|
|
485
|
+
const scope = a.isLocal
|
|
486
|
+
? chalk.green("local")
|
|
487
|
+
: chalk.yellow("remote");
|
|
488
|
+
logger.log(
|
|
489
|
+
` [${scope}] ${chalk.cyan(a.id)} — ${a.name || "-"} (score=${a.score})`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (notes.length > 0) {
|
|
494
|
+
logger.log(chalk.bold("Notes:"));
|
|
495
|
+
for (const n of notes) {
|
|
496
|
+
logger.log(
|
|
497
|
+
` ${chalk.gray(n.actor)} ${chalk.cyan(n.content.slice(0, 80))} (score=${n.score})`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
await shutdown();
|
|
504
|
+
} catch (err) {
|
|
505
|
+
logger.error(`Failed: ${err.message}`);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
ap.command("following <username>")
|
|
511
|
+
.description("List who this actor is following")
|
|
512
|
+
.option("--state <state>", "Filter by state (pending | accepted)")
|
|
513
|
+
.option("--json", "Output as JSON")
|
|
514
|
+
.action(async (username, options) => {
|
|
515
|
+
try {
|
|
516
|
+
await bootstrapDb(program);
|
|
517
|
+
const rows = listFollowing(username, { state: options.state });
|
|
518
|
+
if (options.json) {
|
|
519
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
520
|
+
} else if (rows.length === 0) {
|
|
521
|
+
logger.info("Following no one.");
|
|
522
|
+
} else {
|
|
523
|
+
for (const r of rows) {
|
|
524
|
+
logger.log(` ${chalk.cyan(r.id)} [${r.state}]`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
await shutdown();
|
|
528
|
+
} catch (err) {
|
|
529
|
+
logger.error(`Failed: ${err.message}`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|