@storacha/clawracha 0.1.1 → 0.1.3

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.
@@ -1 +1 @@
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;AAiH7B,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,GAAG,EAAE,iBAAiB,QAkgBpD"}
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;AAgH7B,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,GAAG,EAAE,iBAAiB,QAojBpD"}
package/dist/plugin.js CHANGED
@@ -40,8 +40,7 @@ async function startWorkspaceSync(workspace, agentId, pluginConfig, logger) {
40
40
  if (!deviceConfig || !deviceConfig.setupComplete) {
41
41
  return null;
42
42
  }
43
- const storachaClient = await createStorachaClient(deviceConfig);
44
- const engine = new SyncEngine(storachaClient, workspace);
43
+ const engine = new SyncEngine(workspace);
45
44
  await engine.init(deviceConfig);
46
45
  const watcher = new FileWatcher({
47
46
  workspace,
@@ -112,6 +111,7 @@ export default function plugin(api) {
112
111
  },
113
112
  async stop(ctx) {
114
113
  for (const [workspace, sync] of activeSyncers) {
114
+ sync.engine.stop();
115
115
  await sync.watcher.stop();
116
116
  ctx.logger.info(`[${sync.agentId}] Stopped syncing: ${workspace}`);
117
117
  }
@@ -262,8 +262,7 @@ export default function plugin(api) {
262
262
  deviceConfig.setupComplete = true;
263
263
  await saveDeviceConfig(workspace, deviceConfig);
264
264
  // Initial upload: scan all existing workspace files and sync to Storacha
265
- const storachaClient = await createStorachaClient(deviceConfig);
266
- const engine = new SyncEngine(storachaClient, workspace);
265
+ const engine = new SyncEngine(workspace);
267
266
  await engine.init(deviceConfig);
268
267
  const userIgnored = await readIgnoreFile(workspace);
269
268
  const ignorePatterns = [
@@ -295,6 +294,7 @@ export default function plugin(api) {
295
294
  console.log("\nRestart the gateway to start syncing: `openclaw gateway restart`");
296
295
  console.log(`\nTo add another device, run \`openclaw clawracha grant <their-DID> --agent ${agentId}\` here,`);
297
296
  console.log(`then \`openclaw clawracha join <upload> <name> --agent <id>\` on the other device.`);
297
+ console.log("\nSync is now active (no gateway restart needed).");
298
298
  }
299
299
  catch (err) {
300
300
  console.error(`Error: ${err.message}`);
@@ -334,8 +334,7 @@ export default function plugin(api) {
334
334
  await saveDeviceConfig(workspace, deviceConfig);
335
335
  // Pull remote state before watcher starts
336
336
  let pullCount = 0;
337
- const storachaClient = await createStorachaClient(deviceConfig);
338
- const engine = new SyncEngine(storachaClient, workspace);
337
+ const engine = new SyncEngine(workspace);
339
338
  await engine.init(deviceConfig);
340
339
  pullCount = await engine.pullRemote();
341
340
  // Save name archive after pull
@@ -472,5 +471,51 @@ export default function plugin(api) {
472
471
  process.exit(1);
473
472
  }
474
473
  });
474
+ // --- inspect ---
475
+ clawracha
476
+ .command("inspect")
477
+ .description("Inspect internal sync state for debugging")
478
+ .requiredOption("--agent <id>", "Agent ID")
479
+ .action(async (opts) => {
480
+ try {
481
+ const { agentId, workspace } = requireAgent(opts.agent);
482
+ const deviceConfig = await loadDeviceConfig(workspace);
483
+ if (!deviceConfig?.agentKey || !deviceConfig.setupComplete) {
484
+ console.log(`Not set up. Run \`openclaw clawracha init --agent ${agentId}\` first.`);
485
+ return;
486
+ }
487
+ // Use active syncer if available, otherwise spin up temporary engine
488
+ let engine;
489
+ const activeSync = activeSyncers.get(workspace);
490
+ if (activeSync) {
491
+ engine = activeSync.engine;
492
+ }
493
+ else {
494
+ engine = new SyncEngine(workspace);
495
+ await engine.init(deviceConfig);
496
+ }
497
+ const state = await engine.inspect();
498
+ console.log(`🔥 Storacha Inspect [${agentId}]`);
499
+ console.log(`Workspace: ${workspace}`);
500
+ console.log(`Running: ${state.running}`);
501
+ console.log(`Root CID: ${state.root ?? "(none)"}`);
502
+ console.log(`Revisions: ${state.revisions.length}`);
503
+ for (const r of state.revisions) {
504
+ console.log(` event: ${r.event}`);
505
+ }
506
+ console.log(`\nPail entries (${state.pailKeys.length}):`);
507
+ for (const key of state.pailKeys) {
508
+ console.log(` ${key}`);
509
+ }
510
+ console.log(`\nPending ops (${state.pendingOps.length}):`);
511
+ for (const op of state.pendingOps) {
512
+ console.log(` ${op.type} ${op.key}${op.value ? ` → ${op.value}` : ""}`);
513
+ }
514
+ }
515
+ catch (err) {
516
+ console.error(`Error: ${err.message}`);
517
+ process.exit(1);
518
+ }
519
+ });
475
520
  }, { commands: ["clawracha"] });
476
521
  }
package/dist/sync.d.ts CHANGED
@@ -10,47 +10,70 @@
10
10
  */
11
11
  import type { SyncState, FileChange, DeviceConfig } from "./types/index.js";
12
12
  import { type PailEntries } from "./utils/differ.js";
13
- import { Client } from "@storacha/client";
14
13
  export declare class SyncEngine {
15
14
  private workspace;
16
15
  private blocks;
17
- private name;
16
+ private state;
18
17
  private current;
19
18
  private pendingOps;
20
19
  private carFile;
21
- private running;
22
20
  private lastSync;
23
- private storachaClient;
24
- constructor(storachaClient: Client, workspace: string);
21
+ constructor(workspace: string);
25
22
  /**
26
- * Initialize sync engine with device config
23
+ * Initialize sync engine with device config.
24
+ * Creates the Storacha client and resolves the UCN name.
27
25
  */
28
26
  init(config: DeviceConfig): Promise<void>;
29
27
  /**
30
- * Process a batch of file changes
28
+ * Require running state or throw.
29
+ */
30
+ private requireRunning;
31
+ /**
32
+ * Process a batch of file changes.
33
+ * Can be called even when not running (accumulates pending ops).
31
34
  */
32
35
  processChanges(changes: FileChange[]): Promise<void>;
33
36
  /**
34
- * Execute sync: generate revision, publish, upload, apply remote changes
37
+ * Execute sync: generate revision, publish, upload, apply remote changes.
35
38
  */
36
39
  sync(): Promise<void>;
37
40
  /**
38
- * Create CAR and upload to Storacha
41
+ * Create CAR and upload to Storacha.
39
42
  */
40
43
  private possiblyUploadCAR;
41
44
  /**
42
- * Get current pail entries as map
45
+ * Get current pail entries as map.
43
46
  */
44
47
  getPailEntries(): Promise<PailEntries>;
45
48
  /**
46
- * Apply remote changes to local filesystem
49
+ * Apply remote changes to local filesystem.
47
50
  */
48
51
  private applyRemoteChanges;
52
+ /**
53
+ * Mark the engine as stopped.
54
+ */
55
+ stop(): void;
49
56
  /**
50
57
  * Pull all remote state and write to local filesystem.
51
- * Used by /storacha-join to overwrite local with remote before watcher starts.
58
+ * Used by join to overwrite local with remote before watcher starts.
52
59
  */
53
60
  pullRemote(): Promise<number>;
61
+ /**
62
+ * Inspect internal state for debugging.
63
+ */
64
+ inspect(): Promise<{
65
+ root: string | null;
66
+ revisions: {
67
+ event: string;
68
+ }[];
69
+ pailKeys: string[];
70
+ pendingOps: {
71
+ type: string;
72
+ key: string;
73
+ value?: string;
74
+ }[];
75
+ running: boolean;
76
+ }>;
54
77
  status(): Promise<SyncState>;
55
78
  exportNameArchive(): Promise<string>;
56
79
  private storeBlocks;
@@ -1 +1 @@
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;AAG1C,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"}
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;AAWxE,qBAAa,UAAU;IACrB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,QAAQ,CAAuB;gBAE3B,SAAS,EAAE,MAAM;IAK7B;;;OAGG;IACG,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAoC/C;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAe1D;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgD3B;;OAEG;YACW,iBAAiB;IAe/B;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;IAc5C;;OAEG;YACW,kBAAkB;IAUhC;;OAEG;IACH,IAAI,IAAI,IAAI;IAIZ;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IA0BnC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC;QACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,SAAS,EAAE;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC/B,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,UAAU,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC5D,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC;IAiBI,MAAM,IAAI,OAAO,CAAC,SAAS,CAAC;IAW5B,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;YAM5B,WAAW;CAK1B"}
package/dist/sync.js CHANGED
@@ -16,31 +16,32 @@ import { applyRemoteChanges } from "./handlers/remote.js";
16
16
  import { processChanges } from "./handlers/process.js";
17
17
  import { diffRemoteChanges } from "./utils/differ.js";
18
18
  import { makeTempCar } from "./utils/tempcar.js";
19
+ import { createStorachaClient } from "./utils/client.js";
19
20
  import { decodeDelegation, encodeDelegation } from "./utils/delegation.js";
20
21
  export class SyncEngine {
21
22
  workspace;
22
23
  blocks;
23
- name = null;
24
+ state = { running: false };
24
25
  current = null;
25
26
  pendingOps = [];
26
27
  carFile = null;
27
- running = false;
28
28
  lastSync = null;
29
- storachaClient;
30
- constructor(storachaClient, workspace) {
31
- this.storachaClient = storachaClient;
29
+ constructor(workspace) {
32
30
  this.workspace = workspace;
33
31
  this.blocks = createWorkspaceBlockstore(workspace);
34
32
  }
35
33
  /**
36
- * Initialize sync engine with device config
34
+ * Initialize sync engine with device config.
35
+ * Creates the Storacha client and resolves the UCN name.
37
36
  */
38
37
  async init(config) {
38
+ const storachaClient = await createStorachaClient(config);
39
39
  const agent = Agent.parse(config.agentKey);
40
+ let name;
40
41
  if (config.nameArchive) {
41
42
  // Restore from previously saved archive (has full state)
42
43
  const archiveBytes = decodeDelegation(config.nameArchive);
43
- this.name = await Name.extract(agent, archiveBytes);
44
+ name = await Name.extract(agent, archiveBytes);
44
45
  }
45
46
  else if (config.nameDelegation) {
46
47
  // Reconstruct from delegation (granted by another device)
@@ -49,14 +50,15 @@ export class SyncEngine {
49
50
  const { ok: delegation } = await extract(nameBytes);
50
51
  if (!delegation)
51
52
  throw new Error("Failed to extract name delegation");
52
- this.name = Name.from(agent, [delegation]);
53
+ name = Name.from(agent, [delegation]);
53
54
  }
54
55
  else {
55
56
  // First device — create a new name
56
- this.name = await Name.create(agent);
57
+ name = await Name.create(agent);
57
58
  }
59
+ this.state = { running: true, name, storachaClient };
58
60
  try {
59
- const result = await Revision.resolve(this.blocks, this.name);
61
+ const result = await Revision.resolve(this.blocks, name);
60
62
  this.current = result.value;
61
63
  await this.storeBlocks(result.additions);
62
64
  }
@@ -70,7 +72,17 @@ export class SyncEngine {
70
72
  }
71
73
  }
72
74
  /**
73
- * Process a batch of file changes
75
+ * Require running state or throw.
76
+ */
77
+ requireRunning() {
78
+ if (!this.state.running) {
79
+ throw new Error("Sync engine not initialized");
80
+ }
81
+ return this.state;
82
+ }
83
+ /**
84
+ * Process a batch of file changes.
85
+ * Can be called even when not running (accumulates pending ops).
74
86
  */
75
87
  async processChanges(changes) {
76
88
  if (!this.carFile) {
@@ -80,18 +92,16 @@ export class SyncEngine {
80
92
  this.pendingOps.push(...pendingOps);
81
93
  }
82
94
  /**
83
- * Execute sync: generate revision, publish, upload, apply remote changes
95
+ * Execute sync: generate revision, publish, upload, apply remote changes.
84
96
  */
85
97
  async sync() {
86
- if (!this.name) {
87
- throw new Error("Sync engine not initialized");
88
- }
98
+ const { name } = this.requireRunning();
89
99
  const beforeEntries = await this.getPailEntries();
90
100
  if (this.pendingOps.length > 0) {
91
101
  if (!this.carFile) {
92
102
  throw new Error("CAR file not initialized");
93
103
  }
94
- const result = await applyPendingOps(this.blocks, this.name, this.current, this.pendingOps);
104
+ const result = await applyPendingOps(this.blocks, name, this.current, this.pendingOps);
95
105
  this.current = result.current;
96
106
  for (const block of result.revisionBlocks) {
97
107
  await this.carFile.put(block);
@@ -101,7 +111,7 @@ export class SyncEngine {
101
111
  else {
102
112
  // No pending ops — just pull remote
103
113
  try {
104
- const result = await Revision.resolve(this.blocks, this.name, {
114
+ const result = await Revision.resolve(this.blocks, name, {
105
115
  base: this.current ?? undefined,
106
116
  });
107
117
  await this.storeBlocks(result.additions);
@@ -124,15 +134,16 @@ export class SyncEngine {
124
134
  this.lastSync = Date.now();
125
135
  }
126
136
  /**
127
- * Create CAR and upload to Storacha
137
+ * Create CAR and upload to Storacha.
128
138
  */
129
139
  async possiblyUploadCAR() {
130
140
  if (this.carFile) {
141
+ const { storachaClient } = this.requireRunning();
131
142
  const readableCar = await this.carFile.switchToReadable();
132
143
  this.carFile = null;
133
144
  if (readableCar) {
134
145
  try {
135
- await this.storachaClient.uploadCAR(readableCar.readable);
146
+ await storachaClient.uploadCAR(readableCar.readable);
136
147
  }
137
148
  finally {
138
149
  await readableCar.cleanup();
@@ -141,7 +152,7 @@ export class SyncEngine {
141
152
  }
142
153
  }
143
154
  /**
144
- * Get current pail entries as map
155
+ * Get current pail entries as map.
145
156
  */
146
157
  async getPailEntries() {
147
158
  const entries = new Map();
@@ -155,7 +166,7 @@ export class SyncEngine {
155
166
  return entries;
156
167
  }
157
168
  /**
158
- * Apply remote changes to local filesystem
169
+ * Apply remote changes to local filesystem.
159
170
  */
160
171
  async applyRemoteChanges(changedPaths, entries) {
161
172
  await applyRemoteChanges(changedPaths, entries, this.workspace, {
@@ -163,15 +174,20 @@ export class SyncEngine {
163
174
  current: this.current ?? undefined,
164
175
  });
165
176
  }
177
+ /**
178
+ * Mark the engine as stopped.
179
+ */
180
+ stop() {
181
+ this.state = { running: false };
182
+ }
166
183
  /**
167
184
  * Pull all remote state and write to local filesystem.
168
- * Used by /storacha-join to overwrite local with remote before watcher starts.
185
+ * Used by join to overwrite local with remote before watcher starts.
169
186
  */
170
187
  async pullRemote() {
171
- if (!this.name)
172
- throw new Error("Sync engine not initialized");
188
+ const { name } = this.requireRunning();
173
189
  try {
174
- const result = await Revision.resolve(this.blocks, this.name, {
190
+ const result = await Revision.resolve(this.blocks, name, {
175
191
  base: this.current ?? undefined,
176
192
  });
177
193
  await this.storeBlocks(result.additions);
@@ -192,10 +208,29 @@ export class SyncEngine {
192
208
  this.lastSync = Date.now();
193
209
  return entries.size;
194
210
  }
211
+ /**
212
+ * Inspect internal state for debugging.
213
+ */
214
+ async inspect() {
215
+ const entries = await this.getPailEntries();
216
+ return {
217
+ root: this.current?.root?.toString() ?? null,
218
+ revisions: this.current?.revision?.map((r) => ({
219
+ event: r.event.cid.toString(),
220
+ })) ?? [],
221
+ pailKeys: [...entries.keys()],
222
+ pendingOps: this.pendingOps.map((op) => ({
223
+ type: op.type,
224
+ key: op.key,
225
+ value: op.value?.toString(),
226
+ })),
227
+ running: this.state.running,
228
+ };
229
+ }
195
230
  async status() {
196
231
  const entries = await this.getPailEntries();
197
232
  return {
198
- running: this.running,
233
+ running: this.state.running,
199
234
  lastSync: this.lastSync,
200
235
  root: this.current?.root ?? null,
201
236
  entryCount: entries.size,
@@ -203,9 +238,8 @@ export class SyncEngine {
203
238
  };
204
239
  }
205
240
  async exportNameArchive() {
206
- if (!this.name)
207
- throw new Error("Sync engine not initialized");
208
- const bytes = await this.name.archive();
241
+ const { name } = this.requireRunning();
242
+ const bytes = await name.archive();
209
243
  return encodeDelegation(bytes);
210
244
  }
211
245
  async storeBlocks(blocks) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storacha/clawracha",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "OpenClaw plugin for Storacha workspace sync via UCN Pail",
5
5
  "type": "module",
6
6
  "files": [