@memfork/cli 0.1.26 → 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,14 +15,6 @@ async function getClient() {
14
15
  const client = await MemForksClient.connect(toClientConfig(cfg));
15
16
  return { client, cfg };
16
17
  }
17
- function currentGitBranch() {
18
- try {
19
- return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
20
- }
21
- catch {
22
- return "main";
23
- }
24
- }
25
18
  // ─── status ───────────────────────────────────────────────────────────────────
26
19
  export async function cmdStatus() {
27
20
  const { client, cfg } = await getClient();
@@ -29,16 +22,18 @@ export async function cmdStatus() {
29
22
  console.log("");
30
23
  console.log(chalk.bold("MemForks status"));
31
24
  console.log("");
25
+ const currentBranch = resolveBranch({ configDefault: cfg.defaultBranch });
32
26
  console.log(` Tree ${chalk.cyan(cfg.treeId)}`);
33
27
  console.log(` Network ${cfg.network}`);
34
- 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))}`);
35
30
  console.log(` Signer ${client.keypair.toSuiAddress()}`);
36
31
  console.log("");
37
32
  }
38
33
  // ─── log ──────────────────────────────────────────────────────────────────────
39
34
  export async function cmdLog(opts) {
40
35
  const { client, cfg } = await getClient();
41
- const branch = opts.branch ?? currentGitBranch();
36
+ const branch = resolveBranch({ explicit: opts.branch, configDefault: cfg.defaultBranch });
42
37
  console.log("");
43
38
  console.log(`${chalk.bold("memfork log")} ${chalk.dim("branch:")} ${chalk.green(branch)}`);
44
39
  console.log("");
@@ -74,7 +69,7 @@ export async function cmdLog(opts) {
74
69
  // ─── recall ───────────────────────────────────────────────────────────────────
75
70
  export async function cmdRecall(query, opts) {
76
71
  const { client, cfg } = await getClient();
77
- const branch = opts.branch ?? currentGitBranch();
72
+ const branch = resolveBranch({ explicit: opts.branch, configDefault: cfg.defaultBranch });
78
73
  const results = await client.recall(query, { branch, limit: opts.limit ?? 5 });
79
74
  if (opts.json) {
80
75
  console.log(JSON.stringify(results));
@@ -98,7 +93,7 @@ export async function cmdRecall(query, opts) {
98
93
  // ─── commit ───────────────────────────────────────────────────────────────────
99
94
  export async function cmdCommit(opts) {
100
95
  const { client, cfg } = await getClient();
101
- const branch = opts.branch ?? currentGitBranch();
96
+ const branch = resolveBranch({ explicit: opts.branch, configDefault: cfg.defaultBranch });
102
97
  let facts = opts.facts ?? [];
103
98
  // --from-response + --auto-extract: stub for LLM extraction
104
99
  // In production this calls the configured LLM to distil durable facts.
@@ -288,7 +283,12 @@ export async function cmdPrComment(opts) {
288
283
  }
289
284
  catch { /* non-critical */ }
290
285
  // Get the decided fact from the into_branch via recall.
291
- 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
+ });
292
292
  let decision = `Use ${fromBranch || "winning branch"} approach.`;
293
293
  try {
294
294
  const results = await client.recall("decided", { branch: targetBranch, limit: 1 });
@@ -355,7 +355,12 @@ export async function cmdUi(opts = {}) {
355
355
  console.log(chalk.dim("Build the app manually: cd apps/visualizer && npm run build"));
356
356
  return;
357
357
  }
358
- 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");
359
364
  const indexHtml = path.join(distDir, "index.html");
360
365
  // ── Share mode: build → publish to Walrus Site ──────────────────────────
361
366
  if (opts.share) {
@@ -572,7 +577,7 @@ export async function cmdRevoke(address) {
572
577
  // ─── branch ───────────────────────────────────────────────────────────────────
573
578
  export async function cmdBranch(name, opts = {}) {
574
579
  const { client, cfg } = await getClient();
575
- const from = opts.from ?? cfg.defaultBranch ?? currentGitBranch();
580
+ const from = resolveBranch({ explicit: opts.from, configDefault: cfg.defaultBranch });
576
581
  process.stdout.write(chalk.dim(`Creating branch ${chalk.green(name)} from ${chalk.green(from)} … `));
577
582
  const digest = await client.branch(name, { from });
578
583
  console.log(chalk.green("done"));
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.26",
3
+ "version": "0.1.27",
4
4
  "description": "MemForks CLI — init, commit, recall, merge, install plugins",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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:**