@storacha/clawracha 0.0.11-rc.0 → 0.1.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.
package/README.md CHANGED
@@ -21,12 +21,14 @@ openclaw plugins install @storacha/clawracha
21
21
 
22
22
  ## Setup
23
23
 
24
- Setup is done via slash commands in your chat session with the agent (not CLI commands).
24
+ Setup is done via CLI commands on the host (not slash commands in chat).
25
+
26
+ All commands require `--agent <id>` to specify which agent workspace to configure.
25
27
 
26
28
  ### Step 1: Initialize the agent
27
29
 
28
- ```
29
- /storacha-init
30
+ ```bash
31
+ openclaw clawracha init --agent <id>
30
32
  ```
31
33
 
32
34
  Generates an agent identity and displays the Agent DID. You'll need this DID to create delegations.
@@ -35,32 +37,32 @@ Generates an agent identity and displays the Agent DID. You'll need this DID to
35
37
 
36
38
  **New workspace (first device):**
37
39
 
38
- ```
39
- /storacha-setup <upload-delegation-b64>
40
+ ```bash
41
+ openclaw clawracha setup <delegation> --agent <id>
40
42
  ```
41
43
 
42
- Have the space owner create an upload delegation for your Agent DID, then import it. Creates a fresh UCN Name and starts syncing.
44
+ `<delegation>` can be a file path (raw CAR) or a base64 CID string. Have the space owner create an upload delegation for your Agent DID, then import it.
43
45
 
44
46
  **Join an existing workspace (additional devices):**
45
47
 
46
- ```
47
- /storacha-join <upload-delegation-b64> <name-delegation-b64>
48
+ ```bash
49
+ openclaw clawracha join <upload-delegation> <name-delegation> --agent <id>
48
50
  ```
49
51
 
50
- Get both delegations by running `/storacha-grant` on the existing device. The join command pulls all remote files before the watcher starts, so your local workspace is fully synced from the start.
52
+ Get both delegations by running `openclaw clawracha grant` on the existing device. Arguments can be file paths or base64 CID strings. The join command pulls all remote files before the watcher starts.
51
53
 
52
54
  ### Grant access to another device
53
55
 
54
- ```
55
- /storacha-grant <target-agent-DID>
56
+ ```bash
57
+ openclaw clawracha grant <target-agent-DID> --agent <id>
56
58
  ```
57
59
 
58
- Generates upload and name delegations for the target device. The target device uses these with `/storacha-join`.
60
+ Generates upload and name delegations for the target device.
59
61
 
60
62
  ### Check status
61
63
 
