@balacode/mental 0.2.2 → 0.2.3

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mental",
3
3
  "displayName": "Mental",
4
- "version": "0.2.2",
4
+ "version": "0.2.3",
5
5
  "description": "Never reconstruct where you left off. Mental keeps the resume, the decisions, and what's still in the air — so you and your agents continue as if the last session never ended.",
6
6
  "author": {
7
7
  "name": "Ali Farahat",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mental",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Never reconstruct where you left off. Mental keeps the resume, the decisions, and what's still in the air — so you and your agents continue as if the last session never ended.",
5
5
  "author": {
6
6
  "name": "Ali Farahat",
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.3] - 2026-08-26
11
+
12
+ ### Added
13
+
14
+ - `mental doctor` checks npm for a newer `@balacode/mental` (warn only; fail open). `mental install` from a published install upgrades the global CLI when npm is ahead, then re-runs so skills match. `MENTAL_SKIP_UPDATE_CHECK=1` skips the network. Heartbeat never checks.
15
+
16
+ ### Changed
17
+
18
+ - Agent Mental receipt wraps with a separator line (`────────`) instead of `</br>`, which printed as literal tags in Claude Code and other agents.
19
+
20
+ ### Fixed
21
+
22
+ - `mental install` / `npm i -g` overwrites an existing global `mental` bin (npm 11 EEXIST). Leftover `@mental/cli` links no longer block `@balacode/mental`.
23
+
10
24
  ## [0.2.2] - 2026-08-26
11
25
 
12
26
  ### Added
@@ -49,7 +63,8 @@ First public release of the Mental CLI.
49
63
  - `install --mcp` registers `serve`; `decide` updates by title so an open decision can close.
50
64
  - Install, doctor, uninstall; hooks stay off by default.
51
65
 
52
- [Unreleased]: https://github.com/afaraha8403/mental/compare/v0.2.2...HEAD
66
+ [Unreleased]: https://github.com/afaraha8403/mental/compare/v0.2.3...HEAD
67
+ [0.2.3]: https://github.com/afaraha8403/mental/releases/tag/v0.2.3
53
68
  [0.2.2]: https://github.com/afaraha8403/mental/releases/tag/v0.2.2
54
69
  [0.2.1]: https://github.com/afaraha8403/mental/releases/tag/v0.2.1
55
70
  [0.2.0]: https://github.com/afaraha8403/mental/releases/tag/v0.2.0
package/README.md CHANGED
@@ -76,7 +76,7 @@ npm i -g @balacode/mental
76
76
  mental install
77
77
  ```
78
78
 
79
- Last install wins. That puts `mental` on PATH (typically `~/.local/bin/mental`), copies the skill + tiny always-on rule into `~/.claude`, `~/.cursor`, `~/.agents`, and `~/.config/opencode`, and creates a `~/.mental` skeleton. It also **removes leftover Balakit Mental skill/rule copies** (the old `npx balakit doctor` pointer) so they cannot fight the new rule. It does **not** turn on hooks or MCP, and it does not delete journals.
79
+ Last install wins: an existing global `mental` bin is overwritten (npm 11 no longer fails with EEXIST). That puts `mental` on PATH (typically `~/.local/bin/mental`), copies the skill + tiny always-on rule into `~/.claude`, `~/.cursor`, `~/.agents`, and `~/.config/opencode`, and creates a `~/.mental` skeleton. From a published install it also **upgrades** the CLI when npm has a newer version, then re-runs so skills match. It **removes leftover Balakit Mental skill/rule copies** (the old `npx balakit doctor` pointer) so they cannot fight the new rule. It does **not** turn on hooks or MCP, and it does not delete journals. A git checkout installs that tree and does not clobber it with the registry.
80
80
 
81
81
  From a clone, without npm:
82
82
 
@@ -86,7 +86,7 @@ node bin/cli.mjs install --json
86
86
  ```
87
87
 
88
88
  ```bash
89
- mental doctor # PATH, bindings, ignore, skills
89
+ mental doctor # PATH, bindings, ignore, skills, npm update
90
90
  mental doctor --fix-ignore # add .mental/ and .mental-id to your global git excludes
91
91
  ```
92
92
 
@@ -176,7 +176,7 @@ Mid-chat, not just start/finish: search decisions before changing an approach, r
176
176
 
177
177
  Do not grep `.mental` or parse YAML. If `mental` is missing, try `npx @balacode/mental …`. If that fails, continue the coding task (fail open) and mention `npm i -g @balacode/mental` then `mental install`.
178
178
 
179
- Turns that invoked `mental` end with `</br>`, title `🧠 Mental ` (two trailing spaces so chat markdown does not join lines), indented `Kind: Verb` items, then `</br>` (see the skill).
179
+ Turns that invoked `mental` end with a separator line (`────────`), title `🧠 Mental ` (two trailing spaces so chat markdown does not join lines), indented `Kind: Verb` items, then `────────` (see the skill). Do not emit `</br>` — it prints as literal tags.
180
180
 
181
181
  ## Identity (UUID, not the folder)
182
182
 
@@ -231,11 +231,11 @@ mental uninstall --delete-data --confirm DELETE # wipe ~/.mental too
231
231
  | `mental remap [--to id]` | List or retarget this clone’s UUID |
232
232
  | `mental split [--copy]` | New UUID for this clone |
233
233
  | `mental link --to <id>` | Point this clone at an existing UUID |
234
- | `mental install` | User skill + rule; `~/.mental` skeleton; CLI on PATH; `--mcp` registers MCP config |
234
+ | `mental install` | User skill + rule; `~/.mental` skeleton; CLI on PATH (overwrites existing bin; upgrades if npm is newer); `--mcp` registers MCP config |
235
235
  | `mental uninstall` | Remove installed skill/rule/hooks/MCP entries |
236
236
  | `mental hooks on\|off` | Optional session hooks |
237
237
  | `mental serve` | Optional MCP stdio (full command surface) |
238
- | `mental doctor` | PATH, bindings, ignore, skills. `--fix-ignore` adds `.mental/` to global excludes |
238
+ | `mental doctor` | PATH, bindings, ignore, skills, npm update. `--fix-ignore` adds `.mental/` to global excludes |
239
239
 
240
240
  Global flags: `--json`, `--dir <path>` (same as `MENTAL_DIR`).
241
241
 
@@ -9,11 +9,12 @@ import { loadBindings } from "../lib/bindings.mjs";
9
9
  import { checkMentalIgnored, ensureMentalExcluded, gitAvailable } from "../lib/ignore.mjs";
10
10
  import { skillsPresent } from "../lib/install-skills.mjs";
11
11
  import { printResult, brandMark } from "../lib/output.mjs";
12
- import { CMD } from "../lib/pkg.mjs";
12
+ import { CMD, NAME, VERSION } from "../lib/pkg.mjs";
13
13
  import { isOptedInLocal } from "../lib/import-legacy.mjs";
14
14
  import { findGitRoot } from "../lib/git.mjs";
15
15
  import { indexPath } from "../lib/index.mjs";
16
16
  import { leftoverBalakitMentalCount, findBalakitMental } from "../lib/legacy-balakit.mjs";
17
+ import { checkForUpdate, cmpSemver, updateHint } from "../lib/update.mjs";
17
18
 
18
19
  function check(id, ok, message, level = "error") {
19
20
  return { id, ok, level, message };
@@ -150,6 +151,19 @@ export function cmdDoctor(args, io = {}) {
150
151
  }
151
152
  }
152
153
 
154
+ const upd = checkForUpdate({ env });
155
+ if (!upd.skipped && upd.latest) {
156
+ const behind = cmpSemver(upd.latest, VERSION) > 0;
157
+ checks.push(
158
+ check(
159
+ "update",
160
+ !behind,
161
+ behind ? updateHint(VERSION, upd.latest, NAME) : `CLI ${VERSION} (npm ${upd.latest})`,
162
+ behind ? "warn" : "info",
163
+ ),
164
+ );
165
+ }
166
+
153
167
  const problems = checks.filter((c) => !c.ok && c.level === "error");
154
168
  const data = {
155
169
  checks,
@@ -2,6 +2,7 @@
2
2
  * `mental install` — copy skill + tiny rule to user agent dirs; ~/.mental skeleton.
3
3
  * `--hooks` and `--mcp` are optional and default off.
4
4
  */
5
+ import { spawnSync } from "node:child_process";
5
6
  import { resolveBundle } from "../lib/resolve.mjs";
6
7
  import { userMentalDir } from "../lib/bindings.mjs";
7
8
  import { ensureSkeleton } from "../lib/okf.mjs";
@@ -9,9 +10,10 @@ import { installSkills } from "../lib/install-skills.mjs";
9
10
  import { installGlobalCli } from "../lib/install-cli.mjs";
10
11
  import { enableHooks } from "../lib/hooks.mjs";
11
12
  import { enableMcp } from "../lib/mcp.mjs";
12
- import { CMD } from "../lib/pkg.mjs";
13
+ import { CMD, NAME, VERSION } from "../lib/pkg.mjs";
13
14
  import { printResult, brandLine } from "../lib/output.mjs";
14
15
  import { purgeBalakitMental } from "../lib/legacy-balakit.mjs";
16
+ import { checkForUpdate, cmpSemver, isDevCheckout } from "../lib/update.mjs";
15
17
 
16
18
  export function cmdInstall(args, io = {}) {
17
19
  const stdout = io.stdout ?? process.stdout;
@@ -28,6 +30,35 @@ export function cmdInstall(args, io = {}) {
28
30
  const hooks = Boolean(args.flags?.hooks);
29
31
  const mcp = Boolean(args.flags?.mcp);
30
32
  const cwd = args.cwd ?? process.cwd();
33
+ const env = args.env ?? process.env;
34
+ const stderr = io.stderr ?? process.stderr;
35
+
36
+ if (env.MENTAL_SKIP_SELF_UPDATE !== "1" && !isDevCheckout()) {
37
+ const upd = checkForUpdate({ env });
38
+ if (upd.latest && cmpSemver(upd.latest, VERSION) > 0) {
39
+ const bumped = installGlobalCli({ home, env, spec: NAME });
40
+ const newBin = bumped.bin;
41
+ if (bumped.npm && newBin) {
42
+ if (!args.json) {
43
+ stdout.write(`${brandLine(`updating CLI ${VERSION} → ${upd.latest}`)}\n`);
44
+ }
45
+ const childArgs = ["install"];
46
+ if (args.json) childArgs.push("--json");
47
+ if (project) childArgs.push("--project");
48
+ if (hooks) childArgs.push("--hooks");
49
+ if (mcp) childArgs.push("--mcp");
50
+ if (args.dir) childArgs.push("--dir", args.dir);
51
+ const child = spawnSync(newBin, childArgs, {
52
+ encoding: "utf8",
53
+ cwd,
54
+ env: { ...env, MENTAL_SKIP_SELF_UPDATE: "1", MENTAL_SKIP_UPDATE_CHECK: "1" },
55
+ });
56
+ if (child.stdout) stdout.write(child.stdout);
57
+ if (child.stderr) stderr.write(child.stderr);
58
+ return child.status ?? 1;
59
+ }
60
+ }
61
+ }
31
62
 
32
63
  const legacy = purgeBalakitMental({
33
64
  home,
@@ -37,7 +68,7 @@ export function cmdInstall(args, io = {}) {
37
68
  home,
38
69
  projectDir: project ? cwd : null,
39
70
  });
40
- const cli = installGlobalCli({ home, env: args.env ?? process.env });
71
+ const cli = installGlobalCli({ home, env });
41
72
  const personal = userMentalDir(home);
42
73
  ensureSkeleton(personal, { name: "personal" });
43
74
 
@@ -50,7 +81,7 @@ export function cmdInstall(args, io = {}) {
50
81
  const resolved = resolveBundle({
51
82
  cwd,
52
83
  home,
53
- env: args.env ?? process.env,
84
+ env,
54
85
  dir: args.dir ?? null,
55
86
  write: true,
56
87
  });
package/bin/lib/args.mjs CHANGED
@@ -22,11 +22,11 @@ Usage:
22
22
  ${CMD} remap List UUID bindings, or --to <id> / --from <id> for this clone
23
23
  ${CMD} split New UUID for this clone (--copy keeps OKF files)
24
24
  ${CMD} link Point this clone at --to <id>
25
- ${CMD} install Skill + rule + PATH; removes leftover Balakit Mental skill/rule copies
25
+ ${CMD} install Skill + rule + PATH; overwrites existing bin; upgrades if npm is newer
26
26
  ${CMD} uninstall Remove installed skill/rule/hooks (OKF stays unless --delete-data DELETE)
27
27
  ${CMD} hooks on|off Optional session-start hooks (default off)
28
28
  ${CMD} serve Optional MCP stdio (heartbeat/where/status/search/list/show/journal/attention/decide/note)
29
- ${CMD} doctor PATH, bindings, ignore, skill, index
29
+ ${CMD} doctor PATH, bindings, ignore, skill, index, update
30
30
  ${CMD} reindex Rebuild derived sqlite index from OKF files
31
31
 
32
32
  TTY: no args prints a one-shot heartbeat and exits. Named commands are one-shot.
@@ -41,7 +41,7 @@ function replaceWithSymlink(dest, target) {
41
41
  }
42
42
 
43
43
  /**
44
- * @param {{ home: string, env?: NodeJS.ProcessEnv }} opts
44
+ * @param {{ home: string, env?: NodeJS.ProcessEnv, spec?: string }} opts
45
45
  * @returns {{
46
46
  * ok: boolean,
47
47
  * bin: string | null,
@@ -50,13 +50,30 @@ function replaceWithSymlink(dest, target) {
50
50
  * message: string,
51
51
  * }}
52
52
  */
53
- export function installGlobalCli({ home, env = process.env }) {
53
+ export function installGlobalCli({ home, env = process.env, spec = PKG_ROOT }) {
54
+ const prefix = npmGlobalPrefix(env);
55
+ const npmBin = prefix ? npmGlobalBin(prefix) : null;
56
+ // npm 11 refuses to replace an existing global bin (EEXIST). Last install
57
+ // wins: drop our previous `mental` link (often leftover @mental/cli).
58
+ if (npmBin) {
59
+ try {
60
+ unlinkSync(npmBin);
61
+ } catch {
62
+ // missing
63
+ }
64
+ }
54
65
  const npm = runNpm(
55
- ["install", "-g", "--no-fund", "--no-audit", "--no-package-lock", PKG_ROOT],
66
+ [
67
+ "install",
68
+ "-g",
69
+ "--force",
70
+ "--no-fund",
71
+ "--no-audit",
72
+ "--no-package-lock",
73
+ spec,
74
+ ],
56
75
  env,
57
76
  );
58
- const prefix = npmGlobalPrefix(env);
59
- const npmBin = prefix ? npmGlobalBin(prefix) : null;
60
77
  const pathBin = join(home, ".local", "bin", CMD);
61
78
  const fallback = join(PKG_ROOT, "bin", "cli.mjs");
62
79
  const target =
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Fail-open npm update check. Heartbeat does not call this (must stay cheap).
3
+ * `MENTAL_SKIP_UPDATE_CHECK=1` skips. `MENTAL_NPM_LATEST` pins a version (tests).
4
+ */
5
+ import { existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+ import { NAME, PKG_ROOT } from "./pkg.mjs";
9
+
10
+ /**
11
+ * @param {string} a
12
+ * @param {string} b
13
+ * @returns {-1 | 0 | 1}
14
+ */
15
+ export function cmpSemver(a, b) {
16
+ const pa = String(a).split(".").map((n) => Number.parseInt(n, 10) || 0);
17
+ const pb = String(b).split(".").map((n) => Number.parseInt(n, 10) || 0);
18
+ for (let i = 0; i < 3; i++) {
19
+ if (pa[i] > pb[i]) return 1;
20
+ if (pa[i] < pb[i]) return -1;
21
+ }
22
+ return 0;
23
+ }
24
+
25
+ /** True when running from a git checkout (not the published tarball). */
26
+ export function isDevCheckout() {
27
+ return existsSync(join(PKG_ROOT, ".git"));
28
+ }
29
+
30
+ /**
31
+ * @param {NodeJS.ProcessEnv} [env]
32
+ */
33
+ export function skipUpdateCheck(env = process.env) {
34
+ const v = env.MENTAL_SKIP_UPDATE_CHECK;
35
+ return v === "1" || v === "true" || v === "yes";
36
+ }
37
+
38
+ /**
39
+ * @param {{ env?: NodeJS.ProcessEnv, name?: string, timeoutMs?: number }} [opts]
40
+ * @returns {{ skipped: boolean, latest: string | null }}
41
+ */
42
+ export function checkForUpdate(opts = {}) {
43
+ const env = opts.env ?? process.env;
44
+ if (skipUpdateCheck(env)) return { skipped: true, latest: null };
45
+ const pinned = env.MENTAL_NPM_LATEST;
46
+ if (typeof pinned === "string" && /^\d+\.\d+\.\d+/.test(pinned.trim())) {
47
+ return { skipped: false, latest: pinned.trim().split(/\s+/)[0] };
48
+ }
49
+ const name = opts.name ?? NAME;
50
+ const timeout = opts.timeoutMs ?? 5000;
51
+ const r = spawnSync("npm", ["view", name, "version"], {
52
+ encoding: "utf8",
53
+ env,
54
+ timeout,
55
+ });
56
+ if (r.status !== 0) return { skipped: false, latest: null };
57
+ const v = (r.stdout || "").trim().split(/\s+/)[0];
58
+ if (!/^\d+\.\d+\.\d+/.test(v)) return { skipped: false, latest: null };
59
+ return { skipped: false, latest: v };
60
+ }
61
+
62
+ /**
63
+ * @param {string} current
64
+ * @param {string} latest
65
+ * @param {string} [pkg]
66
+ */
67
+ export function updateHint(current, latest, pkg = NAME) {
68
+ return `CLI ${current}; npm ${latest}. Run \`mental install\` or \`npm i -g --force ${pkg}\`.`;
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balacode/mental",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Never reconstruct where you left off. Mental keeps the resume, the decisions, and what's still in the air.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "mental",
4
- "version": "0.2.2",
4
+ "version": "0.2.3",
5
5
  "description": "Never reconstruct where you left off. Mental keeps the resume, the decisions, and what's still in the air — so you and your agents continue as if the last session never ended.",
6
6
  "author": {
7
7
  "name": "Ali Farahat",
package/rules/mental.mdc CHANGED
@@ -13,6 +13,6 @@ If `mental` is not on PATH, try `npx @balacode/mental …`. If that fails, conti
13
13
 
14
14
  Never commit Mental data. Never write secrets. Never edit gitignore; tell the user to run `mental doctor`.
15
15
 
16
- If you invoked `mental` this turn, end the user-visible reply with `</br>`, then `🧠 Mental ` (two trailing spaces), then indented `Kind: Verb` lines, then `</br>`. Never one line. Example in the Mental skill. Not a code fence.
16
+ If you invoked `mental` this turn, end the user-visible reply with a separator line (`────────`), then `🧠 Mental ` (two trailing spaces), then indented `Kind: Verb` lines, then `────────`. Never one line. Not `</br>`. Example in the Mental skill. Not a code fence.
17
17
 
18
18
  On a pasted transcript or a plan-progress question ("where in the plan / what's left?"), use the Mental skill. Cheap reload is `mental heartbeat --json`, not a notes dump.
@@ -91,7 +91,10 @@ Cursor markdown joins adjacent lines into one paragraph. A lone `<br>`
91
91
  becomes a break *inside* that paragraph, so `🧠 Mental` and the item
92
92
  collapse to one line. Do **not** put the title and items on the same line.
93
93
 
94
- Wrap with literal `</br>` (open and close, each on its own line).
94
+ Wrap with a separator line (`────────`) on its own line above and below.
95
+ That is a visible rule even as plain text. Do not emit `<br>` or `</br>` —
96
+ those print as literal tags in Claude Code and other agents. Do not use
97
+ markdown `---` here: it can become a heading underline under the last item.
95
98
  Title line is `🧠 Mental` plus **two trailing spaces** (markdown hard break).
96
99
  Each item is indented two spaces: `emoji Kind: Verb “title”`.
97
100
  At most four item lines; if more, keep the writes and end with `+N more`.
@@ -102,21 +105,21 @@ not this repo, so links would 404. The CLI tool card already has the path.
102
105
  **Writes (copy this shape, including the two spaces after `Mental`):**
103
106
 
104
107
  ```text
105
- </br>
108
+ ────────
106
109
  🧠 Mental
107
110
  🚦 Attention: Recorded “Tom said ship”
108
111
  📓 Journal: Recorded “Resolver landed”
109
112
  🎯 Decision: Decided “Keep the JSON envelope”
110
- </br>
113
+ ────────
111
114
  ```
112
115
 
113
116
  **Read-only (heartbeat / search / show / list):**
114
117
 
115
118
  ```text
116
- </br>
119
+ ────────
117
120
  🧠 Mental
118
121
  🔍 Read: Heartbeat
119
- </br>
122
+ ────────
120
123
  ```
121
124
 
122
125
  | Emoji | Kind | Item line |