@aigne/afs-persona 1.11.0-beta.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/dist/index.cjs ADDED
@@ -0,0 +1,987 @@
1
+ const require_db = require('./db.cjs');
2
+ const require_decorate = require('./_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs');
3
+ let node_fs = require("node:fs");
4
+ let node_fs_promises = require("node:fs/promises");
5
+ let node_path = require("node:path");
6
+ let _aigne_afs = require("@aigne/afs");
7
+ let _aigne_afs_utils_zod = require("@aigne/afs/utils/zod");
8
+ let ufo = require("ufo");
9
+ let yaml = require("yaml");
10
+ let zod = require("zod");
11
+
12
+ //#region src/index.ts
13
+ const personaOptionsSchema = zod.z.object({
14
+ name: zod.z.string().optional(),
15
+ description: zod.z.string().optional(),
16
+ accessMode: zod.z.enum(["readonly", "readwrite"]).optional(),
17
+ dataDir: zod.z.string().describe("Root data directory for persona storage"),
18
+ database: zod.z.string().optional().describe("SQLite database path (defaults to dataDir/persona.db)"),
19
+ model: zod.z.string().optional().describe("Default LLM model path for ASH agent-run"),
20
+ ashPath: zod.z.string().optional().describe("ASH provider mount path"),
21
+ tickInterval: zod.z.number().positive().optional().describe("Tick interval in milliseconds"),
22
+ safety: zod.z.object({
23
+ blocked_keywords: zod.z.array(zod.z.string()).optional(),
24
+ review_mode: zod.z.enum(["auto", "manual"]).optional()
25
+ }).optional()
26
+ });
27
+ const INVALID_NAME_PATTERN = /[/\\]/;
28
+ const MAX_NAME_LENGTH = 64;
29
+ function validateCharacterName(name) {
30
+ if (typeof name !== "string" || name.length === 0) return {
31
+ valid: false,
32
+ reason: "Character name must be a non-empty string"
33
+ };
34
+ if (name === "." || name === "..") return {
35
+ valid: false,
36
+ reason: "Character name cannot be . or .."
37
+ };
38
+ if (name.includes("..")) return {
39
+ valid: false,
40
+ reason: "Character name cannot contain path traversal"
41
+ };
42
+ if (INVALID_NAME_PATTERN.test(name)) return {
43
+ valid: false,
44
+ reason: "Character name cannot contain / or \\"
45
+ };
46
+ if (name.length > MAX_NAME_LENGTH) return {
47
+ valid: false,
48
+ reason: `Character name cannot exceed ${MAX_NAME_LENGTH} characters`
49
+ };
50
+ return { valid: true };
51
+ }
52
+ const WORKSPACE_TOML_TEMPLATE = `[[mounts]]
53
+ path = "/profile"
54
+ uri = "json://./profile.yaml"
55
+ access_mode = "readwrite"
56
+
57
+ [[mounts]]
58
+ path = "/browser"
59
+ uri = "mcp+stdio://npx @anthropic-ai/mcp-server-playwright"
60
+ [mounts.options]
61
+ userDataDir = "./browser"
62
+
63
+ [[mounts]]
64
+ path = "/data"
65
+ uri = "fs://./data"
66
+ access_mode = "readwrite"
67
+ `;
68
+ var AFSPersonaProvider = class AFSPersonaProvider extends _aigne_afs.AFSBaseProvider {
69
+ name;
70
+ description;
71
+ accessMode;
72
+ dataDir;
73
+ database;
74
+ model;
75
+ ashPath;
76
+ tickInterval;
77
+ safety;
78
+ afs;
79
+ db;
80
+ tickTimer;
81
+ destroyed = false;
82
+ constructor(options) {
83
+ super();
84
+ const validated = personaOptionsSchema.parse(options);
85
+ this.name = validated.name || "persona";
86
+ this.description = validated.description || "Virtual persona management";
87
+ this.accessMode = validated.accessMode || "readwrite";
88
+ this.dataDir = (0, node_path.resolve)(validated.dataDir);
89
+ this.database = validated.database || (0, node_path.resolve)(this.dataDir, "persona.db");
90
+ this.model = validated.model || "/modules/aignehub/defaults/chat";
91
+ this.ashPath = validated.ashPath || "/modules/ash";
92
+ this.tickInterval = validated.tickInterval || 300 * 1e3;
93
+ this.safety = validated.safety || {};
94
+ if (!(0, node_fs.existsSync)(this.dataDir)) (0, node_fs.mkdirSync)(this.dataDir, { recursive: true });
95
+ }
96
+ static schema() {
97
+ return personaOptionsSchema;
98
+ }
99
+ static checkKeywords(content, keywords) {
100
+ if (!content || keywords.length === 0) return { blocked: false };
101
+ const lowerContent = content.toLowerCase();
102
+ for (const keyword of keywords) if (lowerContent.includes(keyword.toLowerCase())) return {
103
+ blocked: true,
104
+ keyword
105
+ };
106
+ return { blocked: false };
107
+ }
108
+ loadBlockedKeywords() {
109
+ const entries = this.safety.blocked_keywords || [];
110
+ const keywords = [];
111
+ for (const entry of entries) {
112
+ const resolved = (0, node_path.resolve)(this.dataDir, entry);
113
+ if (!resolved.startsWith(`${this.dataDir}/`) && resolved !== this.dataDir) continue;
114
+ if (!(0, node_fs.existsSync)(resolved)) continue;
115
+ const content = (0, node_fs.readFileSync)(resolved, "utf-8");
116
+ for (const line of content.split("\n")) {
117
+ const trimmed = line.trim();
118
+ if (trimmed) keywords.push(trimmed);
119
+ }
120
+ }
121
+ return keywords;
122
+ }
123
+ static manifest() {
124
+ return {
125
+ name: "persona",
126
+ description: "Virtual persona manager — creates isolated character workspaces, drives session-based social media interaction via ASH agent-run.",
127
+ uriTemplate: "persona://{dataDir}",
128
+ category: "agent",
129
+ schema: zod.z.object({
130
+ dataDir: zod.z.string(),
131
+ database: zod.z.string().optional(),
132
+ model: zod.z.string().optional(),
133
+ ashPath: zod.z.string().optional(),
134
+ tickInterval: zod.z.number().optional()
135
+ }),
136
+ tags: [
137
+ "persona",
138
+ "social",
139
+ "agent",
140
+ "automation"
141
+ ],
142
+ capabilityTags: ["read-write"],
143
+ security: {
144
+ riskLevel: "external",
145
+ resourceAccess: ["internet", "process-spawn"],
146
+ requires: ["sqlite"],
147
+ dataSensitivity: ["personal-data"],
148
+ notes: ["Automates social media interactions via Playwright browser"]
149
+ }
150
+ };
151
+ }
152
+ static treeSchema() {
153
+ return {
154
+ operations: [
155
+ "list",
156
+ "read",
157
+ "exec",
158
+ "stat",
159
+ "explain"
160
+ ],
161
+ tree: {
162
+ "/": {
163
+ kind: "persona:root",
164
+ operations: ["list", "read"],
165
+ actions: [
166
+ "create-character",
167
+ "delete-character",
168
+ "tick",
169
+ "dispatch-task"
170
+ ]
171
+ },
172
+ "/characters": {
173
+ kind: "persona:collection",
174
+ operations: ["list"]
175
+ },
176
+ "/characters/{name}": {
177
+ kind: "persona:character",
178
+ operations: [
179
+ "list",
180
+ "read",
181
+ "exec"
182
+ ],
183
+ actions: [
184
+ "session",
185
+ "post",
186
+ "reply",
187
+ "pause",
188
+ "resume"
189
+ ]
190
+ }
191
+ },
192
+ bestFor: [
193
+ "persona management",
194
+ "social media automation",
195
+ "character orchestration"
196
+ ],
197
+ notFor: ["file storage", "databases"]
198
+ };
199
+ }
200
+ static async load({ basePath, config } = {}) {
201
+ return new AFSPersonaProvider((0, _aigne_afs_utils_zod.zodParse)(personaOptionsSchema, config, { prefix: basePath }));
202
+ }
203
+ onMount(afs, _mountPath) {
204
+ this.afs = afs;
205
+ this.scheduleNextTick();
206
+ }
207
+ get charactersDir() {
208
+ return (0, node_path.resolve)(this.dataDir, "characters");
209
+ }
210
+ async getCharacterNames() {
211
+ if (!(0, node_fs.existsSync)(this.charactersDir)) return [];
212
+ return (await (0, node_fs_promises.readdir)(this.charactersDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
213
+ }
214
+ characterDir(name) {
215
+ return (0, node_path.resolve)(this.charactersDir, name);
216
+ }
217
+ characterProfilePath(name) {
218
+ return (0, node_path.resolve)(this.characterDir(name), "profile.yaml");
219
+ }
220
+ readCharacterProfile(name) {
221
+ const profilePath = this.characterProfilePath(name);
222
+ if (!(0, node_fs.existsSync)(profilePath)) return null;
223
+ return (0, yaml.parse)((0, node_fs.readFileSync)(profilePath, "utf-8"));
224
+ }
225
+ writeCharacterProfile(name, profile) {
226
+ (0, node_fs.writeFileSync)(this.characterProfilePath(name), (0, yaml.stringify)(profile), "utf-8");
227
+ }
228
+ getDB() {
229
+ if (!this.db) this.db = new require_db.PersonaDB(this.database);
230
+ return this.db;
231
+ }
232
+ scheduleNextTick() {
233
+ if (this.destroyed) return;
234
+ this.tickTimer = setTimeout(async () => {
235
+ try {
236
+ await this.runTick();
237
+ } catch {}
238
+ this.scheduleNextTick();
239
+ }, this.tickInterval);
240
+ }
241
+ shouldStartSession(gap, offlineMinutes) {
242
+ const gapMin = gap[0] ?? 30;
243
+ const gapMax = gap[1] ?? 240;
244
+ if (offlineMinutes >= gapMax) return true;
245
+ if (offlineMinutes < gapMin) return false;
246
+ if (gapMax <= gapMin) return true;
247
+ const probability = (offlineMinutes - gapMin) / (gapMax - gapMin);
248
+ return Math.random() < probability;
249
+ }
250
+ async runTick(characterFilter) {
251
+ const db = this.getDB();
252
+ const stale_recovered = db.markStaleSessions(60);
253
+ const characterNames = characterFilter ? [characterFilter] : await this.getCharacterNames();
254
+ let sessions_started = 0;
255
+ for (const name of characterNames) {
256
+ const profile = this.readCharacterProfile(name);
257
+ if (!profile) continue;
258
+ if (profile.status === "paused") continue;
259
+ if (db.getRunningSession(name)) continue;
260
+ const lastDone = db.getLastDoneSession(name);
261
+ let offlineMinutes;
262
+ if (!lastDone || !lastDone.ended_at) offlineMinutes = Number.POSITIVE_INFINITY;
263
+ else {
264
+ const endedAt = new Date(lastDone.ended_at).getTime();
265
+ offlineMinutes = (Date.now() - endedAt) / (60 * 1e3);
266
+ }
267
+ const gap = Array.isArray(profile.gap) ? profile.gap : [30, 240];
268
+ if (!this.shouldStartSession(gap, offlineMinutes)) continue;
269
+ const sessionId = db.insertSession(name);
270
+ db.updateSession(sessionId, {
271
+ status: "done",
272
+ ended_at: (/* @__PURE__ */ new Date()).toISOString()
273
+ });
274
+ sessions_started++;
275
+ }
276
+ return {
277
+ sessions_started,
278
+ stale_recovered
279
+ };
280
+ }
281
+ async runSessionSteps(name, profile, _sessionId) {
282
+ const db = this.getDB();
283
+ const platforms = profile.platforms || [];
284
+ const model = profile.model || this.model;
285
+ const recentInteractions = db.getRecentInteractions(name, 10);
286
+ let steps_completed = 0;
287
+ let interactions_saved = 0;
288
+ for (const platform of platforms) {
289
+ const steps = this.buildStepsForPlatform(name, platform);
290
+ for (const step of steps) {
291
+ const result = await this.executeStep(name, profile, platform, step, model, recentInteractions);
292
+ if (result.trace) try {
293
+ db.insertInteraction({
294
+ character: name,
295
+ platform,
296
+ type: step.name,
297
+ content: null,
298
+ metadata: {
299
+ step: step.name,
300
+ trace_rounds: Array.isArray(result.trace) ? result.trace.length : 0,
301
+ status: result.status
302
+ }
303
+ });
304
+ interactions_saved++;
305
+ } catch {}
306
+ if (result.status === "error") throw new Error(`Step '${step.name}' failed`);
307
+ if (result.status !== "budget_exhausted") steps_completed++;
308
+ }
309
+ }
310
+ return {
311
+ steps_completed,
312
+ interactions_saved
313
+ };
314
+ }
315
+ buildStepsForPlatform(name, platform) {
316
+ const db = this.getDB();
317
+ const steps = [{
318
+ name: "notifications",
319
+ platform,
320
+ budget: { max_rounds: 15 }
321
+ }, {
322
+ name: "browse",
323
+ platform,
324
+ budget: { max_rounds: 25 }
325
+ }];
326
+ if (Math.random() < .3) steps.push({
327
+ name: "post",
328
+ platform,
329
+ budget: { max_rounds: 20 }
330
+ });
331
+ const seededTopics = db.getSeededTopics(name, "pending");
332
+ for (const topic of seededTopics) if (db.claimSeededTopic(topic.id, topic.task_id)) steps.push({
333
+ name: "seeded_topic",
334
+ platform,
335
+ topic: topic.topic,
336
+ angle: topic.angle,
337
+ budget: { max_rounds: 20 }
338
+ });
339
+ return steps;
340
+ }
341
+ async executeStep(name, profile, platform, step, model, recentInteractions) {
342
+ const system = this.buildSystemPrompt(name, profile, platform, recentInteractions);
343
+ const tools = this.buildToolsWhitelist(name);
344
+ const task = this.buildStepTask(step, name, platform);
345
+ try {
346
+ const result = await this.afs.exec((0, ufo.joinURL)(this.ashPath, ".actions/agent-run"), {
347
+ task,
348
+ model,
349
+ system,
350
+ tools,
351
+ budget: step.budget
352
+ }, {});
353
+ if (!result.success) return {
354
+ status: "error",
355
+ error: result.error?.message
356
+ };
357
+ const data = result.data || {};
358
+ const trace = data.trace;
359
+ if (data.status === "error") return {
360
+ status: "error",
361
+ error: String(data.message || "Unknown error"),
362
+ trace
363
+ };
364
+ if (data.status === "budget_exhausted") return {
365
+ status: "budget_exhausted",
366
+ trace
367
+ };
368
+ return {
369
+ status: "completed",
370
+ trace
371
+ };
372
+ } catch (e) {
373
+ return {
374
+ status: "error",
375
+ error: e instanceof Error ? e.message : "Unknown error"
376
+ };
377
+ }
378
+ }
379
+ buildSystemPrompt(name, profile, platform, recentInteractions) {
380
+ const personality = profile.personality || {};
381
+ const interests = profile.interests || [];
382
+ const parts = [];
383
+ parts.push(`你是 ${name}。${personality.tone || "neutral"}。`);
384
+ parts.push(`兴趣:${interests.map((i) => i.topic).join(", ") || "广泛"}。风格:${personality.style || "简洁"}。语言:${personality.language || "en"}。`);
385
+ if (recentInteractions.length > 0) {
386
+ parts.push("最近发过的内容:");
387
+ for (const interaction of recentInteractions.slice(0, 5)) parts.push(`- [${interaction.type}] ${interaction.content || "(no content)"}`);
388
+ }
389
+ parts.push(`当前平台:${platform}`);
390
+ return parts.join("\n");
391
+ }
392
+ buildToolsWhitelist(characterName) {
393
+ return [
394
+ {
395
+ path: `/modules/persona/characters/${characterName}/browser/tools/*`,
396
+ ops: ["exec"]
397
+ },
398
+ {
399
+ path: `/modules/persona/characters/${characterName}/browser/tools`,
400
+ ops: ["list"]
401
+ },
402
+ {
403
+ path: `/modules/persona/characters/${characterName}/profile/**`,
404
+ ops: ["read", "list"],
405
+ maxDepth: 3
406
+ },
407
+ {
408
+ path: `/modules/persona/characters/${characterName}/data/**`,
409
+ ops: [
410
+ "read",
411
+ "write",
412
+ "list"
413
+ ],
414
+ maxDepth: 3
415
+ }
416
+ ];
417
+ }
418
+ buildStepTask(step, name, platform) {
419
+ switch (step.name) {
420
+ case "notifications": return `Check notifications on ${platform} and reply to any mentions or messages as ${name}.`;
421
+ case "browse": return `Browse the ${platform} feed, engage with interesting posts that match your interests.`;
422
+ case "post": return `Create an original post on ${platform} about something you find interesting, in character as ${name}.`;
423
+ case "seeded_topic": return `Create a post on ${platform} about: ${step.topic}. ${step.angle ? `Angle: ${step.angle}.` : ""} Make it natural and in-character as ${name}.`;
424
+ default: return `Perform ${step.name} on ${platform} as ${name}.`;
425
+ }
426
+ }
427
+ buildActionEntry(name, description, basePath = "/") {
428
+ return {
429
+ id: name,
430
+ path: (0, ufo.joinURL)(basePath, ".actions", name),
431
+ summary: name,
432
+ meta: {
433
+ kind: "afs:executable",
434
+ name,
435
+ description
436
+ },
437
+ actions: [{
438
+ name,
439
+ description
440
+ }]
441
+ };
442
+ }
443
+ async listRoot(_ctx) {
444
+ return { data: [{
445
+ id: "persona:characters",
446
+ path: "/characters",
447
+ meta: {
448
+ kind: "persona:collection",
449
+ childrenCount: (await this.getCharacterNames()).length
450
+ }
451
+ }] };
452
+ }
453
+ async listCharacters(_ctx) {
454
+ return { data: (await this.getCharacterNames()).map((name) => ({
455
+ id: `persona:character:${name}`,
456
+ path: (0, ufo.joinURL)("/characters", name),
457
+ meta: {
458
+ kind: "persona:character",
459
+ childrenCount: -1
460
+ }
461
+ })) };
462
+ }
463
+ async readRoot(_ctx) {
464
+ return {
465
+ id: "persona:root",
466
+ path: "/",
467
+ content: {
468
+ type: "persona-provider",
469
+ characterCount: (await this.getCharacterNames()).length
470
+ },
471
+ meta: {
472
+ kind: "persona:root",
473
+ childrenCount: 1
474
+ }
475
+ };
476
+ }
477
+ async readCharacters(_ctx) {
478
+ const characterNames = await this.getCharacterNames();
479
+ return {
480
+ id: "persona:characters",
481
+ path: "/characters",
482
+ content: {
483
+ type: "directory",
484
+ count: characterNames.length
485
+ },
486
+ meta: {
487
+ kind: "persona:collection",
488
+ childrenCount: characterNames.length
489
+ }
490
+ };
491
+ }
492
+ async readCharacter(ctx) {
493
+ const name = ctx.params?.name;
494
+ const profile = this.readCharacterProfile(name);
495
+ if (!profile) throw new Error(`Character not found: ${name}`);
496
+ return {
497
+ id: `persona:character:${name}`,
498
+ path: (0, ufo.joinURL)("/characters", name),
499
+ content: profile,
500
+ meta: {
501
+ kind: "persona:character",
502
+ childrenCount: -1
503
+ }
504
+ };
505
+ }
506
+ async metaRoot(_ctx) {
507
+ return {
508
+ id: "persona:root",
509
+ path: "/.meta",
510
+ content: {
511
+ characterCount: (await this.getCharacterNames()).length,
512
+ provider: "persona"
513
+ },
514
+ meta: {
515
+ kind: "persona:root",
516
+ childrenCount: 1
517
+ }
518
+ };
519
+ }
520
+ async metaCharacters(_ctx) {
521
+ const characterNames = await this.getCharacterNames();
522
+ return {
523
+ id: "persona:characters",
524
+ path: "/characters/.meta",
525
+ content: {
526
+ type: "collection",
527
+ count: characterNames.length
528
+ },
529
+ meta: {
530
+ kind: "persona:collection",
531
+ childrenCount: characterNames.length
532
+ }
533
+ };
534
+ }
535
+ async readCapabilities(_ctx) {
536
+ return {
537
+ id: "/.meta/.capabilities",
538
+ path: "/.meta/.capabilities",
539
+ content: {
540
+ schemaVersion: 1,
541
+ provider: "persona",
542
+ description: "Virtual persona management — character lifecycle, session scheduling, social media automation",
543
+ tools: [],
544
+ actions: [{
545
+ kind: "persona:root",
546
+ description: "Root-level persona management actions",
547
+ catalog: [
548
+ {
549
+ name: "create-character",
550
+ description: "Create a new character workspace",
551
+ inputSchema: {
552
+ type: "object",
553
+ properties: {
554
+ name: {
555
+ type: "string",
556
+ description: "Character name"
557
+ },
558
+ profile: {
559
+ type: "object",
560
+ description: "Character profile data"
561
+ }
562
+ },
563
+ required: ["name", "profile"]
564
+ }
565
+ },
566
+ {
567
+ name: "delete-character",
568
+ description: "Remove a character and optionally archive",
569
+ inputSchema: {
570
+ type: "object",
571
+ properties: {
572
+ name: { type: "string" },
573
+ archive: { type: "boolean" }
574
+ },
575
+ required: ["name"]
576
+ }
577
+ },
578
+ {
579
+ name: "tick",
580
+ description: "Check and run due sessions",
581
+ inputSchema: {
582
+ type: "object",
583
+ properties: { character: {
584
+ type: "string",
585
+ description: "Optional: tick single character"
586
+ } }
587
+ }
588
+ },
589
+ {
590
+ name: "dispatch-task",
591
+ description: "Seed topic into characters' feeds",
592
+ inputSchema: {
593
+ type: "object",
594
+ properties: {
595
+ topic: { type: "string" },
596
+ goal: { type: "string" }
597
+ },
598
+ required: ["topic", "goal"]
599
+ }
600
+ }
601
+ ],
602
+ discovery: {
603
+ pathTemplate: "/.actions",
604
+ note: "Root actions for persona management"
605
+ }
606
+ }],
607
+ operations: {
608
+ list: true,
609
+ read: true,
610
+ write: false,
611
+ delete: false,
612
+ search: false,
613
+ exec: true,
614
+ stat: true,
615
+ explain: true
616
+ }
617
+ },
618
+ meta: { kind: "afs:capabilities" }
619
+ };
620
+ }
621
+ async statRoot(_ctx) {
622
+ return { data: {
623
+ id: "persona:root",
624
+ path: "/",
625
+ meta: {
626
+ kind: "persona:root",
627
+ childrenCount: 1
628
+ }
629
+ } };
630
+ }
631
+ async statCharacters(_ctx) {
632
+ return { data: {
633
+ id: "persona:characters",
634
+ path: "/characters",
635
+ meta: {
636
+ kind: "persona:collection",
637
+ childrenCount: (await this.getCharacterNames()).length
638
+ }
639
+ } };
640
+ }
641
+ async explainRoot(_ctx) {
642
+ return {
643
+ format: "markdown",
644
+ content: [
645
+ "# Persona Provider",
646
+ "",
647
+ "Manages virtual characters that autonomously interact on social platforms.",
648
+ "",
649
+ "## Actions",
650
+ "- `create-character` — Create a new character workspace",
651
+ "- `delete-character` — Remove a character and optionally archive",
652
+ "- `tick` — Check and run due sessions",
653
+ "- `dispatch-task` — Seed topic into characters' feeds",
654
+ "",
655
+ "## Structure",
656
+ "- `/characters/` — Character workspaces"
657
+ ].join("\n")
658
+ };
659
+ }
660
+ async listRootActions(_ctx) {
661
+ return { data: [
662
+ this.buildActionEntry("create-character", "Create a new character workspace"),
663
+ this.buildActionEntry("delete-character", "Remove a character and optionally archive"),
664
+ this.buildActionEntry("tick", "Check and run due sessions"),
665
+ this.buildActionEntry("dispatch-task", "Seed topic into characters' feeds")
666
+ ] };
667
+ }
668
+ async execCreateCharacter(_ctx, args) {
669
+ const nameResult = validateCharacterName(args.name);
670
+ if (!nameResult.valid) return {
671
+ success: false,
672
+ error: {
673
+ code: "INVALID_NAME",
674
+ message: nameResult.reason
675
+ }
676
+ };
677
+ const name = args.name;
678
+ const charDir = this.characterDir(name);
679
+ if ((0, node_fs.existsSync)(charDir)) return {
680
+ success: false,
681
+ error: {
682
+ code: "ALREADY_EXISTS",
683
+ message: `Character '${name}' already exists`
684
+ }
685
+ };
686
+ (0, node_fs.mkdirSync)(charDir, { recursive: true });
687
+ (0, node_fs.mkdirSync)((0, node_path.resolve)(charDir, "browser"), { recursive: true });
688
+ (0, node_fs.mkdirSync)((0, node_path.resolve)(charDir, "data"), { recursive: true });
689
+ const profileInput = args.profile || {};
690
+ const profile = {
691
+ name,
692
+ description: profileInput.description || "",
693
+ model: profileInput.model || void 0,
694
+ timezone: profileInput.timezone || "UTC",
695
+ gap: profileInput.gap || [30, 240],
696
+ platforms: profileInput.platforms || ["x"],
697
+ interests: profileInput.interests || [],
698
+ personality: profileInput.personality || {
699
+ tone: "neutral",
700
+ style: "concise",
701
+ language: "en"
702
+ },
703
+ reply_policy: profileInput.reply_policy || {
704
+ enabled: true,
705
+ max_daily: 10
706
+ },
707
+ status: "active"
708
+ };
709
+ this.writeCharacterProfile(name, profile);
710
+ (0, node_fs.writeFileSync)((0, node_path.resolve)(charDir, ".afs-workspace.toml"), WORKSPACE_TOML_TEMPLATE, "utf-8");
711
+ return {
712
+ success: true,
713
+ data: {
714
+ name,
715
+ path: (0, ufo.joinURL)("/characters", name)
716
+ }
717
+ };
718
+ }
719
+ async execDeleteCharacter(_ctx, args) {
720
+ const nameResult = validateCharacterName(args.name);
721
+ if (!nameResult.valid) return {
722
+ success: false,
723
+ error: {
724
+ code: "INVALID_NAME",
725
+ message: nameResult.reason
726
+ }
727
+ };
728
+ const name = args.name;
729
+ const charDir = this.characterDir(name);
730
+ if (!(0, node_fs.existsSync)(charDir)) return {
731
+ success: false,
732
+ error: {
733
+ code: "NOT_FOUND",
734
+ message: `Character '${name}' not found`
735
+ }
736
+ };
737
+ if (args.archive) {
738
+ const archiveDir = (0, node_path.resolve)(this.dataDir, "archive");
739
+ if (!(0, node_fs.existsSync)(archiveDir)) (0, node_fs.mkdirSync)(archiveDir, { recursive: true });
740
+ (0, node_fs.renameSync)(charDir, (0, node_path.resolve)(archiveDir, name));
741
+ } else (0, node_fs.rmSync)(charDir, {
742
+ recursive: true,
743
+ force: true
744
+ });
745
+ return { success: true };
746
+ }
747
+ async execTick(_ctx, args) {
748
+ const characterFilter = args.character;
749
+ if (characterFilter) {
750
+ if (!this.readCharacterProfile(characterFilter)) return {
751
+ success: false,
752
+ error: {
753
+ code: "NOT_FOUND",
754
+ message: `Character '${characterFilter}' not found`
755
+ }
756
+ };
757
+ }
758
+ try {
759
+ return {
760
+ success: true,
761
+ data: await this.runTick(characterFilter)
762
+ };
763
+ } catch (e) {
764
+ return {
765
+ success: false,
766
+ error: {
767
+ code: "TICK_ERROR",
768
+ message: e instanceof Error ? e.message : "Unknown error"
769
+ }
770
+ };
771
+ }
772
+ }
773
+ async execDispatchTask(_ctx, args) {
774
+ const topic = args.topic;
775
+ if (!topic) return {
776
+ success: false,
777
+ error: {
778
+ code: "INVALID_INPUT",
779
+ message: "topic is required"
780
+ }
781
+ };
782
+ const goal = args.goal;
783
+ const selectCount = args.select_count;
784
+ const material = args.material;
785
+ const characterNames = await this.getCharacterNames();
786
+ const activeChars = [];
787
+ for (const name of characterNames) {
788
+ const profile = this.readCharacterProfile(name);
789
+ if (!profile || profile.status === "paused") continue;
790
+ const interests = profile.interests || [];
791
+ activeChars.push({
792
+ name,
793
+ interests
794
+ });
795
+ }
796
+ const topicLower = topic.toLowerCase();
797
+ const matched = activeChars.filter((char) => char.interests.some((i) => topicLower.includes(i.topic.toLowerCase())));
798
+ let selected = matched;
799
+ if (selectCount !== void 0 && selectCount < matched.length) selected = [...matched].sort(() => Math.random() - .5).slice(0, selectCount);
800
+ if (selected.length === 0) return {
801
+ success: true,
802
+ data: {
803
+ assigned: [],
804
+ angles: {},
805
+ warning: "No characters matched the given topic"
806
+ }
807
+ };
808
+ const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
809
+ const angles = {};
810
+ for (const char of selected) {
811
+ const matchingInterest = char.interests.find((i) => topicLower.includes(i.topic.toLowerCase()));
812
+ angles[char.name] = `From ${matchingInterest?.topic || "general"} perspective on ${topic}`;
813
+ }
814
+ const db = this.getDB();
815
+ const assigned = selected.map((c) => c.name);
816
+ db.insertTask({
817
+ id: taskId,
818
+ topic,
819
+ goal: goal ?? null,
820
+ material: material ?? null,
821
+ assignments: Object.fromEntries(assigned.map((n) => [n, angles[n]]))
822
+ });
823
+ for (const name of assigned) db.insertSeededTopic({
824
+ character: name,
825
+ task_id: taskId,
826
+ topic,
827
+ angle: angles[name]
828
+ });
829
+ return {
830
+ success: true,
831
+ data: {
832
+ task_id: taskId,
833
+ assigned,
834
+ angles
835
+ }
836
+ };
837
+ }
838
+ async listCharacterActions(ctx) {
839
+ const name = ctx.params?.name;
840
+ const basePath = (0, ufo.joinURL)("/characters", name);
841
+ return { data: [
842
+ this.buildActionEntry("session", "Run one browsing session", basePath),
843
+ this.buildActionEntry("post", "Manual post trigger", basePath),
844
+ this.buildActionEntry("reply", "Manual reply trigger", basePath),
845
+ this.buildActionEntry("pause", "Pause character scheduling", basePath),
846
+ this.buildActionEntry("resume", "Resume character scheduling", basePath)
847
+ ] };
848
+ }
849
+ async execPause(ctx, _args) {
850
+ const name = ctx.params?.name;
851
+ const profile = this.readCharacterProfile(name);
852
+ if (!profile) return {
853
+ success: false,
854
+ error: {
855
+ code: "NOT_FOUND",
856
+ message: `Character '${name}' not found`
857
+ }
858
+ };
859
+ profile.status = "paused";
860
+ this.writeCharacterProfile(name, profile);
861
+ return {
862
+ success: true,
863
+ data: {
864
+ name,
865
+ status: "paused"
866
+ }
867
+ };
868
+ }
869
+ async execResume(ctx, _args) {
870
+ const name = ctx.params?.name;
871
+ const profile = this.readCharacterProfile(name);
872
+ if (!profile) return {
873
+ success: false,
874
+ error: {
875
+ code: "NOT_FOUND",
876
+ message: `Character '${name}' not found`
877
+ }
878
+ };
879
+ profile.status = "active";
880
+ this.writeCharacterProfile(name, profile);
881
+ return {
882
+ success: true,
883
+ data: {
884
+ name,
885
+ status: "active"
886
+ }
887
+ };
888
+ }
889
+ async execSession(ctx, _args) {
890
+ const name = ctx.params?.name;
891
+ const profile = this.readCharacterProfile(name);
892
+ if (!profile) return {
893
+ success: false,
894
+ error: {
895
+ code: "NOT_FOUND",
896
+ message: `Character '${name}' not found`
897
+ }
898
+ };
899
+ if (!this.afs) return {
900
+ success: false,
901
+ error: {
902
+ code: "NOT_MOUNTED",
903
+ message: "Provider not mounted to AFS"
904
+ }
905
+ };
906
+ const db = this.getDB();
907
+ const sessionId = db.insertSession(name);
908
+ try {
909
+ const result = await this.runSessionSteps(name, profile, sessionId);
910
+ db.updateSession(sessionId, {
911
+ status: "done",
912
+ ended_at: (/* @__PURE__ */ new Date()).toISOString()
913
+ });
914
+ return {
915
+ success: true,
916
+ data: result
917
+ };
918
+ } catch {
919
+ db.updateSession(sessionId, {
920
+ status: "failed",
921
+ ended_at: (/* @__PURE__ */ new Date()).toISOString()
922
+ });
923
+ return {
924
+ success: false,
925
+ error: {
926
+ code: "SESSION_FAILED",
927
+ message: `Session failed for character '${name}'`
928
+ }
929
+ };
930
+ }
931
+ }
932
+ async execPost(_ctx, _args) {
933
+ return {
934
+ success: false,
935
+ error: {
936
+ code: "NOT_IMPLEMENTED",
937
+ message: "Not implemented yet"
938
+ }
939
+ };
940
+ }
941
+ async execReply(_ctx, _args) {
942
+ return {
943
+ success: false,
944
+ error: {
945
+ code: "NOT_IMPLEMENTED",
946
+ message: "Not implemented yet"
947
+ }
948
+ };
949
+ }
950
+ destroy() {
951
+ this.destroyed = true;
952
+ if (this.tickTimer) {
953
+ clearTimeout(this.tickTimer);
954
+ this.tickTimer = void 0;
955
+ }
956
+ if (this.db) {
957
+ this.db.close();
958
+ this.db = void 0;
959
+ }
960
+ }
961
+ };
962
+ require_decorate.__decorate([(0, _aigne_afs.List)("/")], AFSPersonaProvider.prototype, "listRoot", null);
963
+ require_decorate.__decorate([(0, _aigne_afs.List)("/characters")], AFSPersonaProvider.prototype, "listCharacters", null);
964
+ require_decorate.__decorate([(0, _aigne_afs.Read)("/")], AFSPersonaProvider.prototype, "readRoot", null);
965
+ require_decorate.__decorate([(0, _aigne_afs.Read)("/characters")], AFSPersonaProvider.prototype, "readCharacters", null);
966
+ require_decorate.__decorate([(0, _aigne_afs.Read)("/characters/:name")], AFSPersonaProvider.prototype, "readCharacter", null);
967
+ require_decorate.__decorate([(0, _aigne_afs.Meta)("/")], AFSPersonaProvider.prototype, "metaRoot", null);
968
+ require_decorate.__decorate([(0, _aigne_afs.Meta)("/characters")], AFSPersonaProvider.prototype, "metaCharacters", null);
969
+ require_decorate.__decorate([(0, _aigne_afs.Read)("/.meta/.capabilities")], AFSPersonaProvider.prototype, "readCapabilities", null);
970
+ require_decorate.__decorate([(0, _aigne_afs.Stat)("/")], AFSPersonaProvider.prototype, "statRoot", null);
971
+ require_decorate.__decorate([(0, _aigne_afs.Stat)("/characters")], AFSPersonaProvider.prototype, "statCharacters", null);
972
+ require_decorate.__decorate([(0, _aigne_afs.Explain)("/")], AFSPersonaProvider.prototype, "explainRoot", null);
973
+ require_decorate.__decorate([(0, _aigne_afs.Actions)("/")], AFSPersonaProvider.prototype, "listRootActions", null);
974
+ require_decorate.__decorate([_aigne_afs.Actions.Exec("/", "create-character")], AFSPersonaProvider.prototype, "execCreateCharacter", null);
975
+ require_decorate.__decorate([_aigne_afs.Actions.Exec("/", "delete-character")], AFSPersonaProvider.prototype, "execDeleteCharacter", null);
976
+ require_decorate.__decorate([_aigne_afs.Actions.Exec("/", "tick")], AFSPersonaProvider.prototype, "execTick", null);
977
+ require_decorate.__decorate([_aigne_afs.Actions.Exec("/", "dispatch-task")], AFSPersonaProvider.prototype, "execDispatchTask", null);
978
+ require_decorate.__decorate([(0, _aigne_afs.Actions)("/characters/:name")], AFSPersonaProvider.prototype, "listCharacterActions", null);
979
+ require_decorate.__decorate([_aigne_afs.Actions.Exec("/characters/:name", "pause")], AFSPersonaProvider.prototype, "execPause", null);
980
+ require_decorate.__decorate([_aigne_afs.Actions.Exec("/characters/:name", "resume")], AFSPersonaProvider.prototype, "execResume", null);
981
+ require_decorate.__decorate([_aigne_afs.Actions.Exec("/characters/:name", "session")], AFSPersonaProvider.prototype, "execSession", null);
982
+ require_decorate.__decorate([_aigne_afs.Actions.Exec("/characters/:name", "post")], AFSPersonaProvider.prototype, "execPost", null);
983
+ require_decorate.__decorate([_aigne_afs.Actions.Exec("/characters/:name", "reply")], AFSPersonaProvider.prototype, "execReply", null);
984
+
985
+ //#endregion
986
+ exports.AFSPersonaProvider = AFSPersonaProvider;
987
+ exports.PersonaDB = require_db.PersonaDB;