62
- ```
63
- /storacha-status
64
+ ```bash
65
+ openclaw clawracha status --agent <id>
64
66
  ```
65
67
 
66
68
  After setup, restart the gateway to start syncing:
package/dist/plugin.d.ts CHANGED
@@ -2,13 +2,10 @@
2
2
  * OpenClaw Plugin Entry Point
3
3
  *
4
4
  * Registers:
5
- * - Background service for file watching and sync
5
+ * - Background service that syncs ALL agent workspaces with .storacha configs
6
+ * - CLI commands for setup and management (openclaw clawracha ...)
6
7
  * - Agent tools for manual sync control
7
- * - Slash commands for setup
8
8
  */
9
9
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
10
- /**
11
- * Plugin entry — called by OpenClaw when the plugin is loaded.
12
- */
13
10
  export default function plugin(api: OpenClawPluginApi): void;
14
11
  //# sourceMappingURL=plugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EACV,iBAAiB,EAGlB,MAAM,qBAAqB,CAAC;AAyC7B;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,GAAG,EAAE,iBAAiB,QA4hBpD"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EACV,iBAAiB,EAGlB,MAAM,qBAAqB,CAAC;AAiG7B,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,GAAG,EAAE,iBAAiB,QAoepD"}
package/dist/plugin.js CHANGED
@@ -2,24 +2,19 @@
2
2
  * OpenClaw Plugin Entry Point
3
3
  *
4
4
  * Registers:
5
- * - Background service for file watching and sync
5
+ * - Background service that syncs ALL agent workspaces with .storacha configs
6
+ * - CLI commands for setup and management (openclaw clawracha ...)
6
7
  * - Agent tools for manual sync control
7
- * - Slash commands for setup
8
8
  */
9
9
  import * as fs from "node:fs/promises";
10
10
  import * as path from "node:path";
11
- import { extract as extractDelegation } from "@storacha/client/delegation";
12
11
  import { SyncEngine } from "./sync.js";
13
- import { decodeDelegation, encodeDelegation } from "./utils/delegation.js";
14
12
  import { FileWatcher } from "./watcher.js";
15
13
  import { createStorachaClient } from "./utils/client.js";
16
- // Global state
17
- let syncEngine = null;
18
- let fileWatcher = null;
19
- let workspaceDir;
20
- /**
21
- * Load device config from .storacha/config.json
22
- */
14
+ import { decodeDelegation, encodeDelegation, readDelegationArg, } from "./utils/delegation.js";
15
+ import { resolveAgentWorkspace, getAgentIds, } from "./utils/workspace.js";
16
+ const activeSyncers = new Map();
17
+ // --- Config helpers ---
23
18
  async function loadDeviceConfig(workspace) {
24
19
  const configPath = path.join(workspace, ".storacha", "config.json");
25
20
  try {
@@ -32,22 +27,49 @@ async function loadDeviceConfig(workspace) {
32
27
  throw err;
33
28
  }
34
29
  }
35
- /**
36
- * Save device config
37
- */
38
30
  async function saveDeviceConfig(workspace, config) {
39
31
  const configDir = path.join(workspace, ".storacha");
40
32
  await fs.mkdir(configDir, { recursive: true });
41
33
  const configPath = path.join(configDir, "config.json");
42
34
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
43
35
  }
44
- /**
45
- * Plugin entry called by OpenClaw when the plugin is loaded.
46
- */
36
+ // --- Service helpers ---
37
+ async function startWorkspaceSync(workspace, agentId, pluginConfig, logger) {
38
+ const deviceConfig = await loadDeviceConfig(workspace);
39
+ if (!deviceConfig || !deviceConfig.setupComplete) {
40
+ return null;
41
+ }
42
+ const storachaClient = await createStorachaClient(deviceConfig);
43
+ const engine = new SyncEngine(storachaClient, workspace);
44
+ await engine.init(deviceConfig);
45
+ const watcher = new FileWatcher({
46
+ workspace,
47
+ config: {
48
+ enabled: true,
49
+ watchPatterns: pluginConfig.watchPatterns ?? ["**/*"],
50
+ ignorePatterns: pluginConfig.ignorePatterns ?? [
51
+ ".storacha/**",
52
+ "node_modules/**",
53
+ ".git/**",
54
+ "dist/**",
55
+ ],
56
+ },
57
+ onChanges: async (changes) => {
58
+ await engine.processChanges(changes);
59
+ await engine.sync();
60
+ const nameArchive = await engine.exportNameArchive();
61
+ const updatedConfig = { ...deviceConfig, nameArchive };
62
+ await saveDeviceConfig(workspace, updatedConfig);
63
+ },
64
+ });
65
+ await watcher.start();
66
+ logger.info(`[${agentId}] Started syncing workspace: ${workspace}`);
67
+ return { engine, watcher, workspace, agentId };
68
+ }
69
+ // --- Plugin entry ---
47
70
  export default function plugin(api) {
48
- // Capture plugin-specific config at registration time
49
71
  const pluginConfig = (api.pluginConfig ?? {});
50
- // Register background service
72
+ // --- Background service: one syncer per agent workspace ---
51
73
  api.registerService({
52
74
  id: "storacha-sync",
53
75
  async start(ctx) {
@@ -55,73 +77,55 @@ export default function plugin(api) {
55
77
  ctx.logger.info("Storacha sync disabled via config.");
56
78
  return;
57
79
  }
58
- workspaceDir = ctx.workspaceDir;
59
- const workspace = workspaceDir;
60
- if (!workspace) {
61
- ctx.logger.warn("No workspace directory configured");
62
- return;
80
+ const agentIds = getAgentIds(ctx.config);
81
+ for (const agentId of agentIds) {
82
+ const workspace = resolveAgentWorkspace(ctx.config, agentId);
83
+ try {
84
+ const sync = await startWorkspaceSync(workspace, agentId, pluginConfig, ctx.logger);
85
+ if (sync) {
86
+ activeSyncers.set(workspace, sync);
87
+ }
88
+ }
89
+ catch (err) {
90
+ ctx.logger.warn(`[${agentId}] Failed to start sync: ${err.message}`);
91
+ }
63
92
  }
64
- const deviceConfig = await loadDeviceConfig(workspace);
65
- if (!deviceConfig || !deviceConfig.setupComplete) {
66
- ctx.logger.info("Setup not complete. Run /storacha-init first, then /storacha-setup or /storacha-join.");
67
- return;
93
+ if (activeSyncers.size === 0) {
94
+ ctx.logger.info("No agent workspaces configured for Storacha sync. Use `openclaw clawracha init --agent <id>` to set up.");
95
+ }
96
+ else {
97
+ ctx.logger.info(`Storacha sync active for ${activeSyncers.size} workspace(s).`);
68
98
  }
69
- const storachaClient = await createStorachaClient(deviceConfig);
70
- syncEngine = new SyncEngine(storachaClient, workspace);
71
- await syncEngine.init(deviceConfig);
72
- fileWatcher = new FileWatcher({
73
- workspace,
74
- config: {
75
- enabled: true,
76
- watchPatterns: pluginConfig.watchPatterns ?? ["**/*"],
77
- ignorePatterns: pluginConfig.ignorePatterns ?? [
78
- ".storacha/**",
79
- "node_modules/**",
80
- ".git/**",
81
- "dist/**",
82
- ],
83
- },
84
- onChanges: async (changes) => {
85
- if (!syncEngine)
86
- return;
87
- await syncEngine.processChanges(changes);
88
- await syncEngine.sync();
89
- const nameArchive = await syncEngine.exportNameArchive();
90
- const updatedConfig = { ...deviceConfig, nameArchive };
91
- await saveDeviceConfig(workspace, updatedConfig);
92
- },
93
- });
94
- await fileWatcher.start();
95
- ctx.logger.info("Started watching workspace");
96
99
  },
97
100
  async stop(ctx) {
98
- if (fileWatcher) {
99
- await fileWatcher.stop();
100
- fileWatcher = null;
101
+ for (const [workspace, sync] of activeSyncers) {
102
+ await sync.watcher.stop();
103
+ ctx.logger.info(`[${sync.agentId}] Stopped syncing: ${workspace}`);
101
104
  }
102
- syncEngine = null;
103
- ctx.logger.info("Stopped");
105
+ activeSyncers.clear();
104
106
  },
105
107
  });
106
- // Register agent tools
108
+ // --- Agent tools (keyed by workspace dir) ---
107
109
  api.registerTool({
108
110
  name: "storacha_sync_status",
109
111
  label: "Storacha Sync Status",
110
112
  description: "Get the current Storacha workspace sync status",
111
113
  parameters: { type: "object", properties: {} },
112
- execute: async () => {
113
- if (!syncEngine) {
114
+ execute: async (_params, ctx) => {
115
+ const workspace = ctx?.workspaceDir;
116
+ const sync = workspace ? activeSyncers.get(workspace) : undefined;
117
+ if (!sync) {
114
118
  return {
115
119
  content: [
116
120
  {
117
121
  type: "text",
118
- text: "Sync not initialized. Run /storacha-init first, then /storacha-setup or /storacha-join.",
122
+ text: "Sync not active for this workspace. Set up with `openclaw clawracha init --agent <id>`.",
119
123
  },
120
124
  ],
121
125
  details: null,
122
126
  };
123
127
  }
124
- const status = await syncEngine.status();
128
+ const status = await sync.engine.status();
125
129
  return {
126
130
  content: [
127
131
  { type: "text", text: JSON.stringify(status, null, 2) },
@@ -135,20 +139,22 @@ export default function plugin(api) {
135
139
  label: "Storacha Sync Now",
136
140
  description: "Trigger an immediate workspace sync to Storacha",
137
141
  parameters: { type: "object", properties: {} },
138
- execute: async () => {
139
- if (!syncEngine) {
142
+ execute: async (_params, ctx) => {
143
+ const workspace = ctx?.workspaceDir;
144
+ const sync = workspace ? activeSyncers.get(workspace) : undefined;
145
+ if (!sync) {
140
146
  return {
141
147
  content: [
142
148
  {
143
149
  type: "text",
144
- text: "Sync not initialized. Run /storacha-init first, then /storacha-setup or /storacha-join.",
150
+ text: "Sync not active for this workspace. Set up with `openclaw clawracha init --agent <id>`.",
145
151
  },
146
152
  ],
147
153
  details: null,
148
154
  };
149
155
  }
150
- await syncEngine.sync();
151
- const status = await syncEngine.status();
156
+ await sync.engine.sync();
157
+ const status = await sync.engine.status();
152
158
  return {
153
159
  content: [
154
160
  {
@@ -160,375 +166,272 @@ export default function plugin(api) {
160
166
  };
161
167
  },
162
168
  });
163
- // --- Slash Commands ---
164
- api.registerCommand({
165
- name: "storacha-init",
166
- description: "Generate an agent identity for Storacha sync",
167
- handler: async (_ctx) => {
169
+ // --- CLI commands: openclaw clawracha <subcommand> ---
170
+ api.registerCli(({ program, config }) => {
171
+ const clawracha = program
172
+ .command("clawracha")
173
+ .description("Storacha workspace sync commands");
174
+ // Helper to resolve workspace from --agent
175
+ function requireAgent(agentId) {
176
+ if (!agentId) {
177
+ console.error("Error: --agent <id> is required. Specify which agent workspace to configure.");
178
+ process.exit(1);
179
+ }
180
+ return {
181
+ agentId,
182
+ workspace: resolveAgentWorkspace(config, agentId),
183
+ };
184
+ }
185
+ // --- init ---
186
+ clawracha
187
+ .command("init")
188
+ .description("Generate an agent identity for Storacha sync")
189
+ .requiredOption("--agent <id>", "Agent ID")
190
+ .action(async (opts) => {
168
191
  try {
169
- const workspace = workspaceDir;
170
- if (!workspace)
171
- return { text: "No workspace configured." };
172
- // Check if already initialized
192
+ const { agentId, workspace } = requireAgent(opts.agent);
173
193
  const existing = await loadDeviceConfig(workspace);
174
194
  if (existing?.agentKey) {
175
195
  const { Agent } = await import("@storacha/ucn/pail");
176
196
  const agent = Agent.parse(existing.agentKey);
177
- return {
178
- text: [
179
- "Agent already initialized.",
180
- `Agent DID: \`${agent.did()}\``,
181
- "",
182
- existing.setupComplete
183
- ? "Setup is complete. Use `/storacha-status` to check sync state."
184
- : "**Next step choose one:**",
185
- ...(!existing.setupComplete
186
- ? [
187
- "- **New workspace:** Have the space owner create an upload delegation for this DID, then run `/storacha-setup <upload-b64>`",
188
- "- **Join existing:** Have the other device run `/storacha-grant <this-DID>`, then run `/storacha-join <upload-b64> <name-b64>`",
189
- ]
190
- : []),
191
- ].join("\n"),
192
- };
197
+ console.log(`Agent already initialized for ${agentId}.`);
198
+ console.log(`Agent DID: ${agent.did()}`);
199
+ if (existing.setupComplete) {
200
+ console.log(`\nSetup is complete. Use \`openclaw clawracha status --agent ${agentId}\` to check sync state.`);
201
+ }
202
+ else {
203
+ console.log("\nNext step choose one:");
204
+ console.log(` New workspace: openclaw clawracha setup <delegation> --agent ${agentId}`);
205
+ console.log(` Join existing: openclaw clawracha join <upload> <name> --agent ${agentId}`);
206
+ }
207
+ return;
193
208
  }
