@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.
@@ -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
+ }
@@ -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(String(tree["default_branch"] ?? cfg.defaultBranch))}`);
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 ?? currentGitBranch();
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 ?? currentGitBranch();
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 ?? currentGitBranch();
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 withRetry(() => client.commit(branch, { facts, message: opts.message }));
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 withRetry(() => client.merge(from, into));
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
- const targetBranch = opts.branch ?? intoBranch ?? currentGitBranch();
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
- const distDir = path.join(appDir, "dist");
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 ?? cfg.defaultBranch ?? currentGitBranch();
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 withRetry(() => client.branch(name, { from }));
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.25",
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.10",
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
- - Memory is scoped to the current Git branch via the `namespace` parameter
70
- - When the user switches branches, recall from the new branch namespace
71
- - Use `memfork status` to see the on-chain tree state and open merge proposals
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> --from $(git rev-parse --abbrev-ref HEAD)
85
- memfork branch dev/<hypothesis-b> --from $(git rev-parse --abbrev-ref HEAD)
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:**