@balacode/mental 0.2.0
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/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.cursor-plugin/plugin.json +21 -0
- package/.mcp.json +8 -0
- package/CHANGELOG.md +42 -0
- package/LICENSE +21 -0
- package/README.md +277 -0
- package/assets/logo.svg +19 -0
- package/bin/cli.mjs +135 -0
- package/bin/commands/attention.mjs +139 -0
- package/bin/commands/decide.mjs +104 -0
- package/bin/commands/doctor.mjs +150 -0
- package/bin/commands/heartbeat.mjs +21 -0
- package/bin/commands/hooks.mjs +41 -0
- package/bin/commands/install.mjs +86 -0
- package/bin/commands/journal.mjs +54 -0
- package/bin/commands/link.mjs +18 -0
- package/bin/commands/list.mjs +51 -0
- package/bin/commands/local.mjs +118 -0
- package/bin/commands/note.mjs +61 -0
- package/bin/commands/reindex.mjs +48 -0
- package/bin/commands/remap.mjs +76 -0
- package/bin/commands/search.mjs +55 -0
- package/bin/commands/serve.mjs +16 -0
- package/bin/commands/show.mjs +61 -0
- package/bin/commands/split.mjs +56 -0
- package/bin/commands/status.mjs +136 -0
- package/bin/commands/uninstall.mjs +58 -0
- package/bin/commands/where.mjs +29 -0
- package/bin/lib/args.mjs +117 -0
- package/bin/lib/bindings.mjs +404 -0
- package/bin/lib/entry.mjs +35 -0
- package/bin/lib/git.mjs +149 -0
- package/bin/lib/heartbeat.mjs +118 -0
- package/bin/lib/hooks.mjs +144 -0
- package/bin/lib/ignore.mjs +122 -0
- package/bin/lib/import-legacy.mjs +183 -0
- package/bin/lib/index.mjs +574 -0
- package/bin/lib/install-cli.mjs +100 -0
- package/bin/lib/install-skills.mjs +120 -0
- package/bin/lib/mcp.mjs +389 -0
- package/bin/lib/okf.mjs +746 -0
- package/bin/lib/output.mjs +112 -0
- package/bin/lib/pkg.mjs +22 -0
- package/bin/lib/resolve.mjs +302 -0
- package/bin/lib/uninstall.mjs +56 -0
- package/hooks/session-start.sh +4 -0
- package/mcp.json +11 -0
- package/package.json +43 -0
- package/plugin.json +21 -0
- package/rules/mental.mdc +18 -0
- package/skills/mental/SKILL.md +277 -0
- package/skills/mental/references/templates.md +186 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable agent JSON envelope + human printers.
|
|
3
|
+
* Emoji is TTY-only. `--json` never includes a brand mark.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `MENTAL_ASCII=1` for consoles that cannot render emoji (legacy cmd.exe).
|
|
8
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
9
|
+
*/
|
|
10
|
+
export function useAsciiBrand(env = process.env) {
|
|
11
|
+
const v = env.MENTAL_ASCII;
|
|
12
|
+
return v === "1" || v === "true" || v === "yes";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
17
|
+
*/
|
|
18
|
+
export function brandMark(env = process.env) {
|
|
19
|
+
return useAsciiBrand(env) ? "[mental]" : "🧠";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const KIND_EMOJI = {
|
|
23
|
+
journal: "📓",
|
|
24
|
+
attention: "🚦",
|
|
25
|
+
decision: "🎯",
|
|
26
|
+
note: "📝",
|
|
27
|
+
read: "🔍",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const KIND_ASCII = {
|
|
31
|
+
journal: "[journal]",
|
|
32
|
+
attention: "[attention]",
|
|
33
|
+
decision: "[decision]",
|
|
34
|
+
note: "[note]",
|
|
35
|
+
read: "[read]",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type mark for TTY writes/reads. `--json` must not call this.
|
|
40
|
+
* @param {"journal" | "attention" | "decision" | "note" | "read"} kind
|
|
41
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
42
|
+
*/
|
|
43
|
+
export function kindMark(kind, env = process.env) {
|
|
44
|
+
if (useAsciiBrand(env)) return KIND_ASCII[kind] || "[mental]";
|
|
45
|
+
return KIND_EMOJI[kind] || brandMark(env);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {"journal" | "attention" | "decision" | "note" | "read"} kind
|
|
50
|
+
* @param {string} text
|
|
51
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
52
|
+
*/
|
|
53
|
+
export function kindLine(kind, text, env = process.env) {
|
|
54
|
+
return `${kindMark(kind, env)} ${text}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Prefix a TTY success line with the Mental mark.
|
|
59
|
+
* @param {string} text
|
|
60
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
61
|
+
*/
|
|
62
|
+
export function brandLine(text, env = process.env) {
|
|
63
|
+
return `${brandMark(env)} ${text}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {boolean} ok
|
|
68
|
+
* @param {object} [data]
|
|
69
|
+
* @param {{ code: string, message: string }} [error]
|
|
70
|
+
*/
|
|
71
|
+
export function envelope(ok, data, error) {
|
|
72
|
+
return ok ? { ok: true, data } : { ok: false, error };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {NodeJS.WritableStream} out
|
|
77
|
+
* @param {boolean} json
|
|
78
|
+
* @param {boolean} ok
|
|
79
|
+
* @param {object} [data]
|
|
80
|
+
* @param {{ code: string, message: string }} [error]
|
|
81
|
+
* @param {(data: object) => string} [format]
|
|
82
|
+
*/
|
|
83
|
+
export function printResult(out, json, ok, data, error, format) {
|
|
84
|
+
if (json) {
|
|
85
|
+
out.write(`${JSON.stringify(envelope(ok, data, error))}\n`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!ok) {
|
|
89
|
+
out.write(`${error?.message || "error"}\n`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
out.write(`${format ? format(data) : JSON.stringify(data, null, 2)}\n`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @param {import('./resolve.mjs').WhereData} data */
|
|
96
|
+
export function formatWhere(data) {
|
|
97
|
+
const id = data.id ?? "—";
|
|
98
|
+
const lines = [
|
|
99
|
+
`root: ${data.root}`,
|
|
100
|
+
`id: ${id}`,
|
|
101
|
+
`mode: ${data.mode}`,
|
|
102
|
+
`reason: ${data.reason}`,
|
|
103
|
+
`gitRoot: ${data.gitRoot ?? "—"}`,
|
|
104
|
+
];
|
|
105
|
+
if (data.imported?.copied?.length) {
|
|
106
|
+
lines.push(`imported: ${data.imported.copied.length} file(s) from ${data.imported.from}`);
|
|
107
|
+
}
|
|
108
|
+
if (data.indexed?.ok) {
|
|
109
|
+
lines.push(`index: ${data.indexed.concepts} concept(s) → ${data.indexed.path}`);
|
|
110
|
+
}
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
package/bin/lib/pkg.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package identity shared across the CLI.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
/** Absolute path to the package root (parent of `bin/`). */
|
|
9
|
+
export const PKG_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
10
|
+
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
12
|
+
|
|
13
|
+
export const NAME = pkg.name;
|
|
14
|
+
export const CMD = Object.keys(pkg.bin ?? {})[0] ?? "mental";
|
|
15
|
+
export const VERSION = pkg.version;
|
|
16
|
+
|
|
17
|
+
export const SKILLS_DIR = join(PKG_ROOT, "skills");
|
|
18
|
+
export const RULES_DIR = join(PKG_ROOT, "rules");
|
|
19
|
+
|
|
20
|
+
/** Managed-block markers for user AGENTS.md / CLAUDE.md. */
|
|
21
|
+
export const BEGIN = `<!-- BEGIN ${CMD} (managed — edits inside are overwritten on reinstall) -->`;
|
|
22
|
+
export const END = `<!-- END ${CMD} -->`;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic active-bundle resolution. Exclusive nearest-wins — no overlay.
|
|
3
|
+
*
|
|
4
|
+
* Order: --dir / MENTAL_DIR → opted-in ./.mental/ (`mental local`) → git binding
|
|
5
|
+
* → personal ~/.mental.
|
|
6
|
+
*
|
|
7
|
+
* Leftover Balakit `./.mental` (no `.mental-local` marker) is imported into
|
|
8
|
+
* `~/.mental/projects/<uuid>/` on write resolve. Source is never deleted.
|
|
9
|
+
*/
|
|
10
|
+
import { statSync } from "node:fs";
|
|
11
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
12
|
+
import { findGitRoot, getRemoteUrl } from "./git.mjs";
|
|
13
|
+
import {
|
|
14
|
+
projectSliceDir,
|
|
15
|
+
recordLegacyImport,
|
|
16
|
+
resolveOrCreateBinding,
|
|
17
|
+
userMentalDir,
|
|
18
|
+
} from "./bindings.mjs";
|
|
19
|
+
import { importLegacyBundle, isOptedInLocal } from "./import-legacy.mjs";
|
|
20
|
+
import { reindexBundle } from "./index.mjs";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {'env' | 'local' | 'home' | 'personal'} ResolveMode
|
|
24
|
+
*
|
|
25
|
+
* @typedef {{
|
|
26
|
+
* from: string,
|
|
27
|
+
* to: string,
|
|
28
|
+
* copied: string[],
|
|
29
|
+
* skipped: number,
|
|
30
|
+
* error?: string,
|
|
31
|
+
* }} ImportResult
|
|
32
|
+
*
|
|
33
|
+
* @typedef {{
|
|
34
|
+
* root: string,
|
|
35
|
+
* id: string | null,
|
|
36
|
+
* mode: ResolveMode,
|
|
37
|
+
* reason: string,
|
|
38
|
+
* gitRoot: string | null,
|
|
39
|
+
* imported?: ImportResult | null,
|
|
40
|
+
* indexed?: { ok: boolean, path: string | null, concepts: number, backend: string, error?: string },
|
|
41
|
+
* }} WhereData
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
function isDir(p) {
|
|
45
|
+
try {
|
|
46
|
+
return statSync(p).isDirectory();
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Walk cwd → git root (or filesystem root, stop at $HOME) for a project
|
|
54
|
+
* `.mental/` directory. Never treat the user store `~/.mental` as a local
|
|
55
|
+
* project bundle (that would silently overlay every non-git cwd under home).
|
|
56
|
+
*
|
|
57
|
+
* @param {string} cwd
|
|
58
|
+
* @param {{ home: string, gitRoot: string | null }} opts
|
|
59
|
+
* @returns {string | null} absolute path to the `.mental` directory
|
|
60
|
+
*/
|
|
61
|
+
export function findLocalMental(cwd, { home, gitRoot }) {
|
|
62
|
+
const start = resolve(cwd);
|
|
63
|
+
const homeAbs = resolve(home);
|
|
64
|
+
const userStore = resolve(userMentalDir(home));
|
|
65
|
+
const gitAbs = gitRoot ? resolve(gitRoot) : null;
|
|
66
|
+
const { root: fsRoot } = parse(start);
|
|
67
|
+
|
|
68
|
+
let dir = start;
|
|
69
|
+
while (true) {
|
|
70
|
+
const candidate = join(dir, ".mental");
|
|
71
|
+
if (isDir(candidate)) {
|
|
72
|
+
const abs = resolve(candidate);
|
|
73
|
+
if (abs !== userStore) return abs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (gitAbs && dir === gitAbs) break;
|
|
77
|
+
if (dir === homeAbs) break;
|
|
78
|
+
if (dir === fsRoot) break;
|
|
79
|
+
|
|
80
|
+
const parent = dirname(dir);
|
|
81
|
+
if (parent === dir) break;
|
|
82
|
+
dir = parent;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {{
|
|
89
|
+
* gitRoot: string,
|
|
90
|
+
* home: string,
|
|
91
|
+
* env: NodeJS.ProcessEnv,
|
|
92
|
+
* write: boolean,
|
|
93
|
+
* }} opts
|
|
94
|
+
*/
|
|
95
|
+
function bindGit({ gitRoot, home, env, write }) {
|
|
96
|
+
const origin = getRemoteUrl(gitRoot, "origin", { env });
|
|
97
|
+
const upstream = getRemoteUrl(gitRoot, "upstream", { env });
|
|
98
|
+
return resolveOrCreateBinding({
|
|
99
|
+
gitRoot,
|
|
100
|
+
origin,
|
|
101
|
+
upstream,
|
|
102
|
+
home,
|
|
103
|
+
write,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Copy leftover `./.mental` into the UUID slice. Fail open: import errors
|
|
109
|
+
* do not block resolve.
|
|
110
|
+
*
|
|
111
|
+
* @param {{ leftover: string, dest: string, home: string, id: string }} opts
|
|
112
|
+
* @returns {ImportResult}
|
|
113
|
+
*/
|
|
114
|
+
function importLeftover({ leftover, dest, home, id }) {
|
|
115
|
+
try {
|
|
116
|
+
const result = importLegacyBundle(leftover, dest);
|
|
117
|
+
recordLegacyImport(home, id, leftover, { copied: result.copied });
|
|
118
|
+
return result;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return {
|
|
121
|
+
from: leftover,
|
|
122
|
+
to: dest,
|
|
123
|
+
copied: [],
|
|
124
|
+
skipped: 0,
|
|
125
|
+
error: err instanceof Error ? err.message : String(err),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {WhereData} data
|
|
132
|
+
* @param {{ write: boolean, home: string, env: NodeJS.ProcessEnv }} ctx
|
|
133
|
+
*/
|
|
134
|
+
function withIndex(data, { write, home, env }) {
|
|
135
|
+
if (write && data.id && home) {
|
|
136
|
+
data.indexed = reindexBundle({ root: data.root, id: data.id, home, env });
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, data };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {{
|
|
143
|
+
* cwd?: string,
|
|
144
|
+
* home?: string | null,
|
|
145
|
+
* env?: NodeJS.ProcessEnv,
|
|
146
|
+
* dir?: string | null,
|
|
147
|
+
* write?: boolean,
|
|
148
|
+
* }} [opts]
|
|
149
|
+
* @returns {{ ok: true, data: WhereData } | { ok: false, error: { code: string, message: string } }}
|
|
150
|
+
*/
|
|
151
|
+
export function resolveBundle({
|
|
152
|
+
cwd = process.cwd(),
|
|
153
|
+
home = process.env.HOME ?? process.env.USERPROFILE ?? null,
|
|
154
|
+
env = process.env,
|
|
155
|
+
dir = null,
|
|
156
|
+
write = true,
|
|
157
|
+
} = {}) {
|
|
158
|
+
const gitRoot = findGitRoot(cwd, { env });
|
|
159
|
+
|
|
160
|
+
const dirOverride = dir || env.MENTAL_DIR || null;
|
|
161
|
+
if (dirOverride) {
|
|
162
|
+
const abs = resolve(cwd, dirOverride);
|
|
163
|
+
if (!isDir(abs)) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
error: {
|
|
167
|
+
code: "env-dir-missing",
|
|
168
|
+
message: `MENTAL_DIR / --dir is not a directory: ${abs}`,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
data: {
|
|
175
|
+
root: abs,
|
|
176
|
+
id: null,
|
|
177
|
+
mode: "env",
|
|
178
|
+
reason: dir ? `--dir ${abs}` : `MENTAL_DIR=${abs}`,
|
|
179
|
+
gitRoot,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!home) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
error: {
|
|
188
|
+
code: "no-home",
|
|
189
|
+
message: "HOME is unset; Mental will not write. Set HOME or MENTAL_DIR.",
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const leftover = findLocalMental(cwd, { home, gitRoot });
|
|
195
|
+
const optedIn = Boolean(leftover && isOptedInLocal(leftover));
|
|
196
|
+
|
|
197
|
+
if (optedIn && leftover) {
|
|
198
|
+
let id = null;
|
|
199
|
+
let reason = `walk-up found opted-in ${leftover}`;
|
|
200
|
+
if (gitRoot) {
|
|
201
|
+
try {
|
|
202
|
+
const bound = bindGit({ gitRoot, home, env, write });
|
|
203
|
+
if (!bound.ok) {
|
|
204
|
+
return { ok: false, error: { code: bound.code, message: bound.message } };
|
|
205
|
+
}
|
|
206
|
+
if (bound.id) {
|
|
207
|
+
id = bound.id;
|
|
208
|
+
reason = `${reason}; ${bound.reason}`;
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
error: {
|
|
214
|
+
code: "bindings",
|
|
215
|
+
message: err instanceof Error ? err.message : String(err),
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return withIndex(
|
|
221
|
+
{
|
|
222
|
+
root: leftover,
|
|
223
|
+
id,
|
|
224
|
+
mode: "local",
|
|
225
|
+
reason,
|
|
226
|
+
gitRoot,
|
|
227
|
+
},
|
|
228
|
+
{ write, home, env },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (gitRoot) {
|
|
233
|
+
try {
|
|
234
|
+
const bound = bindGit({ gitRoot, home, env, write });
|
|
235
|
+
if (!bound.ok) {
|
|
236
|
+
return { ok: false, error: { code: bound.code, message: bound.message } };
|
|
237
|
+
}
|
|
238
|
+
if (!bound.id) {
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
data: {
|
|
242
|
+
root: join(userMentalDir(home), "projects"),
|
|
243
|
+
id: null,
|
|
244
|
+
mode: "home",
|
|
245
|
+
reason: leftover
|
|
246
|
+
? `${bound.reason}; leftover ${leftover} imports on next write`
|
|
247
|
+
: bound.reason,
|
|
248
|
+
gitRoot,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const dest = projectSliceDir(home, bound.id);
|
|
253
|
+
/** @type {ImportResult | null} */
|
|
254
|
+
let imported = null;
|
|
255
|
+
let reason = bound.reason;
|
|
256
|
+
if (write && leftover) {
|
|
257
|
+
imported = importLeftover({ leftover, dest, home, id: bound.id });
|
|
258
|
+
const n = imported.copied.length;
|
|
259
|
+
reason =
|
|
260
|
+
n > 0
|
|
261
|
+
? `${bound.reason}; imported ${n} leftover file(s) from ${leftover}`
|
|
262
|
+
: imported.error
|
|
263
|
+
? `${bound.reason}; leftover import failed: ${imported.error}`
|
|
264
|
+
: `${bound.reason}; leftover ${leftover} already imported`;
|
|
265
|
+
} else if (leftover) {
|
|
266
|
+
reason = `${bound.reason}; leftover ${leftover} imports on next write`;
|
|
267
|
+
}
|
|
268
|
+
return withIndex(
|
|
269
|
+
{
|
|
270
|
+
root: dest,
|
|
271
|
+
id: bound.id,
|
|
272
|
+
mode: "home",
|
|
273
|
+
reason,
|
|
274
|
+
gitRoot,
|
|
275
|
+
imported,
|
|
276
|
+
},
|
|
277
|
+
{ write, home, env },
|
|
278
|
+
);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
error: {
|
|
283
|
+
code: "bindings",
|
|
284
|
+
message: err instanceof Error ? err.message : String(err),
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
ok: true,
|
|
292
|
+
data: {
|
|
293
|
+
root: userMentalDir(home),
|
|
294
|
+
id: null,
|
|
295
|
+
mode: "personal",
|
|
296
|
+
reason: leftover
|
|
297
|
+
? `not a git repo; leftover ${leftover} not imported (need a git root)`
|
|
298
|
+
: "not a git repo; personal ~/.mental",
|
|
299
|
+
gitRoot: null,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove Mental skill+rule copies from user agent dirs. Never deletes OKF
|
|
3
|
+
* unless the uninstall command also gets `--delete-data DELETE`.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { BEGIN, END } from "./pkg.mjs";
|
|
8
|
+
import { userInstallTargets } from "./install-skills.mjs";
|
|
9
|
+
|
|
10
|
+
function esc(s) {
|
|
11
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Strip the managed BEGIN/END block from AGENTS.md / CLAUDE.md.
|
|
16
|
+
* @param {string} file
|
|
17
|
+
*/
|
|
18
|
+
export function removeManaged(file) {
|
|
19
|
+
if (!existsSync(file)) return false;
|
|
20
|
+
const cur = readFileSync(file, "utf8");
|
|
21
|
+
if (!cur.includes(BEGIN) || !cur.includes(END)) return false;
|
|
22
|
+
const re = new RegExp(`\\n?${esc(BEGIN)}[\\s\\S]*?${esc(END)}\\n?`);
|
|
23
|
+
const next = cur.replace(re, "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
24
|
+
writeFileSync(file, next);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {{ home: string, projectDir?: string | null }} opts
|
|
30
|
+
*/
|
|
31
|
+
export function uninstallSkills({ home, projectDir = null }) {
|
|
32
|
+
const targets = userInstallTargets(home);
|
|
33
|
+
/** @type {string[]} */
|
|
34
|
+
const removed = [];
|
|
35
|
+
for (const dest of targets.skills) {
|
|
36
|
+
if (existsSync(dest)) {
|
|
37
|
+
rmSync(dest, { recursive: true, force: true });
|
|
38
|
+
removed.push(dest);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (existsSync(targets.cursorRule)) {
|
|
42
|
+
rmSync(targets.cursorRule, { force: true });
|
|
43
|
+
removed.push(targets.cursorRule);
|
|
44
|
+
}
|
|
45
|
+
for (const doc of targets.managedDocs) {
|
|
46
|
+
if (removeManaged(doc)) removed.push(doc);
|
|
47
|
+
}
|
|
48
|
+
if (projectDir) {
|
|
49
|
+
const vendored = join(projectDir, ".github", "skills", "mental");
|
|
50
|
+
if (existsSync(vendored)) {
|
|
51
|
+
rmSync(vendored, { recursive: true, force: true });
|
|
52
|
+
removed.push(vendored);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { ok: true, removed };
|
|
56
|
+
}
|
package/mcp.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@balacode/mental",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Never reconstruct where you left off. Mental keeps the resume, the decisions, and what's still in the air.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mental": "bin/cli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test test/*.test.mjs",
|
|
15
|
+
"mental": "node bin/cli.mjs"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"skills",
|
|
20
|
+
"rules",
|
|
21
|
+
"hooks",
|
|
22
|
+
"plugin.json",
|
|
23
|
+
"mcp.json",
|
|
24
|
+
"assets",
|
|
25
|
+
".cursor-plugin",
|
|
26
|
+
".claude-plugin",
|
|
27
|
+
".mcp.json",
|
|
28
|
+
"README.md",
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/afaraha8403/mental.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/afaraha8403/mental#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/afaraha8403/mental/issues"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "mental",
|
|
4
|
+
"version": "0.2.0",
|
|
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
|
+
"author": {
|
|
7
|
+
"name": "Ali Farahat",
|
|
8
|
+
"url": "https://github.com/afaraha8403"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/afaraha8403/mental#readme",
|
|
11
|
+
"repository": "https://github.com/afaraha8403/mental",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"continuity",
|
|
15
|
+
"journal",
|
|
16
|
+
"decisions",
|
|
17
|
+
"okf",
|
|
18
|
+
"skills",
|
|
19
|
+
"mcp"
|
|
20
|
+
]
|
|
21
|
+
}
|
package/rules/mental.mdc
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Project continuity is Mental. Orient and hand off via the CLI; never grep OKF.
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Continuity is Mental. On start/finish of real work, or orientation questions, use the Mental skill.
|
|
7
|
+
|
|
8
|
+
Run `mental where` then `mental heartbeat --json` (or `mental status --json` when notes are needed). Do not grep `.mental` or `~/.mental`. Do not parse YAML frontmatter yourself.
|
|
9
|
+
|
|
10
|
+
Mid-chat, not just start/finish: before changing an approach, `mental search "…" --json` for prior decisions. When residue surfaces ("X said…", a concern, "park this"), record `mental attention … --json` immediately — do not wait for the handoff. If other agents may have written since you oriented, re-pulse `mental heartbeat --json`; it is cheap and derives git live.
|
|
11
|
+
|
|
12
|
+
If `mental` is not on PATH, try `npx @balacode/mental …`. If that fails, continue the user's coding task and mention install. Missing Mental must not block work.
|
|
13
|
+
|
|
14
|
+
Never commit Mental data. Never write secrets. Never edit gitignore; tell the user to run `mental doctor`.
|
|
15
|
+
|
|
16
|
+
If you invoked `mental` this turn, end the user-visible reply with `<br>`, then the title `🧠 Mental`, then type lines under it (example in the Mental skill). Not a code fence.
|
|
17
|
+
|
|
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.
|