@storacha/clawracha 0.0.4 → 0.0.6

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/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@storacha/clawracha",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "OpenClaw plugin for Storacha workspace sync via UCN Pail",
5
5
  "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "openclaw.plugin.json",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
6
12
  "main": "dist/plugin.js",
7
13
  "types": "dist/plugin.d.ts",
8
14
  "openclaw": {
@@ -1,57 +0,0 @@
1
- /**
2
- * Filesystem-backed blockstore — the one thing UCN doesn't ship.
3
- * Persists blocks to .storacha/blocks/ in workspace.
4
- */
5
-
6
- import * as fs from "node:fs/promises";
7
- import * as path from "node:path";
8
- import type { Link, Block, Version } from "multiformats";
9
- import { decode } from "multiformats/block";
10
- import * as raw from "multiformats/codecs/raw";
11
- import { sha256 } from "multiformats/hashes/sha2";
12
-
13
- export class DiskBlockstore {
14
- private dir: string;
15
- private initialized = false;
16
-
17
- constructor(workspacePath: string) {
18
- this.dir = path.join(workspacePath, ".storacha", "blocks");
19
- }
20
-
21
- private async ensureDir(): Promise<void> {
22
- if (!this.initialized) {
23
- await fs.mkdir(this.dir, { recursive: true });
24
- this.initialized = true;
25
- }
26
- }
27
-
28
- private cidPath(cid: Link<unknown, number, number, Version>): string {
29
- return path.join(this.dir, cid.toString());
30
- }
31
-
32
- async get<
33
- T = unknown,
34
- C extends number = number,
35
- A extends number = number,
36
- V extends Version = 1
37
- >(link: Link<T, C, A, V>): Promise<Block<T, C, A, V> | undefined> {
38
- try {
39
- const bytes = new Uint8Array(await fs.readFile(this.cidPath(link)));
40
- // Return a minimal block — decoder doesn't matter for storage, the CID is the truth
41
- return { cid: link, bytes, links: () => [] } as unknown as Block<
42
- T,
43
- C,
44
- A,
45
- V
46
- >;
47
- } catch (err: any) {
48
- if (err.code === "ENOENT") return undefined;
49
- throw err;
50
- }
51
- }
52
-
53
- async put(block: Block): Promise<void> {
54
- await this.ensureDir();
55
- await fs.writeFile(this.cidPath(block.cid), block.bytes);
56
- }
57
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Blockstore layer — thin wrapper around @storacha/ucn/block
3
- *
4
- * UCN provides: MemoryBlockstore, LRUBlockstore, GatewayBlockFetcher,
5
- * TieredBlockFetcher, withCache.
6
- *
7
- * We add: DiskBlockstore (filesystem persistence) and a pre-configured
8
- * tiered setup for workspace sync.
9
- */
10
-
11
- export {
12
- MemoryBlockstore,
13
- LRUBlockstore,
14
- GatewayBlockFetcher,
15
- TieredBlockFetcher,
16
- withCache,
17
- } from "@storacha/ucn/block";
18
-
19
- export { DiskBlockstore } from "./disk.js";
20
- export {
21
- createWorkspaceBlockstore,
22
- type WorkspaceBlockstore,
23
- } from "./workspace.js";
@@ -1,41 +0,0 @@
1
- /**
2
- * Pre-configured blockstore for workspace sync:
3
- * Memory (LRU) → Disk → Gateway, with cache promotion.
4
- */
5
-
6
- import {
7
- LRUBlockstore,
8
- GatewayBlockFetcher,
9
- TieredBlockFetcher,
10
- withCache,
11
- } from "@storacha/ucn/block";
12
- import { DiskBlockstore } from "./disk.js";
13
- import type { Block } from "multiformats";
14
-
15
- export interface WorkspaceBlockstore {
16
- get: TieredBlockFetcher["get"];
17
- put: (block: Block) => Promise<void>;
18
- }
19
-
20
- export function createWorkspaceBlockstore(
21
- workspacePath: string,
22
- options?: { gateway?: string; lruMax?: number }
23
- ): WorkspaceBlockstore {
24
- const memory = new LRUBlockstore(options?.lruMax ?? 1024);
25
- const disk = new DiskBlockstore(workspacePath);
26
- const gateway = new GatewayBlockFetcher(options?.gateway);
27
-
28
- // Reads: memory → disk → gateway, with cache promotion to memory
29
- const fetcher = withCache(
30
- new TieredBlockFetcher(memory, disk, gateway),
31
- memory
32
- );
33
-
34
- return {
35
- get: fetcher.get.bind(fetcher),
36
- async put(block: Block) {
37
- await memory.put(block);
38
- await disk.put(block);
39
- },
40
- };
41
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * Pure function to apply pending pail operations (puts/dels) and publish.
3
- *
4
- * Cycle: operation → publish → return value.
5
- * Bootstraps with v0Put when no current value exists.
6
- */
7
-
8
- import { Revision } from "@storacha/ucn/pail";
9
- import * as Batch from "@storacha/ucn/pail/batch";
10
- import type {
11
- ClockConnection,
12
- NameView,
13
- ValueView,
14
- } from "@storacha/ucn/pail/api";
15
- import type { Block } from "multiformats";
16
- import { MemoryBlockstore, withCache } from "@storacha/ucn/block";
17
-
18
- import type { PailOp } from "../types/index.js";
19
- import type { WorkspaceBlockstore } from "../blockstore/index.js";
20
-
21
- export interface ApplyResult {
22
- current: ValueView | null;
23
- revisionBlocks: Block[];
24
- }
25
-
26
- export async function applyPendingOps(
27
- blocks: WorkspaceBlockstore,
28
- name: NameView,
29
- current: ValueView | null,
30
- pendingOps: PailOp[],
31
- options?: { remotes?: ClockConnection[] },
32
- ): Promise<ApplyResult> {
33
- const revisionBlocks: Block[] = [];
34
- let ops = [...pendingOps];
35
-
36
- // Local cache so each step can read blocks produced by previous steps
37
- const cache = new MemoryBlockstore();
38
- const fetcher = withCache(blocks, cache);
39
-
40
- const accumulate = (additions: Block[]) => {
41
- for (const block of additions) {
42
- cache.put(block);
43
- }
44
- revisionBlocks.push(...additions);
45
- };
46
-
47
- if (!current) {
48
- const firstPut = ops.find((op) => op.type === "put" && op.value);
49
- if (firstPut?.value) {
50
- const result = await Revision.v0Put(fetcher, firstPut.key, firstPut.value);
51
- accumulate(result.additions);
52
-
53
- const pubResult = await Revision.publish(fetcher, name, result.revision, { remotes: options?.remotes });
54
- accumulate(pubResult.additions);
55
- current = pubResult.value;
56
-
57
- ops = ops.filter((op) => op !== firstPut);
58
- }
59
- }
60
-
61
- if (ops.length > 0 && current) {
62
- const batcher = await Batch.create(fetcher, current);
63
- for (const op of ops) {
64
- if (op.type === "put" && op.value) {
65
- await batcher.put(op.key, op.value);
66
- } else if (op.type === "del") {
67
- await batcher.del(op.key);
68
- }
69
- }
70
- const result = await batcher.commit();
71
- accumulate(result.additions);
72
-
73
- const pubResult = await Revision.publish(fetcher, name, result.revision, { remotes: options?.remotes });
74
- accumulate(pubResult.additions);
75
- current = pubResult.value;
76
- }
77
-
78
- return { current, revisionBlocks };
79
- }
@@ -1,118 +0,0 @@
1
- /**
2
- * Pure function to diff file changes against pail state and produce ops.
3
- *
4
- * Markdown files (.md) are handled via mdsync — CRDT merge rather than
5
- * whole-file UnixFS replacement. Regular files go through encodeFiles.
6
- */
7
-
8
- import { CID } from "multiformats/cid";
9
- import { Revision } from "@storacha/ucn/pail";
10
- import type {
11
- BlockFetcher,
12
- UnknownLink,
13
- ValueView,
14
- } from "@storacha/ucn/pail/api";
15
- import type { Block } from "multiformats";
16
- import type { FileChange, PailOp } from "../types/index.js";
17
- import { encodeFiles } from "../utils/encoder.js";
18
- import * as mdsync from "../mdsync/index.js";
19
- import * as fs from "node:fs/promises";
20
- import * as path from "node:path";
21
-
22
- /** Callback to persist a block to the CAR file for upload. */
23
- export type BlockSink = (block: Block) => Promise<void>;
24
-
25
- /** Callback to persist a block to the local blockstore for future reads. */
26
- export type BlockStore = (block: Block) => Promise<void>;
27
-
28
- const isMarkdown = (filePath: string) => filePath.endsWith(".md");
29
-
30
- export async function processChanges(
31
- changes: FileChange[],
32
- workspace: string,
33
- current: ValueView | null,
34
- blocks: BlockFetcher,
35
- sink: BlockSink,
36
- store?: BlockStore,
37
- ): Promise<PailOp[]> {
38
- const pendingOps: PailOp[] = [];
39
-
40
- const mdChanges = changes.filter((c) => isMarkdown(c.path));
41
- const regularChanges = changes.filter((c) => !isMarkdown(c.path));
42
-
43
- // --- Regular files (UnixFS encode) ---
44
- const toEncode = regularChanges
45
- .filter((c) => c.type !== "unlink")
46
- .map((c) => c.path);
47
-
48
- const encoded = await encodeFiles(workspace, toEncode);
49
-
50
- const files = [];
51
- for (const file of encoded) {
52
- let root: UnknownLink | null = null;
53
- const reader = file.blocks.getReader();
54
- while (true) {
55
- const { done, value } = await reader.read();
56
- if (done) break;
57
- root = value.cid;
58
- await sink(value as unknown as Block);
59
- }
60
- if (!root) {
61
- throw new Error(`Failed to encode file: ${file.path}`);
62
- }
63
- files.push({ path: file.path, rootCID: root as UnknownLink });
64
- }
65
-
66
- for (const file of files) {
67
- const existing = current
68
- ? await Revision.get(blocks, current, file.path)
69
- : null;
70
- if (!existing || !existing.equals(file.rootCID)) {
71
- pendingOps.push({
72
- type: "put",
73
- key: file.path,
74
- value: file.rootCID as CID,
75
- });
76
- }
77
- }
78
-
79
- // --- Markdown files (CRDT merge via mdsync) ---
80
- const mdPuts = mdChanges.filter((c) => c.type !== "unlink");
81
- for (const change of mdPuts) {
82
- const content = await fs.readFile(
83
- path.join(workspace, change.path),
84
- "utf-8",
85
- );
86
- const { mdEntryCid, additions } = current
87
- ? await mdsync.put(blocks, current, change.path, content)
88
- : await mdsync.v0Put(content);
89
-
90
- // Sink blocks to CAR for upload, and store locally for future resolveValue calls.
91
- for (const block of additions) {
92
- await sink(block);
93
- if (store) await store(block);
94
- }
95
-
96
- pendingOps.push({
97
- type: "put",
98
- key: change.path,
99
- value: mdEntryCid as CID,
100
- });
101
- }
102
-
103
- // --- Deletes (both regular and markdown) ---
104
- const toDelete = changes
105
- .filter((c) => c.type === "unlink")
106
- .map((c) => c.path);
107
-
108
- for (const deletePath of toDelete) {
109
- const existing = current
110
- ? await Revision.get(blocks, current, deletePath)
111
- : null;
112
- if (existing) {
113
- pendingOps.push({ type: "del", key: deletePath });
114
- }
115
- }
116
-
117
- return pendingOps;
118
- }
@@ -1,61 +0,0 @@
1
- /**
2
- * Apply remote changes to local filesystem.
3
- *
4
- * Regular files are fetched from the IPFS gateway (handles UnixFS reassembly).
5
- * Markdown files (.md) are resolved via mdsync CRDT merge — the tiered
6
- * blockstore's gateway layer handles fetching any missing blocks.
7
- */
8
-
9
- import * as fs from "node:fs/promises";
10
- import * as path from "node:path";
11
- import type { CID } from "multiformats/cid";
12
- import type { BlockFetcher, ValueView } from "@storacha/ucn/pail/api";
13
- import * as mdsync from "../mdsync/index.js";
14
-
15
- const DEFAULT_GATEWAY = "https://storacha.link";
16
-
17
- const isMarkdown = (filePath: string) => filePath.endsWith(".md");
18
-
19
- export async function applyRemoteChanges(
20
- changedPaths: string[],
21
- entries: Map<string, CID>,
22
- workspace: string,
23
- options?: {
24
- gateway?: string;
25
- blocks?: BlockFetcher;
26
- current?: ValueView;
27
- },
28
- ): Promise<void> {
29
- const gateway = options?.gateway ?? DEFAULT_GATEWAY;
30
-
31
- for (const relativePath of changedPaths) {
32
- const cid = entries.get(relativePath);
33
- const fullPath = path.join(workspace, relativePath);
34
-
35
- if (!cid) {
36
- // Deleted remotely
37
- try {
38
- await fs.unlink(fullPath);
39
- } catch (err: any) {
40
- if (err.code !== "ENOENT") throw err;
41
- }
42
- } else if (isMarkdown(relativePath) && options?.blocks && options?.current) {
43
- // Markdown: resolve via mdsync CRDT merge.
44
- // The blockstore's lowest tier is a gateway fetcher, so any blocks
45
- // we don't have locally will be fetched transparently.
46
- const content = await mdsync.get(options.blocks, options.current, relativePath);
47
- if (content != null) {
48
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
49
- await fs.writeFile(fullPath, content);
50
- }
51
- } else {
52
- // Regular file: fetch full file from gateway (handles UnixFS reassembly)
53
- const res = await fetch(`${gateway}/ipfs/${cid}`);
54
- if (!res.ok) {
55
- throw new Error(`Gateway fetch failed for ${cid}: ${res.status}`);
56
- }
57
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
58
- await fs.writeFile(fullPath, new Uint8Array(await res.arrayBuffer()));
59
- }
60
- }
61
- }
package/src/index.ts DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * @storacha/clawracha - OpenClaw plugin for Storacha workspace sync
3
- */
4
-
5
- export * from "./types/index.js";
6
- export * from "./blockstore/index.js";
7
- export { encodeWorkspaceFile, encodeFiles } from "./utils/encoder.js";
8
- export { diffEntries, diffRemoteChanges } from "./utils/differ.js";
9
- export { SyncEngine } from "./sync.js";
10
- export { FileWatcher } from "./watcher.js";
11
-
12
- // Re-export plugin definition (default export)
13
- export { default as plugin } from "./plugin.js";