@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.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @memfork/cli — entry point
4
+ *
5
+ * Usage:
6
+ * memfork init — first-run interactive setup
7
+ * memfork doctor — verify the full setup
8
+ * memfork install cursor|codex — install IDE plugin
9
+ *
10
+ * memfork status — tree status
11
+ * memfork log [--branch <b>] [-n <n>] — commit log
12
+ * memfork recall <query> [--branch <b>] — semantic recall
13
+ * memfork commit -m <msg> --facts <...> — write facts on-chain
14
+ * memfork merge <from> <into> --resolver <id> — propose merge
15
+ * memfork proposals — list open proposals
16
+ * memfork ui — open DAG visualizer
17
+ *
18
+ * Config API re-exported for use by other packages:
19
+ * import { resolveConfig, toClientConfig } from "@memfork/cli"
20
+ */
21
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @memfork/cli — entry point
4
+ *
5
+ * Usage:
6
+ * memfork init — first-run interactive setup
7
+ * memfork doctor — verify the full setup
8
+ * memfork install cursor|codex — install IDE plugin
9
+ *
10
+ * memfork status — tree status
11
+ * memfork log [--branch <b>] [-n <n>] — commit log
12
+ * memfork recall <query> [--branch <b>] — semantic recall
13
+ * memfork commit -m <msg> --facts <...> — write facts on-chain
14
+ * memfork merge <from> <into> --resolver <id> — propose merge
15
+ * memfork proposals — list open proposals
16
+ * memfork ui — open DAG visualizer
17
+ *
18
+ * Config API re-exported for use by other packages:
19
+ * import { resolveConfig, toClientConfig } from "@memfork/cli"
20
+ */
21
+ import { Command } from "commander";
22
+ import chalk from "chalk";
23
+ import { cmdInit } from "./commands/init.js";
24
+ import { cmdDoctor } from "./commands/doctor.js";
25
+ import { cmdInstall } from "./commands/install.js";
26
+ import { cmdStatus, cmdLog, cmdRecall, cmdCommit, cmdMerge, cmdProposals, cmdUi, cmdShow, cmdDiff, cmdDelegates, cmdGrant, cmdGrantMemwal, cmdRevoke, cmdBranch, cmdCheckout, } from "./commands/ops.js";
27
+ import { cmdJoin } from "./commands/join.js";
28
+ const program = new Command();
29
+ program
30
+ .name("memfork")
31
+ .description("MemForks CLI — on-chain, branch-aware agent memory")
32
+ .version("0.1.0");
33
+ // ─── Setup ────────────────────────────────────────────────────────────────────
34
+ program
35
+ .command("init")
36
+ .description("interactive first-run setup — create or link a memory tree")
37
+ .option("-q, --quick", "auto-provision: keygen → faucet → MemWal account → tree (no copy-paste needed)")
38
+ .action(wrap((opts) => cmdInit({ quick: opts.quick })));
39
+ program
40
+ .command("doctor")
41
+ .description("verify config, credentials, Sui connection, and MemWal")
42
+ .action(wrap(cmdDoctor));
43
+ program
44
+ .command("join")
45
+ .description("onboard to an existing tree (team member setup)")
46
+ .action(wrap(cmdJoin));
47
+ program
48
+ .command("install <target>")
49
+ .description("install an IDE plugin: cursor | codex")
50
+ .action((target) => cmdInstall(target));
51
+ // ─── Operations ───────────────────────────────────────────────────────────────
52
+ program
53
+ .command("branch <name>")
54
+ .description("create a new branch from the current (or specified) branch")
55
+ .option("-f, --from <branch>", "source branch (default: current branch)")
56
+ .action(wrap((name, opts) => cmdBranch(name, opts)));
57
+ program
58
+ .command("checkout <name>")
59
+ .description("switch the active branch")
60
+ .action(wrap((name) => cmdCheckout(name)));
61
+ program
62
+ .command("status")
63
+ .description("show current tree, network, branch, and signer")
64
+ .action(wrap(cmdStatus));
65
+ program
66
+ .command("log")
67
+ .description("show recent commits on a branch")
68
+ .option("-b, --branch <name>", "branch name (default: current git branch)")
69
+ .option("-n, --limit <n>", "number of commits", parseInt, 20)
70
+ .action(wrap((opts) => cmdLog(opts)));
71
+ program
72
+ .command("recall [query]")
73
+ .description("semantic recall from branch memory")
74
+ .option("-b, --branch <name>", "branch to recall from")
75
+ .option("-n, --limit <n>", "max results", parseInt, 5)
76
+ .option("--json", "output as JSON (used by plugin hooks)")
77
+ .action(wrap((query, opts) => cmdRecall(query ?? "", opts)));
78
+ program
79
+ .command("commit")
80
+ .description("commit facts to the current branch")
81
+ .requiredOption("-m, --message <msg>", "commit message")
82
+ .option("-b, --branch <name>", "branch (default: current git branch)")
83
+ .option("-f, --facts <facts...>", "one or more fact strings")
84
+ .option("--from-response <text>", "extract facts from a full response text")
85
+ .option("--auto-extract", "use LLM to extract durable facts (requires --from-response)")
86
+ .action(wrap((opts) => cmdCommit(opts)));
87
+ program
88
+ .command("merge <from> <into>")
89
+ .description("propose a merge from one branch into another")
90
+ .requiredOption("-r, --resolver <id>", "ResolverRef object ID")
91
+ .option("--ttl <ms>", "TTL in milliseconds", parseInt, 86_400_000)
92
+ .action(wrap((from, into, opts) => cmdMerge(from, into, opts)));
93
+ program
94
+ .command("proposals")
95
+ .description("list open merge proposals")
96
+ .action(wrap(cmdProposals));
97
+ program
98
+ .command("ui")
99
+ .description("open the MemForks DAG visualizer")
100
+ .option("--share", "build and publish to a Walrus Site (shareable URL)")
101
+ .option("-p, --port <n>", "local server port", (v) => parseInt(v, 10), 4242)
102
+ .action(wrap((opts) => cmdUi(opts)));
103
+ program
104
+ .command("show <commitId>")
105
+ .description("show details of a single commit")
106
+ .action(wrap((commitId) => cmdShow(commitId)));
107
+ program
108
+ .command("diff <from> <to>")
109
+ .description("show fact differences between two branches or commits")
110
+ .action(wrap((from, to) => cmdDiff(from, to)));
111
+ // ─── ACL ─────────────────────────────────────────────────────────────────────
112
+ program
113
+ .command("delegates")
114
+ .description("list all delegates for the current tree")
115
+ .action(wrap(cmdDelegates));
116
+ program
117
+ .command("grant <address>")
118
+ .description("grant a delegate key write access to the tree")
119
+ .option("-p, --permissions <hex>", "permission bitmask in hex (default: 0xFF = all)", "0xFF")
120
+ .option("--expiry <ms>", "expiry timestamp in epoch ms (default: never)", parseInt)
121
+ .option("-b, --branches <names...>", "restrict to specific branches")
122
+ .action(wrap((address, opts) => cmdGrant({ address, ...opts })));
123
+ program
124
+ .command("grant-memwal <address>")
125
+ .description("register a team member's MemWal key (run by the owner after `memfork join`)")
126
+ .requiredOption("--pubkey <hex>", "MemWal delegate public key (hex) — printed by `memfork join`")
127
+ .action(wrap((address, opts) => cmdGrantMemwal({ agent: address, pubkey: opts.pubkey })));
128
+ program
129
+ .command("revoke <address>")
130
+ .description("revoke a delegate key")
131
+ .action(wrap((address) => cmdRevoke(address)));
132
+ // ─── Error handling ───────────────────────────────────────────────────────────
133
+ program.configureOutput({
134
+ writeErr: (str) => process.stderr.write(chalk.red(str)),
135
+ });
136
+ program.parseAsync(process.argv).catch((e) => {
137
+ console.error(chalk.red("Error: " + String(e)));
138
+ process.exit(1);
139
+ });
140
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
141
+ function wrap(fn) {
142
+ return (...args) => {
143
+ fn(...args).catch((e) => {
144
+ if (e.name === "ConfigError") {
145
+ console.error(chalk.red("\n " + String(e.message)));
146
+ console.error(chalk.cyan(" → Run `memfork init` to configure.\n"));
147
+ }
148
+ else {
149
+ console.error(chalk.red("\nError: " + String(e)));
150
+ }
151
+ process.exit(1);
152
+ });
153
+ };
154
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `memfork doctor`
3
+ *
4
+ * Verifies the full setup end-to-end:
5
+ * ✓ / ✗ .memfork/config.json exists
6
+ * ✓ / ✗ credentials file exists and is chmod 600
7
+ * ✓ / ✗ Sui RPC reachable
8
+ * ✓ / ✗ MemoryTree object found on-chain
9
+ * ✓ / ✗ Signer address matches tree owner (or has delegate)
10
+ * ✓ / ✗ MemWal account reachable
11
+ */
12
+ export declare function cmdDoctor(): Promise<void>;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * `memfork doctor`
3
+ *
4
+ * Verifies the full setup end-to-end:
5
+ * ✓ / ✗ .memfork/config.json exists
6
+ * ✓ / ✗ credentials file exists and is chmod 600
7
+ * ✓ / ✗ Sui RPC reachable
8
+ * ✓ / ✗ MemoryTree object found on-chain
9
+ * ✓ / ✗ Signer address matches tree owner (or has delegate)
10
+ * ✓ / ✗ MemWal account reachable
11
+ */
12
+ import chalk from "chalk";
13
+ import fs from "node:fs";
14
+ import { resolveConfig, readProjectConfig, credentialsPath, } from "../config.js";
15
+ import { MemForksClient } from "@memfork/core";
16
+ function icon(s) {
17
+ return { ok: chalk.green("✓"), fail: chalk.red("✗"), warn: chalk.yellow("⚠"), skip: chalk.dim("·") }[s];
18
+ }
19
+ function printCheck(c) {
20
+ console.log(` ${icon(c.status)} ${c.label}` + (c.detail ? chalk.dim(" — " + c.detail) : ""));
21
+ if (c.fix && c.status !== "ok" && c.status !== "skip") {
22
+ console.log(` ${chalk.cyan("→")} ${c.fix}`);
23
+ }
24
+ }
25
+ export async function cmdDoctor() {
26
+ console.log("");
27
+ console.log(chalk.bold("memfork doctor"));
28
+ console.log("");
29
+ const checks = [];
30
+ let cfg;
31
+ // ── 1. Project config ──────────────────────────────────────────────────────
32
+ const project = readProjectConfig();
33
+ checks.push({
34
+ label: ".memfork/config.json",
35
+ status: project ? "ok" : "warn",
36
+ detail: project ? `tree: ${project.treeId?.slice(0, 10)}…` : "not found (credentials-only mode)",
37
+ fix: project ? undefined : "Run `memfork init` from the project root to create it",
38
+ });
39
+ // ── 2. Credentials file ─────────────────────────────────────────────────────
40
+ const credsPath = credentialsPath();
41
+ const credsExists = fs.existsSync(credsPath);
42
+ let credsPerms = "skip";
43
+ if (credsExists) {
44
+ const mode = fs.statSync(credsPath).mode & 0o777;
45
+ credsPerms = (mode === 0o600) ? "ok" : "warn";
46
+ }
47
+ checks.push({
48
+ label: "~/.memfork/credentials.json",
49
+ status: credsExists ? (credsPerms === "ok" ? "ok" : "warn") : "fail",
50
+ detail: credsExists ? (credsPerms === "ok" ? "chmod 600 ✓" : "permissions too open") : "not found",
51
+ fix: credsExists
52
+ ? "Run: chmod 600 ~/.memfork/credentials.json"
53
+ : "Run `memfork init` to create it",
54
+ });
55
+ // ── 3. Config resolution ────────────────────────────────────────────────────
56
+ try {
57
+ cfg = resolveConfig();
58
+ checks.push({
59
+ label: "Config resolution",
60
+ status: "ok",
61
+ detail: `tree ${cfg.treeId.slice(0, 10)}… / ${cfg.network}`,
62
+ });
63
+ }
64
+ catch (e) {
65
+ checks.push({
66
+ label: "Config resolution",
67
+ status: "fail",
68
+ detail: e.message,
69
+ fix: "Run `memfork init`",
70
+ });
71
+ checks.forEach(printCheck);
72
+ console.log("");
73
+ console.log(chalk.red(" Setup incomplete. Run `memfork init` to fix."));
74
+ console.log("");
75
+ process.exit(1);
76
+ }
77
+ // ── 4. Sui RPC reachable ────────────────────────────────────────────────────
78
+ let client;
79
+ try {
80
+ client = await MemForksClient.connect({
81
+ treeId: cfg.treeId,
82
+ signer: cfg.privateKey,
83
+ network: cfg.network,
84
+ rpcUrl: cfg.rpcUrl,
85
+ packageId: cfg.packageId,
86
+ });
87
+ // Quick liveness ping — getChainIdentifier is cheap.
88
+ await client.suiClient.getChainIdentifier();
89
+ checks.push({ label: "Sui RPC", status: "ok", detail: cfg.rpcUrl ?? `${cfg.network} default` });
90
+ }
91
+ catch (e) {
92
+ checks.push({
93
+ label: "Sui RPC",
94
+ status: "fail",
95
+ detail: String(e),
96
+ fix: "Check your network connection or set a custom MEMFORK_RPC_URL",
97
+ });
98
+ checks.forEach(printCheck);
99
+ console.log("");
100
+ process.exit(1);
101
+ }
102
+ // ── 5. MemoryTree on-chain ──────────────────────────────────────────────────
103
+ try {
104
+ const tree = await client.getTree();
105
+ checks.push({
106
+ label: "MemoryTree on-chain",
107
+ status: "ok",
108
+ detail: `default branch: ${tree.default_branch}`,
109
+ });
110
+ }
111
+ catch (e) {
112
+ checks.push({
113
+ label: "MemoryTree on-chain",
114
+ status: "fail",
115
+ detail: `object not found: ${cfg.treeId.slice(0, 10)}…`,
116
+ fix: "Check the treeId in .memfork/config.json or run `memfork init`",
117
+ });
118
+ }
119
+ // ── 6. Signer balance (warn if low) ─────────────────────────────────────────
120
+ try {
121
+ const addr = client.keypair.toSuiAddress();
122
+ const balance = await client.suiClient.getBalance({ owner: addr });
123
+ const sui = Number(balance.totalBalance) / 1e9;
124
+ const low = sui < 0.1;
125
+ checks.push({
126
+ label: "Signer balance",
127
+ status: low ? "warn" : "ok",
128
+ detail: `${sui.toFixed(4)} SUI (${addr.slice(0, 10)}…)`,
129
+ fix: low ? (cfg.network === "mainnet" ? "Gas is sponsored — no SUI needed. If you need a balance, send SUI to the address above." : "Fund via faucet: sui client faucet or https://faucet.testnet.sui.io") : undefined,
130
+ });
131
+ }
132
+ catch {
133
+ checks.push({ label: "Signer balance", status: "skip", detail: "could not fetch" });
134
+ }
135
+ // ── 7. MemWal reachable ──────────────────────────────────────────────────────
136
+ try {
137
+ const resp = await fetch(cfg.memwalRelayer + "/health", { signal: AbortSignal.timeout(5000) });
138
+ checks.push({
139
+ label: "MemWal relayer",
140
+ status: resp.ok ? "ok" : "warn",
141
+ detail: resp.ok ? cfg.memwalRelayer : `HTTP ${resp.status}`,
142
+ fix: resp.ok ? undefined : "Check MEMFORK_MEMWAL_RELAYER or try again",
143
+ });
144
+ }
145
+ catch {
146
+ checks.push({
147
+ label: "MemWal relayer",
148
+ status: "warn",
149
+ detail: "could not reach " + cfg.memwalRelayer,
150
+ fix: "Check your network. MemWal read/write will be unavailable.",
151
+ });
152
+ }
153
+ // ── Print all checks ─────────────────────────────────────────────────────────
154
+ console.log("");
155
+ checks.forEach(printCheck);
156
+ console.log("");
157
+ const failed = checks.filter((c) => c.status === "fail").length;
158
+ const warned = checks.filter((c) => c.status === "warn").length;
159
+ if (failed > 0) {
160
+ console.log(chalk.red(` ${failed} check(s) failed. Run \`memfork init\` to fix.`));
161
+ process.exit(1);
162
+ }
163
+ else if (warned > 0) {
164
+ console.log(chalk.yellow(` ${warned} warning(s). Setup is functional but review the items above.`));
165
+ }
166
+ else {
167
+ console.log(chalk.green(" Everything looks good."));
168
+ }
169
+ console.log("");
170
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `memfork init`
3
+ *
4
+ * Interactive first-run setup. Writes:
5
+ * .memfork/config.json — committable project config (treeId, network, branch)
6
+ * ~/.memfork/credentials.json — secrets, chmod 600 (privateKey, memwalKey, etc.)
7
+ *
8
+ * Idempotent: re-running updates values without destroying existing ones.
9
+ */
10
+ export declare function cmdInit(opts?: {
11
+ quick?: boolean;
12
+ }): Promise<void>;
@@ -0,0 +1,286 @@
1
+ /**
2
+ * `memfork init`
3
+ *
4
+ * Interactive first-run setup. Writes:
5
+ * .memfork/config.json — committable project config (treeId, network, branch)
6
+ * ~/.memfork/credentials.json — secrets, chmod 600 (privateKey, memwalKey, etc.)
7
+ *
8
+ * Idempotent: re-running updates values without destroying existing ones.
9
+ */
10
+ import { input, select, password, confirm } from "@inquirer/prompts";
11
+ import chalk from "chalk";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { readProjectConfig, readCredentials, writeProjectConfig, upsertCredential, credentialsPath, } from "../config.js";
15
+ import { MemForksClient } from "@memfork/core";
16
+ import { autoProvision } from "./provision.js";
17
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
18
+ function dim(s) { return chalk.dim(s); }
19
+ function ok(s) { return chalk.green("✓") + " " + s; }
20
+ function err(s) { return chalk.red("✗") + " " + chalk.red(s); }
21
+ function tip(s) { return chalk.cyan("→") + " " + s; }
22
+ // ─── Command ──────────────────────────────────────────────────────────────────
23
+ export async function cmdInit(opts = {}) {
24
+ console.log("");
25
+ console.log(chalk.bold("MemForks init") + " " + dim("configure your memory tree"));
26
+ console.log("");
27
+ // ── Mode selection ──────────────────────────────────────────────────────────
28
+ const mode = opts.quick
29
+ ? "quick"
30
+ : await select({
31
+ message: "How do you want to set up?",
32
+ choices: [
33
+ {
34
+ value: "quick",
35
+ name: "Quick setup " + chalk.dim("— auto-provision: keygen → faucet → MemWal → tree (recommended)"),
36
+ },
37
+ {
38
+ value: "manual",
39
+ name: "Manual setup " + chalk.dim("— paste existing keys & IDs"),
40
+ },
41
+ ],
42
+ });
43
+ if (mode === "quick") {
44
+ await cmdInitQuick();
45
+ }
46
+ else {
47
+ await cmdInitManual();
48
+ }
49
+ }
50
+ // ─── Quick path ───────────────────────────────────────────────────────────────
51
+ async function cmdInitQuick() {
52
+ console.log("");
53
+ console.log(dim(" We'll generate a fresh keypair, fund it from the faucet,"));
54
+ console.log(dim(" create your MemWal account and memory tree automatically."));
55
+ console.log(dim(" Nothing to copy-paste."));
56
+ console.log("");
57
+ const network = await select({
58
+ message: "Sui network",
59
+ choices: [
60
+ { value: "mainnet", name: "mainnet (recommended — gas sponsored by MemForks)" },
61
+ { value: "testnet", name: "testnet (free gas via faucet)" },
62
+ ],
63
+ });
64
+ const existingKey = readProjectConfig()
65
+ ? (await confirm({
66
+ message: "Reuse your existing Sui key instead of generating a new one?",
67
+ default: false,
68
+ }))
69
+ ? await password({
70
+ message: "Sui private key (suiprivkey1… or 64-char hex)",
71
+ mask: "*",
72
+ })
73
+ : undefined
74
+ : undefined;
75
+ console.log("");
76
+ try {
77
+ const result = await autoProvision({
78
+ network,
79
+ existingKey: existingKey || undefined,
80
+ });
81
+ // Persist
82
+ writeProjectConfig({
83
+ treeId: result.treeId,
84
+ network: result.network,
85
+ defaultBranch: "main",
86
+ });
87
+ upsertCredential(result.treeId, {
88
+ privateKey: result.privateKey,
89
+ memwalAccountId: result.memwalAccountId,
90
+ memwalKey: result.memwalKey,
91
+ });
92
+ ensureGitignore();
93
+ console.log("");
94
+ console.log(chalk.green.bold(" Setup complete!"));
95
+ console.log("");
96
+ console.log(ok(`Tree ID: ${chalk.bold(result.treeId)}`));
97
+ console.log(ok(`Address: ${chalk.dim("(saved to credentials)")}`));
98
+ console.log(ok(`Project cfg: ${chalk.bold(".memfork/config.json")} ${dim("(safe to commit)")}`));
99
+ console.log(ok(`Credentials: ${chalk.bold(credentialsPath())} ${dim("(chmod 600, gitignored)")}`));
100
+ printNextSteps();
101
+ }
102
+ catch (e) {
103
+ console.log("");
104
+ console.log(err(String(e)));
105
+ process.exit(1);
106
+ }
107
+ }
108
+ // ─── Manual path ──────────────────────────────────────────────────────────────
109
+ async function cmdInitManual() {
110
+ const existing = readProjectConfig();
111
+ const creds = readCredentials();
112
+ // ── Step 1: network ──────────────────────────────────────────────────────────
113
+ const network = await select({
114
+ message: "Sui network",
115
+ default: existing?.network ?? "mainnet",
116
+ choices: [
117
+ { value: "mainnet", name: "mainnet (recommended — gas sponsored by MemForks)" },
118
+ { value: "testnet", name: "testnet (free gas via faucet)" },
119
+ { value: "devnet", name: "devnet" },
120
+ { value: "localnet", name: "localnet" },
121
+ ],
122
+ });
123
+ // ── Step 2: tree ID ──────────────────────────────────────────────────────────
124
+ const treeMode = await select({
125
+ message: "MemoryTree",
126
+ choices: [
127
+ { value: "existing", name: "Use an existing tree (paste the object ID)" },
128
+ { value: "new", name: "Create a new tree now" },
129
+ ],
130
+ });
131
+ let treeId = "";
132
+ if (treeMode === "existing") {
133
+ treeId = (await input({
134
+ message: "Tree object ID (0x…)",
135
+ default: existing?.treeId,
136
+ validate: (v) => /^0x[0-9a-fA-F]{64}$/.test(v.trim()) ? true : "Must be a 64-char hex address starting with 0x",
137
+ })).trim();
138
+ }
139
+ // ── Step 3: Sui private key ───────────────────────────────────────────────────
140
+ const storedKey = treeMode === "existing" ? creds.trees[treeId]?.privateKey : undefined;
141
+ const privateKeyInput = await password({
142
+ message: storedKey
143
+ ? "Sui private key (suiprivkey1… or hex — enter to keep existing)"
144
+ : "Sui private key (suiprivkey1… bech32 or 64-char hex)",
145
+ mask: "*",
146
+ validate: (v) => {
147
+ if (storedKey && v === "")
148
+ return true;
149
+ if (v.startsWith("suiprivkey"))
150
+ return true;
151
+ if (/^[0-9a-fA-F]{64}$/.test(v))
152
+ return true;
153
+ return "Expected suiprivkey1… (Sui CLI format) or 64-char hex";
154
+ },
155
+ });
156
+ const resolvedKey = privateKeyInput === "" && storedKey ? storedKey : privateKeyInput;
157
+ // ── Step 4: create tree if needed ─────────────────────────────────────────────
158
+ if (treeMode === "new") {
159
+ console.log("");
160
+ const defaultBranch = await input({ message: "Default branch name", default: "main" });
161
+ const memwalAccId = (await input({
162
+ message: "MemWal account ID (0x…)",
163
+ validate: (v) => /^0x[0-9a-fA-F]{64}$/.test(v.trim()) ? true : "Must be 0x… address",
164
+ })).trim();
165
+ process.stdout.write(chalk.dim(" Creating MemoryTree on " + network + "… "));
166
+ try {
167
+ const tempClient = await MemForksClient.connect({
168
+ treeId: "0x" + "0".repeat(64),
169
+ signer: resolvedKey,
170
+ network,
171
+ });
172
+ const { treeId: newId, digest } = await tempClient.initTree(memwalAccId, defaultBranch);
173
+ treeId = newId;
174
+ console.log(chalk.green("done"));
175
+ console.log(ok(`Tree created: ${chalk.bold(treeId)}`));
176
+ console.log(dim(` tx: ${digest}`));
177
+ }
178
+ catch (e) {
179
+ console.log(chalk.red("failed"));
180
+ console.error(err(String(e)));
181
+ process.exit(1);
182
+ }
183
+ }
184
+ // ── Step 5: MemWal credentials ────────────────────────────────────────────────
185
+ console.log("");
186
+ console.log(dim(" MemWal — decentralised blob storage for memory contents."));
187
+ console.log("");
188
+ const storedCred = creds.trees[treeId];
189
+ const memwalAccountId = (await input({
190
+ message: "MemWal account ID (0x…)",
191
+ default: storedCred?.memwalAccountId,
192
+ validate: (v) => /^0x[0-9a-fA-F]{64}$/.test(v.trim()) ? true : "Must be 0x… address",
193
+ })).trim();
194
+ const memwalKeyInput = await password({
195
+ message: storedCred?.memwalKey
196
+ ? "MemWal delegate key (enter to keep existing)"
197
+ : "MemWal delegate key (64-char hex)",
198
+ mask: "*",
199
+ validate: (v) => {
200
+ if (storedCred?.memwalKey && v === "")
201
+ return true;
202
+ if (/^[0-9a-fA-F]{64}$/.test(v))
203
+ return true;
204
+ return "Expected 64-char hex";
205
+ },
206
+ });
207
+ const resolvedMemwalKey = memwalKeyInput === "" && storedCred?.memwalKey
208
+ ? storedCred.memwalKey
209
+ : memwalKeyInput;
210
+ // ── Step 6: optional overrides ────────────────────────────────────────────────
211
+ const advanced = await confirm({
212
+ message: "Configure advanced options (RPC URL, package ID override)?",
213
+ default: false,
214
+ });
215
+ let rpcUrl;
216
+ let packageId;
217
+ let defaultBranch = existing?.defaultBranch ?? "main";
218
+ if (advanced) {
219
+ rpcUrl = (await input({ message: "Custom RPC URL (leave blank for default)" })).trim() || undefined;
220
+ packageId = (await input({ message: "Package ID override (leave blank for default)" })).trim() || undefined;
221
+ defaultBranch = await input({ message: "Default branch", default: defaultBranch });
222
+ }
223
+ // ── Write ──────────────────────────────────────────────────────────────────────
224
+ writeProjectConfig({
225
+ treeId,
226
+ network: network ?? "testnet",
227
+ defaultBranch,
228
+ ...(rpcUrl ? { rpcUrl } : {}),
229
+ ...(packageId ? { packageId } : {}),
230
+ });
231
+ upsertCredential(treeId, {
232
+ privateKey: resolvedKey,
233
+ memwalAccountId,
234
+ memwalKey: resolvedMemwalKey,
235
+ });
236
+ ensureGitignore();
237
+ console.log("");
238
+ console.log(ok(`Project config written to ${chalk.bold(".memfork/config.json")} ${dim("(safe to commit)")}`));
239
+ console.log(ok(`Credentials stored in ${chalk.bold(credentialsPath())} ${dim("(chmod 600, gitignored)")}`));
240
+ console.log("");
241
+ // ── Verify ────────────────────────────────────────────────────────────────────
242
+ process.stdout.write(chalk.dim(" Verifying connection to Sui… "));
243
+ try {
244
+ const client = await MemForksClient.connect({
245
+ treeId,
246
+ signer: resolvedKey,
247
+ network: network ?? "testnet",
248
+ ...(rpcUrl ? { rpcUrl } : {}),
249
+ ...(packageId ? { packageId } : {}),
250
+ });
251
+ await client.getTree();
252
+ console.log(chalk.green("ok"));
253
+ }
254
+ catch {
255
+ console.log(chalk.yellow("failed"));
256
+ console.log(chalk.yellow(" ⚠ Could not reach tree — run `memfork doctor` to diagnose."));
257
+ }
258
+ printNextSteps();
259
+ }
260
+ // ─── Shared footer ─────────────────────────────────────────────────────────────
261
+ function printNextSteps() {
262
+ console.log("");
263
+ console.log(chalk.bold("Next steps:"));
264
+ console.log(tip("memfork doctor — verify the full setup"));
265
+ console.log(tip("memfork install cursor — install the Cursor plugin"));
266
+ console.log(tip("memfork install codex — install the Codex plugin"));
267
+ console.log(tip("memfork status — show tree status"));
268
+ console.log("");
269
+ }
270
+ // ─── .gitignore helper ────────────────────────────────────────────────────────
271
+ function ensureGitignore(cwd = process.cwd()) {
272
+ const lines = [
273
+ ".memfork/credentials.json",
274
+ ".memfork/*.local.json",
275
+ ];
276
+ const gitignorePath = path.join(cwd, ".gitignore");
277
+ let existing = "";
278
+ if (fs.existsSync(gitignorePath)) {
279
+ existing = fs.readFileSync(gitignorePath, "utf8");
280
+ }
281
+ const toAdd = lines.filter((l) => !existing.includes(l));
282
+ if (toAdd.length === 0)
283
+ return;
284
+ const block = "\n# MemForks — never commit private keys\n" + toAdd.join("\n") + "\n";
285
+ fs.appendFileSync(gitignorePath, block, "utf8");
286
+ }