@hyperspell/openclaw-hyperspell 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +37 -1
  2. package/dist/client.js +341 -0
  3. package/dist/commands/setup.js +569 -0
  4. package/dist/commands/slash.js +159 -0
  5. package/dist/config.js +349 -0
  6. package/{graph/cron.ts → dist/graph/cron.js} +11 -15
  7. package/dist/graph/index.js +4 -0
  8. package/dist/graph/ops.js +221 -0
  9. package/dist/graph/state.js +68 -0
  10. package/dist/graph/tools.js +87 -0
  11. package/dist/hooks/auto-context.js +199 -0
  12. package/dist/hooks/auto-trace.js +141 -0
  13. package/dist/hooks/emotional-state.js +155 -0
  14. package/dist/hooks/memory-sync.js +54 -0
  15. package/dist/hooks/startup-orientation.js +156 -0
  16. package/dist/index.js +132 -0
  17. package/dist/lib/browser.js +29 -0
  18. package/dist/lib/sender.js +173 -0
  19. package/dist/lib/voice-id.js +22 -0
  20. package/dist/logger.js +32 -0
  21. package/dist/sync/markdown.js +151 -0
  22. package/dist/tools/remember.js +97 -0
  23. package/dist/tools/search.js +87 -0
  24. package/openclaw.plugin.json +17 -1
  25. package/package.json +7 -12
  26. package/client.ts +0 -566
  27. package/commands/setup.ts +0 -673
  28. package/commands/slash.ts +0 -198
  29. package/config.test.ts +0 -202
  30. package/config.ts +0 -497
  31. package/graph/index.ts +0 -5
  32. package/graph/ops.ts +0 -259
  33. package/graph/state.ts +0 -79
  34. package/graph/tools.ts +0 -117
  35. package/hooks/auto-context.ts +0 -272
  36. package/hooks/auto-trace.test.ts +0 -81
  37. package/hooks/auto-trace.ts +0 -197
  38. package/hooks/emotional-state.test.ts +0 -160
  39. package/hooks/emotional-state.ts +0 -179
  40. package/hooks/memory-sync.ts +0 -65
  41. package/index.ts +0 -152
  42. package/lib/browser.ts +0 -31
  43. package/lib/sender.test.ts +0 -234
  44. package/lib/sender.ts +0 -234
  45. package/lib/voice-id.ts +0 -39
  46. package/logger.ts +0 -41
  47. package/sync/markdown.ts +0 -186
  48. package/tools/remember.ts +0 -132
  49. package/tools/search.ts +0 -131
  50. package/types/openclaw.d.ts +0 -76
