@memfork/cli 0.1.19 → 0.1.21
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.js +18 -2
- package/dist/commands/install.js +2 -2
- package/dist/commands/ops.d.ts +10 -0
- package/dist/commands/ops.js +196 -17
- package/package.json +6 -4
- package/plugins/codex/.codex-plugin/plugin.json +41 -0
- package/plugins/codex/README.md +81 -0
- package/plugins/codex/skills/memforks-status/SKILL.md +86 -0
- package/plugins/codex/skills/memory-fork/SKILL.md +102 -0
- package/plugins/codex/skills/memory-recall/SKILL.md +40 -0
- package/plugins/cursor/README.md +86 -0
- package/plugins/cursor/rules/memforks.mdc +137 -0
- package/ui/assets/index-kBDA9A8R.js +67 -0
- package/ui/assets/index-kBDA9A8R.js.map +1 -0
- package/ui/assets/index-paYDwRGH.css +1 -0
- package/ui/index.html +14 -0
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ const { version } = createRequire(import.meta.url)("../package.json");
|
|
|
25
25
|
import { cmdInit } from "./commands/init.js";
|
|
26
26
|
import { cmdDoctor, cmdDoctorEnv } from "./commands/doctor.js";
|
|
27
27
|
import { cmdInstall } from "./commands/install.js";
|
|
28
|
-
import { cmdStatus, cmdLog, cmdRecall, cmdCommit, cmdMerge, cmdProposals, cmdUi, cmdShow, cmdDiff, cmdDelegates, cmdGrant, cmdGrantMemwal, cmdRevoke, cmdBranch, cmdCheckout, } from "./commands/ops.js";
|
|
28
|
+
import { cmdStatus, cmdLog, cmdRecall, cmdCommit, cmdMerge, cmdProposals, cmdResolverCreate, cmdPrComment, cmdUi, cmdShow, cmdDiff, cmdDelegates, cmdGrant, cmdGrantMemwal, cmdRevoke, cmdBranch, cmdCheckout, } from "./commands/ops.js";
|
|
29
29
|
import { cmdJoin } from "./commands/join.js";
|
|
30
30
|
const program = new Command();
|
|
31
31
|
program
|
|
@@ -90,13 +90,29 @@ program
|
|
|
90
90
|
program
|
|
91
91
|
.command("merge <from> <into>")
|
|
92
92
|
.description("merge memory from one branch into another")
|
|
93
|
-
.option("-r, --resolver <id>", "ResolverRef object ID
|
|
93
|
+
.option("-r, --resolver <id>", "ResolverRef object ID — enables governed jury merge (or set MEMFORK_RESOLVER_ID)")
|
|
94
|
+
.option("--lww", "force LastWriteWins even when MEMFORK_RESOLVER_ID is set")
|
|
94
95
|
.option("--ttl <ms>", "proposal TTL in milliseconds", parseInt, 86_400_000)
|
|
95
96
|
.action(wrap((from, into, opts) => cmdMerge(from, into, opts)));
|
|
96
97
|
program
|
|
97
98
|
.command("proposals")
|
|
98
99
|
.description("list open merge proposals")
|
|
99
100
|
.action(wrap(cmdProposals));
|
|
101
|
+
const resolverCmd = new Command("resolver").description("manage resolver objects");
|
|
102
|
+
resolverCmd
|
|
103
|
+
.command("create")
|
|
104
|
+
.description("create a jury resolver (k-of-n)")
|
|
105
|
+
.requiredOption("--jury <addresses>", "comma-separated judge Sui addresses")
|
|
106
|
+
.option("-k, --k <n>", "approval threshold (default: majority)", parseInt)
|
|
107
|
+
.action(wrap((opts) => cmdResolverCreate({ jury: opts.jury, k: opts.k ?? 2 })));
|
|
108
|
+
program.addCommand(resolverCmd);
|
|
109
|
+
program
|
|
110
|
+
.command("pr-comment")
|
|
111
|
+
.description("post a MemForks decision summary to a GitHub PR")
|
|
112
|
+
.requiredOption("--pr <number>", "PR number", parseInt)
|
|
113
|
+
.option("--repo <owner/repo>", "GitHub repo (default: inferred from git remote)")
|
|
114
|
+
.option("--branch <name>", "branch to recall decided fact from (default: into_branch of last merge)")
|
|
115
|
+
.action(wrap((opts) => cmdPrComment(opts)));
|
|
100
116
|
program
|
|
101
117
|
.command("ui")
|
|
102
118
|
.description("open the MemForks DAG visualizer")
|
package/dist/commands/install.js
CHANGED
|
@@ -21,8 +21,8 @@ import path from "node:path";
|
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import { readCredentials, readProjectConfig, MEMWAL_CONSTANTS } from "../config.js";
|
|
23
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
// dist/commands/install.js →
|
|
25
|
-
const PLUGIN_ROOT = path.resolve(__dirname, "..", "..", "
|
|
24
|
+
// dist/commands/install.js → dist/ → package root → plugins/
|
|
25
|
+
const PLUGIN_ROOT = path.resolve(__dirname, "..", "..", "plugins");
|
|
26
26
|
function ok(s) { return chalk.green("✓") + " " + s; }
|
|
27
27
|
function warn(s) { return chalk.yellow("⚠") + " " + s; }
|
|
28
28
|
function tip(s) { return chalk.cyan("→") + " " + s; }
|
package/dist/commands/ops.d.ts
CHANGED
|
@@ -22,8 +22,18 @@ export declare function cmdCommit(opts: {
|
|
|
22
22
|
export declare function cmdMerge(from: string, into: string, opts: {
|
|
23
23
|
resolver?: string;
|
|
24
24
|
ttl?: number;
|
|
25
|
+
lww?: boolean;
|
|
25
26
|
}): Promise<void>;
|
|
26
27
|
export declare function cmdProposals(): Promise<void>;
|
|
28
|
+
export declare function cmdResolverCreate(opts: {
|
|
29
|
+
jury: string;
|
|
30
|
+
k: number;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
export declare function cmdPrComment(opts: {
|
|
33
|
+
pr: number;
|
|
34
|
+
repo?: string;
|
|
35
|
+
branch?: string;
|
|
36
|
+
}): Promise<void>;
|
|
27
37
|
export declare function cmdUi(opts?: {
|
|
28
38
|
share?: boolean;
|
|
29
39
|
port?: number;
|
package/dist/commands/ops.js
CHANGED
|
@@ -129,11 +129,12 @@ export async function cmdMerge(from, into, opts) {
|
|
|
129
129
|
const cfg = resolveConfig();
|
|
130
130
|
const clientCfg = {
|
|
131
131
|
...toClientConfig(cfg),
|
|
132
|
-
// --resolver flag overrides MEMFORK_RESOLVER_ID env var for this call
|
|
133
|
-
|
|
132
|
+
// --resolver flag overrides MEMFORK_RESOLVER_ID env var for this call.
|
|
133
|
+
// --lww forces the LWW path even when MEMFORK_RESOLVER_ID is set.
|
|
134
|
+
...(opts.lww ? { defaultResolverId: undefined } : opts.resolver ? { defaultResolverId: opts.resolver } : {}),
|
|
134
135
|
};
|
|
135
136
|
const client = await MemForksClient.connect(clientCfg);
|
|
136
|
-
const governed = !!(opts.resolver ?? process.env["MEMFORK_RESOLVER_ID"]);
|
|
137
|
+
const governed = !opts.lww && !!(opts.resolver ?? process.env["MEMFORK_RESOLVER_ID"]);
|
|
137
138
|
process.stdout.write(chalk.dim(`Merging ${chalk.green(from)} → ${chalk.green(into)}`) +
|
|
138
139
|
chalk.dim(governed ? " (governed — awaiting resolver…)" : " (LWW — self-finalizing…)") +
|
|
139
140
|
" ");
|
|
@@ -158,20 +159,192 @@ export async function cmdMerge(from, into, opts) {
|
|
|
158
159
|
}
|
|
159
160
|
// ─── proposals ────────────────────────────────────────────────────────────────
|
|
160
161
|
export async function cmdProposals() {
|
|
161
|
-
const {
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
const { cfg } = await getClient();
|
|
163
|
+
const { SuiJsonRpcClient, JsonRpcHTTPTransport, getJsonRpcFullnodeUrl } = await import("@mysten/sui/jsonRpc");
|
|
164
|
+
const rpcUrl = cfg.rpcUrl ?? getJsonRpcFullnodeUrl(cfg.network ?? "testnet");
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
|
+
const sui = new SuiJsonRpcClient({ transport: new JsonRpcHTTPTransport({ url: rpcUrl }), network: cfg.network ?? "testnet" });
|
|
164
167
|
console.log("");
|
|
165
|
-
console.log(chalk.bold("
|
|
166
|
-
console.log(chalk.dim(" (live status in the visualizer: memfork ui)"));
|
|
168
|
+
console.log(chalk.bold("Merge proposals") + chalk.dim(" tree: " + cfg.treeId.slice(0, 12) + "…"));
|
|
167
169
|
console.log("");
|
|
168
|
-
|
|
170
|
+
const PROPOSAL_STATUS = { PENDING: 0, FINALIZED: 1, ABORTED: 2 };
|
|
171
|
+
let events;
|
|
172
|
+
try {
|
|
173
|
+
const result = await sui.queryEvents({
|
|
174
|
+
query: { MoveEventType: `${cfg.packageId}::resolver::MergeProposed` },
|
|
175
|
+
limit: 20,
|
|
176
|
+
order: "descending",
|
|
177
|
+
});
|
|
178
|
+
events = result.data.filter((e) => e.parsedJson["tree_id"] === cfg.treeId);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
console.log(chalk.dim(" Could not query Sui events."));
|
|
182
|
+
console.log(chalk.cyan(" →") + " Run " + chalk.bold("memfork ui") + " for the live view.");
|
|
183
|
+
console.log("");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (events.length === 0) {
|
|
187
|
+
console.log(chalk.dim(" No proposals found for this tree."));
|
|
188
|
+
console.log("");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
for (const ev of events.slice(0, 10)) {
|
|
192
|
+
const p = ev.parsedJson;
|
|
193
|
+
const id = String(p["proposal_id"] ?? "");
|
|
194
|
+
// Fetch live status from the proposal object.
|
|
195
|
+
let statusLabel = chalk.yellow("pending");
|
|
196
|
+
try {
|
|
197
|
+
const obj = await sui.getObject({ id, options: { showContent: true } });
|
|
198
|
+
if (obj.data?.content && obj.data.content.dataType === "moveObject") {
|
|
199
|
+
const status = Number(obj.data.content.fields["status"]);
|
|
200
|
+
if (status === PROPOSAL_STATUS.FINALIZED)
|
|
201
|
+
statusLabel = chalk.green("finalized");
|
|
202
|
+
else if (status === PROPOSAL_STATUS.ABORTED)
|
|
203
|
+
statusLabel = chalk.red("aborted");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch { /* proposal may be consumed */ }
|
|
207
|
+
console.log(` ${statusLabel} ` +
|
|
208
|
+
chalk.green(String(p["from_branch"]) + " → " + String(p["into_branch"])) + " " +
|
|
209
|
+
chalk.dim(id.slice(0, 12) + "…"));
|
|
210
|
+
}
|
|
169
211
|
console.log("");
|
|
170
|
-
console.log(chalk.dim("
|
|
171
|
-
// TODO: drive through MemForksIndexer once it's wired into the CLI.
|
|
172
|
-
// For the hackathon: redirect to the visualizer.
|
|
212
|
+
console.log(chalk.dim(" Full detail: memfork ui → Merges view"));
|
|
173
213
|
console.log("");
|
|
174
|
-
|
|
214
|
+
}
|
|
215
|
+
// ─── resolver create ──────────────────────────────────────────────────────────
|
|
216
|
+
export async function cmdResolverCreate(opts) {
|
|
217
|
+
const { client } = await getClient();
|
|
218
|
+
const { resolvers } = await import("@memfork/core");
|
|
219
|
+
const juryAddrs = opts.jury.split(",").map((a) => a.trim()).filter(Boolean);
|
|
220
|
+
if (juryAddrs.length === 0) {
|
|
221
|
+
console.error(chalk.red("Pass at least one judge address via --jury <addr1,addr2,...>"));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
const k = opts.k ?? Math.ceil(juryAddrs.length / 2 + 0.5);
|
|
225
|
+
process.stdout.write(chalk.dim(`Creating jury resolver (${k}-of-${juryAddrs.length}) … `));
|
|
226
|
+
const def = resolvers.jury(juryAddrs, k, juryAddrs.length);
|
|
227
|
+
const { digest, resolverId } = await client.createResolver(def);
|
|
228
|
+
console.log(chalk.green("done"));
|
|
229
|
+
console.log("");
|
|
230
|
+
console.log(chalk.dim(" ResolverRef: ") + chalk.cyan(resolverId));
|
|
231
|
+
console.log(chalk.dim(" tx: ") + chalk.dim(digest));
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log(chalk.bold(" Save this to your environment:"));
|
|
234
|
+
console.log(" " + chalk.cyan(`export MEMFORK_RESOLVER_ID=${resolverId}`));
|
|
235
|
+
console.log("");
|
|
236
|
+
console.log(chalk.dim(" Or add resolverId to .memfork/config.json for project-wide use."));
|
|
237
|
+
console.log("");
|
|
238
|
+
}
|
|
239
|
+
// ─── pr-comment ───────────────────────────────────────────────────────────────
|
|
240
|
+
export async function cmdPrComment(opts) {
|
|
241
|
+
const { client, cfg } = await getClient();
|
|
242
|
+
const { SuiJsonRpcClient, JsonRpcHTTPTransport, getJsonRpcFullnodeUrl } = await import("@mysten/sui/jsonRpc");
|
|
243
|
+
const rpcUrl = cfg.rpcUrl ?? getJsonRpcFullnodeUrl(cfg.network ?? "testnet");
|
|
244
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
245
|
+
const sui = new SuiJsonRpcClient({ transport: new JsonRpcHTTPTransport({ url: rpcUrl }), network: cfg.network ?? "testnet" });
|
|
246
|
+
console.log("");
|
|
247
|
+
console.log(chalk.dim("Fetching latest merge anchor…"));
|
|
248
|
+
// Find the most recent MergeFinalized event for this tree.
|
|
249
|
+
let anchorId = "";
|
|
250
|
+
let proposalId = "";
|
|
251
|
+
let suiTx = "";
|
|
252
|
+
let walrusBlob = "";
|
|
253
|
+
let fromBranch = "";
|
|
254
|
+
let intoBranch = "";
|
|
255
|
+
try {
|
|
256
|
+
const result = await sui.queryEvents({
|
|
257
|
+
query: { MoveEventType: `${cfg.packageId}::resolver::MergeFinalized` },
|
|
258
|
+
limit: 10,
|
|
259
|
+
order: "descending",
|
|
260
|
+
});
|
|
261
|
+
const ev = result.data
|
|
262
|
+
.find((e) => e.parsedJson["tree_id"] === cfg.treeId);
|
|
263
|
+
if (!ev) {
|
|
264
|
+
console.error(chalk.red("No finalized merges found for this tree. Run `memfork merge` first."));
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
anchorId = String(ev.parsedJson["merge_commit_id"] ?? "");
|
|
268
|
+
walrusBlob = String(ev.parsedJson["resolved_blob_id"] ?? "");
|
|
269
|
+
suiTx = ev.id.txDigest;
|
|
270
|
+
proposalId = String(ev.parsedJson["proposal_id"] ?? "");
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
console.error(chalk.red("Failed to query Sui: " + String(e)));
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
// Fetch proposal for branch names and attestation count.
|
|
277
|
+
let voteCount = "?";
|
|
278
|
+
let threshold = "?";
|
|
279
|
+
try {
|
|
280
|
+
const obj = await sui.getObject({ id: proposalId, options: { showContent: true } });
|
|
281
|
+
if (obj.data?.content && obj.data.content.dataType === "moveObject") {
|
|
282
|
+
const fields = obj.data.content.fields;
|
|
283
|
+
fromBranch = String(fields["from_branch"] ?? "");
|
|
284
|
+
intoBranch = String(fields["into_branch"] ?? "");
|
|
285
|
+
const attests = fields["attestations"];
|
|
286
|
+
voteCount = String(attests?.length ?? "?");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch { /* non-critical */ }
|
|
290
|
+
// Get the decided fact from the into_branch via recall.
|
|
291
|
+
const targetBranch = opts.branch ?? intoBranch ?? currentGitBranch();
|
|
292
|
+
let decision = `Use ${fromBranch || "winning branch"} approach.`;
|
|
293
|
+
try {
|
|
294
|
+
const results = await client.recall("decided", { branch: targetBranch, limit: 1 });
|
|
295
|
+
if (results.length > 0) {
|
|
296
|
+
decision = results[0].text.replace(/^decided:\s*/i, "").split(".")[0] + ".";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch { /* fallback to default */ }
|
|
300
|
+
// Find rejected paths via recall on the into_branch.
|
|
301
|
+
let rejectedPath = "";
|
|
302
|
+
try {
|
|
303
|
+
const rejected = await client.recall("rejected-path", { branch: targetBranch, limit: 1 });
|
|
304
|
+
if (rejected.length > 0) {
|
|
305
|
+
const match = rejected[0].text.match(/(\S+)\s+was not merged/);
|
|
306
|
+
if (match)
|
|
307
|
+
rejectedPath = match[1];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch { /* non-critical */ }
|
|
311
|
+
const shortAnchor = anchorId.replace(/^0x/, "").slice(0, 7);
|
|
312
|
+
const shortTx = suiTx.replace(/^0x/, "").slice(0, 8);
|
|
313
|
+
const shortBlob = walrusBlob.slice(0, 12);
|
|
314
|
+
const vizUrl = `memforks.dev/${cfg.treeId.replace(/^0x/, "").slice(0, 8)}#${shortAnchor}`;
|
|
315
|
+
const body = [
|
|
316
|
+
`🔗 **MemForks decision attached**`,
|
|
317
|
+
``,
|
|
318
|
+
`**Decision:**`,
|
|
319
|
+
decision,
|
|
320
|
+
``,
|
|
321
|
+
`**How it was decided:**`,
|
|
322
|
+
`Jury vote, ${voteCount} of ${threshold} — enforced on Sui`,
|
|
323
|
+
``,
|
|
324
|
+
`**Merge:** \`${shortAnchor}\``,
|
|
325
|
+
``,
|
|
326
|
+
`**Sui:** \`${shortTx}…\``,
|
|
327
|
+
``,
|
|
328
|
+
`**Walrus:** \`${shortBlob}…\``,
|
|
329
|
+
rejectedPath
|
|
330
|
+
? [``, `**Rejected path:**`, `\`${rejectedPath}@latest\` remains queryable`].join("\n")
|
|
331
|
+
: "",
|
|
332
|
+
``,
|
|
333
|
+
`**Full audit trail:** ${vizUrl}`,
|
|
334
|
+
].filter((l) => l !== undefined).join("\n");
|
|
335
|
+
// Post via gh CLI.
|
|
336
|
+
const repoFlag = opts.repo ? `--repo ${opts.repo}` : "";
|
|
337
|
+
try {
|
|
338
|
+
execSync(`gh pr comment ${opts.pr} ${repoFlag} --body ${JSON.stringify(body)}`, {
|
|
339
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
340
|
+
});
|
|
341
|
+
console.log(chalk.green("✓") + " Comment posted to PR #" + opts.pr);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
console.log(chalk.yellow("gh CLI not available or auth required. Copy this comment:"));
|
|
345
|
+
console.log("");
|
|
346
|
+
console.log(body);
|
|
347
|
+
}
|
|
175
348
|
console.log("");
|
|
176
349
|
}
|
|
177
350
|
// ─── ui ───────────────────────────────────────────────────────────────────────
|
|
@@ -441,15 +614,21 @@ function extractFacts(response) {
|
|
|
441
614
|
}
|
|
442
615
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
443
616
|
function findAppDir() {
|
|
444
|
-
//
|
|
617
|
+
// Resolution order:
|
|
618
|
+
// 1. packages/cli/ui/ — bundled at publish time (npm install path)
|
|
619
|
+
// 2. apps/visualizer/ — monorepo dev path (two depths to handle symlinks)
|
|
445
620
|
const candidates = [
|
|
446
|
-
new URL("
|
|
447
|
-
new URL("
|
|
621
|
+
new URL("../../ui", import.meta.url).pathname, // dist/commands/ → cli root → ui/
|
|
622
|
+
new URL("../../../../apps/visualizer", import.meta.url).pathname, // monorepo: packages/cli
|
|
623
|
+
new URL("../../../../../apps/visualizer", import.meta.url).pathname, // monorepo: alternate depth
|
|
448
624
|
];
|
|
449
625
|
for (const c of candidates) {
|
|
450
626
|
try {
|
|
451
|
-
|
|
627
|
+
// Bundled path: presence of index.html is the signal (no package.json shipped).
|
|
628
|
+
// Monorepo path: package.json marks the source root.
|
|
629
|
+
if (fs.existsSync(path.join(c, "index.html")) || fs.existsSync(path.join(c, "package.json"))) {
|
|
452
630
|
return c;
|
|
631
|
+
}
|
|
453
632
|
}
|
|
454
633
|
catch {
|
|
455
634
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memfork/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "MemForks CLI — init, commit, recall, merge, install plugins",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"./config": "./dist/config.js"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"build": "tsc",
|
|
24
|
+
"build": "node scripts/copy-plugins.mjs && tsc && node scripts/bundle-ui.mjs",
|
|
25
25
|
"dev": "tsc --watch",
|
|
26
26
|
"start": "node dist/index.js"
|
|
27
27
|
},
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
|
-
"dist"
|
|
32
|
+
"dist",
|
|
33
|
+
"plugins",
|
|
34
|
+
"ui"
|
|
33
35
|
],
|
|
34
36
|
"dependencies": {
|
|
35
37
|
"@inquirer/prompts": "^8.5.2",
|
|
36
|
-
"@memfork/core": "^0.1.
|
|
38
|
+
"@memfork/core": "^0.1.9",
|
|
37
39
|
"chalk": "^5.6.2",
|
|
38
40
|
"commander": "^15.0.0"
|
|
39
41
|
},
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memforks",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "On-chain, branch-aware memory DAG for Codex. MemForks anchors decisions to Sui and proposes cross-branch merges — memory storage is handled by the MemWal MCP server.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "MemForks",
|
|
7
|
+
"email": "team@memforks.dev"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/memforks-dev/memforks",
|
|
10
|
+
"repository": "https://github.com/memforks-dev/memforks",
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"memory",
|
|
14
|
+
"sui",
|
|
15
|
+
"blockchain",
|
|
16
|
+
"provenance",
|
|
17
|
+
"branching",
|
|
18
|
+
"codex",
|
|
19
|
+
"agent-memory",
|
|
20
|
+
"memwal"
|
|
21
|
+
],
|
|
22
|
+
"skills": "./skills/",
|
|
23
|
+
"interface": {
|
|
24
|
+
"displayName": "MemForks",
|
|
25
|
+
"shortDescription": "On-chain, branch-aware memory DAG",
|
|
26
|
+
"longDescription": "MemForks gives Codex a tamper-proof, branch-synced memory DAG anchored on Sui. Memory recall and storage are handled natively by the MemWal MCP server. MemForks adds the version-control layer: immutable on-chain commits, Git branch-scoped history, and conflict-free cross-branch merges via an on-chain resolver.",
|
|
27
|
+
"developerName": "MemForks",
|
|
28
|
+
"category": "Productivity",
|
|
29
|
+
"capabilities": ["Read", "Write"],
|
|
30
|
+
"websiteURL": "https://github.com/memforks-dev/memforks",
|
|
31
|
+
"privacyPolicyURL": "https://github.com/memforks-dev/memforks",
|
|
32
|
+
"termsOfServiceURL": "https://github.com/memforks-dev/memforks/blob/main/LICENSE",
|
|
33
|
+
"defaultPrompt": [
|
|
34
|
+
"Recall any relevant context for this branch",
|
|
35
|
+
"Show my MemForks memory status",
|
|
36
|
+
"Commit the decisions we made today"
|
|
37
|
+
],
|
|
38
|
+
"brandColor": "#1f9d72",
|
|
39
|
+
"screenshots": []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# MemForks — Codex Plugin
|
|
2
|
+
|
|
3
|
+
On-chain, branch-aware memory DAG for Codex.
|
|
4
|
+
|
|
5
|
+
**Memory storage** is handled by the MemWal MCP server — the agent calls
|
|
6
|
+
`memwal_recall` and `memwal_remember` natively as tool calls.
|
|
7
|
+
|
|
8
|
+
**On-chain versioning** is handled by the `memfork` CLI — decisions get
|
|
9
|
+
cryptographically anchored to Sui with branch context and a full commit DAG.
|
|
10
|
+
|
|
11
|
+
## Setup (one time, per machine)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @memfork/cli
|
|
15
|
+
|
|
16
|
+
# Recommended — zero copy-paste, ~30 seconds on testnet:
|
|
17
|
+
memfork init --quick
|
|
18
|
+
|
|
19
|
+
# Or manual if you already have a Sui key + MemWal account:
|
|
20
|
+
memfork init
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install the plugin
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
memfork install codex
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This does two things:
|
|
30
|
+
|
|
31
|
+
1. **Writes `~/.codex/config.toml`** — adds a `[mcp_servers.memwal]` entry using
|
|
32
|
+
the delegate key provisioned by `memfork init`. No browser login needed.
|
|
33
|
+
|
|
34
|
+
2. **Copies `.codex-plugin/`** — installs the plugin skills into the current project.
|
|
35
|
+
|
|
36
|
+
Then register with Codex:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
codex plugin add .codex-plugin
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Verify
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
memfork doctor
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## What the agent can do
|
|
49
|
+
|
|
50
|
+
| Tool / Command | What it does |
|
|
51
|
+
|----------------|-------------|
|
|
52
|
+
| `memwal_recall(query, namespace)` | Semantic search over branch memory (MCP tool) |
|
|
53
|
+
| `memwal_remember(text, namespace)` | Save a fact to branch memory (MCP tool) |
|
|
54
|
+
| `memwal_analyze(text)` | Extract and save multiple facts at once (MCP tool) |
|
|
55
|
+
| `memfork commit --facts …` | Anchor a decision on-chain with full provenance |
|
|
56
|
+
| `memfork merge <src> <dst>` | Propose a cross-branch memory merge |
|
|
57
|
+
| `memfork status / log / proposals` | Inspect the on-chain DAG |
|
|
58
|
+
|
|
59
|
+
Memory is namespaced by Git branch — `namespace="branch/<branch-name>"`.
|
|
60
|
+
|
|
61
|
+
## What gets installed
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
~/.codex/config.toml ← MemWal MCP server entry (auto-configured)
|
|
65
|
+
.codex-plugin/
|
|
66
|
+
plugin.json ← plugin metadata
|
|
67
|
+
skills/
|
|
68
|
+
memory-recall/ ← when/how to use memwal_recall
|
|
69
|
+
memforks-status/ ← when/how to use memfork commit/merge/status
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
No shell hooks. The MCP server is the transport.
|
|
73
|
+
|
|
74
|
+
## Override for CI / headless use
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
MEMFORK_TREE_ID=0x…
|
|
78
|
+
MEMFORK_PRIVATE_KEY=suiprivkey1…
|
|
79
|
+
MEMFORK_MEMWAL_ACCOUNT=0x…
|
|
80
|
+
MEMFORK_MEMWAL_KEY=<hex>
|
|
81
|
+
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memforks-status
|
|
3
|
+
description: >-
|
|
4
|
+
Show MemForks on-chain status: branch DAG, open merge proposals, recent commits.
|
|
5
|
+
Use when the user asks about memory status, proposals, or the commit log.
|
|
6
|
+
Also use when committing decisions or proposing a merge.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# MemForks On-Chain Operations
|
|
10
|
+
|
|
11
|
+
MemForks is the version-control layer on top of MemWal. Use the `memfork` CLI for
|
|
12
|
+
DAG operations — not for routine recall/remember (that's the MCP server's job).
|
|
13
|
+
|
|
14
|
+
## Check status
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
memfork status # current tree, branch, signer, head commit
|
|
18
|
+
memfork log --branch <branch> # recent on-chain commits
|
|
19
|
+
memfork proposals # open merge proposals
|
|
20
|
+
memfork ui # open the DAG visualizer
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Commit a decision on-chain
|
|
24
|
+
|
|
25
|
+
Use this after significant architectural decisions — not for routine facts.
|
|
26
|
+
(Routine facts go through `memwal_remember` via MCP.)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
memfork commit \
|
|
30
|
+
--branch $(git rev-parse --abbrev-ref HEAD) \
|
|
31
|
+
--message "decided: <one-line summary>" \
|
|
32
|
+
--facts "<fact 1>" "<fact 2>"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Merge branches
|
|
36
|
+
|
|
37
|
+
When two branches need to reconcile their memory:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Zero-config — LastWriteWins, self-finalizes immediately
|
|
41
|
+
memfork merge <from-branch> <into-branch>
|
|
42
|
+
|
|
43
|
+
# Governed — jury / LLM resolver (requires MEMFORK_RESOLVER_ID env var or --resolver flag)
|
|
44
|
+
memfork merge <from-branch> <into-branch> --resolver <resolver-id>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
With no `--resolver` flag and no `MEMFORK_RESOLVER_ID` set, the merge uses
|
|
48
|
+
LastWriteWins and finalizes immediately — no resolver service required.
|
|
49
|
+
|
|
50
|
+
When a resolver is configured, the command waits for the on-chain resolver service
|
|
51
|
+
to collect attestations and finalize before returning.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Suggesting a merge — proactive but not autonomous
|
|
56
|
+
|
|
57
|
+
You may **suggest** a merge when you notice the current branch has accumulated
|
|
58
|
+
durable facts not yet on `main`. Phrase it as an offer:
|
|
59
|
+
|
|
60
|
+
> "This branch has several facts that aren't on main yet — want me to merge them?
|
|
61
|
+
> I'll run `memfork merge <branch> main`."
|
|
62
|
+
|
|
63
|
+
**Never run `memfork merge` without the user explicitly confirming.** Merging
|
|
64
|
+
changes shared team memory and creates an on-chain anchor — it is a governance
|
|
65
|
+
act, not a routine commit.
|
|
66
|
+
|
|
67
|
+
Suggest a merge when:
|
|
68
|
+
- The user says "we're done with this branch" or "I'm about to open a PR"
|
|
69
|
+
- You've committed 3+ significant facts this session and the user hasn't merged
|
|
70
|
+
- The user asks "what should I do next?" at the end of a long session
|
|
71
|
+
|
|
72
|
+
Once the user confirms, run:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
memfork merge <current-branch> main
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## When to use this skill
|
|
81
|
+
|
|
82
|
+
- User asks "what's the status of my memory?"
|
|
83
|
+
- User asks "are there any pending merges?"
|
|
84
|
+
- User says "commit what we decided today"
|
|
85
|
+
- User confirms they want to merge memory from one branch into another
|
|
86
|
+
- User wants to open the DAG visualizer
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-fork
|
|
3
|
+
description: >-
|
|
4
|
+
Fork agent memory into parallel branches to explore competing hypotheses.
|
|
5
|
+
Use when the user asks to explore multiple approaches, compare options, or
|
|
6
|
+
says phrases like "explore both", "try two paths", "compare A vs B".
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Memory Fork
|
|
10
|
+
|
|
11
|
+
When you detect a multi-hypothesis prompt — the user wants to explore two or
|
|
12
|
+
more competing approaches — fork the MemForks memory tree so each path stays
|
|
13
|
+
isolated. Never collapse competing ideas into a single stream.
|
|
14
|
+
|
|
15
|
+
## When to trigger
|
|
16
|
+
|
|
17
|
+
Trigger this skill when the user prompt contains signals like:
|
|
18
|
+
- "explore both paths" / "try both" / "compare X and Y"
|
|
19
|
+
- "what if we did X instead of Y" (two real alternatives)
|
|
20
|
+
- "should we do A or B?" (genuine decision fork, not a rhetorical question)
|
|
21
|
+
- Any request to investigate multiple competing solutions side-by-side
|
|
22
|
+
|
|
23
|
+
## Procedure
|
|
24
|
+
|
|
25
|
+
### 1. Announce the fork
|
|
26
|
+
|
|
27
|
+
Print exactly:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
[memforks] Multi-hypothesis detected.
|
|
31
|
+
[memforks] Forking agent memory from <current-branch>@HEAD
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then list the branches you will create, one per hypothesis:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
├── dev/<short-hypothesis-a>
|
|
38
|
+
└── dev/<short-hypothesis-b>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use kebab-case branch names derived from the hypothesis (e.g. `dev/redis-first`,
|
|
42
|
+
`dev/bcrypt-cost`, `dev/approach-a`).
|
|
43
|
+
|
|
44
|
+
### 2. Create the branches
|
|
45
|
+
|
|
46
|
+
For each hypothesis, run:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
memfork branch dev/<hypothesis> --from <current-branch>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. Investigate hypothesis A
|
|
53
|
+
|
|
54
|
+
Switch to the first branch and investigate:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
memfork checkout dev/<hypothesis-a>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Work through the hypothesis. As you discover facts, commit them:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
memfork commit \
|
|
64
|
+
--branch dev/<hypothesis-a> \
|
|
65
|
+
--message "<what you found>" \
|
|
66
|
+
--facts "<concrete measurable fact>" "<another fact>"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Commit at each meaningful step — hypothesis statement, baseline measurement,
|
|
70
|
+
result. Three commits is normal; more is fine.
|
|
71
|
+
|
|
72
|
+
### 4. Investigate hypothesis B
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
memfork checkout dev/<hypothesis-b>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Repeat the same commit cadence.
|
|
79
|
+
|
|
80
|
+
### 5. Summarise
|
|
81
|
+
|
|
82
|
+
After both branches have evidence, summarise findings side by side and tell
|
|
83
|
+
the user which branch has stronger evidence. Do NOT merge — merging is a
|
|
84
|
+
human governance act (`memfork merge`).
|
|
85
|
+
|
|
86
|
+
## Output format for each commit
|
|
87
|
+
|
|
88
|
+
Use this fact structure for clarity and later recall:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
hypothesis: <one-sentence statement of what this branch is testing>
|
|
92
|
+
fact: <measured or researched datum — numbers are better than adjectives>
|
|
93
|
+
result: <conclusion or outcome of the investigation>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Rules
|
|
97
|
+
|
|
98
|
+
- Never commit to `main` or the parent branch during a fork investigation.
|
|
99
|
+
- Never type `memfork merge` — that is the operator's call.
|
|
100
|
+
- If the user asks "which won?", answer from memory; do not merge.
|
|
101
|
+
- Keep branch names short and descriptive (`dev/redis-first` not `dev/add-redis-caching-to-auth-flow`).
|
|
102
|
+
- If `memfork branch` fails because the branch already exists, use `memfork checkout` and continue.
|