194
209
  const { Agent } = await import("@storacha/ucn/pail");
195
210
  const agent = await Agent.generate();
196
211
  const agentKey = Agent.format(agent);
197
- const config = { agentKey };
198
- await saveDeviceConfig(workspace, config);
199
- return {
200
- text: [
201
- "\u{1f525} Agent initialized!",
202
- `Agent DID: \`${agent.did()}\``,
203
- "",
204
- "**Next step \u2014 choose one:**",
205
- "- **New workspace:** Have the space owner create an upload delegation for this DID, then run `/storacha-setup <upload-b64>`",
206
- "- **Join existing workspace:** Have the other device run `/storacha-grant <this-DID>`, then run `/storacha-join <upload-b64> <name-b64>`",
207
- ].join("\n"),
208
- };
212
+ await saveDeviceConfig(workspace, { agentKey });
213
+ console.log(`🔥 Agent initialized for ${agentId}!`);
214
+ console.log(`Agent DID: ${agent.did()}`);
215
+ console.log("\nNext step — choose one:");
216
+ console.log(` New workspace: openclaw clawracha setup <delegation> --agent ${agentId}`);
217
+ console.log(` Join existing: openclaw clawracha join <upload> <name> --agent ${agentId}`);
209
218
  }
210
219
  catch (err) {
211
- return {
212
- text: `\u274c Command failed: ${err.message}\n\`\`\`\n${err.stack ?? err}\n\`\`\``,
213
- };
220
+ console.error(`Error: ${err.message}`);
221
+ process.exit(1);
214
222
  }