@@ -0,0 +1,569 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { userInfo } from "node:os";
5
+ import * as p from "@clack/prompts";
6
+ import Hyperspell from "hyperspell";
7
+ import { syncAllMemoryFiles, getMemoryFiles } from "../sync/markdown.js";
8
+ import { openInBrowser } from "../lib/browser.js";
9
+ import { HyperspellClient } from "../client.js";
10
+ import { getWorkspaceDir, parseConfig, resolveConfigPath } from "../config.js";
11
+ import { buildExtractionPrompt, CRON_JOB_NAME } from "../graph/cron.js";
12
+ import { NetworkStateManager } from "../graph/state.js";
13
+ import { scanMemories, formatScanResults, completeMemories } from "../graph/ops.js";
14
+ async function fetchConnectionSources(client, userId) {
15
+ try {
16
+ const userClient = new Hyperspell({
17
+ apiKey: client.apiKey,
18
+ userID: userId,
19
+ });
20
+ const response = await userClient.connections.list();
21
+ const providers = response.connections.map((conn) => conn.provider);
22
+ // Add vault and deduplicate
23
+ const sources = [...new Set(["vault", ...providers])];
24
+ return sources;
25
+ }
26
+ catch (_error) {
27
+ return ["vault"];
28
+ }
29
+ }
30
+ function updateConfigSources(configPath, sources) {
31
+ if (!fs.existsSync(configPath))
32
+ return;
33
+ const content = fs.readFileSync(configPath, "utf-8");
34
+ const config = JSON.parse(content);
35
+ const pluginConfig = config?.plugins?.entries?.["openclaw-hyperspell"]?.config;
36
+ if (pluginConfig) {
37
+ pluginConfig.sources = sources.join(",");
38
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
39
+ }
40
+ }
41
+ async function runSetup() {
42
+ p.intro("Hyperspell Setup");
43
+ // Step 1: Check if they have an account
44
+ const hasAccount = await p.confirm({
45
+ message: "Do you already have a Hyperspell account?",
46
+ });
47
+ if (p.isCancel(hasAccount)) {
48
+ p.cancel("Setup cancelled");
49
+ return;
50
+ }
51
+ if (!hasAccount) {
52
+ p.note("1. Go to https://app.hyperspell.com to create a free account\n" +
53
+ "2. Create a new app for your AI agent\n" +
54
+ "3. Select which integrations you want to connect (Notion, Slack, etc.)", "Create an account");
55
+ const openSignup = await p.confirm({
56
+ message: "Open app.hyperspell.com in your browser?",
57
+ });
58
+ if (p.isCancel(openSignup)) {
59
+ p.cancel("Setup cancelled");
60
+ return;
61
+ }
62
+ if (openSignup) {
63
+ await openInBrowser("https://app.hyperspell.com");
64
+ p.log.info("Browser opened. Come back when you've created your account.");
65
+ }
66
+ await p.confirm({
67
+ message: "Ready to continue?",
68
+ active: "Yes",
69
+ inactive: "No",
70
+ });
71
+ }
72
+ // Step 2: Get API Key
73
+ p.note("1. Go to your app in https://app.hyperspell.com\n" +
74
+ "2. Navigate to Settings > API Keys\n" +
75
+ "3. Create a new API key", "API Key");
76
+ const apiKey = await p.text({
77
+ message: "Paste your API key",
78
+ placeholder: "hs_...",
79
+ validate: (value) => {
80
+ if (!value)
81
+ return "API key is required";
82
+ },
83
+ });
84
+ if (p.isCancel(apiKey)) {
85
+ p.cancel("Setup cancelled");
86
+ return;
87
+ }
88
+ // Validate API key
89
+ const s = p.spinner();
90
+ s.start("Validating API key");
91
+ let client;
92
+ try {
93
+ client = new Hyperspell({ apiKey });
94
+ await client.integrations.list();
95
+ s.stop("API key is valid");
96
+ }
97
+ catch (_error) {
98
+ s.stop("API key validation failed");
99
+ p.log.error("Please check that your API key is correct and try again.");
100
+ return;
101
+ }
102
+ // Step 3: User ID
103
+ p.note("Hyperspell is a multi-tenant memory platform. Each user's memories\n" +
104
+ "are stored separately, identified by a User ID.\n\n" +
105
+ "For a personal agent, use your email address or username.", "User ID");
106
+ const systemUser = userInfo().username;
107
+ const userId = await p.text({
108
+ message: "Enter a User ID for this agent",
109
+ placeholder: systemUser || "your-email@example.com",
110
+ defaultValue: systemUser,
111
+ });
112
+ if (p.isCancel(userId)) {
113
+ p.cancel("Setup cancelled");
114
+ return;
115
+ }
116
+ // Step 4: List and connect integrations
117
+ let integrations;
118
+ try {
119
+ integrations = await client.integrations.list();
120
+ }
121
+ catch (_error) {
122
+ integrations = { integrations: [] };
123
+ }
124
+ if (integrations.integrations.length === 0) {
125
+ p.note("No integrations are configured in your app yet.\n" +
126
+ "Go to https://app.hyperspell.com to add integrations,\n" +
127
+ "then use /connect <source> to connect them.", "Connect Your Apps");
128
+ }
129
+ else {
130
+ const integrationList = integrations.integrations
131
+ .map((int) => `• ${int.name} (${int.provider})`)
132
+ .join("\n");
133
+ // Get a user token for the connect page
134
+ let connectUrl;
135
+ try {
136
+ const tokenResponse = await client.auth.userToken({ user_id: userId });
137
+ connectUrl = `https://connect.hyperspell.com?token=${tokenResponse.token}`;
138
+ }
139
+ catch (_error) {
140
+ p.log.error("Could not generate connect URL. You can connect apps later using /connect.");
141
+ connectUrl = "";
142
+ }
143
+ p.note(`Available integrations:\n${integrationList}` +
144
+ (connectUrl ? `\n\nConnect your accounts at:\n${connectUrl}` : ""), "Connect Your Apps");
145
+ if (connectUrl) {
146
+ const openConnect = await p.confirm({
147
+ message: "Open connection page in your browser?",
148
+ });
149
+ if (!p.isCancel(openConnect) && openConnect) {
150
+ await openInBrowser(connectUrl);
151
+ p.log.info("Browser opened. Connect your accounts and come back when done.");
152
+ await p.confirm({
153
+ message: "Finished connecting accounts?",
154
+ active: "Yes",
155
+ inactive: "Not yet",
156
+ });
157
+ }
158
+ }
159
+ }
160
+ // Fetch connected sources
161
+ const s1 = p.spinner();
162
+ s1.start("Fetching connected sources");
163
+ const sources = await fetchConnectionSources(client, userId);
164
+ s1.stop(`Found ${sources.length} sources: ${sources.join(", ")}`);
165
+ // Step 5: Ask about memory sync
166
+ p.note("OpenClaw can automatically sync markdown files in your workspace's\n" +
167
+ "memory/ directory with Hyperspell. This allows you to:\n\n" +
168
+ "• Store notes and context that persist across sessions\n" +
169
+ "• Have the AI reference your local documentation\n" +
170
+ "• Keep local files in sync with Hyperspell's search", "Memory Sync");
171
+ const syncMemories = await p.confirm({
172
+ message: "Enable automatic memory sync for markdown files?",
173
+ initialValue: true,
174
+ });
175
+ if (p.isCancel(syncMemories)) {
176
+ p.cancel("Setup cancelled");
177
+ return;
178
+ }
179
+ // Step 5: Save configuration
180
+ const s2 = p.spinner();
181
+ s2.start("Saving configuration");
182
+ try {
183
+ const configPath = resolveConfigPath();
184
+ const openclawDir = path.dirname(configPath);
185
+ const envPath = path.join(openclawDir, ".env");
186
+ // Ensure directory exists
187
+ if (!fs.existsSync(openclawDir)) {
188
+ fs.mkdirSync(openclawDir, { recursive: true });
189
+ }
190
+ // Read existing config or create new one
191
+ let config = {};
192
+ if (fs.existsSync(configPath)) {
193
+ const existing = fs.readFileSync(configPath, "utf-8");
194
+ config = JSON.parse(existing);
195
+ }
196
+ // Merge in plugin configuration
197
+ if (!config.plugins) {
198
+ config.plugins = {};
199
+ }
200
+ const plugins = config.plugins;
201
+ if (!plugins.entries) {
202
+ plugins.entries = {};
203
+ }
204
+ const entries = plugins.entries;
205
+ entries["openclaw-hyperspell"] = {
206
+ enabled: true,
207
+ config: {
208
+ apiKey: "${HYPERSPELL_API_KEY}",
209
+ userId,
210
+ sources: sources.join(","),
211
+ autoContext: true,
212
+ syncMemories,
213
+ },
214
+ };
215
+ // Write config
216
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
217
+ // Write or append to .env
218
+ const envLine = `HYPERSPELL_API_KEY=${apiKey}`;
219
+ if (fs.existsSync(envPath)) {
220
+ const envContent = fs.readFileSync(envPath, "utf-8");
221
+ if (envContent.includes("HYPERSPELL_API_KEY=")) {
222
+ // Replace existing line
223
+ const updated = envContent.replace(/^HYPERSPELL_API_KEY=.*$/m, envLine);
224
+ fs.writeFileSync(envPath, updated);
225
+ }
226
+ else {
227
+ // Append new line
228
+ fs.appendFileSync(envPath, (envContent.endsWith("\n") ? "" : "\n") + envLine + "\n");
229
+ }
230
+ }
231
+ else {
232
+ fs.writeFileSync(envPath, envLine + "\n");
233
+ }
234
+ s2.stop("Configuration saved");
235
+ p.note(`Config: ${configPath}\n` +
236
+ `API Key: ${envPath}`, "Files updated");
237
+ }
238
+ catch (error) {
239
+ s2.stop("Failed to save configuration");
240
+ // Fall back to showing manual instructions
241
+ const configJson = JSON.stringify({
242
+ plugins: {
243
+ entries: {
244
+ "openclaw-hyperspell": {
245
+ enabled: true,
246
+ config: {
247
+ apiKey: "${HYPERSPELL_API_KEY}",
248
+ userId,
249
+ sources: sources.join(","),
250
+ autoContext: true,
251
+ syncMemories,
252
+ },
253
+ },
254
+ },
255
+ },
256
+ }, null, 2);
257
+ p.note(`Add to your openclaw.json:\n\n${configJson}\n\n` +
258
+ `Set the environment variable:\n export HYPERSPELL_API_KEY=${apiKey}`, "Manual configuration required");
259
+ }
260
+ // Step 7: Sync existing memories if enabled
261
+ if (syncMemories) {
262
+ const workspaceDir = getWorkspaceDir();
263
+ const memoryFiles = getMemoryFiles(workspaceDir);
264
+ if (memoryFiles.length > 0) {
265
+ const s3 = p.spinner();
266
+ s3.start(`Syncing ${memoryFiles.length} memory file(s)`);
267
+ const hyperspellClient = new HyperspellClient({
268
+ apiKey,
269
+ userId,
270
+ autoContext: true,
271
+ autoTrace: { enabled: false, extract: ["procedure"] },
272
+ emotionalContext: false,
273
+ syncMemories: true,
274
+ sources: [],
275
+ maxResults: 10,
276
+ relevanceThreshold: 0.6,
277
+ debug: false,
278
+ knowledgeGraph: { enabled: false, scanIntervalMinutes: 60, batchSize: 20 },
279
+ startupOrientation: {
280
+ enabled: false,
281
+ recentDays: 7,
282
+ recentLimit: 5,
283
+ loopsLimit: 3,
284
+ recentQuery: "conversation session interaction",
285
+ loopsQuery: "open tasks pending questions unfinished promised need to follow up",
286
+ },
287
+ });
288
+ const result = await syncAllMemoryFiles(hyperspellClient, workspaceDir);
289
+ if (result.failed > 0) {
290
+ s3.stop(`Synced ${result.synced} files, ${result.failed} failed`);
291
+ for (const error of result.errors) {
292
+ p.log.error(` ${error}`);
293
+ }
294
+ }
295
+ else {
296
+ s3.stop(`Synced ${result.synced} memory files`);
297
+ }
298
+ }
299
+ else {
300
+ p.log.info("No memory files found in memory/ directory");
301
+ }
302
+ }
303
+ // Step 8: Memory Network setup
304
+ p.note("The Memory Network automatically extracts entities (people, projects,\n" +
305
+ "organizations, topics) from your memories into structured markdown\n" +
306
+ "files. This runs as a periodic cron job in the main session.", "Memory Network");
307
+ const enableNetwork = await p.confirm({
308
+ message: "Enable the Memory Network?",
309
+ initialValue: false,
310
+ });
311
+ if (!p.isCancel(enableNetwork) && enableNetwork) {
312
+ // Update config to enable knowledgeGraph
313
+ try {
314
+ const configPath = resolveConfigPath();
315
+ if (fs.existsSync(configPath)) {
316
+ const configContent = fs.readFileSync(configPath, "utf-8");
317
+ const config = JSON.parse(configContent);
318
+ const pluginEntry = config?.plugins?.entries?.["openclaw-hyperspell"]?.config;
319
+ if (pluginEntry) {
320
+ pluginEntry.knowledgeGraph = { enabled: true };
321
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
322
+ }
323
+ }
324
+ p.log.success("Memory Network enabled in config");
325
+ }
326
+ catch {
327
+ p.log.warn("Could not update config — add knowledgeGraph.enabled: true manually");
328
+ }
329
+ // Write the extraction prompt to a file and create the cron job
330
+ const networkWorkspaceDir = getWorkspaceDir();
331
+ const promptPath = path.join(networkWorkspaceDir, "HYPERSPELL-MEMORY-NETWORK.md");
332
+ const prompt = buildExtractionPrompt(networkWorkspaceDir);
333
+ fs.mkdirSync(path.dirname(promptPath), { recursive: true });
334
+ fs.writeFileSync(promptPath, prompt);
335
+ const s4 = p.spinner();
336
+ s4.start("Creating Memory Network cron job");
337
+ let cronJobId = null;
338
+ try {
339
+ const output = execFileSync("openclaw", [
340
+ "cron", "add",
341
+ "--name", CRON_JOB_NAME,
342
+ "--every", "1h",
343
+ "--session", "isolated",
344
+ "--message", `Read the file at ${promptPath} and follow the instructions inside it.`,
345
+ ], { stdio: ["pipe", "pipe", "pipe"], timeout: 10_000 });
346
+ // Extract job ID from the JSON output
347
+ const text = output.toString().trim();
348
+ // Find the JSON object in the output (skip any non-JSON prefix lines)
349
+ const jsonStart = text.indexOf("{");
350
+ if (jsonStart >= 0) {
351
+ try {
352
+ const job = JSON.parse(text.slice(jsonStart));
353
+ cronJobId = job?.id || null;
354
+ }
355
+ catch { }
356
+ }
357
+ s4.stop("Cron job created — Memory Network will scan every hour");
358
+ }
359
+ catch (cronErr) {
360
+ s4.stop("Could not create cron job automatically");
361
+ p.log.warn(`Cron creation failed. Create it manually:`);
362
+ p.note(`openclaw cron add \\\n` +
363
+ ` --name "${CRON_JOB_NAME}" \\\n` +
364
+ ` --every 1h \\\n` +
365
+ ` --session isolated \\\n` +
366
+ ` --message "Read the file at ${promptPath} and follow the instructions inside it."`, "Manual cron setup");
367
+ }
368
+ // Ask if they want to run the first extraction now
369
+ const runNow = await p.confirm({
370
+ message: "Run the Memory Network now? (If not, it will run automatically on the next cron cycle)",
371
+ initialValue: true,
372
+ });
373
+ if (!p.isCancel(runNow) && runNow) {
374
+ if (cronJobId) {
375
+ const s5 = p.spinner();
376
+ s5.start("Triggering Memory Network extraction");
377
+ try {
378
+ execFileSync("openclaw", [
379
+ "cron", "run", cronJobId,
380
+ ], { stdio: "pipe", timeout: 10_000 });
381
+ s5.stop("Memory Network extraction triggered — running in the background");
382
+ }
383
+ catch {
384
+ s5.stop("Could not trigger automatically");
385
+ p.log.info(`You can trigger it manually with: openclaw cron run ${cronJobId}`);
386
+ }
387
+ }
388
+ else {
389
+ p.log.info("Create the cron job first, then run it with: openclaw cron run <job-id>");
390
+ }
391
+ }
392
+ }
393
+ const syncNote = syncMemories
394
+ ? "\n\nMemory sync is enabled — markdown files in memory/ will be\n" +
395
+ "automatically synced to Hyperspell when they change."
396
+ : "";
397
+ const networkNote = !p.isCancel(enableNetwork) && enableNetwork
398
+ ? "\n\nMemory Network is enabled — entities will be extracted into\n" +
399
+ "memory/people/, memory/projects/, memory/organizations/, memory/topics/"
400
+ : "";
401
+ p.note("/getcontext <query> Search your memories for relevant context\n" +
402
+ "/remember <text> Save something directly to your vault\n\n" +
403
+ "To connect more apps, run: openclaw openclaw-hyperspell connect\n\n" +
404
+ "Auto-context is enabled by default — relevant memories are\n" +
405
+ "automatically injected before each AI response." +
406
+ syncNote +
407
+ networkNote, "How to use Hyperspell");
408
+ p.outro("Setup complete!");
409
+ }
410
+ async function runConnect(pluginConfig) {
411
+ const config = pluginConfig;
412
+ p.intro("Hyperspell Connect");
413
+ if (!config?.apiKey) {
414
+ p.log.error("Not configured");
415
+ p.note("Run 'openclaw openclaw-hyperspell setup' to configure Hyperspell first.");
416
+ p.outro("");
417
+ return;
418
+ }
419
+ const s = p.spinner();
420
+ s.start("Generating connect URL");
421
+ let client;
422
+ let userId;
423
+ try {
424
+ client = new Hyperspell({ apiKey: config.apiKey });
425
+ userId = config.userId || userInfo().username || "user";
426
+ const tokenResponse = await client.auth.userToken({ user_id: userId });
427
+ const connectUrl = `https://connect.hyperspell.com?token=${tokenResponse.token}`;
428
+ s.stop("Connect URL ready");
429
+ await openInBrowser(connectUrl);
430
+ p.log.success("Browser opened to connect.hyperspell.com");
431
+ await p.confirm({
432
+ message: "Finished connecting accounts?",
433
+ active: "Yes",
434
+ inactive: "Not yet",
435
+ });
436
+ // Fetch and update sources
437
+ const s2 = p.spinner();
438
+ s2.start("Updating sources configuration");
439
+ const sources = await fetchConnectionSources(client, userId);
440
+ const configPath = resolveConfigPath();
441
+ updateConfigSources(configPath, sources);
442
+ s2.stop(`Sources updated: ${sources.join(", ")}`);
443
+ p.outro("Done! Restart OpenClaw to apply changes.");
444
+ }
445
+ catch (_error) {
446
+ s.stop("Failed to generate connect URL");
447
+ p.log.error("Check your API key and try again.");
448
+ p.outro("");
449
+ }
450
+ }
451
+ async function runStatus(pluginConfig) {
452
+ const config = pluginConfig;
453
+ p.intro("Hyperspell Status");
454
+ if (!config?.apiKey) {
455
+ p.log.warn("Not configured");
456
+ p.note("Run 'openclaw openclaw-hyperspell setup' to configure Hyperspell.");
457
+ p.outro("");
458
+ return;
459
+ }
460
+ p.log.success("Configured");
461
+ p.log.info(`User ID: ${config.userId || "(not set)"}`);
462
+ p.log.info(`Auto-Context: ${config.autoContext !== false ? "Enabled" : "Disabled"}`);
463
+ p.log.info(`Memory Sync: ${config.syncMemories ? "Enabled" : "Disabled"}`);
464
+ p.log.info(`Sources Filter: ${config.sources || "(all sources)"}`);
465
+ p.log.info(`Max Results: ${config.maxResults || 10}`);
466
+ const s = p.spinner();
467
+ s.start("Testing connection");
468
+ try {
469
+ const client = new Hyperspell({ apiKey: config.apiKey });
470
+ const integrations = await client.integrations.list();
471
+ s.stop(`Connection OK (${integrations.integrations.length} integrations available)`);
472
+ if (integrations.integrations.length > 0) {
473
+ const list = integrations.integrations
474
+ .map((int) => `• ${int.name} (${int.provider})`)
475
+ .join("\n");
476
+ p.note(list, "Available integrations");
477
+ }
478
+ }
479
+ catch (_error) {
480
+ s.stop("Connection failed");
481
+ p.log.error("Check your API key and try again.");
482
+ }
483
+ p.outro("");
484
+ }
485
+ export function registerCliCommands(program, pluginConfig) {
486
+ const hyperspellCmd = program
487
+ .command("openclaw-hyperspell")
488
+ .description("Hyperspell — Memory and context for your AI agent");
489
+ hyperspellCmd
490
+ .command("setup")
491
+ .description("Interactive setup wizard for Hyperspell")
492
+ .action(async () => {
493
+ await runSetup();
494
+ });
495
+ hyperspellCmd
496
+ .command("status")
497
+ .description("Show Hyperspell connection status")
498
+ .action(async () => {
499
+ await runStatus(pluginConfig);
500
+ });
501
+ hyperspellCmd
502
+ .command("connect")
503
+ .description("Open the Hyperspell connect page to link your accounts")
504
+ .action(async () => {
505
+ await runConnect(pluginConfig);
506
+ });
507
+ // Memory Network CLI commands (used by isolated cron sessions via exec)
508
+ const networkCmd = hyperspellCmd
509
+ .command("network")
510
+ .description("Memory Network operations");
511
+ networkCmd
512
+ .command("scan")
513
+ .description("Scan for unprocessed memories and output summaries")
514
+ .option("--batch-size <n>", "Max memories to return", "20")
515
+ .action(async (opts) => {
516
+ try {
517
+ const cfg = parseConfig(pluginConfig);
518
+ const client = new HyperspellClient(cfg);
519
+ const workspaceDir = getWorkspaceDir();
520
+ const stateManager = new NetworkStateManager(workspaceDir);
521
+ const batchSize = Number.parseInt(opts.batchSize, 10) || 20;
522
+ const memories = await scanMemories(client, stateManager, batchSize);
523
+ const text = formatScanResults(memories, stateManager.getProcessedCount(), stateManager.getLastScanAt());
524
+ process.stdout.write(text + "\n");
525
+ }
526
+ catch (err) {
527
+ process.stderr.write(`Scan failed: ${err instanceof Error ? err.message : String(err)}\n`);
528
+ process.exit(1);
529
+ }
530
+ });
531
+ networkCmd
532
+ .command("complete")
533
+ .description("Mark memory IDs as processed")
534
+ .requiredOption("--ids <ids>", "Comma-separated resource_ids")
535
+ .action((opts) => {
536
+ try {
537
+ const workspaceDir = getWorkspaceDir();
538
+ const stateManager = new NetworkStateManager(workspaceDir);
539
+ const memoryIds = opts.ids.split(",").map((s) => s.trim()).filter(Boolean);
540
+ const { newCount, totalCount } = completeMemories(stateManager, memoryIds);
541
+ process.stdout.write(`Marked ${newCount} new memories as processed (${totalCount} total)\n`);
542
+ }
543
+ catch (err) {
544
+ process.stderr.write(`Complete failed: ${err instanceof Error ? err.message : String(err)}\n`);
545
+ process.exit(1);
546
+ }
547
+ });
548
+ networkCmd
549
+ .command("sync")
550
+ .description("Sync entity files in memory/ to Hyperspell")
551
+ .action(async () => {
552
+ try {
553
+ const cfg = parseConfig(pluginConfig);
554
+ const client = new HyperspellClient(cfg);
555
+ const workspaceDir = getWorkspaceDir();
556
+ const result = await syncAllMemoryFiles(client, workspaceDir);
557
+ process.stdout.write(`Synced ${result.synced} files, ${result.failed} failed\n`);
558
+ if (result.errors.length > 0) {
559
+ for (const error of result.errors) {
560
+ process.stderr.write(` ${error}\n`);
561
+ }
562
+ }
563
+ }
564
+ catch (err) {
565
+ process.stderr.write(`Sync failed: ${err instanceof Error ? err.message : String(err)}\n`);
566
+ process.exit(1);
567
+ }
568
+ });
569
+ }