@storacha/clawracha 0.0.1 → 0.0.2

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/plugin.js CHANGED
@@ -8,8 +8,10 @@
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";
11
12
  import { SyncEngine } from "./sync.js";
12
13
  import { FileWatcher } from "./watcher.js";
14
+ import { createStorachaClient } from "./utils/client.js";
13
15
  // Global state
14
16
  let syncEngine = null;
15
17
  let fileWatcher = null;
@@ -53,11 +55,12 @@ export default function plugin(api) {
53
55
  return;
54
56
  }
55
57
  const deviceConfig = await loadDeviceConfig(workspace);
56
- if (!deviceConfig) {
57
- ctx.logger.info("No device config found. Run /storacha-init first.");
58
+ if (!deviceConfig || !deviceConfig.setupComplete) {
59
+ ctx.logger.info("Setup not complete. Run /storacha-init or /storacha-join first.");
58
60
  return;
59
61
  }
60
- syncEngine = new SyncEngine(workspace);
62
+ const storachaClient = await createStorachaClient(deviceConfig);
63
+ syncEngine = new SyncEngine(storachaClient, workspace);
61
64
  await syncEngine.init(deviceConfig);
62
65
  fileWatcher = new FileWatcher({
63
66
  workspace,
@@ -105,7 +108,7 @@ export default function plugin(api) {
105
108
  content: [
106
109
  {
107
110
  type: "text",
108
- text: "Sync not initialized. Run /storacha-init first.",
111
+ text: "Sync not initialized. Run /storacha-init or /storacha-join first.",
109
112
  },
110
113
  ],
111
114
  details: null,
@@ -131,7 +134,7 @@ export default function plugin(api) {
131
134
  content: [
132
135
  {
133
136
  type: "text",
134
- text: "Sync not initialized. Run /storacha-init first.",
137
+ text: "Sync not initialized. Run /storacha-init or /storacha-join first.",
135
138
  },
136
139
  ],
137
140
  details: null,
@@ -150,65 +153,260 @@ export default function plugin(api) {
150
153
  };
151
154
  },
152
155
  });
153
- // Register slash commands
156
+ // --- Slash Commands ---
154
157
  api.registerCommand({
155
158
  name: "storacha-init",
156
- description: "Initialize Storacha sync for this workspace",
157
- handler: async (ctx) => {
159
+ description: "Initialize a NEW Storacha workspace (first device). Usage: /storacha-init <upload-delegation-b64>",
160
+ acceptsArgs: true,
161
+ handler: async (_ctx) => {
158
162
  const workspace = workspaceDir;
159
163
  if (!workspace)
160
164
  return { text: "No workspace configured." };
165
+ const b64 = _ctx.args?.trim();
166
+ if (!b64) {
167
+ return {
168
+ text: [
169
+ "Usage: `/storacha-init <upload-delegation-b64>`",
170
+ "",
171
+ "This creates a **new** workspace. If you're syncing from an existing device, use `/storacha-join` instead.",
172
+ ].join("\n"),
173
+ };
174
+ }
175
+ // Validate delegation
176
+ const bytes = Buffer.from(b64, "base64");
177
+ const { ok: delegation, error } = await extractDelegation(bytes);
178
+ if (!delegation) {
179
+ return { text: `Invalid delegation: ${error}` };
180
+ }
161
181
  const { Agent } = await import("@storacha/ucn/pail");
162
182
  const agent = await Agent.generate();
163
183
  const agentKey = Agent.format(agent);
164
- const config = { agentKey };
184
+ const spaceDID = delegation.capabilities[0]?.with;
185
+ const config = {
186
+ agentKey,
187
+ uploadDelegation: b64,
188
+ spaceDID: spaceDID ?? undefined,
189
+ setupComplete: true,
190
+ };
165
191
  await saveDeviceConfig(workspace, config);
166
192
  return {
167
193
  text: [
168
- "🔥 Storacha sync initialized!",
194
+ "\u{1f525} Storacha workspace initialized!",
169
195
  `Agent DID: \`${agent.did()}\``,
196
+ `Space: \`${spaceDID ?? "unknown"}\``,
170
197
  "",
171
- "Next: get a delegation from a space owner, then run /storacha-delegate",
198
+ "Restart the gateway to start syncing.",
199
+ "",
200
+ "To add another device, run `/storacha-grant <their-agent-DID>` here,",
201
+ "then `/storacha-join <upload-b64> <name-b64>` on the other device.",
172
202
  ].join("\n"),
173
203
  };
174
204
  },
175
205
  });
176
206
  api.registerCommand({
177
- name: "storacha-delegate",
178
- description: "Import a delegation to sync with a Storacha space",
207
+ name: "storacha-join",
208
+ description: "Join an existing Storacha workspace from another device. Usage: /storacha-join <upload-delegation-b64> <name-delegation-b64>",
179
209
  acceptsArgs: true,
180
- handler: async (ctx) => {
210
+ handler: async (_ctx) => {
181
211
  const workspace = workspaceDir;
182
212
  if (!workspace)
183
213
  return { text: "No workspace configured." };
184
- const delegationB64 = ctx.args?.trim();
185
- if (!delegationB64)
186
- return { text: "Usage: /storacha-delegate <base64-delegation>" };
187
- const config = await loadDeviceConfig(workspace);
188
- if (!config)
189
- return { text: "Not initialized. Run /storacha-init first." };
190
- config.delegation = delegationB64;
214
+ const args = _ctx.args?.trim();
215
+ if (!args) {
216
+ return {
217
+ text: [
218
+ "Usage: `/storacha-join <upload-delegation-b64> <name-delegation-b64>`",
219
+ "",
220
+ "Get both delegations by running `/storacha-grant` on the existing device.",
221
+ "If you're setting up a **new** workspace, use `/storacha-init` instead.",
222
+ ].join("\n"),
223
+ };
224
+ }
225
+ const spaceIdx = args.indexOf(" ");
226
+ if (spaceIdx === -1) {
227
+ return {
228
+ text: "Two arguments required: `/storacha-join <upload-b64> <name-b64>`",
229
+ };
230
+ }
231
+ const uploadB64 = args.slice(0, spaceIdx).trim();
232
+ const nameB64 = args.slice(spaceIdx + 1).trim();
233
+ if (!uploadB64 || !nameB64) {
234
+ return {
235
+ text: "Two arguments required: `/storacha-join <upload-b64> <name-b64>`",
236
+ };
237
+ }
238
+ // Validate upload delegation
239
+ const uploadBytes = Buffer.from(uploadB64, "base64");
240
+ const { ok: uploadDelegation, error: uploadErr } = await extractDelegation(uploadBytes);
241
+ if (!uploadDelegation) {
242
+ return { text: `Invalid upload delegation: ${uploadErr}` };
243
+ }
244
+ // Validate name delegation
245
+ const nameBytes = Buffer.from(nameB64, "base64");
246
+ const { ok: nameDelegation, error: nameErr } = await extractDelegation(nameBytes);
247
+ if (!nameDelegation) {
248
+ return { text: `Invalid name delegation: ${nameErr}` };
249
+ }
250
+ const { Agent } = await import("@storacha/ucn/pail");
251
+ const agent = await Agent.generate();
252
+ const agentKey = Agent.format(agent);
253
+ const spaceDID = uploadDelegation.capabilities[0]?.with;
254
+ const config = {
255
+ agentKey,
256
+ uploadDelegation: uploadB64,
257
+ nameDelegation: nameB64,
258
+ spaceDID: spaceDID ?? undefined,
259
+ setupComplete: true,
260
+ };
191
261
  await saveDeviceConfig(workspace, config);
262
+ // Pull remote state immediately before watcher starts
263
+ let pullCount = 0;
264
+ try {
265
+ const storachaClient = await createStorachaClient(config);
266
+ const engine = new SyncEngine(storachaClient, workspace);
267
+ await engine.init(config);
268
+ pullCount = await engine.pullRemote();
269
+ // Save name archive after pull
270
+ const nameArchive = await engine.exportNameArchive();
271
+ config.nameArchive = nameArchive;
272
+ await saveDeviceConfig(workspace, config);
273
+ }
274
+ catch (err) {
275
+ return {
276
+ text: [
277
+ "\u26a0\ufe0f Delegations saved but initial pull failed:",
278
+ `\`${err.message}\``,
279
+ "",
280
+ "Restart the gateway to retry.",
281
+ ].join("\n"),
282
+ };
283
+ }
192
284
  return {
193
- text: "✅ Delegation imported! Restart the gateway to start syncing.",
285
+ text: [
286
+ "\u{1f525} Joined existing Storacha workspace!",
287
+ `Agent DID: \`${agent.did()}\``,
288
+ `Space: \`${spaceDID ?? "unknown"}\``,
289
+ `Pulled ${pullCount} files from remote.`,
290
+ "",
291
+ "Restart the gateway to start syncing.",
292
+ ].join("\n"),
293
+ };
294
+ },
295
+ });
296
+ api.registerCommand({
297
+ name: "storacha-grant",
298
+ description: "Grant another device access. Usage: /storacha-grant <target-DID>",
299
+ acceptsArgs: true,
300
+ handler: async (_ctx) => {
301
+ const workspace = workspaceDir;
302
+ if (!workspace)
303
+ return { text: "No workspace configured." };
304
+ const targetDID = _ctx.args?.trim();
305
+ if (!targetDID || !targetDID.startsWith("did:")) {
306
+ return { text: "Usage: `/storacha-grant <did:key:z...>`" };
307
+ }
308
+ const config = await loadDeviceConfig(workspace);
309
+ if (!config) {
310
+ return {
311
+ text: "Not initialized. Run `/storacha-init` or `/storacha-join` first.",
312
+ };
313
+ }
314
+ const results = [];
315
+ // Re-delegate upload capability
316
+ if (config.uploadDelegation) {
317
+ try {
318
+ const storachaClient = await createStorachaClient(config);
319
+ const audience = { did: () => targetDID };
320
+ const uploadDelegation = await storachaClient.createDelegation(audience, [
321
+ "space/blob/add",
322
+ "space/index/add",
323
+ "upload/add",
324
+ "filecoin/offer",
325
+ ]);
326
+ const { ok: archiveBytes } = await uploadDelegation.archive();
327
+ if (archiveBytes) {
328
+ const b64 = Buffer.from(archiveBytes).toString("base64");
329
+ results.push("**Upload delegation:**\n```\n" + b64 + "\n```");
330
+ }
331
+ }
332
+ catch (err) {
333
+ results.push(`\u274c Failed to create upload delegation: ${err.message}`);
334
+ }
335
+ }
336
+ else {
337
+ results.push("\u26a0\ufe0f No upload delegation to re-delegate.");
338
+ }
339
+ // Re-delegate name (pail sync) capability
340
+ if (config.nameDelegation) {
341
+ try {
342
+ const { Agent, Name } = await import("@storacha/ucn/pail");
343
+ const agent = Agent.parse(config.agentKey);
344
+ let name;
345
+ if (config.nameArchive) {
346
+ const archiveBytes = Buffer.from(config.nameArchive, "base64");
347
+ name = await Name.extract(agent, archiveBytes);
348
+ }
349
+ else {
350
+ const nameBytes = Buffer.from(config.nameDelegation, "base64");
351
+ const { ok: nameDel } = await extractDelegation(nameBytes);
352
+ if (!nameDel) {
353
+ results.push("\u274c Failed to extract name delegation.");
354
+ }
355
+ else {
356
+ name = Name.from(agent, [nameDel]);
357
+ }
358
+ }
359
+ if (name) {
360
+ const nameDel = await name.grant(targetDID);
361
+ const { ok: archiveBytes } = await nameDel.archive();
362
+ if (archiveBytes) {
363
+ const b64 = Buffer.from(archiveBytes).toString("base64");
364
+ results.push("**Name delegation:**\n```\n" + b64 + "\n```");
365
+ }
366
+ }
367
+ }
368
+ catch (err) {
369
+ results.push(`\u274c Failed to create name delegation: ${err.message}`);
370
+ }
371
+ }
372
+ else {
373
+ results.push("\u26a0\ufe0f No name delegation to re-delegate.");
374
+ }
375
+ if (results.length === 0) {
376
+ return { text: "Nothing to grant. Set up this device first." };
377
+ }
378
+ return {
379
+ text: [
380
+ `\u{1f525} Delegations for \`${targetDID}\`:`,
381
+ "",
382
+ ...results,
383
+ "",
384
+ "The target device should run:",
385
+ "`/storacha-join <upload-b64> <name-b64>`",
386
+ ].join("\n"),
194
387
  };
195
388
  },
196
389
  });
197
390
  api.registerCommand({
198
391
  name: "storacha-status",
199
392
  description: "Show Storacha sync status",
200
- handler: async (ctx) => {
393
+ handler: async (_ctx) => {
201
394
  const workspace = workspaceDir;
202
395
  if (!workspace)
203
396
  return { text: "No workspace configured." };
204
397
  const config = await loadDeviceConfig(workspace);
205
398
  if (!config)
206
- return { text: "Not initialized. Run /storacha-init first." };
399
+ return {
400
+ text: "Not initialized. Run /storacha-init or /storacha-join first.",
401
+ };
207
402
  const lines = [
208
- "🔥 Storacha Sync Status",
403
+ "\u{1f525} Storacha Sync Status",
209
404
  `Agent: configured`,
210
- `Delegation: ${config.delegation ? "imported" : "not set"}`,
405
+ `Upload delegation: ${config.uploadDelegation ? "\u2705" : "\u274c not set"}`,
406
+ `Name delegation: ${config.nameDelegation ? "\u2705" : "\u274c not set"}`,
407
+ `Space DID: ${config.spaceDID ?? "unknown"}`,
211
408
  `Name Archive: ${config.nameArchive ? "saved" : "not created"}`,
409
+ `Setup complete: ${config.setupComplete ? "\u2705" : "\u274c"}`,
212
410
  ];
213
411
  if (syncEngine) {
214
412
  const status = await syncEngine.status();
package/dist/sync.d.ts CHANGED
@@ -8,18 +8,20 @@
8
8
  * 5. Upload all blocks as CAR
9
9
  * 6. Apply remote changes to local filesystem
10
10
  */
11
- import type { SyncState, FileChange, DeviceConfig } from "./types.js";
12
- import { type PailEntries } from "./differ.js";
11
+ import type { SyncState, FileChange, DeviceConfig } from "./types/index.js";
12
+ import { type PailEntries } from "./utils/differ.js";
13
+ import { Client } from "@storacha/client";
13
14
  export declare class SyncEngine {
14
15
  private workspace;
15
16
  private blocks;
16
17
  private name;
17
18
  private current;
18
19
  private pendingOps;
19
- private allBlocks;
20
+ private carFile;
20
21
  private running;
21
22
  private lastSync;
22
- constructor(workspace: string);
23
+ private storachaClient;
24
+ constructor(storachaClient: Client, workspace: string);
23
25
  /**
24
26
  * Initialize sync engine with device config
25
27
  */
@@ -35,7 +37,7 @@ export declare class SyncEngine {
35
37
  /**
36
38
  * Create CAR and upload to Storacha
37
39
  */
38
- private uploadCAR;
40
+ private possiblyUploadCAR;
39
41
  /**
40
42
  * Get current pail entries as map
41
43
  */
@@ -44,6 +46,11 @@ export declare class SyncEngine {
44
46
  * Apply remote changes to local filesystem
45
47
  */
46
48
  private applyRemoteChanges;
49
+ /**
50
+ * Pull all remote state and write to local filesystem.
51
+ * Used by /storacha-join to overwrite local with remote before watcher starts.
52
+ */
53
+ pullRemote(): Promise<number>;
47
54
  status(): Promise<SyncState>;
48
55
  exportNameArchive(): Promise<string>;
49
56
  private storeBlocks;
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAYH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAU,YAAY,EAAE,MAAM,YAAY,CAAC;AAO9E,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAiBlE,qBAAa,UAAU;IACrB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAuB;gBAE3B,SAAS,EAAE,MAAM;IAK7B;;OAEG;IACG,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB/C;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC1D;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA4H3B;;OAEG;YACW,SAAS;IAwCvB;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;IAc5C;;OAEG;YACW,kBAAkB;IAsB1B,MAAM,IAAI,OAAO,CAAC,SAAS,CAAC;IAW5B,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;YAM5B,WAAW;CAK1B"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EAEV,YAAY,EACb,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAExE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,qBAAa,UAAU;IACrB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,cAAc,CAAS;gBAEnB,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAMrD;;OAEG;IACG,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAgC/C;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAe1D;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkD3B;;OAEG;YACW,iBAAiB;IAc/B;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;IAc5C;;OAEG;YACW,kBAAkB;IAWhC;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IA0B7B,MAAM,IAAI,OAAO,CAAC,SAAS,CAAC;IAW5B,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;YAM5B,WAAW;CAK1B"}