@memfork/cli 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.
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Operational commands: status, log, recall, commit, merge, proposals.
3
+ * All resolve config via the layered config system — no env vars needed.
4
+ */
5
+ import { execSync, exec } from "node:child_process";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import chalk from "chalk";
9
+ import { resolveConfig, toClientConfig, readProjectConfig, writeProjectConfig, MEMWAL_CONSTANTS, } from "../config.js";
10
+ import { MemForksClient } from "@memfork/core";
11
+ // ─── Shared helpers ───────────────────────────────────────────────────────────
12
+ async function getClient() {
13
+ const cfg = resolveConfig();
14
+ const client = await MemForksClient.connect(toClientConfig(cfg));
15
+ return { client, cfg };
16
+ }
17
+ function currentGitBranch() {
18
+ try {
19
+ return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
20
+ }
21
+ catch {
22
+ return "main";
23
+ }
24
+ }
25
+ // ─── status ───────────────────────────────────────────────────────────────────
26
+ export async function cmdStatus() {
27
+ const { client, cfg } = await getClient();
28
+ const tree = await client.getTree();
29
+ console.log("");
30
+ console.log(chalk.bold("MemForks status"));
31
+ console.log("");
32
+ console.log(` Tree ${chalk.cyan(cfg.treeId)}`);
33
+ console.log(` Network ${cfg.network}`);
34
+ console.log(` Branch ${chalk.green(String(tree["default_branch"] ?? cfg.defaultBranch))}`);
35
+ console.log(` Signer ${client.keypair.toSuiAddress()}`);
36
+ console.log("");
37
+ }
38
+ // ─── log ──────────────────────────────────────────────────────────────────────
39
+ export async function cmdLog(opts) {
40
+ const { client, cfg } = await getClient();
41
+ const branch = opts.branch ?? currentGitBranch();
42
+ console.log("");
43
+ console.log(`${chalk.bold("memfork log")} ${chalk.dim("branch:")} ${chalk.green(branch)}`);
44
+ console.log("");
45
+ // Model A: commits are off-chain Walrus blobs. Use recall() with empty
46
+ // query to list the most recent memories as a commit log approximation.
47
+ try {
48
+ const results = await client.recall("", { branch, limit: opts.limit ?? 20 });
49
+ if (results.length === 0) {
50
+ console.log(chalk.dim(` No commits yet on branch "${branch}"`));
51
+ }
52
+ else {
53
+ for (const r of results) {
54
+ const blobShort = r.blobId.slice(0, 10) + "…";
55
+ let preview = r.text;
56
+ try {
57
+ const p = JSON.parse(r.text);
58
+ const delta = p["delta"];
59
+ const facts = delta?.["facts"] ?? [];
60
+ preview = facts[0] ?? r.text;
61
+ }
62
+ catch { /* not JSON */ }
63
+ console.log(` ${chalk.yellow(blobShort)} ` +
64
+ chalk.dim(`dist:${r.distance.toFixed(3)}`) + " " +
65
+ chalk.white(preview.slice(0, 80)));
66
+ }
67
+ }
68
+ }
69
+ catch {
70
+ console.log(chalk.dim(` No commits yet on branch "${branch}"`));
71
+ }
72
+ console.log("");
73
+ }
74
+ // ─── recall ───────────────────────────────────────────────────────────────────
75
+ export async function cmdRecall(query, opts) {
76
+ const { client, cfg } = await getClient();
77
+ const branch = opts.branch ?? currentGitBranch();
78
+ const results = await client.recall(query, { branch, limit: opts.limit ?? 5 });
79
+ if (opts.json) {
80
+ console.log(JSON.stringify(results));
81
+ return;
82
+ }
83
+ console.log("");
84
+ if (results.length === 0) {
85
+ console.log(chalk.dim(` No results for "${query}" on branch ${branch}`));
86
+ }
87
+ else {
88
+ console.log(`${chalk.bold("recall")} ${chalk.dim('"' + query + '"')} ${chalk.dim("branch:")} ${chalk.green(branch)}`);
89
+ console.log("");
90
+ for (const r of results) {
91
+ const bar = r.distance < 0.2 ? chalk.green("███") :
92
+ r.distance < 0.35 ? chalk.yellow("██░") : chalk.dim("█░░");
93
+ console.log(` ${bar} ${chalk.dim(r.distance.toFixed(3))} ${r.text}`);
94
+ }
95
+ }
96
+ console.log("");
97
+ }
98
+ // ─── commit ───────────────────────────────────────────────────────────────────
99
+ export async function cmdCommit(opts) {
100
+ const { client, cfg } = await getClient();
101
+ const branch = opts.branch ?? currentGitBranch();
102
+ let facts = opts.facts ?? [];
103
+ // --from-response + --auto-extract: stub for LLM extraction
104
+ // In production this calls the configured LLM to distil durable facts.
105
+ if (opts.fromResponse && opts.autoExtract) {
106
+ facts = extractFacts(opts.fromResponse);
107
+ }
108
+ else if (opts.fromResponse) {
109
+ facts = [opts.fromResponse];
110
+ }
111
+ if (facts.length === 0) {
112
+ console.error(chalk.red("No facts to commit. Pass --facts or --from-response."));
113
+ process.exit(1);
114
+ }
115
+ const { blobId } = await client.commit(branch, { facts, message: opts.message });
116
+ const out = { blobId, branch };
117
+ if (process.stdout.isTTY) {
118
+ console.log("");
119
+ console.log(chalk.green("✓") + " Committed to " + chalk.bold(branch));
120
+ console.log(chalk.dim(` blob: ${blobId}`));
121
+ console.log("");
122
+ }
123
+ else {
124
+ console.log(JSON.stringify(out));
125
+ }
126
+ }
127
+ // ─── merge ────────────────────────────────────────────────────────────────────
128
+ export async function cmdMerge(from, into, opts) {
129
+ const { client } = await getClient();
130
+ process.stdout.write(chalk.dim(`Proposing merge ${chalk.green(from)} → ${chalk.green(into)} … `));
131
+ const digest = await client.proposeMerge({
132
+ fromBranch: from,
133
+ intoBranch: into,
134
+ resolverId: opts.resolver,
135
+ ttlMs: opts.ttl ?? 86_400_000,
136
+ });
137
+ console.log(chalk.green("done"));
138
+ console.log("");
139
+ console.log(chalk.dim(` tx: ${digest}`));
140
+ console.log("");
141
+ console.log(chalk.dim("The resolver runtime will handle attestation and finalization automatically."));
142
+ console.log(chalk.dim("Use `memfork proposals` to monitor progress."));
143
+ console.log("");
144
+ }
145
+ // ─── proposals ────────────────────────────────────────────────────────────────
146
+ export async function cmdProposals() {
147
+ const { client, cfg } = await getClient();
148
+ // Fetch open MergeProposed events from the indexer.
149
+ // For now this polls recent events (the indexer / app/ maintains the live view).
150
+ console.log("");
151
+ console.log(chalk.bold("Open merge proposals"));
152
+ console.log(chalk.dim(" (live status in the visualizer: memfork ui)"));
153
+ console.log("");
154
+ console.log(chalk.dim(" Tree: " + cfg.treeId));
155
+ console.log("");
156
+ console.log(chalk.dim(" Polling Sui events…"));
157
+ // TODO: drive through MemForksIndexer once it's wired into the CLI.
158
+ // For the hackathon: redirect to the visualizer.
159
+ console.log("");
160
+ console.log(chalk.cyan(" →") + " Run " + chalk.bold("memfork ui") + " for the full live proposal view.");
161
+ console.log("");
162
+ }
163
+ // ─── ui ───────────────────────────────────────────────────────────────────────
164
+ export async function cmdUi(opts = {}) {
165
+ const appDir = findAppDir();
166
+ if (!appDir) {
167
+ console.log(chalk.yellow("Could not find the MemForks app directory."));
168
+ console.log(chalk.dim("Build the app manually: cd app && npm run build"));
169
+ return;
170
+ }
171
+ const distDir = path.join(appDir, "dist");
172
+ const indexHtml = path.join(distDir, "index.html");
173
+ // ── Share mode: build → publish to Walrus Site ──────────────────────────
174
+ if (opts.share) {
175
+ console.log("");
176
+ console.log(chalk.bold("memfork ui --share") + chalk.dim(" → Walrus Site"));
177
+ console.log("");
178
+ console.log(chalk.dim("Building app for Walrus Site…"));
179
+ execSync("npm run build", {
180
+ cwd: appDir,
181
+ stdio: "inherit",
182
+ env: { ...process.env, VITE_WALRUS_MODE: "true" },
183
+ });
184
+ console.log("");
185
+ console.log(chalk.dim("Publishing to Walrus…"));
186
+ try {
187
+ execSync(`site-builder deploy --epochs 10 ${distDir}`, { stdio: "inherit" });
188
+ }
189
+ catch {
190
+ console.log("");
191
+ console.log(chalk.yellow("site-builder not found."));
192
+ console.log("");
193
+ console.log("Install it with suiup, then run the deploy manually:");
194
+ console.log(chalk.dim(" curl -sSfL https://raw.githubusercontent.com/Mystenlabs/suiup/main/install.sh | sh"));
195
+ console.log(chalk.dim(" suiup install site-builder@mainnet"));
196
+ console.log(chalk.dim(` site-builder deploy --epochs 10 ${distDir}`));
197
+ console.log("");
198
+ console.log(chalk.dim("The build is ready in: " + distDir));
199
+ }
200
+ return;
201
+ }
202
+ // ── Local mode: serve pre-built bundle + /api/* routes ──────────────────
203
+ if (!fs.existsSync(indexHtml)) {
204
+ console.log(chalk.dim("Building app (first run — takes ~10s)…"));
205
+ execSync("npm run build", { cwd: appDir, stdio: "inherit" });
206
+ }
207
+ const port = opts.port ?? 4242;
208
+ console.log("");
209
+ console.log(chalk.bold("MemForks") + chalk.dim(" → starting local server…"));
210
+ console.log("");
211
+ const { startUiServer } = await import("./ui-server.js");
212
+ const server = startUiServer(distDir, port);
213
+ // Open browser after a short delay.
214
+ setTimeout(() => {
215
+ const url = `http://localhost:${port}`;
216
+ const cmd = process.platform === "darwin" ? `open ${url}` :
217
+ process.platform === "win32" ? `start ${url}` :
218
+ `xdg-open ${url}`;
219
+ exec(cmd);
220
+ console.log(chalk.dim(" Press Ctrl+C to stop."));
221
+ console.log("");
222
+ }, 400);
223
+ // Block until the user presses Ctrl+C.
224
+ await new Promise((resolve) => {
225
+ process.on("SIGINT", () => {
226
+ server.close(() => resolve());
227
+ console.log(chalk.dim("\n Server stopped."));
228
+ });
229
+ });
230
+ }
231
+ // ─── show ─────────────────────────────────────────────────────────────────────
232
+ /**
233
+ * `memfork show <anchorId>` — show details of an on-chain merge anchor.
234
+ * For off-chain commit blobs, use `memfork recall` or the UI history view.
235
+ */
236
+ export async function cmdShow(anchorId) {
237
+ const { client } = await getClient();
238
+ const anchor = await client.getMergeAnchor(anchorId);
239
+ const parents = anchor.parents ?? [];
240
+ console.log("");
241
+ console.log(chalk.bold("merge anchor ") + chalk.yellow(anchorId));
242
+ console.log(chalk.dim("tree: ") + chalk.dim(String(anchor.tree_id ?? "")));
243
+ if (parents.length > 0) {
244
+ console.log(chalk.dim("parent blobs: ") + parents.map((p) => chalk.cyan(p.slice(0, 20) + "…")).join(" "));
245
+ }
246
+ console.log(chalk.dim("resolved_blob: ") + chalk.cyan(String(anchor.memwal_blob_id ?? "")));
247
+ console.log(chalk.dim("namespace: ") + chalk.dim(String(anchor.memwal_namespace ?? "")));
248
+ console.log("");
249
+ }
250
+ // ─── diff ─────────────────────────────────────────────────────────────────────
251
+ export async function cmdDiff(fromRef, toRef) {
252
+ const { client } = await getClient();
253
+ console.log("");
254
+ console.log(chalk.bold("diff") + " " +
255
+ chalk.green(fromRef) + chalk.dim("..") + chalk.green(toRef));
256
+ console.log("");
257
+ // Model A: diff is based on recalled facts from each branch namespace.
258
+ const fromBranch = fromRef;
259
+ const toBranch = toRef;
260
+ if (fromBranch !== toBranch) {
261
+ const [fromFacts, toFacts] = await Promise.all([
262
+ client.recall("", { branch: fromBranch, limit: 50 }),
263
+ client.recall("", { branch: toBranch, limit: 50 }),
264
+ ]);
265
+ const fromKeys = new Set(fromFacts.map((f) => f.text));
266
+ const toKeys = new Set(toFacts.map((f) => f.text));
267
+ const added = toFacts.filter((f) => !fromKeys.has(f.text));
268
+ const removed = fromFacts.filter((f) => !toKeys.has(f.text));
269
+ if (added.length > 0) {
270
+ console.log(chalk.green(" + Facts added in " + toBranch + ":"));
271
+ for (const f of added) {
272
+ console.log(chalk.green(" + ") + f.text.slice(0, 100));
273
+ }
274
+ console.log("");
275
+ }
276
+ if (removed.length > 0) {
277
+ console.log(chalk.red(" - Facts only in " + fromBranch + ":"));
278
+ for (const f of removed) {
279
+ console.log(chalk.red(" - ") + f.text.slice(0, 100));
280
+ }
281
+ console.log("");
282
+ }
283
+ if (added.length === 0 && removed.length === 0) {
284
+ console.log(chalk.dim(" No fact differences between the two branches."));
285
+ console.log("");
286
+ }
287
+ }
288
+ else {
289
+ console.log(chalk.dim(" Same branch — commit-level diff not yet supported."));
290
+ console.log(chalk.dim(" Use memfork log --branch " + fromBranch + " to see history."));
291
+ console.log("");
292
+ }
293
+ }
294
+ // ─── delegates ────────────────────────────────────────────────────────────────
295
+ export async function cmdDelegates() {
296
+ const { client, cfg } = await getClient();
297
+ const tree = await client.getTree();
298
+ const delegates = tree["delegates"] ?? [];
299
+ console.log("");
300
+ console.log(chalk.bold("Delegates") + chalk.dim(" tree: " + cfg.treeId.slice(0, 12) + "…"));
301
+ console.log("");
302
+ if (delegates.length === 0) {
303
+ console.log(chalk.dim(" No delegates (only the owner can commit)."));
304
+ }
305
+ else {
306
+ for (const d of delegates) {
307
+ const addr = String(d["address"] ?? d["sui_address"] ?? "");
308
+ const perms = String(d["permissions"] ?? "0xFF");
309
+ const expiry = d["expiry_ms"]
310
+ ? new Date(Number(d["expiry_ms"])).toISOString().slice(0, 10)
311
+ : "never";
312
+ console.log(` ${chalk.cyan(addr.slice(0, 14) + "…")}` +
313
+ chalk.dim(` perms: ${perms} expiry: ${expiry}`));
314
+ }
315
+ }
316
+ console.log("");
317
+ }
318
+ // ─── grant ────────────────────────────────────────────────────────────────────
319
+ export async function cmdGrant(opts) {
320
+ const { client } = await getClient();
321
+ const permissions = opts.permissions ? parseInt(opts.permissions, 16) : 0xFF;
322
+ const expiryMs = opts.expiry ?? Number.MAX_SAFE_INTEGER;
323
+ process.stdout.write(chalk.dim(`Granting delegate to ${chalk.cyan(opts.address)} … `));
324
+ const digest = await client.grantDelegate(opts.address, {
325
+ perms: permissions,
326
+ expiresEpoch: BigInt(expiryMs),
327
+ branches: opts.branches,
328
+ });
329
+ console.log(chalk.green("done"));
330
+ console.log(chalk.dim(` tx: ${digest}`));
331
+ console.log("");
332
+ }
333
+ // ─── grant-memwal ─────────────────────────────────────────────────────────────
334
+ /**
335
+ * Register a new team member's MemWal delegate key with the tree's MemWal account.
336
+ * Run by the tree owner after a teammate runs `memfork join`.
337
+ */
338
+ export async function cmdGrantMemwal(opts) {
339
+ const { cfg } = await getClient();
340
+ const { addDelegateKey } = await import("@mysten-incubation/memwal/account");
341
+ const { JsonRpcHTTPTransport, SuiJsonRpcClient, getJsonRpcFullnodeUrl } = await import("@mysten/sui/jsonRpc");
342
+ const network = cfg.network ?? "testnet";
343
+ const consts = MEMWAL_CONSTANTS[network === "mainnet" ? "mainnet" : "testnet"];
344
+ const rpcUrl = getJsonRpcFullnodeUrl(network);
345
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
346
+ const suiClient = new SuiJsonRpcClient({ transport: new JsonRpcHTTPTransport({ url: rpcUrl }), network });
347
+ const pubkeyBytes = Uint8Array.from(Buffer.from(opts.pubkey, "hex"));
348
+ process.stdout.write(chalk.dim(`Registering MemWal delegate key for ${chalk.cyan(opts.agent.slice(0, 14) + "…")} … `));
349
+ try {
350
+ await addDelegateKey({
351
+ packageId: consts.packageId,
352
+ accountId: cfg.memwalAccountId,
353
+ publicKey: pubkeyBytes,
354
+ label: `memfork-join-${opts.agent.slice(0, 8)}`,
355
+ suiPrivateKey: cfg.privateKey,
356
+ suiClient,
357
+ });
358
+ console.log(chalk.green("done"));
359
+ console.log("");
360
+ console.log(chalk.dim(` The key is now registered on the MemWal account.`));
361
+ console.log(chalk.dim(` Tell ${opts.agent.slice(0, 14)}… to run: memfork doctor`));
362
+ console.log("");
363
+ }
364
+ catch (e) {
365
+ const msg = String(e);
366
+ if (msg.includes("EDelegateKeyAlreadyExists")) {
367
+ console.log(chalk.dim("already registered"));
368
+ console.log("");
369
+ }
370
+ else {
371
+ console.log(chalk.red("failed"));
372
+ throw new Error(`MemWal key registration failed: ${msg}`);
373
+ }
374
+ }
375
+ }
376
+ // ─── revoke ───────────────────────────────────────────────────────────────────
377
+ export async function cmdRevoke(address) {
378
+ const { client } = await getClient();
379
+ process.stdout.write(chalk.dim(`Revoking delegate ${chalk.cyan(address)} … `));
380
+ const digest = await client.revokeDelegate(address);
381
+ console.log(chalk.green("done"));
382
+ console.log(chalk.dim(` tx: ${digest}`));
383
+ console.log("");
384
+ }
385
+ // ─── branch ───────────────────────────────────────────────────────────────────
386
+ export async function cmdBranch(name, opts = {}) {
387
+ const { client, cfg } = await getClient();
388
+ const from = opts.from ?? cfg.defaultBranch ?? currentGitBranch();
389
+ process.stdout.write(chalk.dim(`Creating branch ${chalk.green(name)} from ${chalk.green(from)} … `));
390
+ const digest = await client.branch(name, { from });
391
+ console.log(chalk.green("done"));
392
+ console.log("");
393
+ console.log(chalk.dim(` tx: ${digest}`));
394
+ console.log(chalk.dim(` Run ${chalk.white("memfork checkout " + name)} to switch to it.`));
395
+ console.log("");
396
+ }
397
+ // ─── checkout ─────────────────────────────────────────────────────────────────
398
+ export async function cmdCheckout(name) {
399
+ const { client, cfg } = await getClient();
400
+ // Verify the branch exists on-chain before switching.
401
+ // The on-chain branches field is a Move Table — its keys live in dynamic
402
+ // fields. We use getBranchHead as the existence check (it throws on miss).
403
+ try {
404
+ await client.getBranchHead(name);
405
+ }
406
+ catch {
407
+ console.error(chalk.red(`\n Branch "${name}" not found on tree.`) +
408
+ chalk.dim("\n Use `memfork branch " + name + "` to create it first.\n"));
409
+ process.exit(1);
410
+ }
411
+ // Persist the new default branch in the project config.
412
+ const project = readProjectConfig() ?? {};
413
+ writeProjectConfig({ ...project, defaultBranch: name });
414
+ console.log("");
415
+ console.log(chalk.green("✓") + " Switched to " + chalk.bold(name));
416
+ console.log(chalk.dim(" (updated .memfork/config.json)"));
417
+ console.log("");
418
+ }
419
+ // ─── Fact extraction stub ─────────────────────────────────────────────────────
420
+ // Production implementation: calls the configured LLM with a system prompt that
421
+ // extracts durable facts from the turn response. Stub version pulls sentences.
422
+ function extractFacts(response) {
423
+ return response
424
+ .split(/[.!?\n]/)
425
+ .map((s) => s.trim())
426
+ .filter((s) => s.length > 30 && s.length < 500);
427
+ }
428
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
429
+ function findAppDir() {
430
+ const candidates = [
431
+ new URL("../../../app", import.meta.url).pathname,
432
+ new URL("../../../../app", import.meta.url).pathname,
433
+ ];
434
+ for (const c of candidates) {
435
+ try {
436
+ if (fs.existsSync(c + "/package.json"))
437
+ return c;
438
+ }
439
+ catch {
440
+ continue;
441
+ }
442
+ }
443
+ return null;
444
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Auto-provisioning for `memfork init --quick`.
3
+ *
4
+ * Goes from zero to a fully configured memory tree in one shot:
5
+ *
6
+ * 1. Generate a fresh Ed25519 keypair (or reuse an existing one)
7
+ * 2. Fund it from the Sui testnet faucet
8
+ * 3. Create a MemWal account on-chain → accountId
9
+ * 4. Generate a delegate keypair
10
+ * 5. Register the delegate key with MemWal
11
+ * 6. Create a MemoryTree → treeId
12
+ *
13
+ * Tested against MemWal SDK:
14
+ * createAccount({ packageId, registryId, suiPrivateKey, suiClient })
15
+ * generateDelegateKey() → { privateKey: hex, publicKey: Uint8Array, suiAddress }
16
+ * addDelegateKey({ packageId, accountId, publicKey, suiAddress, label, suiPrivateKey, suiClient })
17
+ *
18
+ * We always pass `suiClient` explicitly because @mysten/sui v2 renames SuiClient
19
+ * to SuiJsonRpcClient, and the MemWal SDK's internal auto-init would fail on v2.
20
+ */
21
+ export interface ProvisionResult {
22
+ treeId: string;
23
+ privateKey: string;
24
+ memwalAccountId: string;
25
+ memwalKey: string;
26
+ network: "testnet" | "mainnet";
27
+ }
28
+ export declare function autoProvision(opts: {
29
+ network: "testnet" | "mainnet";
30
+ existingKey?: string;
31
+ defaultBranch?: string;
32
+ }): Promise<ProvisionResult>;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Auto-provisioning for `memfork init --quick`.
3
+ *
4
+ * Goes from zero to a fully configured memory tree in one shot:
5
+ *
6
+ * 1. Generate a fresh Ed25519 keypair (or reuse an existing one)
7
+ * 2. Fund it from the Sui testnet faucet
8
+ * 3. Create a MemWal account on-chain → accountId
9
+ * 4. Generate a delegate keypair
10
+ * 5. Register the delegate key with MemWal
11
+ * 6. Create a MemoryTree → treeId
12
+ *
13
+ * Tested against MemWal SDK:
14
+ * createAccount({ packageId, registryId, suiPrivateKey, suiClient })
15
+ * generateDelegateKey() → { privateKey: hex, publicKey: Uint8Array, suiAddress }
16
+ * addDelegateKey({ packageId, accountId, publicKey, suiAddress, label, suiPrivateKey, suiClient })
17
+ *
18
+ * We always pass `suiClient` explicitly because @mysten/sui v2 renames SuiClient
19
+ * to SuiJsonRpcClient, and the MemWal SDK's internal auto-init would fail on v2.
20
+ */
21
+ import chalk from "chalk";
22
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
23
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
24
+ import { JsonRpcHTTPTransport, SuiJsonRpcClient, getJsonRpcFullnodeUrl } from "@mysten/sui/jsonRpc";
25
+ import { createAccount, addDelegateKey, generateDelegateKey, } from "@mysten-incubation/memwal/account";
26
+ import { MemForksClient } from "@memfork/core";
27
+ import { MEMWAL_CONSTANTS, } from "../config.js";
28
+ function step(n, msg) {
29
+ process.stdout.write(` ${chalk.dim(`[${n}/6]`)} ${msg}… `);
30
+ }
31
+ function done() { console.log(chalk.green("done")); }
32
+ function skip(reason) { console.log(chalk.dim("skip " + reason)); }
33
+ export async function autoProvision(opts) {
34
+ const network = opts.network;
35
+ const consts = MEMWAL_CONSTANTS[network];
36
+ const rpcUrl = getJsonRpcFullnodeUrl(network);
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ const suiClient = new SuiJsonRpcClient({ transport: new JsonRpcHTTPTransport({ url: rpcUrl }), network });
39
+ // ── 1. Keypair ──────────────────────────────────────────────────────────────
40
+ step(1, "Generating Sui keypair");
41
+ let keypair;
42
+ if (opts.existingKey) {
43
+ keypair = opts.existingKey.startsWith("suiprivkey")
44
+ ? Ed25519Keypair.fromSecretKey(decodeSuiPrivateKey(opts.existingKey).secretKey)
45
+ : Ed25519Keypair.fromSecretKey(Uint8Array.from(Buffer.from(opts.existingKey, "hex")));
46
+ skip("reusing existing key");
47
+ }
48
+ else {
49
+ keypair = new Ed25519Keypair();
50
+ done();
51
+ }
52
+ const address = keypair.toSuiAddress();
53
+ const privateKey = keypair.getSecretKey(); // bech32 suiprivkey1…
54
+ console.log(chalk.dim(` address: ${address}`));
55
+ // ── 2. Faucet (testnet only) ─────────────────────────────────────────────────
56
+ if (network === "testnet") {
57
+ step(2, "Requesting SUI from testnet faucet");
58
+ try {
59
+ const res = await fetch("https://faucet.testnet.sui.io/v1/gas", {
60
+ method: "POST",
61
+ headers: { "Content-Type": "application/json" },
62
+ body: JSON.stringify({ FixedAmountRequest: { recipient: address } }),
63
+ signal: AbortSignal.timeout(20_000),
64
+ });
65
+ if (!res.ok)
66
+ throw new Error(`HTTP ${res.status}`);
67
+ done();
68
+ // Wait for faucet tx to land before we submit our transactions.
69
+ await new Promise(r => setTimeout(r, 4_000));
70
+ }
71
+ catch (e) {
72
+ console.log(chalk.yellow("failed"));
73
+ console.log(chalk.yellow(` ⚠ Faucet error: ${String(e)}`));
74
+ console.log(chalk.yellow(` Fund manually → https://faucet.testnet.sui.io`));
75
+ console.log(chalk.yellow(` Address: ${address}`));
76
+ console.log(chalk.yellow(` Then re-run: memfork init --quick`));
77
+ throw new Error("Faucet unavailable — fund the address above and retry.");
78
+ }
79
+ }
80
+ else {
81
+ step(2, "Mainnet — faucet not available");
82
+ skip("fund your wallet before re-running");
83
+ }
84
+ // ── 3. MemWal account ────────────────────────────────────────────────────────
85
+ step(3, "Creating MemWal account on-chain");
86
+ let accountId;
87
+ try {
88
+ const result = await createAccount({
89
+ packageId: consts.packageId,
90
+ registryId: consts.registryId,
91
+ suiPrivateKey: privateKey,
92
+ suiClient,
93
+ });
94
+ accountId = result.accountId;
95
+ done();
96
+ console.log(chalk.dim(` accountId: ${accountId}`));
97
+ }
98
+ catch (e) {
99
+ const msg = String(e);
100
+ if (msg.includes("EAccountAlreadyExists") || msg.includes("MoveAbort") && msg.includes(", 3)")) {
101
+ // Error code 3 = EAccountAlreadyExists — fine, just fetch the existing one.
102
+ console.log(chalk.dim("already exists"));
103
+ accountId = await resolveExistingMemwalAccount(suiClient, consts.packageId, address);
104
+ console.log(chalk.dim(` accountId: ${accountId}`));
105
+ }
106
+ else {
107
+ console.log(chalk.red("failed"));
108
+ throw new Error(`MemWal account creation failed: ${msg}`);
109
+ }
110
+ }
111
+ // ── 4 + 5. Delegate key ───────────────────────────────────────────────────────
112
+ step(4, "Generating MemWal delegate key");
113
+ const delegate = await generateDelegateKey();
114
+ done();
115
+ step(5, "Registering delegate key with MemWal");
116
+ try {
117
+ await addDelegateKey({
118
+ packageId: consts.packageId,
119
+ accountId,
120
+ publicKey: delegate.publicKey,
121
+ label: `memfork-cli-${new Date().toISOString().slice(0, 10)}`,
122
+ suiPrivateKey: privateKey,
123
+ suiClient,
124
+ });
125
+ done();
126
+ }
127
+ catch (e) {
128
+ const msg = String(e);
129
+ if (msg.includes("EDelegateKeyAlreadyExists") || msg.includes("MoveAbort") && msg.includes(", 0)")) {
130
+ skip("key already registered");
131
+ }
132
+ else {
133
+ console.log(chalk.red("failed"));
134
+ throw new Error(`Failed to register delegate key: ${msg}`);
135
+ }
136
+ }
137
+ // ── 6. MemoryTree ────────────────────────────────────────────────────────────
138
+ step(6, "Creating MemoryTree on Sui");
139
+ const memClient = await MemForksClient.connect({
140
+ treeId: "0x" + "0".repeat(64), // placeholder — initTree creates the object
141
+ signer: privateKey,
142
+ network,
143
+ memwal: {
144
+ accountId,
145
+ delegateKey: delegate.privateKey,
146
+ serverUrl: consts.relayer,
147
+ },
148
+ });
149
+ const { treeId, digest } = await memClient.initTree(accountId, opts.defaultBranch ?? "main");
150
+ done();
151
+ console.log(chalk.dim(` treeId: ${treeId}`));
152
+ console.log(chalk.dim(` tx: ${digest}`));
153
+ return {
154
+ treeId,
155
+ privateKey,
156
+ memwalAccountId: accountId,
157
+ memwalKey: delegate.privateKey,
158
+ network,
159
+ };
160
+ }
161
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
162
+ async function resolveExistingMemwalAccount(suiClient, packageId, owner) {
163
+ const objs = await suiClient.getOwnedObjects({
164
+ owner,
165
+ filter: { StructType: `${packageId}::account::MemWalAccount` },
166
+ options: { showContent: false },
167
+ });
168
+ const first = objs.data?.[0];
169
+ if (!first?.data?.objectId) {
170
+ throw new Error("Could not find existing MemWal account for this address.");
171
+ }
172
+ return first.data.objectId;
173
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `memfork ui` — local HTTP server.
3
+ *
4
+ * Serves the pre-built React app from app/dist/ as static files and
5
+ * exposes two API routes so the React app can discover the current tree
6
+ * config and recall MemWal facts without exposing credentials in the
7
+ * browser bundle.
8
+ *
9
+ * GET /api/config → { treeId, packageId, network, rpcUrl, hasMemwal }
10
+ * GET /api/facts → { facts: MemWal results[] } (proxied server-side)
11
+ * GET /* → index.html (SPA fallback)
12
+ * GET /assets/* → static file
13
+ */
14
+ import http from "node:http";
15
+ export declare function startUiServer(distDir: string, port?: number): http.Server;