@memfork/cli 0.1.25 → 0.1.27
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/branch.d.ts +56 -0
- package/dist/branch.js +74 -0
- package/dist/commands/ops.js +23 -38
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
- package/plugins/codex/skills/memforks-status/SKILL.md +2 -1
- package/plugins/cursor/rules/memforks.mdc +11 -6
package/dist/branch.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branch resolution for CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Memory in MemForks is namespaced per branch. Every CLI command needs to
|
|
5
|
+
* answer the same question — "which branch am I operating on?" — and they must
|
|
6
|
+
* all answer it the same way. This module is the single source of that answer.
|
|
7
|
+
*
|
|
8
|
+
* Precedence (highest wins):
|
|
9
|
+
* 1. explicit — the `--branch` / `--from` flag passed on the command line
|
|
10
|
+
* 2. env — MEMFORK_BRANCH (CI / headless override)
|
|
11
|
+
* 3. git — the current git branch (the dynamic default for humans)
|
|
12
|
+
* 4. config — `defaultBranch` from .memfork/config.json (non-git fallback)
|
|
13
|
+
* 5. "main" — last resort
|
|
14
|
+
*
|
|
15
|
+
* IMPORTANT: this lives in the CLI layer ONLY. The core MemForksClient and the
|
|
16
|
+
* framework adapters (@memfork/langgraph, @memfork/vercel-ai) take an explicit
|
|
17
|
+
* `branch` argument and have no git awareness. Keeping git resolution out of
|
|
18
|
+
* the core guarantees those integrations are unaffected by this logic.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Read the current git branch, or undefined when there is no usable answer.
|
|
22
|
+
*
|
|
23
|
+
* Returns undefined when:
|
|
24
|
+
* - not inside a git repo (command fails)
|
|
25
|
+
* - detached HEAD / mid-rebase / mid-bisect (returns the literal "HEAD")
|
|
26
|
+
*
|
|
27
|
+
* In those cases the caller falls through to the next precedence source rather
|
|
28
|
+
* than writing memory into a bogus namespace literally named "HEAD".
|
|
29
|
+
*/
|
|
30
|
+
export declare function gitBranch(cwd?: string): string | undefined;
|
|
31
|
+
export interface BranchSources {
|
|
32
|
+
/** --branch / --from flag (highest priority). */
|
|
33
|
+
explicit?: string;
|
|
34
|
+
/** MEMFORK_BRANCH env var. */
|
|
35
|
+
env?: string;
|
|
36
|
+
/** Current git branch (already guarded against detached HEAD). */
|
|
37
|
+
git?: string;
|
|
38
|
+
/** defaultBranch from project config (non-git fallback). */
|
|
39
|
+
configDefault?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pure precedence resolver — no I/O. Exposed separately so the precedence
|
|
43
|
+
* rules can be unit-tested exhaustively without a git repo or env mutation.
|
|
44
|
+
*
|
|
45
|
+
* A whitespace-only source is treated as absent.
|
|
46
|
+
*/
|
|
47
|
+
export declare function pickBranch(sources: BranchSources): string;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the branch a command should operate on, applying the full
|
|
50
|
+
* precedence chain (reads MEMFORK_BRANCH and the current git branch).
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveBranch(opts?: {
|
|
53
|
+
explicit?: string;
|
|
54
|
+
configDefault?: string;
|
|
55
|
+
cwd?: string;
|
|
56
|
+
}): string;
|
package/dist/branch.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branch resolution for CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Memory in MemForks is namespaced per branch. Every CLI command needs to
|
|
5
|
+
* answer the same question — "which branch am I operating on?" — and they must
|
|
6
|
+
* all answer it the same way. This module is the single source of that answer.
|
|
7
|
+
*
|
|
8
|
+
* Precedence (highest wins):
|
|
9
|
+
* 1. explicit — the `--branch` / `--from` flag passed on the command line
|
|
10
|
+
* 2. env — MEMFORK_BRANCH (CI / headless override)
|
|
11
|
+
* 3. git — the current git branch (the dynamic default for humans)
|
|
12
|
+
* 4. config — `defaultBranch` from .memfork/config.json (non-git fallback)
|
|
13
|
+
* 5. "main" — last resort
|
|
14
|
+
*
|
|
15
|
+
* IMPORTANT: this lives in the CLI layer ONLY. The core MemForksClient and the
|
|
16
|
+
* framework adapters (@memfork/langgraph, @memfork/vercel-ai) take an explicit
|
|
17
|
+
* `branch` argument and have no git awareness. Keeping git resolution out of
|
|
18
|
+
* the core guarantees those integrations are unaffected by this logic.
|
|
19
|
+
*/
|
|
20
|
+
import { execSync } from "node:child_process";
|
|
21
|
+
/**
|
|
22
|
+
* Read the current git branch, or undefined when there is no usable answer.
|
|
23
|
+
*
|
|
24
|
+
* Returns undefined when:
|
|
25
|
+
* - not inside a git repo (command fails)
|
|
26
|
+
* - detached HEAD / mid-rebase / mid-bisect (returns the literal "HEAD")
|
|
27
|
+
*
|
|
28
|
+
* In those cases the caller falls through to the next precedence source rather
|
|
29
|
+
* than writing memory into a bogus namespace literally named "HEAD".
|
|
30
|
+
*/
|
|
31
|
+
export function gitBranch(cwd = process.cwd()) {
|
|
32
|
+
try {
|
|
33
|
+
const out = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
cwd,
|
|
36
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
37
|
+
}).trim();
|
|
38
|
+
if (!out || out === "HEAD")
|
|
39
|
+
return undefined;
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Pure precedence resolver — no I/O. Exposed separately so the precedence
|
|
48
|
+
* rules can be unit-tested exhaustively without a git repo or env mutation.
|
|
49
|
+
*
|
|
50
|
+
* A whitespace-only source is treated as absent.
|
|
51
|
+
*/
|
|
52
|
+
export function pickBranch(sources) {
|
|
53
|
+
const clean = (s) => {
|
|
54
|
+
const t = s?.trim();
|
|
55
|
+
return t ? t : undefined;
|
|
56
|
+
};
|
|
57
|
+
return (clean(sources.explicit) ??
|
|
58
|
+
clean(sources.env) ??
|
|
59
|
+
clean(sources.git) ??
|
|
60
|
+
clean(sources.configDefault) ??
|
|
61
|
+
"main");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the branch a command should operate on, applying the full
|
|
65
|
+
* precedence chain (reads MEMFORK_BRANCH and the current git branch).
|
|
66
|
+
*/
|
|
67
|
+
export function resolveBranch(opts = {}) {
|
|
68
|
+
return pickBranch({
|
|
69
|
+
explicit: opts.explicit,
|
|
70
|
+
env: process.env["MEMFORK_BRANCH"],
|
|
71
|
+
git: gitBranch(opts.cwd),
|
|
72
|
+
configDefault: opts.configDefault,
|
|
73
|
+
});
|
|
74
|
+
}
|
package/dist/commands/ops.js
CHANGED
|
@@ -7,6 +7,7 @@ import fs from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import { resolveConfig, toClientConfig, readProjectConfig, writeProjectConfig, MEMWAL_CONSTANTS, } from "../config.js";
|
|
10
|
+
import { resolveBranch } from "../branch.js";
|
|
10
11
|
import { MemForksClient } from "@memfork/core";
|
|
11
12
|
// ─── Shared helpers ───────────────────────────────────────────────────────────
|
|
12
13
|
async function getClient() {
|
|
@@ -14,34 +15,6 @@ async function getClient() {
|
|
|
14
15
|
const client = await MemForksClient.connect(toClientConfig(cfg));
|
|
15
16
|
return { client, cfg };
|
|
16
17
|
}
|
|
17
|
-
function isTransientSuiError(e) {
|
|
18
|
-
const msg = String(e);
|
|
19
|
-
return (msg.includes("needs to be rebuilt") ||
|
|
20
|
-
msg.includes("unavailable for consumption") ||
|
|
21
|
-
msg.includes("object version"));
|
|
22
|
-
}
|
|
23
|
-
async function withRetry(fn, retries = 2, delayMs = 1500) {
|
|
24
|
-
for (let attempt = 1;; attempt++) {
|
|
25
|
-
try {
|
|
26
|
-
return await fn();
|
|
27
|
-
}
|
|
28
|
-
catch (e) {
|
|
29
|
-
if (isTransientSuiError(e) && attempt < retries) {
|
|
30
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
throw e;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function currentGitBranch() {
|
|
38
|
-
try {
|
|
39
|
-
return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return "main";
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
18
|
// ─── status ───────────────────────────────────────────────────────────────────
|
|
46
19
|
export async function cmdStatus() {
|
|
47
20
|
const { client, cfg } = await getClient();
|
|
@@ -49,16 +22,18 @@ export async function cmdStatus() {
|
|
|
49
22
|
console.log("");
|
|
50
23
|
console.log(chalk.bold("MemForks status"));
|
|
51
24
|
console.log("");
|
|
25
|
+
const currentBranch = resolveBranch({ configDefault: cfg.defaultBranch });
|
|
52
26
|
console.log(` Tree ${chalk.cyan(cfg.treeId)}`);
|
|
53
27
|
console.log(` Network ${cfg.network}`);
|
|
54
|
-
console.log(` Branch ${chalk.green(
|
|
28
|
+
console.log(` Branch ${chalk.green(currentBranch)}`);
|
|
29
|
+
console.log(` Tree dflt ${chalk.dim(String(tree["default_branch"] ?? cfg.defaultBranch))}`);
|
|
55
30
|
console.log(` Signer ${client.keypair.toSuiAddress()}`);
|
|
56
31
|
console.log("");
|
|
57
32
|
}
|
|
58
33
|
// ─── log ──────────────────────────────────────────────────────────────────────
|
|
59
34
|
export async function cmdLog(opts) {
|
|
60
35
|
const { client, cfg } = await getClient();
|
|
61
|
-
const branch = opts.branch
|
|
36
|
+
const branch = resolveBranch({ explicit: opts.branch, configDefault: cfg.defaultBranch });
|
|
62
37
|
console.log("");
|
|
63
38
|
console.log(`${chalk.bold("memfork log")} ${chalk.dim("branch:")} ${chalk.green(branch)}`);
|
|
64
39
|
console.log("");
|
|
@@ -94,7 +69,7 @@ export async function cmdLog(opts) {
|
|
|
94
69
|
// ─── recall ───────────────────────────────────────────────────────────────────
|
|
95
70
|
export async function cmdRecall(query, opts) {
|
|
96
71
|
const { client, cfg } = await getClient();
|
|
97
|
-
const branch = opts.branch
|
|
72
|
+
const branch = resolveBranch({ explicit: opts.branch, configDefault: cfg.defaultBranch });
|
|
98
73
|
const results = await client.recall(query, { branch, limit: opts.limit ?? 5 });
|
|
99
74
|
if (opts.json) {
|
|
100
75
|
console.log(JSON.stringify(results));
|
|
@@ -118,7 +93,7 @@ export async function cmdRecall(query, opts) {
|
|
|
118
93
|
// ─── commit ───────────────────────────────────────────────────────────────────
|
|
119
94
|
export async function cmdCommit(opts) {
|
|
120
95
|
const { client, cfg } = await getClient();
|
|
121
|
-
const branch = opts.branch
|
|
96
|
+
const branch = resolveBranch({ explicit: opts.branch, configDefault: cfg.defaultBranch });
|
|
122
97
|
let facts = opts.facts ?? [];
|
|
123
98
|
// --from-response + --auto-extract: stub for LLM extraction
|
|
124
99
|
// In production this calls the configured LLM to distil durable facts.
|
|
@@ -132,7 +107,7 @@ export async function cmdCommit(opts) {
|
|
|
132
107
|
console.error(chalk.red("No facts to commit. Pass --facts or --from-response."));
|
|
133
108
|
process.exit(1);
|
|
134
109
|
}
|
|
135
|
-
const { blobId } = await
|
|
110
|
+
const { blobId } = await client.commit(branch, { facts, message: opts.message });
|
|
136
111
|
const out = { blobId, branch };
|
|
137
112
|
if (process.stdout.isTTY) {
|
|
138
113
|
console.log("");
|
|
@@ -158,7 +133,7 @@ export async function cmdMerge(from, into, opts) {
|
|
|
158
133
|
process.stdout.write(chalk.dim(`Merging ${chalk.green(from)} → ${chalk.green(into)}`) +
|
|
159
134
|
chalk.dim(governed ? " (governed — awaiting resolver…)" : " (LWW — self-finalizing…)") +
|
|
160
135
|
" ");
|
|
161
|
-
const { digest, mergedCount, blobId, proposalId } = await
|
|
136
|
+
const { digest, mergedCount, blobId, proposalId } = await client.merge(from, into);
|
|
162
137
|
console.log(chalk.green("done"));
|
|
163
138
|
console.log("");
|
|
164
139
|
console.log(chalk.dim(` facts merged: ${mergedCount}`));
|
|
@@ -308,7 +283,12 @@ export async function cmdPrComment(opts) {
|
|
|
308
283
|
}
|
|
309
284
|
catch { /* non-critical */ }
|
|
310
285
|
// Get the decided fact from the into_branch via recall.
|
|
311
|
-
|
|
286
|
+
// The proposal's into_branch is the most specific target, so it is treated
|
|
287
|
+
// as an explicit selection (ranking above the current git branch).
|
|
288
|
+
const targetBranch = resolveBranch({
|
|
289
|
+
explicit: opts.branch ?? intoBranch,
|
|
290
|
+
configDefault: cfg.defaultBranch,
|
|
291
|
+
});
|
|
312
292
|
let decision = `Use ${fromBranch || "winning branch"} approach.`;
|
|
313
293
|
try {
|
|
314
294
|
const results = await client.recall("decided", { branch: targetBranch, limit: 1 });
|
|
@@ -375,7 +355,12 @@ export async function cmdUi(opts = {}) {
|
|
|
375
355
|
console.log(chalk.dim("Build the app manually: cd apps/visualizer && npm run build"));
|
|
376
356
|
return;
|
|
377
357
|
}
|
|
378
|
-
|
|
358
|
+
// Two layouts: the published bundle (packages/cli/ui/) holds index.html +
|
|
359
|
+
// assets/ directly, while the monorepo source (apps/visualizer/) builds into
|
|
360
|
+
// a dist/ subdir. Detect which one we resolved to.
|
|
361
|
+
const distDir = fs.existsSync(path.join(appDir, "index.html"))
|
|
362
|
+
? appDir
|
|
363
|
+
: path.join(appDir, "dist");
|
|
379
364
|
const indexHtml = path.join(distDir, "index.html");
|
|
380
365
|
// ── Share mode: build → publish to Walrus Site ──────────────────────────
|
|
381
366
|
if (opts.share) {
|
|
@@ -592,9 +577,9 @@ export async function cmdRevoke(address) {
|
|
|
592
577
|
// ─── branch ───────────────────────────────────────────────────────────────────
|
|
593
578
|
export async function cmdBranch(name, opts = {}) {
|
|
594
579
|
const { client, cfg } = await getClient();
|
|
595
|
-
const from = opts.from
|
|
580
|
+
const from = resolveBranch({ explicit: opts.from, configDefault: cfg.defaultBranch });
|
|
596
581
|
process.stdout.write(chalk.dim(`Creating branch ${chalk.green(name)} from ${chalk.green(from)} … `));
|
|
597
|
-
const digest = await
|
|
582
|
+
const digest = await client.branch(name, { from });
|
|
598
583
|
console.log(chalk.green("done"));
|
|
599
584
|
console.log("");
|
|
600
585
|
console.log(chalk.dim(` tx: ${digest}`));
|
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,5 @@
|
|
|
7
7
|
* The CLI binary entry point is src/cli.ts → dist/cli.js
|
|
8
8
|
*/
|
|
9
9
|
export { resolveConfig, toClientConfig, readProjectConfig, writeProjectConfig, readCredentials, writeCredentials, upsertCredential, setDefaultTree, projectConfigPath, credentialsPath, ConfigError, } from "./config.js";
|
|
10
|
+
export { resolveBranch, pickBranch, gitBranch } from "./branch.js";
|
|
11
|
+
export type { BranchSources } from "./branch.js";
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,4 @@
|
|
|
7
7
|
* The CLI binary entry point is src/cli.ts → dist/cli.js
|
|
8
8
|
*/
|
|
9
9
|
export { resolveConfig, toClientConfig, readProjectConfig, writeProjectConfig, readCredentials, writeCredentials, upsertCredential, setDefaultTree, projectConfigPath, credentialsPath, ConfigError, } from "./config.js";
|
|
10
|
+
export { resolveBranch, pickBranch, gitBranch } from "./branch.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memfork/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"description": "MemForks CLI — init, commit, recall, merge, install plugins",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@inquirer/prompts": "^8.5.2",
|
|
38
|
-
"@memfork/core": "^0.1.
|
|
38
|
+
"@memfork/core": "^0.1.11",
|
|
39
39
|
"chalk": "^5.6.2",
|
|
40
40
|
"commander": "^15.0.0"
|
|
41
41
|
},
|
|
@@ -27,11 +27,12 @@ Use this after significant architectural decisions — not for routine facts.
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
memfork commit \
|
|
30
|
-
--branch $(git rev-parse --abbrev-ref HEAD) \
|
|
31
30
|
--message "decided: <one-line summary>" \
|
|
32
31
|
--facts "<fact 1>" "<fact 2>"
|
|
33
32
|
```
|
|
34
33
|
|
|
34
|
+
The CLI auto-detects the current Git branch; pass `--branch <name>` only to target a different one.
|
|
35
|
+
|
|
35
36
|
## Merge branches
|
|
36
37
|
|
|
37
38
|
When two branches need to reconcile their memory:
|
|
@@ -54,11 +54,13 @@ a non-trivial problem, also anchor it on-chain for immutable versioning:
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
memfork commit \
|
|
57
|
-
--branch $(git rev-parse --abbrev-ref HEAD) \
|
|
58
57
|
--message "decided: <one-line summary>" \
|
|
59
58
|
--facts "<fact 1>" "<fact 2>"
|
|
60
59
|
```
|
|
61
60
|
|
|
61
|
+
The CLI auto-detects the current Git branch, so you do not pass `--branch`
|
|
62
|
+
unless you want to target a different branch.
|
|
63
|
+
|
|
62
64
|
This creates a cryptographically verifiable commit on Sui — not just a blob.
|
|
63
65
|
Use it for decisions that matter for audit trail, not routine facts.
|
|
64
66
|
|
|
@@ -66,9 +68,10 @@ Use it for decisions that matter for audit trail, not routine facts.
|
|
|
66
68
|
|
|
67
69
|
## Branch awareness
|
|
68
70
|
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
71
|
+
- The `memfork` CLI auto-detects the current Git branch; pass `--branch` only to override it.
|
|
72
|
+
- The `memwal_*` MCP tools have no Git context, so pass `namespace="branch/<current-branch>"` explicitly.
|
|
73
|
+
- When the user switches branches, recall from the new branch namespace.
|
|
74
|
+
- Use `memfork status` to see the current branch, on-chain tree state, and open merge proposals.
|
|
72
75
|
|
|
73
76
|
---
|
|
74
77
|
|
|
@@ -81,10 +84,12 @@ stays isolated. Never collapse competing ideas into a single branch.
|
|
|
81
84
|
**Step 1 — announce and create branches:**
|
|
82
85
|
|
|
83
86
|
```bash
|
|
84
|
-
memfork branch dev/<hypothesis-a>
|
|
85
|
-
memfork branch dev/<hypothesis-b>
|
|
87
|
+
memfork branch dev/<hypothesis-a>
|
|
88
|
+
memfork branch dev/<hypothesis-b>
|
|
86
89
|
```
|
|
87
90
|
|
|
91
|
+
Both fork from the current Git branch by default; add `--from <branch>` to fork from another.
|
|
92
|
+
|
|
88
93
|
Use short kebab-case names (`dev/redis-first`, `dev/bcrypt-cost`).
|
|
89
94
|
|
|
90
95
|
**Step 2 — investigate each path and commit evidence:**
|