@balacode/mental 0.2.0 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +31 -2
- package/README.md +10 -7
- package/bin/commands/doctor.mjs +37 -1
- package/bin/commands/install.mjs +49 -4
- package/bin/lib/args.mjs +2 -2
- package/bin/lib/install-cli.mjs +22 -5
- package/bin/lib/legacy-balakit.mjs +220 -0
- package/bin/lib/update.mjs +69 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/rules/mental.mdc +1 -1
- package/skills/mental/SKILL.md +27 -18
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mental",
|
|
3
3
|
"displayName": "Mental",
|
|
4
|
-
"version": "0.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.
|
|
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,32 @@ 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
|
+
|
|
24
|
+
## [0.2.2] - 2026-08-26
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `mental install` finds leftover **Balakit** Mental skill/rule copies (fingerprint: `npx balakit doctor`, `mentalDataPolicy`, …) and deletes them before writing the standalone skill. Mental-only `<!-- BEGIN balakit -->` blocks are stripped. Journals and `./.mental` data are not deleted. `mental doctor` warns if any remain.
|
|
29
|
+
|
|
30
|
+
## [0.2.1] - 2026-08-26
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Agent Mental receipt wraps with `</br>`, uses a markdown hard break after `🧠 Mental`, and prints indented `Kind: Verb` lines so Cursor chat does not collapse the block onto one line.
|
|
35
|
+
|
|
10
36
|
## [0.2.0] - 2026-08-26
|
|
11
37
|
|
|
12
38
|
### Added
|
|
@@ -20,7 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
20
46
|
|
|
21
47
|
### Changed
|
|
22
48
|
|
|
23
|
-
- README now shows the logo,
|
|
49
|
+
- README now shows the logo, npm (`@balacode/mental`), [Agent Plugins 1.0.0](https://agent-plugins.org/specification) packaging, and copy-paste install prompts for Cursor, Claude Code, VS Code, and GitHub Copilot.
|
|
24
50
|
- npm package is `@balacode/mental` (unscoped `mental` is taken). The CLI bin stays `mental`.
|
|
25
51
|
|
|
26
52
|
## [0.1.0] - 2026-08-26
|
|
@@ -37,6 +63,9 @@ First public release of the Mental CLI.
|
|
|
37
63
|
- `install --mcp` registers `serve`; `decide` updates by title so an open decision can close.
|
|
38
64
|
- Install, doctor, uninstall; hooks stay off by default.
|
|
39
65
|
|
|
40
|
-
[Unreleased]: https://github.com/afaraha8403/mental/compare/v0.2.
|
|
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
|
|
68
|
+
[0.2.2]: https://github.com/afaraha8403/mental/releases/tag/v0.2.2
|
|
69
|
+
[0.2.1]: https://github.com/afaraha8403/mental/releases/tag/v0.2.1
|
|
41
70
|
[0.2.0]: https://github.com/afaraha8403/mental/releases/tag/v0.2.0
|
|
42
71
|
[0.1.0]: https://github.com/afaraha8403/mental/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -4,15 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
# Mental
|
|
6
6
|
|
|
7
|
+
[](https://www.npmjs.com/package/@balacode/mental)
|
|
8
|
+
|
|
7
9
|
Local-first continuity layer for you and your coding agents.
|
|
8
10
|
|
|
9
11
|
Git records **what** changed. Mental records the small amount git cannot explain: where you left off, why a decision was made, what is still in the air after a hop, and the next exact action. **OKF markdown is the source of truth.** SQLite is a derived cache. Agents call `mental … --json` — they do not grep YAML.
|
|
10
12
|
|
|
13
|
+
- **npm:** [@balacode/mental](https://www.npmjs.com/package/@balacode/mental) — install this; the CLI binary is `mental`
|
|
11
14
|
- **Repo:** https://github.com/afaraha8403/mental
|
|
12
15
|
- **Spec:** [PLAN.md](./PLAN.md)
|
|
13
16
|
- **Plugin standard:** [Agent Plugins 1.0.0](https://agent-plugins.org/specification)
|
|
14
17
|
|
|
15
|
-
Mental is **not** a Balakit plugin. Default data lives in `~/.mental` (UUID bindings). Project `./.mental` only after `mental local`.
|
|
18
|
+
Mental is **not** a Balakit plugin. Default data lives in `~/.mental` (UUID bindings). Project `./.mental` only after `mental local`. The unscoped npm package [`mental`](https://www.npmjs.com/package/mental) is a different project.
|
|
16
19
|
|
|
17
20
|
## Install
|
|
18
21
|
|
|
@@ -73,7 +76,7 @@ npm i -g @balacode/mental
|
|
|
73
76
|
mental install
|
|
74
77
|
```
|
|
75
78
|
|
|
76
|
-
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 does **not** turn on hooks or MCP.
|
|
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.
|
|
77
80
|
|
|
78
81
|
From a clone, without npm:
|
|
79
82
|
|
|
@@ -83,7 +86,7 @@ node bin/cli.mjs install --json
|
|
|
83
86
|
```
|
|
84
87
|
|
|
85
88
|
```bash
|
|
86
|
-
mental doctor # PATH, bindings, ignore, skills
|
|
89
|
+
mental doctor # PATH, bindings, ignore, skills, npm update
|
|
87
90
|
mental doctor --fix-ignore # add .mental/ and .mental-id to your global git excludes
|
|
88
91
|
```
|
|
89
92
|
|
|
@@ -171,9 +174,9 @@ mental status --json
|
|
|
171
174
|
|
|
172
175
|
Mid-chat, not just start/finish: search decisions before changing an approach, record attention the moment residue surfaces, and re-pulse `mental heartbeat --json` whenever other agents may have written — it is cheap and derives git live.
|
|
173
176
|
|
|
174
|
-
Do not grep `.mental` or parse YAML. If `mental` is missing, continue the coding task (fail open) and mention install
|
|
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`.
|
|
175
178
|
|
|
176
|
-
Turns that invoked `mental` end with
|
|
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.
|
|
177
180
|
|
|
178
181
|
## Identity (UUID, not the folder)
|
|
179
182
|
|
|
@@ -228,11 +231,11 @@ mental uninstall --delete-data --confirm DELETE # wipe ~/.mental too
|
|
|
228
231
|
| `mental remap [--to id]` | List or retarget this clone’s UUID |
|
|
229
232
|
| `mental split [--copy]` | New UUID for this clone |
|
|
230
233
|
| `mental link --to <id>` | Point this clone at an existing UUID |
|
|
231
|
-
| `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 |
|
|
232
235
|
| `mental uninstall` | Remove installed skill/rule/hooks/MCP entries |
|
|
233
236
|
| `mental hooks on\|off` | Optional session hooks |
|
|
234
237
|
| `mental serve` | Optional MCP stdio (full command surface) |
|
|
235
|
-
| `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 |
|
|
236
239
|
|
|
237
240
|
Global flags: `--json`, `--dir <path>` (same as `MENTAL_DIR`).
|
|
238
241
|
|
package/bin/commands/doctor.mjs
CHANGED
|
@@ -9,10 +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
|
+
import { leftoverBalakitMentalCount, findBalakitMental } from "../lib/legacy-balakit.mjs";
|
|
17
|
+
import { checkForUpdate, cmpSemver, updateHint } from "../lib/update.mjs";
|
|
16
18
|
|
|
17
19
|
function check(id, ok, message, level = "error") {
|
|
18
20
|
return { id, ok, level, message };
|
|
@@ -126,6 +128,40 @@ export function cmdDoctor(args, io = {}) {
|
|
|
126
128
|
),
|
|
127
129
|
);
|
|
128
130
|
}
|
|
131
|
+
|
|
132
|
+
const leftoverWiring = leftoverBalakitMentalCount({ home, projectDir: gitRoot || cwd });
|
|
133
|
+
if (leftoverWiring > 0) {
|
|
134
|
+
const found = findBalakitMental({ home, projectDir: gitRoot || cwd });
|
|
135
|
+
const sample = [
|
|
136
|
+
...found.skills,
|
|
137
|
+
...found.rules,
|
|
138
|
+
...found.plugins,
|
|
139
|
+
...found.blocks.map((b) => b.file),
|
|
140
|
+
]
|
|
141
|
+
.slice(0, 4)
|
|
142
|
+
.join(", ");
|
|
143
|
+
checks.push(
|
|
144
|
+
check(
|
|
145
|
+
"legacy-balakit",
|
|
146
|
+
false,
|
|
147
|
+
`Balakit Mental skill/rule still present (${sample}). Run \`${CMD} install\` to remove it.`,
|
|
148
|
+
"warn",
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
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
|
+
);
|
|
129
165
|
}
|
|
130
166
|
|
|
131
167
|
const problems = checks.filter((c) => !c.ok && c.level === "error");
|
package/bin/commands/install.mjs
CHANGED
|
@@ -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,8 +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";
|
|
15
|
+
import { purgeBalakitMental } from "../lib/legacy-balakit.mjs";
|
|
16
|
+
import { checkForUpdate, cmpSemver, isDevCheckout } from "../lib/update.mjs";
|
|
14
17
|
|
|
15
18
|
export function cmdInstall(args, io = {}) {
|
|
16
19
|
const stdout = io.stdout ?? process.stdout;
|
|
@@ -27,12 +30,45 @@ export function cmdInstall(args, io = {}) {
|
|
|
27
30
|
const hooks = Boolean(args.flags?.hooks);
|
|
28
31
|
const mcp = Boolean(args.flags?.mcp);
|
|
29
32
|
const cwd = args.cwd ?? process.cwd();
|
|
33
|
+
const env = args.env ?? process.env;
|
|
34
|
+
const stderr = io.stderr ?? process.stderr;
|
|
30
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
|
+
}
|
|
62
|
+
|
|
63
|
+
const legacy = purgeBalakitMental({
|
|
64
|
+
home,
|
|
65
|
+
projectDir: cwd,
|
|
66
|
+
});
|
|
31
67
|
const installed = installSkills({
|
|
32
68
|
home,
|
|
33
69
|
projectDir: project ? cwd : null,
|
|
34
70
|
});
|
|
35
|
-
const cli = installGlobalCli({ home, env
|
|
71
|
+
const cli = installGlobalCli({ home, env });
|
|
36
72
|
const personal = userMentalDir(home);
|
|
37
73
|
ensureSkeleton(personal, { name: "personal" });
|
|
38
74
|
|
|
@@ -45,7 +81,7 @@ export function cmdInstall(args, io = {}) {
|
|
|
45
81
|
const resolved = resolveBundle({
|
|
46
82
|
cwd,
|
|
47
83
|
home,
|
|
48
|
-
env
|
|
84
|
+
env,
|
|
49
85
|
dir: args.dir ?? null,
|
|
50
86
|
write: true,
|
|
51
87
|
});
|
|
@@ -61,6 +97,8 @@ export function cmdInstall(args, io = {}) {
|
|
|
61
97
|
mcp: mcpResult,
|
|
62
98
|
where: resolved.ok ? resolved.data : null,
|
|
63
99
|
imported: imported || null,
|
|
100
|
+
legacyRemoved: legacy.removed,
|
|
101
|
+
legacyLeftover: legacy.leftover,
|
|
64
102
|
};
|
|
65
103
|
const importLine =
|
|
66
104
|
imported?.copied?.length
|
|
@@ -73,6 +111,13 @@ export function cmdInstall(args, io = {}) {
|
|
|
73
111
|
? `\nMCP: ${CMD} serve registered in ${mcpResult.written.join(", ")}`
|
|
74
112
|
: `\nMCP: config write failed (${mcpResult?.error?.message ?? "unknown"}) — add \`${CMD} serve\` manually`
|
|
75
113
|
: "";
|
|
114
|
+
const legacyLine = legacy.removed.length
|
|
115
|
+
? `\nremoved ${legacy.removed.length} Balakit Mental leftover(s)`
|
|
116
|
+
: "";
|
|
117
|
+
const leftoverLine =
|
|
118
|
+
legacy.leftover.length
|
|
119
|
+
? `\nstill mixed Balakit block(s) (Mental text inside a kit block): ${legacy.leftover.join(", ")}`
|
|
120
|
+
: "";
|
|
76
121
|
printResult(
|
|
77
122
|
stdout,
|
|
78
123
|
args.json,
|
|
@@ -80,7 +125,7 @@ export function cmdInstall(args, io = {}) {
|
|
|
80
125
|
data,
|
|
81
126
|
undefined,
|
|
82
127
|
() =>
|
|
83
|
-
`${brandLine(`installed skill + rule (${installed.written.length} paths)`)}\n~/.mental skeleton: ${personal}${cliLine}${hookLine}${mcpLine}${importLine}`,
|
|
128
|
+
`${brandLine(`installed skill + rule (${installed.written.length} paths)`)}\n~/.mental skeleton: ${personal}${cliLine}${hookLine}${mcpLine}${importLine}${legacyLine}${leftoverLine}`,
|
|
84
129
|
);
|
|
85
130
|
return 0;
|
|
86
131
|
}
|
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 +
|
|
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.
|
package/bin/lib/install-cli.mjs
CHANGED
|
@@ -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
|
-
[
|
|
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,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find and remove Balakit-era Mental *tooling* (skill + always-on rule).
|
|
3
|
+
* Never deletes OKF data (`~/.mental` or `./.mental`).
|
|
4
|
+
*
|
|
5
|
+
* Balakit installed the personal Mental rule user-wide (`~/.cursor/rules/mental.mdc`,
|
|
6
|
+
* managed `BEGIN balakit` blocks) and the skill via skills.sh (`-g`) into agent
|
|
7
|
+
* skill dirs. Those copies still tell agents to `npx balakit doctor` and fight
|
|
8
|
+
* the standalone CLI.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, lstatSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
const BALAKIT_BEGIN_RE = /<!-- BEGIN balakit\b[^>]*-->/;
|
|
14
|
+
const BALAKIT_BLOCK_RE =
|
|
15
|
+
/<!-- BEGIN balakit\b[^>]*-->\r?\n[\s\S]*?<!-- END balakit\b[^>]*-->/g;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Distinctive strings from the Balakit `mental` rule/skill (removed from the
|
|
19
|
+
* kit in 02bf1c2). Our current skill/rule never contain these.
|
|
20
|
+
* @param {string} text
|
|
21
|
+
*/
|
|
22
|
+
export function isBalakitMentalText(text) {
|
|
23
|
+
if (!text) return false;
|
|
24
|
+
return (
|
|
25
|
+
/npx balakit doctor/i.test(text) ||
|
|
26
|
+
/mentalDataPolicy/.test(text) ||
|
|
27
|
+
/\.balakit\/installed\.json/.test(text) ||
|
|
28
|
+
/Respect the installed Mental data policy/.test(text) ||
|
|
29
|
+
/The `\.mental\/` Project Continuity Layer/.test(text) ||
|
|
30
|
+
/plugins\/balakit-mental/.test(text)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** User-global skill dirs skills.sh used for a global `mental` install. */
|
|
35
|
+
export function userSkillCandidates(home) {
|
|
36
|
+
return [
|
|
37
|
+
join(home, ".claude", "skills", "mental"),
|
|
38
|
+
join(home, ".cursor", "skills", "mental"),
|
|
39
|
+
join(home, ".agents", "skills", "mental"),
|
|
40
|
+
join(home, ".codex", "skills", "mental"),
|
|
41
|
+
join(home, ".config", "opencode", "skills", "mental"),
|
|
42
|
+
join(home, ".gemini", "skills", "mental"),
|
|
43
|
+
join(home, ".codeium", "windsurf", "skills", "mental"),
|
|
44
|
+
join(home, ".kilocode", "skills", "mental"),
|
|
45
|
+
join(home, ".continue", "skills", "mental"),
|
|
46
|
+
join(home, ".pi", "agent", "skills", "mental"),
|
|
47
|
+
join(home, ".amp", "skills", "mental"),
|
|
48
|
+
join(home, ".roo", "skills", "mental"),
|
|
49
|
+
join(home, ".windsurf", "skills", "mental"),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function projectSkillCandidates(root) {
|
|
54
|
+
if (!root) return [];
|
|
55
|
+
return [
|
|
56
|
+
join(root, ".claude", "skills", "mental"),
|
|
57
|
+
join(root, ".cursor", "skills", "mental"),
|
|
58
|
+
join(root, ".agents", "skills", "mental"),
|
|
59
|
+
join(root, ".github", "skills", "mental"),
|
|
60
|
+
join(root, "plugins", "balakit-mental"),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function userRuleCandidates(home) {
|
|
65
|
+
return [join(home, ".cursor", "rules", "mental.mdc")];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function projectRuleCandidates(root) {
|
|
69
|
+
if (!root) return [];
|
|
70
|
+
return [
|
|
71
|
+
join(root, ".cursor", "rules", "mental.mdc"),
|
|
72
|
+
join(root, ".cursor", "rules", "balakit-mental.mdc"),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function managedDocCandidates(home, projectDir) {
|
|
77
|
+
const files = [
|
|
78
|
+
join(home, ".claude", "CLAUDE.md"),
|
|
79
|
+
join(home, ".codex", "AGENTS.md"),
|
|
80
|
+
join(home, ".agents", "AGENTS.md"),
|
|
81
|
+
];
|
|
82
|
+
if (projectDir) {
|
|
83
|
+
files.push(join(projectDir, "AGENTS.md"), join(projectDir, "CLAUDE.md"));
|
|
84
|
+
}
|
|
85
|
+
return files;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pluginCandidates(home) {
|
|
89
|
+
return [join(home, ".cursor", "plugins", "local", "balakit-mental")];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function skillMarkdown(dir) {
|
|
93
|
+
try {
|
|
94
|
+
return readFileSync(join(dir, "SKILL.md"), "utf8");
|
|
95
|
+
} catch {
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readIfFile(file) {
|
|
101
|
+
try {
|
|
102
|
+
const st = lstatSync(file);
|
|
103
|
+
if (!st.isFile()) return "";
|
|
104
|
+
return readFileSync(file, "utf8");
|
|
105
|
+
} catch {
|
|
106
|
+
return "";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isDir(p) {
|
|
111
|
+
try {
|
|
112
|
+
return lstatSync(p).isDirectory();
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A Balakit managed block that is only the old Mental rule (safe to delete).
|
|
120
|
+
* Mixed kit blocks (base/testing + mental) are reported, not stripped.
|
|
121
|
+
* @param {string} block
|
|
122
|
+
*/
|
|
123
|
+
export function isMentalOnlyBalakitBlock(block) {
|
|
124
|
+
if (!isBalakitMentalText(block)) return false;
|
|
125
|
+
const headings = block.match(/^# .+$/gm) || [];
|
|
126
|
+
return headings.length <= 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {{ home: string, projectDir?: string | null }} opts
|
|
131
|
+
* @returns {{
|
|
132
|
+
* skills: string[],
|
|
133
|
+
* rules: string[],
|
|
134
|
+
* plugins: string[],
|
|
135
|
+
* blocks: Array<{ file: string, strip: boolean }>,
|
|
136
|
+
* }}
|
|
137
|
+
*/
|
|
138
|
+
export function findBalakitMental({ home, projectDir = null }) {
|
|
139
|
+
/** @type {string[]} */
|
|
140
|
+
const skills = [];
|
|
141
|
+
/** @type {string[]} */
|
|
142
|
+
const rules = [];
|
|
143
|
+
/** @type {string[]} */
|
|
144
|
+
const plugins = [];
|
|
145
|
+
/** @type {Array<{ file: string, strip: boolean }>} */
|
|
146
|
+
const blocks = [];
|
|
147
|
+
|
|
148
|
+
for (const dir of [...userSkillCandidates(home), ...projectSkillCandidates(projectDir)]) {
|
|
149
|
+
if (isDir(dir) && isBalakitMentalText(skillMarkdown(dir))) skills.push(dir);
|
|
150
|
+
}
|
|
151
|
+
for (const file of [...userRuleCandidates(home), ...projectRuleCandidates(projectDir)]) {
|
|
152
|
+
if (isBalakitMentalText(readIfFile(file))) rules.push(file);
|
|
153
|
+
}
|
|
154
|
+
for (const dir of pluginCandidates(home)) {
|
|
155
|
+
if (existsSync(dir)) plugins.push(dir);
|
|
156
|
+
}
|
|
157
|
+
for (const file of managedDocCandidates(home, projectDir)) {
|
|
158
|
+
const cur = readIfFile(file);
|
|
159
|
+
if (!BALAKIT_BEGIN_RE.test(cur) || !isBalakitMentalText(cur)) continue;
|
|
160
|
+
const m = cur.match(BALAKIT_BLOCK_RE);
|
|
161
|
+
const block = m ? m.join("\n") : cur;
|
|
162
|
+
blocks.push({ file, strip: isMentalOnlyBalakitBlock(block) });
|
|
163
|
+
}
|
|
164
|
+
return { skills, rules, plugins, blocks };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function rmPath(p) {
|
|
168
|
+
rmSync(p, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Drop Mental-only `BEGIN balakit` blocks. Leave mixed kit blocks intact.
|
|
173
|
+
* @param {string} file
|
|
174
|
+
*/
|
|
175
|
+
export function stripMentalOnlyBalakitBlock(file) {
|
|
176
|
+
const cur = readIfFile(file);
|
|
177
|
+
if (!cur) return false;
|
|
178
|
+
const next = cur.replace(BALAKIT_BLOCK_RE, (block) =>
|
|
179
|
+
isMentalOnlyBalakitBlock(block) ? "" : block,
|
|
180
|
+
);
|
|
181
|
+
if (next === cur) return false;
|
|
182
|
+
writeFileSync(file, next.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, ""));
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Remove Balakit Mental skill/rule copies. Call *before* writing the new ones.
|
|
188
|
+
* @param {{ home: string, projectDir?: string | null }} opts
|
|
189
|
+
*/
|
|
190
|
+
export function purgeBalakitMental({ home, projectDir = null }) {
|
|
191
|
+
const found = findBalakitMental({ home, projectDir });
|
|
192
|
+
/** @type {string[]} */
|
|
193
|
+
const removed = [];
|
|
194
|
+
/** @type {string[]} */
|
|
195
|
+
const leftover = [];
|
|
196
|
+
|
|
197
|
+
for (const p of [...found.skills, ...found.rules, ...found.plugins]) {
|
|
198
|
+
try {
|
|
199
|
+
rmPath(p);
|
|
200
|
+
removed.push(p);
|
|
201
|
+
} catch {
|
|
202
|
+
leftover.push(p);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const b of found.blocks) {
|
|
206
|
+
if (b.strip) {
|
|
207
|
+
if (stripMentalOnlyBalakitBlock(b.file)) removed.push(b.file);
|
|
208
|
+
else leftover.push(b.file);
|
|
209
|
+
} else {
|
|
210
|
+
leftover.push(b.file);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { removed, leftover, found };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** @param {{ home: string, projectDir?: string | null }} opts */
|
|
217
|
+
export function leftoverBalakitMentalCount(opts) {
|
|
218
|
+
const f = findBalakitMental(opts);
|
|
219
|
+
return f.skills.length + f.rules.length + f.plugins.length + f.blocks.length;
|
|
220
|
+
}
|
|
@@ -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
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.
|
|
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
|
|
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.
|
package/skills/mental/SKILL.md
CHANGED
|
@@ -87,39 +87,48 @@ If you invoked `mental` this turn, end the **user-visible** reply with this bloc
|
|
|
87
87
|
Last thing in the message. Not a code fence (so emojis render).
|
|
88
88
|
Skip the whole block if you did not run `mental`.
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
Cursor markdown joins adjacent lines into one paragraph. A lone `<br>`
|
|
91
|
+
becomes a break *inside* that paragraph, so `🧠 Mental` and the item
|
|
92
|
+
collapse to one line. Do **not** put the title and items on the same line.
|
|
93
|
+
|
|
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.
|
|
98
|
+
Title line is `🧠 Mental` plus **two trailing spaces** (markdown hard break).
|
|
99
|
+
Each item is indented two spaces: `emoji Kind: Verb “title”`.
|
|
100
|
+
At most four item lines; if more, keep the writes and end with `+N more`.
|
|
94
101
|
|
|
95
102
|
Titles only — no `file://` or markdown links. Mental files live in `~/.mental`,
|
|
96
103
|
not this repo, so links would 404. The CLI tool card already has the path.
|
|
97
104
|
|
|
98
|
-
**Writes (copy this shape):**
|
|
105
|
+
**Writes (copy this shape, including the two spaces after `Mental`):**
|
|
99
106
|
|
|
100
107
|
```text
|
|
101
|
-
|
|
102
|
-
🧠 Mental
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
🎯
|
|
108
|
+
────────
|
|
109
|
+
🧠 Mental
|
|
110
|
+
🚦 Attention: Recorded “Tom said ship”
|
|
111
|
+
📓 Journal: Recorded “Resolver landed”
|
|
112
|
+
🎯 Decision: Decided “Keep the JSON envelope”
|
|
113
|
+
────────
|
|
106
114
|
```
|
|
107
115
|
|
|
108
116
|
**Read-only (heartbeat / search / show / list):**
|
|
109
117
|
|
|
110
118
|
```text
|
|
111
|
-
|
|
112
|
-
🧠 Mental
|
|
113
|
-
🔍
|
|
119
|
+
────────
|
|
120
|
+
🧠 Mental
|
|
121
|
+
🔍 Read: Heartbeat
|
|
122
|
+
────────
|
|
114
123
|
```
|
|
115
124
|
|
|
116
125
|
| Emoji | Kind | Item line |
|
|
117
126
|
| --- | --- | --- |
|
|
118
|
-
| 📓 | Journal |
|
|
119
|
-
| 🚦 | Attention |
|
|
120
|
-
| 🎯 | Decision |
|
|
121
|
-
| 📝 | Note |
|
|
122
|
-
| 🔍 | Read |
|
|
127
|
+
| 📓 | Journal | ` 📓 Journal: Recorded “<title>”` |
|
|
128
|
+
| 🚦 | Attention | ` 🚦 Attention: Recorded “<title>”` or ` 🚦 Attention: Resolved “<title>”` |
|
|
129
|
+
| 🎯 | Decision | ` 🎯 Decision: Decided “<title>”` or ` 🎯 Decision: Opened “<title>”` |
|
|
130
|
+
| 📝 | Note | ` 📝 Note: Recorded “<title>”` |
|
|
131
|
+
| 🔍 | Read | ` 🔍 Read: Heartbeat` / `Searched` / `Showed` / `Listed` |
|
|
123
132
|
|
|
124
133
|
Mix writes and a read in one block if both happened. Never invent Mental activity.
|
|
125
134
|
|