@storacha/clawracha 0.0.4 → 0.0.5

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/src/plugin.ts DELETED
@@ -1,489 +0,0 @@
1
- /**
2
- * OpenClaw Plugin Entry Point
3
- *
4
- * Registers:
5
- * - Background service for file watching and sync
6
- * - Agent tools for manual sync control
7
- * - Slash commands for setup
8
- */
9
-
10
- import * as fs from "node:fs/promises";
11
- import * as path from "node:path";
12
- import { extract as extractDelegation } from "@storacha/client/delegation";
13
- import type {
14
- OpenClawPluginApi,
15
- OpenClawPluginServiceContext,
16
- AnyAgentTool,
17
- } from "openclaw/plugin-sdk";
18
- import type { DeviceConfig, SyncPluginConfig } from "./types/index.js";
19
- import { SyncEngine } from "./sync.js";
20
- import { FileWatcher } from "./watcher.js";
21
- import { createStorachaClient } from "./utils/client.js";
22
-
23
- // Global state
24
- let syncEngine: SyncEngine | null = null;
25
- let fileWatcher: FileWatcher | null = null;
26
- let workspaceDir: string | undefined;
27
-
28
- /**
29
- * Load device config from .storacha/config.json
30
- */
31
- async function loadDeviceConfig(
32
- workspace: string,
33
- ): Promise<DeviceConfig | null> {
34
- const configPath = path.join(workspace, ".storacha", "config.json");
35
- try {
36
- const content = await fs.readFile(configPath, "utf-8");
37
- return JSON.parse(content) as DeviceConfig;
38
- } catch (err: any) {
39
- if (err.code === "ENOENT") return null;
40
- throw err;
41
- }
42
- }
43
-
44
- /**
45
- * Save device config
46
- */
47
- async function saveDeviceConfig(
48
- workspace: string,
49
- config: DeviceConfig,
50
- ): Promise<void> {
51
- const configDir = path.join(workspace, ".storacha");
52
- await fs.mkdir(configDir, { recursive: true });
53
- const configPath = path.join(configDir, "config.json");
54
- await fs.writeFile(configPath, JSON.stringify(config, null, 2));
55
- }
56
-
57
- /**
58
- * Plugin entry — called by OpenClaw when the plugin is loaded.
59
- */
60
- export default function plugin(api: OpenClawPluginApi) {
61
- // Capture plugin-specific config at registration time
62
- const pluginConfig = (api.pluginConfig ?? {}) as Partial<SyncPluginConfig>;
63
-
64
- // Register background service
65
- api.registerService({
66
- id: "storacha-sync",
67
- async start(ctx: OpenClawPluginServiceContext) {
68
- if (pluginConfig.enabled === false) {
69
- ctx.logger.info("Storacha sync disabled via config.");
70
- return;
71
- }
72
-
73
- workspaceDir = ctx.workspaceDir;
74
- const workspace = workspaceDir;
75
- if (!workspace) {
76
- ctx.logger.warn("No workspace directory configured");
77
- return;
78
- }
79
-
80
- const deviceConfig = await loadDeviceConfig(workspace);
81
- if (!deviceConfig || !deviceConfig.setupComplete) {
82
- ctx.logger.info(
83
- "Setup not complete. Run /storacha-init or /storacha-join first.",
84
- );
85
- return;
86
- }
87
-
88
- const storachaClient = await createStorachaClient(deviceConfig);
89
- syncEngine = new SyncEngine(storachaClient, workspace);
90
- await syncEngine.init(deviceConfig);
91
-
92
- fileWatcher = new FileWatcher({
93
- workspace,
94
- config: {
95
- enabled: true,
96
- watchPatterns: pluginConfig.watchPatterns ?? ["**/*"],
97
- ignorePatterns: pluginConfig.ignorePatterns ?? [
98
- ".storacha/**",
99
- "node_modules/**",
100
- ".git/**",
101
- "dist/**",
102
- ],
103
- },
104
- onChanges: async (changes) => {
105
- if (!syncEngine) return;
106
- await syncEngine.processChanges(changes);
107
- await syncEngine.sync();
108
-
109
- const nameArchive = await syncEngine.exportNameArchive();
110
- const updatedConfig = { ...deviceConfig, nameArchive };
111
- await saveDeviceConfig(workspace, updatedConfig);
112
- },
113
- });
114
-
115
- await fileWatcher.start();
116
- ctx.logger.info("Started watching workspace");
117
- },
118
-
119
- async stop(ctx: OpenClawPluginServiceContext) {
120
- if (fileWatcher) {
121
- await fileWatcher.stop();
122
- fileWatcher = null;
123
- }
124
- syncEngine = null;
125
- ctx.logger.info("Stopped");
126
- },
127
- });
128
-
129
- // Register agent tools
130
- api.registerTool({
131
- name: "storacha_sync_status",
132
- label: "Storacha Sync Status",
133
- description: "Get the current Storacha workspace sync status",
134
- parameters: { type: "object", properties: {} } as any,
135
- execute: async () => {
136
- if (!syncEngine) {
137
- return {
138
- content: [
139
- {
140
- type: "text" as const,
141
- text: "Sync not initialized. Run /storacha-init or /storacha-join first.",
142
- },
143
- ],
144
- details: null,
145
- };
146
- }
147
- const status = await syncEngine.status();
148
- return {
149
- content: [
150
- { type: "text" as const, text: JSON.stringify(status, null, 2) },
151
- ],
152
- details: status,
153
- };
154
- },
155
- } as AnyAgentTool);
156
-
157
- api.registerTool({
158
- name: "storacha_sync_now",
159
- label: "Storacha Sync Now",
160
- description: "Trigger an immediate workspace sync to Storacha",
161
- parameters: { type: "object", properties: {} } as any,
162
- execute: async () => {
163
- if (!syncEngine) {
164
- return {
165
- content: [
166
- {
167
- type: "text" as const,
168
- text: "Sync not initialized. Run /storacha-init or /storacha-join first.",
169
- },
170
- ],
171
- details: null,
172
- };
173
- }
174
- await syncEngine.sync();
175
- const status = await syncEngine.status();
176
- return {
177
- content: [
178
- {
179
- type: "text" as const,
180
- text: JSON.stringify({ success: true, status }, null, 2),
181
- },
182
- ],
183
- details: status,
184
- };
185
- },
186
- } as AnyAgentTool);
187
-
188
- // --- Slash Commands ---
189
-
190
- api.registerCommand({
191
- name: "storacha-init",
192
- description:
193
- "Initialize a NEW Storacha workspace (first device). Usage: /storacha-init <upload-delegation-b64>",
194
- acceptsArgs: true,
195
- handler: async (_ctx) => {
196
- const workspace = workspaceDir;
197
- if (!workspace) return { text: "No workspace configured." };
198
-
199
- const b64 = _ctx.args?.trim();
200
- if (!b64) {
201
- return {
202
- text: [
203
- "Usage: `/storacha-init <upload-delegation-b64>`",
204
- "",
205
- "This creates a **new** workspace. If you're syncing from an existing device, use `/storacha-join` instead.",
206
- ].join("\n"),
207
- };
208
- }
209
-
210
- // Validate delegation
211
- const bytes = Buffer.from(b64, "base64");
212
- const { ok: delegation, error } = await extractDelegation(bytes);
213
- if (!delegation) {
214
- return { text: `Invalid delegation: ${error}` };
215
- }
216
-
217
- const { Agent } = await import("@storacha/ucn/pail");
218
- const agent = await Agent.generate();
219
- const agentKey = Agent.format(agent);
220
-
221
- const spaceDID = delegation.capabilities[0]?.with;
222
-
223
- const config: DeviceConfig = {
224
- agentKey,
225
- uploadDelegation: b64,
226
- spaceDID: spaceDID ?? undefined,
227
- setupComplete: true,
228
- };
229
- await saveDeviceConfig(workspace, config);
230
-
231
- return {
232
- text: [
233
- "\u{1f525} Storacha workspace initialized!",
234
- `Agent DID: \`${agent.did()}\``,
235
- `Space: \`${spaceDID ?? "unknown"}\``,
236
- "",
237
- "Restart the gateway to start syncing.",
238
- "",
239
- "To add another device, run `/storacha-grant <their-agent-DID>` here,",
240
- "then `/storacha-join <upload-b64> <name-b64>` on the other device.",
241
- ].join("\n"),
242
- };
243
- },
244
- });
245
-
246
- api.registerCommand({
247
- name: "storacha-join",
248
- description:
249
- "Join an existing Storacha workspace from another device. Usage: /storacha-join <upload-delegation-b64> <name-delegation-b64>",
250
- acceptsArgs: true,
251
- handler: async (_ctx) => {
252
- const workspace = workspaceDir;
253
- if (!workspace) return { text: "No workspace configured." };
254
-
255
- const args = _ctx.args?.trim();
256
- if (!args) {
257
- return {
258
- text: [
259
- "Usage: `/storacha-join <upload-delegation-b64> <name-delegation-b64>`",
260
- "",
261
- "Get both delegations by running `/storacha-grant` on the existing device.",
262
- "If you're setting up a **new** workspace, use `/storacha-init` instead.",
263
- ].join("\n"),
264
- };
265
- }
266
-
267
- const spaceIdx = args.indexOf(" ");
268
- if (spaceIdx === -1) {
269
- return {
270
- text: "Two arguments required: `/storacha-join <upload-b64> <name-b64>`",
271
- };
272
- }
273
-
274
- const uploadB64 = args.slice(0, spaceIdx).trim();
275
- const nameB64 = args.slice(spaceIdx + 1).trim();
276
-
277
- if (!uploadB64 || !nameB64) {
278
- return {
279
- text: "Two arguments required: `/storacha-join <upload-b64> <name-b64>`",
280
- };
281
- }
282
-
283
- // Validate upload delegation
284
- const uploadBytes = Buffer.from(uploadB64, "base64");
285
- const { ok: uploadDelegation, error: uploadErr } =
286
- await extractDelegation(uploadBytes);
287
- if (!uploadDelegation) {
288
- return { text: `Invalid upload delegation: ${uploadErr}` };
289
- }
290
-
291
- // Validate name delegation
292
- const nameBytes = Buffer.from(nameB64, "base64");
293
- const { ok: nameDelegation, error: nameErr } =
294
- await extractDelegation(nameBytes);
295
- if (!nameDelegation) {
296
- return { text: `Invalid name delegation: ${nameErr}` };
297
- }
298
-
299
- const { Agent } = await import("@storacha/ucn/pail");
300
- const agent = await Agent.generate();
301
- const agentKey = Agent.format(agent);
302
-
303
- const spaceDID = uploadDelegation.capabilities[0]?.with;
304
-
305
- const config: DeviceConfig = {
306
- agentKey,
307
- uploadDelegation: uploadB64,
308
- nameDelegation: nameB64,
309
- spaceDID: spaceDID ?? undefined,
310
- setupComplete: true,
311
- };
312
- await saveDeviceConfig(workspace, config);
313
-
314
- // Pull remote state immediately before watcher starts
315
- let pullCount = 0;
316
- try {
317
- const storachaClient = await createStorachaClient(config);
318
- const engine = new SyncEngine(storachaClient, workspace);
319
- await engine.init(config);
320
- pullCount = await engine.pullRemote();
321
-
322
- // Save name archive after pull
323
- const nameArchive = await engine.exportNameArchive();
324
- config.nameArchive = nameArchive;
325
- await saveDeviceConfig(workspace, config);
326
- } catch (err: any) {
327
- return {
328
- text: [
329
- "\u26a0\ufe0f Delegations saved but initial pull failed:",
330
- `\`${err.message}\``,
331
- "",
332
- "Restart the gateway to retry.",
333
- ].join("\n"),
334
- };
335
- }
336
-
337
- return {
338
- text: [
339
- "\u{1f525} Joined existing Storacha workspace!",
340
- `Agent DID: \`${agent.did()}\``,
341
- `Space: \`${spaceDID ?? "unknown"}\``,
342
- `Pulled ${pullCount} files from remote.`,
343
- "",
344
- "Restart the gateway to start syncing.",
345
- ].join("\n"),
346
- };
347
- },
348
- });
349
-
350
- api.registerCommand({
351
- name: "storacha-grant",
352
- description:
353
- "Grant another device access. Usage: /storacha-grant <target-DID>",
354
- acceptsArgs: true,
355
- handler: async (_ctx) => {
356
- const workspace = workspaceDir;
357
- if (!workspace) return { text: "No workspace configured." };
358
-
359
- const targetDID = _ctx.args?.trim() as `did:${string}:${string}`;
360
- if (!targetDID || !targetDID.startsWith("did:")) {
361
- return { text: "Usage: `/storacha-grant <did:key:z...>`" };
362
- }
363
-
364
- const config = await loadDeviceConfig(workspace);
365
- if (!config) {
366
- return {
367
- text: "Not initialized. Run `/storacha-init` or `/storacha-join` first.",
368
- };
369
- }
370
-
371
- const results: string[] = [];
372
-
373
- // Re-delegate upload capability
374
- if (config.uploadDelegation) {
375
- try {
376
- const storachaClient = await createStorachaClient(config);
377
- const audience = { did: () => targetDID } as any;
378
- const uploadDelegation = await storachaClient.createDelegation(
379
- audience,
380
- [
381
- "space/blob/add",
382
- "space/index/add",
383
- "upload/add",
384
- "filecoin/offer",
385
- ],
386
- );
387
- const { ok: archiveBytes } = await uploadDelegation.archive();
388
- if (archiveBytes) {
389
- const b64 = Buffer.from(archiveBytes).toString("base64");
390
- results.push("**Upload delegation:**\n```\n" + b64 + "\n```");
391
- }
392
- } catch (err: any) {
393
- results.push(`\u274c Failed to create upload delegation: ${err.message}`);
394
- }
395
- } else {
396
- results.push("\u26a0\ufe0f No upload delegation to re-delegate.");
397
- }
398
-
399
- // Re-delegate name (pail sync) capability
400
- if (config.nameDelegation) {
401
- try {
402
- const { Agent, Name } = await import("@storacha/ucn/pail");
403
- const agent = Agent.parse(config.agentKey);
404
-
405
- let name;
406
- if (config.nameArchive) {
407
- const archiveBytes = Buffer.from(config.nameArchive, "base64");
408
- name = await Name.extract(agent, archiveBytes);
409
- } else {
410
- const nameBytes = Buffer.from(config.nameDelegation, "base64");
411
- const { ok: nameDel } = await extractDelegation(nameBytes);
412
- if (!nameDel) {
413
- results.push("\u274c Failed to extract name delegation.");
414
- } else {
415
- name = Name.from(agent, [nameDel]);
416
- }
417
- }
418
-
419
- if (name) {
420
- const nameDel = await name.grant(targetDID);
421
- const { ok: archiveBytes } = await nameDel.archive();
422
- if (archiveBytes) {
423
- const b64 = Buffer.from(archiveBytes).toString("base64");
424
- results.push("**Name delegation:**\n```\n" + b64 + "\n```");
425
- }
426
- }
427
- } catch (err: any) {
428
- results.push(`\u274c Failed to create name delegation: ${err.message}`);
429
- }
430
- } else {
431
- results.push("\u26a0\ufe0f No name delegation to re-delegate.");
432
- }
433
-
434
- if (results.length === 0) {
435
- return { text: "Nothing to grant. Set up this device first." };
436
- }
437
-
438
- return {
439
- text: [
440
- `\u{1f525} Delegations for \`${targetDID}\`:`,
441
- "",
442
- ...results,
443
- "",
444
- "The target device should run:",
445
- "`/storacha-join <upload-b64> <name-b64>`",
446
- ].join("\n"),
447
- };
448
- },
449
- });
450
-
451
- api.registerCommand({
452
- name: "storacha-status",
453
- description: "Show Storacha sync status",
454
- handler: async (_ctx) => {
455
- const workspace = workspaceDir;
456
- if (!workspace) return { text: "No workspace configured." };
457
-
458
- const config = await loadDeviceConfig(workspace);
459
- if (!config)
460
- return {
461
- text: "Not initialized. Run /storacha-init or /storacha-join first.",
462
- };
463
-
464
- const lines = [
465
- "\u{1f525} Storacha Sync Status",
466
- `Agent: configured`,
467
- `Upload delegation: ${config.uploadDelegation ? "\u2705" : "\u274c not set"}`,
468
- `Name delegation: ${config.nameDelegation ? "\u2705" : "\u274c not set"}`,
469
- `Space DID: ${config.spaceDID ?? "unknown"}`,
470
- `Name Archive: ${config.nameArchive ? "saved" : "not created"}`,
471
- `Setup complete: ${config.setupComplete ? "\u2705" : "\u274c"}`,
472
- ];
473
-
474
- if (syncEngine) {
475
- const status = await syncEngine.status();
476
- lines.push(
477
- `Running: ${status.running}`,
478
- `Last Sync: ${
479
- status.lastSync ? new Date(status.lastSync).toISOString() : "never"
480
- }`,
481
- `Entries: ${status.entryCount}`,
482
- `Pending: ${status.pendingChanges}`,
483
- );
484
- }
485
-
486
- return { text: lines.join("\n") };
487
- },
488
- });
489
- }