215
- },
216
- });
217
- api.registerCommand({
218
- name: "storacha-setup",
219
- description: "Set up a NEW Storacha workspace (first device). Usage: /storacha-setup <upload-delegation-b64>",
220
- acceptsArgs: true,
221
- handler: async (_ctx) => {
223
+ });
224
+ // --- setup ---
225
+ clawracha
226
+ .command("setup <delegation>")
227
+ .description("Set up a NEW workspace (first device). <delegation> is a file path or base64 CID string.")
228
+ .requiredOption("--agent <id>", "Agent ID")
229
+ .action(async (delegationArg, opts) => {
222
230
  try {
223
- const workspace = workspaceDir;
224
- if (!workspace)
225
- return { text: "No workspace configured." };
226
- const config = await loadDeviceConfig(workspace);
227
- if (!config?.agentKey) {
228
- return {
229
- text: "Run `/storacha-init` first to generate an agent identity.",
230
- };
231
+ const { agentId, workspace } = requireAgent(opts.agent);
232
+ const deviceConfig = await loadDeviceConfig(workspace);
233
+ if (!deviceConfig?.agentKey) {
234
+ console.error(`Run \`openclaw clawracha init --agent ${agentId}\` first.`);
235
+ process.exit(1);
231
236
  }
232
- if (config.setupComplete) {
233
- return {
234
- text: "Setup already complete. Use `/storacha-status` to check sync state.",
235
- };
237
+ if (deviceConfig.setupComplete) {
238
+ console.log("Setup already complete.");
239
+ return;
236
240
  }
237
- const b64 = _ctx.args?.trim();
238
- if (!b64) {
239
- return {
240
- text: [
241
- "Usage: `/storacha-setup <upload-delegation-b64>`",
242
- "",
243
- "This creates a **new** workspace. If you're joining an existing workspace, use `/storacha-join` instead.",
244
- ].join("\n"),
245
- };
246
- }
247
- return {
248
- text: [
249
- "You ran '/storacha-setup' without the following command argument:",
250
- "",
251
- b64,
252
- ].join("\n"),
253
- };
254
- /*
255
- // Validate delegation
256
- const bytes = decodeDelegation(b64);
257
- const { ok: delegation, error } = await extractDelegation(bytes);
258
- if (!delegation) {
259
- return { text: `Invalid delegation: ${error}` };
260
- }
261
-
241
+ const delegation = await readDelegationArg(delegationArg);
262
242
  const spaceDID = delegation.capabilities[0]?.with;
263
-
264
- config.uploadDelegation = b64;
265
- config.spaceDID = spaceDID ?? undefined;
266
- config.setupComplete = true;
267
- await saveDeviceConfig(workspace, config);
268
-
243
+ const { ok: archiveBytes } = await delegation.archive();
244
+ if (!archiveBytes) {
245
+ throw new Error("Failed to archive delegation");
246
+ }
247
+ deviceConfig.uploadDelegation = encodeDelegation(archiveBytes);
248
+ deviceConfig.spaceDID = spaceDID ?? undefined;
249
+ deviceConfig.setupComplete = true;
250
+ await saveDeviceConfig(workspace, deviceConfig);
269
251
  const { Agent } = await import("@storacha/ucn/pail");
270
- const agent = Agent.parse(config.agentKey);
271
-
272
- return {
273
- text: [
274
- "\u{1f525} Storacha workspace ready!",
275
- `Agent DID: \`${agent.did()}\``,
276
- `Space: \`${spaceDID ?? "unknown"}\``,
277
- "",
278
- "Restart the gateway to start syncing.",
279
- "",
280
- "To add another device, run `/storacha-grant <their-agent-DID>` here,",
281
- "then `/storacha-join <upload-b64> <name-b64>` on the other device.",
282
- ].join("\n"),
283
- };*/
252
+ const agent = Agent.parse(deviceConfig.agentKey);
253
+ console.log(`🔥 Storacha workspace ready for ${agentId}!`);
254
+ console.log(`Agent DID: ${agent.did()}`);
255
+ console.log(`Space: ${spaceDID ?? "unknown"}`);
256
+ console.log("\nRestart the gateway to start syncing: `openclaw gateway restart`");
257
+ console.log(`\nTo add another device, run \`openclaw clawracha grant <their-DID> --agent ${agentId}\` here,`);
258
+ console.log(`then \`openclaw clawracha join <upload> <name> --agent <id>\` on the other device.`);
284
259
  }
285
260
  catch (err) {
286
- return {
287
- text: `\u274c Command failed: ${err.message}\n\`\`\`\n${err.stack ?? err}\n\`\`\``,
288
- };
261
+ console.error(`Error: ${err.message}`);
262
+ process.exit(1);
289
263
  }
290
- },
291
- });
292
- api.registerCommand({
293
- name: "storacha-join",
294
- description: "Join an existing Storacha workspace from another device. Run /storacha-init first. Usage: /storacha-join <upload-delegation-b64> <name-delegation-b64>",
295
- acceptsArgs: true,
296
- handler: async (_ctx) => {
264
+ });
265
+ // --- join ---
266
+ clawracha
267
+ .command("join <upload-delegation> <name-delegation>")
268
+ .description("Join an existing workspace from another device. Arguments are file paths or base64 CID strings.")
269
+ .requiredOption("--agent <id>", "Agent ID")
270
+ .action(async (uploadArg, nameArg, opts) => {
297
271
  try {
298
- const workspace = workspaceDir;
299
- if (!workspace)
300
- return { text: "No workspace configured." };
301
- const args = _ctx.args?.trim();
302
- if (!args) {
303
- return {
304
- text: [
305
- "Usage: `/storacha-join <upload-delegation-b64> <name-delegation-b64>`",
306
- "",
307
- "Get both delegations by running `/storacha-grant` on the existing device.",
308
- "If you're setting up a **new** workspace, use `/storacha-setup` instead.",
309
- ].join("\n"),
310
- };
272
+ const { agentId, workspace } = requireAgent(opts.agent);
273
+ const deviceConfig = await loadDeviceConfig(workspace);
274
+ if (!deviceConfig?.agentKey) {
275
+ console.error(`Run \`openclaw clawracha init --agent ${agentId}\` first.`);
276
+ process.exit(1);
311
277
  }
312
- const spaceIdx = args.indexOf(" ");
313
- if (spaceIdx === -1) {
314
- return {
315
- text: "Two arguments required: `/storacha-join <upload-b64> <name-b64>`",
316
- };
317
- }
318
- const uploadB64 = args.slice(0, spaceIdx).trim();
319
- const nameB64 = args.slice(spaceIdx + 1).trim();
320
- if (!uploadB64 || !nameB64) {
321
- return {
322
- text: "Two arguments required: `/storacha-join <upload-b64> <name-b64>`",
323
- };
324
- }
325
- // Validate upload delegation
326
- const uploadBytes = decodeDelegation(uploadB64);
327
- const { ok: uploadDelegation, error: uploadErr } = await extractDelegation(uploadBytes);
328
- if (!uploadDelegation) {
329
- return { text: `Invalid upload delegation: ${uploadErr}` };
330
- }
331
- // Validate name delegation
332
- const nameBytes = decodeDelegation(nameB64);
333
- const { ok: nameDelegation, error: nameErr } = await extractDelegation(nameBytes);
334
- if (!nameDelegation) {
335
- return { text: `Invalid name delegation: ${nameErr}` };
336
- }
337
- const config = await loadDeviceConfig(workspace);
338
- if (!config?.agentKey) {
339
- return {
340
- text: "Run `/storacha-init` first to generate an agent identity.",
341
- };
342
- }
343
- if (config.setupComplete) {
344
- return {
345
- text: "Setup already complete. Use `/storacha-status` to check sync state.",
346
- };
278
+ if (deviceConfig.setupComplete) {
279
+ console.log("Setup already complete.");
280
+ return;
347
281
  }
348
- const { Agent } = await import("@storacha/ucn/pail");
349
- const agent = Agent.parse(config.agentKey);
282
+ const uploadDelegation = await readDelegationArg(uploadArg);
283
+ const nameDelegation = await readDelegationArg(nameArg);
350
284
  const spaceDID = uploadDelegation.capabilities[0]?.with;
351
- config.uploadDelegation = uploadB64;
352
- config.nameDelegation = nameB64;
353
- config.spaceDID = spaceDID ?? undefined;
354
- config.setupComplete = true;
355
- await saveDeviceConfig(workspace, config);
356
- // Pull remote state immediately before watcher starts
285
+ const { ok: uploadArchive } = await uploadDelegation.archive();
286
+ if (!uploadArchive)
287
+ throw new Error("Failed to archive upload delegation");
288
+ const { ok: nameArchiveBytes } = await nameDelegation.archive();
289
+ if (!nameArchiveBytes)
290
+ throw new Error("Failed to archive name delegation");
291
+ deviceConfig.uploadDelegation = encodeDelegation(uploadArchive);
292
+ deviceConfig.nameDelegation = encodeDelegation(nameArchiveBytes);
293
+ deviceConfig.spaceDID = spaceDID ?? undefined;
294
+ deviceConfig.setupComplete = true;
295
+ await saveDeviceConfig(workspace, deviceConfig);
296
+ // Pull remote state before watcher starts
357
297
  let pullCount = 0;
358
- try {
359
- const storachaClient = await createStorachaClient(config);
360
- const engine = new SyncEngine(storachaClient, workspace);
361
- await engine.init(config);
362
- pullCount = await engine.pullRemote();
363
- // Save name archive after pull
364
- const nameArchive = await engine.exportNameArchive();
365
- config.nameArchive = nameArchive;
366
- await saveDeviceConfig(workspace, config);
367
- }
368
- catch (err) {
369
- return {
370
- text: [
371
- "\u26a0\ufe0f Delegations saved but initial pull failed:",
372
- `\`${err.message}\``,
373
- "",
374
- "Restart the gateway to retry.",
375
- ].join("\n"),
376
- };
377
- }
378
- return {
379
- text: [
380
- "\u{1f525} Joined existing Storacha workspace!",
381
- `Agent DID: \`${agent.did()}\``,
382
- `Space: \`${spaceDID ?? "unknown"}\``,
383
- `Pulled ${pullCount} files from remote.`,
384
- "",
385
- "Restart the gateway to start syncing.",
386
- ].join("\n"),
387
- };
298
+ const storachaClient = await createStorachaClient(deviceConfig);
299
+ const engine = new SyncEngine(storachaClient, workspace);
300
+ await engine.init(deviceConfig);
301
+ pullCount = await engine.pullRemote();
302
+ // Save name archive after pull
303
+ const exportedArchive = await engine.exportNameArchive();
304
+ deviceConfig.nameArchive = exportedArchive;
305
+ await saveDeviceConfig(workspace, deviceConfig);
306
+ const { Agent } = await import("@storacha/ucn/pail");
307
+ const agent = Agent.parse(deviceConfig.agentKey);
308
+ console.log(`🔥 Joined existing Storacha workspace for ${agentId}!`);
309
+ console.log(`Agent DID: ${agent.did()}`);
310
+ console.log(`Space: ${spaceDID ?? "unknown"}`);
311
+ console.log(`Pulled ${pullCount} files from remote.`);
312
+ console.log("\nRestart the gateway to start syncing: `openclaw gateway restart`");
388
313
  }
389
314
  catch (err) {
390
- return {
391
- text: `\u274c Command failed: ${err.message}\n\`\`\`\n${err.stack ?? err}\n\`\`\``,
392
- };
315
+ console.error(`Error: ${err.message}`);
316
+ process.exit(1);
393
317
  }
394
- },
395
- });
396
- api.registerCommand({
397
- name: "storacha-grant",
398
- description: "Grant another device access. Usage: /storacha-grant <target-DID>",
399
- acceptsArgs: true,
400
- handler: async (_ctx) => {
318
+ });
319
+ // --- grant ---
320
+ clawracha
321
+ .command("grant <target-DID>")
322
+ .description("Grant another device access to this workspace")
323
+ .requiredOption("--agent <id>", "Agent ID")
324
+ .action(async (targetDID, opts) => {
401
325
  try {
402
- const workspace = workspaceDir;
403
- if (!workspace)
404
- return { text: "No workspace configured." };
405
- const targetDID = _ctx.args?.trim();
406
- if (!targetDID || !targetDID.startsWith("did:")) {
407
- return { text: "Usage: `/storacha-grant <did:key:z...>`" };
326
+ const { agentId, workspace } = requireAgent(opts.agent);
327
+ if (!targetDID.startsWith("did:")) {
328
+ console.error("Error: target must be a DID (did:key:z...)");
329
+ process.exit(1);
408
330
  }
409
- const config = await loadDeviceConfig(workspace);
410
- if (!config) {
411
- return {
412
- text: "Not initialized. Run `/storacha-init` first.",
413
- };
331
+ const deviceConfig = await loadDeviceConfig(workspace);
332
+ if (!deviceConfig) {
333
+ console.error(`Not initialized. Run \`openclaw clawracha init --agent ${agentId}\` first.`);
334
+ process.exit(1);
414
335
  }
415
336
  const results = [];
416
337
  // Re-delegate upload capability
417
- if (config.uploadDelegation) {
418
- try {
419
- const storachaClient = await createStorachaClient(config);
420
- const audience = { did: () => targetDID };
421
- const uploadDelegation = await storachaClient.createDelegation(audience, [
422
- "space/blob/add",
423
- "space/index/add",
424
- "upload/add",
425
- "filecoin/offer",
426
- ]);
427
- const { ok: archiveBytes } = await uploadDelegation.archive();
428
- if (archiveBytes) {
429
- const b64 = encodeDelegation(archiveBytes);
430
- results.push("**Upload delegation:**\n```\n" + b64 + "\n```");
431
- }
432
- }
433
- catch (err) {
434
- results.push(`\u274c Failed to create upload delegation: ${err.message}`);
338
+ if (deviceConfig.uploadDelegation) {
339
+ const storachaClient = await createStorachaClient(deviceConfig);
340
+ const audience = {
341
+ did: () => targetDID,
342
+ };
343
+ const uploadDel = await storachaClient.createDelegation(audience, [
344
+ "space/blob/add",
345
+ "space/index/add",
346
+ "upload/add",
347
+ "filecoin/offer",
348
+ ]);
349
+ const { ok: archiveBytes } = await uploadDel.archive();
350
+ if (archiveBytes) {
351
+ results.push(`Upload delegation:\n${encodeDelegation(archiveBytes)}`);
435
352
  }
436
353
  }
437
354
  else {
438
- results.push("\u26a0\ufe0f No upload delegation to re-delegate.");
355
+ results.push("⚠️ No upload delegation to re-delegate.");
439
356
  }
440
- // Re-delegate name (pail sync) capability
441
- if (config.nameDelegation) {
442
- try {
443
- const { Agent, Name } = await import("@storacha/ucn/pail");
444
- const agent = Agent.parse(config.agentKey);
445
- let name;
446
- if (config.nameArchive) {
447
- const archiveBytes = decodeDelegation(config.nameArchive);
448
- name = await Name.extract(agent, archiveBytes);
449
- }
450
- else {
451
- const nameBytes = decodeDelegation(config.nameDelegation);
452
- const { ok: nameDel } = await extractDelegation(nameBytes);
453
- if (!nameDel) {
454
- results.push("\u274c Failed to extract name delegation.");
455
- }
456
- else {
457
- name = Name.from(agent, [nameDel]);
458
- }
459
- }
460
- if (name) {
461
- const nameDel = await name.grant(targetDID);
462
- const { ok: archiveBytes } = await nameDel.archive();
463
- if (archiveBytes) {
464
- const b64 = encodeDelegation(archiveBytes);
465
- results.push("**Name delegation:**\n```\n" + b64 + "\n```");
466
- }
357
+ // Re-delegate name capability
358
+ if (deviceConfig.nameDelegation) {
359
+ const { Agent, Name } = await import("@storacha/ucn/pail");
360
+ const { extract } = await import("@storacha/client/delegation");
361
+ const agent = Agent.parse(deviceConfig.agentKey);
362
+ let name;
363
+ if (deviceConfig.nameArchive) {
364
+ const archiveBytes = decodeDelegation(deviceConfig.nameArchive);
365
+ name = await Name.extract(agent, archiveBytes);
366
+ }
367
+ else {
368
+ const nameBytes = decodeDelegation(deviceConfig.nameDelegation);
369
+ const { ok: nameDel } = await extract(nameBytes);
370
+ if (nameDel) {
371
+ name = Name.from(agent, [nameDel]);
467
372
  }
468
373
  }
469
- catch (err) {
470
- results.push(`\u274c Failed to create name delegation: ${err.message}`);
374
+ if (name) {
375
+ const nameDel = await name.grant(targetDID);
376
+ const { ok: archiveBytes } = await nameDel.archive();
377
+ if (archiveBytes) {
378
+ results.push(`Name delegation:\n${encodeDelegation(archiveBytes)}`);
379
+ }
471
380
  }
472
381
  }
473
382
  else {
474
- results.push("\u26a0\ufe0f No name delegation to re-delegate.");
383
+ results.push("⚠️ No name delegation to re-delegate.");
475
384
  }
476
- if (results.length === 0) {
477
- return { text: "Nothing to grant. Set up this device first." };
385
+ console.log(`🔥 Delegations for ${targetDID}:\n`);
386
+ for (const r of results) {
387
+ console.log(r);
388
+ console.log();
478
389
  }
479
- return {
480
- text: [
481
- `\u{1f525} Delegations for \`${targetDID}\`:`,
482
- "",
483
- ...results,
484
- "",
485
- "The target device should run:",
486
- "`/storacha-join <upload-b64> <name-b64>`",
487
- ].join("\n"),
488
- };
390
+ console.log("The target device should run:");
391
+ console.log(` openclaw clawracha join <upload-delegation> <name-delegation> --agent <id>`);
392
+ console.log("\nThen restart the gateway: `openclaw gateway restart`");
489
393
  }
490
394
  catch (err) {
491
- return {
492
- text: `\u274c Command failed: ${err.message}\n\`\`\`\n${err.stack ?? err}\n\`\`\``,
493
- };
395
+ console.error(`Error: ${err.message}`);
396
+ process.exit(1);
494
397
  }
495
- },
496
- });
497
- api.registerCommand({
498
- name: "storacha-status",
499
- description: "Show Storacha sync status",
500
- handler: async (_ctx) => {
398
+ });
399
+ // --- status ---
400
+ clawracha
401
+ .command("status")
402
+ .description("Show Storacha sync status for an agent workspace")
403
+ .requiredOption("--agent <id>", "Agent ID")
404
+ .action(async (opts) => {
501
405
  try {
502
- const workspace = workspaceDir;
503
- if (!workspace)
504
- return { text: "No workspace configured." };
505
- const config = await loadDeviceConfig(workspace);
506
- if (!config)
507
- return {
508
- text: "Not initialized. Run `/storacha-init` first.",
509
- };
510
- const lines = [
511
- "\u{1f525} Storacha Sync Status",
512
- `Agent: configured`,
513
- `Upload delegation: ${config.uploadDelegation ? "\u2705" : "\u274c not set"}`,
514
- `Name delegation: ${config.nameDelegation ? "\u2705" : "\u274c not set"}`,
515
- `Space DID: ${config.spaceDID ?? "unknown"}`,
516
- `Name Archive: ${config.nameArchive ? "saved" : "not created"}`,
517
- `Setup complete: ${config.setupComplete ? "\u2705" : "\u274c"}`,
518
- ];
519
- if (syncEngine) {
520
- const status = await syncEngine.status();
521
- lines.push(`Running: ${status.running}`, `Last Sync: ${status.lastSync
522
- ? new Date(status.lastSync).toISOString()
523
- : "never"}`, `Entries: ${status.entryCount}`, `Pending: ${status.pendingChanges}`);
406
+ const { agentId, workspace } = requireAgent(opts.agent);
407
+ const deviceConfig = await loadDeviceConfig(workspace);
408
+ if (!deviceConfig) {
409
+ console.log(`Not initialized. Run \`openclaw clawracha init --agent ${agentId}\` first.`);
410
+ return;
411
+ }
412
+ console.log(`🔥 Storacha Sync Status [${agentId}]`);
413
+ console.log(`Workspace: ${workspace}`);
414
+ console.log(`Upload delegation: ${deviceConfig.uploadDelegation ? "✅" : "❌ not set"}`);
415
+ console.log(`Name delegation: ${deviceConfig.nameDelegation ? "✅" : "❌ not set"}`);
416
+ console.log(`Space DID: ${deviceConfig.spaceDID ?? "unknown"}`);
417
+ console.log(`Name Archive: ${deviceConfig.nameArchive ? "saved" : "not created"}`);
418
+ console.log(`Setup complete: ${deviceConfig.setupComplete ? "" : ""}`);
419
+ const sync = activeSyncers.get(workspace);
420
+ if (sync) {
421
+ const status = await sync.engine.status();
422
+ console.log(`Running: true`);
423
+ console.log(`Last Sync: ${status.lastSync ? new Date(status.lastSync).toISOString() : "never"}`);
424
+ console.log(`Entries: ${status.entryCount}`);
425
+ console.log(`Pending: ${status.pendingChanges}`);
426
+ }
427
+ else {
428
+ console.log(`Running: false`);
524
429
  }
525
- return { text: lines.join("\n") };
526
430
  }
527
431
  catch (err) {
528
- return {
529
- text: `\u274c Command failed: ${err.message}\n\`\`\`\n${err.stack ?? err}\n\`\`\``,
530
- };
432
+ console.error(`Error: ${err.message}`);
433
+ process.exit(1);
531
434
  }
532
- },
533
- });
435
+ });
436
+ }, { commands: ["clawracha"] });
534
437
  }
@@ -15,4 +15,9 @@ export declare function encodeDelegation(archiveBytes: Uint8Array): string;
15
15
  * base64 string → CID → identity multihash digest → bytes
16
16
  */
17
17
  export declare function decodeDelegation(encoded: string): Uint8Array;
18
+ /**
19
+ * Read a delegation from either a file path (raw CAR) or a CID string (base64).
20
+ * Returns the extracted delegation.
21
+ */
22
+ export declare function readDelegationArg(input: string): Promise<import("@ucanto/interface").Delegation<import("@ipld/dag-ucan").Capabilities>>;
18
23
  //# sourceMappingURL=delegation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"delegation.d.ts","sourceRoot":"","sources":["../../src/utils/delegation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,UAAU,GAAG,MAAM,CAGjE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAQ5D"}
1
+ {"version":3,"file":"delegation.d.ts","sourceRoot":"","sources":["../../src/utils/delegation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,UAAU,GAAG,MAAM,CAGjE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAQ5D;AAGD;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,0FAcpD"}
@@ -5,7 +5,9 @@
5
5
  * and identity multihash, encoded as base64. This matches the format
6
6
  * used by the Storacha CLI (`storacha delegation create --base64`).
7
7
  */
8
+ import * as fs from "node:fs/promises";
8
9
  import { CID } from "multiformats/cid";
10
+ import { extract } from "@storacha/client/delegation";
9
11
  import { base64 } from "multiformats/bases/base64";
10
12
  import { identity } from "multiformats/hashes/identity";
11
13
  /** CAR codec code (multicodec 0x0202) */
@@ -29,3 +31,23 @@ export function decodeDelegation(encoded) {
29
31
  }
30
32
  return cid.multihash.digest;
31
33
  }
34
+ /**
35
+ * Read a delegation from either a file path (raw CAR) or a CID string (base64).
36
+ * Returns the extracted delegation.
37
+ */
38
+ export async function readDelegationArg(input) {
39
+ let bytes;
40
+ try {
41
+ // Try reading as file first
42
+ bytes = await fs.readFile(input);
43
+ }
44
+ catch {
45
+ // Not a file — treat as CID string
46
+ bytes = decodeDelegation(input);
47
+ }
48
+ const { ok: delegation, error } = await extract(bytes);
49
+ if (!delegation) {
50
+ throw new Error(`Invalid delegation: ${error}`);
51
+ }
52
+ return delegation;
53
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Agent workspace resolution.
3
+ *
4
+ * Mirrors OpenClaw's resolveAgentWorkspaceDir logic from
5
+ * src/agents/agent-scope.ts — resolves workspace dir for a given agent ID.
6
+ */
7
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
8
+ /**
9
+ * Resolve the workspace directory for an agent by ID.
10
+ * Matches OpenClaw's resolution order:
11
+ * 1. Agent-specific workspace from config
12
+ * 2. For default agent: agents.defaults.workspace or ~/.openclaw/workspace
13
+ * 3. For other agents: ~/.openclaw/workspace-{agentId}
14
+ */
15
+ export declare function resolveAgentWorkspace(config: OpenClawConfig, agentId: string): string;
16
+ /**
17
+ * Get all agent IDs from config.
18
+ */
19
+ export declare function getAgentIds(config: OpenClawConfig): string[];
20
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/utils/workspace.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,MAAM,GACd,MAAM,CAsBR;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,EAAE,CAQ5D"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Agent workspace resolution.
3
+ *
4
+ * Mirrors OpenClaw's resolveAgentWorkspaceDir logic from
5
+ * src/agents/agent-scope.ts — resolves workspace dir for a given agent ID.
6
+ */
7
+ import * as path from "node:path";
8
+ import * as os from "node:os";
9
+ /**
10
+ * Resolve the workspace directory for an agent by ID.
11
+ * Matches OpenClaw's resolution order:
12
+ * 1. Agent-specific workspace from config
13
+ * 2. For default agent: agents.defaults.workspace or ~/.openclaw/workspace
14
+ * 3. For other agents: ~/.openclaw/workspace-{agentId}
15
+ */
16
+ export function resolveAgentWorkspace(config, agentId) {
17
+ const agent = config.agents?.list?.find((a) => a.id.toLowerCase() === agentId.toLowerCase());
18
+ // Agent-specific workspace
19
+ if (agent?.workspace?.trim()) {
20
+ return resolveUserPath(agent.workspace.trim());
21
+ }
22
+ // Default agent gets the default workspace
23
+ const defaultId = resolveDefaultAgentId(config);
24
+ if (agentId.toLowerCase() === defaultId.toLowerCase()) {
25
+ const fallback = config.agents?.defaults?.workspace?.trim();
26
+ if (fallback) {
27
+ return resolveUserPath(fallback);
28
+ }
29
+ return path.join(resolveStateDir(), "workspace");
30
+ }
31
+ // Other agents: workspace-{id}
32
+ return path.join(resolveStateDir(), `workspace-${agentId}`);
33
+ }
34
+ /**
35
+ * Get all agent IDs from config.
36
+ */
37
+ export function getAgentIds(config) {
38
+ const list = config.agents?.list;
39
+ if (!Array.isArray(list) || list.length === 0) {
40
+ return ["default"];
41
+ }
42
+ return list
43
+ .filter((a) => a && typeof a === "object" && a.id)
44
+ .map((a) => a.id);
45
+ }
46
+ /**
47
+ * Resolve the default agent ID from config.
48
+ */
49
+ function resolveDefaultAgentId(config) {
50
+ const list = config.agents?.list;
51
+ if (!Array.isArray(list) || list.length === 0) {
52
+ return "default";
53
+ }
54
+ const defaultAgent = list.find((a) => a.default);
55
+ return (defaultAgent ?? list[0])?.id?.trim() || "default";
56
+ }
57
+ /**
58
+ * Resolve OpenClaw state directory.
59
+ */
60
+ function resolveStateDir() {
61
+ return (process.env.OPENCLAW_STATE_DIR ||
62
+ path.join(os.homedir(), ".openclaw"));
63
+ }
64
+ /**
65
+ * Expand ~ in paths.
66
+ */
67
+ function resolveUserPath(p) {
68
+ if (p.startsWith("~/")) {
69
+ return path.join(os.homedir(), p.slice(2));
70
+ }
71
+ return p;
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storacha/clawracha",
3
- "version": "0.0.11-rc.0",
3
+ "version": "0.1.0",
4
4
  "description": "OpenClaw plugin for Storacha workspace sync via UCN Pail",
5
5
  "type": "module",
6
6
  "files": [
@@ -36,7 +36,7 @@
36
36
  "@web3-storage/pail": "0.6.3-rc.3",
37
37
  "carstream": "^2.3.0",
38
38
  "chokidar": "^3.6.0",
39
- "multiformats": "^13.3.6"
39
+ "multiformats": "^13.0.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ipld/unixfs": "^3.0.0",