@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental doctor` — PATH, where, bindings, ignore, skill presence.
|
|
3
|
+
* Exit 3 when problems exist (still prints JSON).
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { resolve as resolvePath } from "node:path";
|
|
7
|
+
import { resolveBundle, findLocalMental } from "../lib/resolve.mjs";
|
|
8
|
+
import { loadBindings } from "../lib/bindings.mjs";
|
|
9
|
+
import { checkMentalIgnored, ensureMentalExcluded, gitAvailable } from "../lib/ignore.mjs";
|
|
10
|
+
import { skillsPresent } from "../lib/install-skills.mjs";
|
|
11
|
+
import { printResult, brandMark } from "../lib/output.mjs";
|
|
12
|
+
import { CMD } from "../lib/pkg.mjs";
|
|
13
|
+
import { isOptedInLocal } from "../lib/import-legacy.mjs";
|
|
14
|
+
import { findGitRoot } from "../lib/git.mjs";
|
|
15
|
+
import { indexPath } from "../lib/index.mjs";
|
|
16
|
+
|
|
17
|
+
function check(id, ok, message, level = "error") {
|
|
18
|
+
return { id, ok, level, message };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cmdDoctor(args, io = {}) {
|
|
22
|
+
const stdout = io.stdout ?? process.stdout;
|
|
23
|
+
const home = args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null;
|
|
24
|
+
const cwd = args.cwd ?? process.cwd();
|
|
25
|
+
const env = args.env ?? process.env;
|
|
26
|
+
const fixIgnore = Boolean(args.flags?.["fix-ignore"]);
|
|
27
|
+
|
|
28
|
+
/** @type {ReturnType<typeof check>[]} */
|
|
29
|
+
const checks = [];
|
|
30
|
+
|
|
31
|
+
if (!home) {
|
|
32
|
+
checks.push(check("home", false, "HOME unset; no writes"));
|
|
33
|
+
} else {
|
|
34
|
+
checks.push(check("home", true, home));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
checks.push(
|
|
38
|
+
check("git", gitAvailable({ env }), gitAvailable({ env }) ? "git on PATH" : "git missing"),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (fixIgnore && home) {
|
|
42
|
+
const r = ensureMentalExcluded({ home, env });
|
|
43
|
+
checks.push(
|
|
44
|
+
check(
|
|
45
|
+
"fix-ignore",
|
|
46
|
+
Boolean(r.ok),
|
|
47
|
+
r.ok ? `excludes: ${r.file}` : r.reason || "fix-ignore failed",
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const resolved = resolveBundle({
|
|
53
|
+
cwd,
|
|
54
|
+
home,
|
|
55
|
+
env,
|
|
56
|
+
dir: args.dir ?? null,
|
|
57
|
+
write: false,
|
|
58
|
+
});
|
|
59
|
+
if (resolved.ok) {
|
|
60
|
+
checks.push(check("where", true, `${resolved.data.mode} ${resolved.data.root}`));
|
|
61
|
+
if (resolved.data.mode === "local") {
|
|
62
|
+
const ign = checkMentalIgnored({ cwd, env });
|
|
63
|
+
checks.push(
|
|
64
|
+
check(
|
|
65
|
+
"local-ignore",
|
|
66
|
+
ign.liveIgnored === true,
|
|
67
|
+
ign.liveIgnored
|
|
68
|
+
? ".mental/ is ignored"
|
|
69
|
+
: "local .mental/ is NOT ignored — run mental doctor --fix-ignore",
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (resolved.data.id) {
|
|
74
|
+
const file = indexPath(home, resolved.data.id, env);
|
|
75
|
+
const has = existsSync(file);
|
|
76
|
+
checks.push(
|
|
77
|
+
check(
|
|
78
|
+
"index",
|
|
79
|
+
true,
|
|
80
|
+
has ? `sqlite ${file}` : "no sqlite index yet — run mental where or mental reindex",
|
|
81
|
+
has ? "info" : "warn",
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
checks.push(check("where", false, resolved.error.message));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (home) {
|
|
90
|
+
/** @type {ReturnType<typeof loadBindings> | null} */
|
|
91
|
+
let bindings = null;
|
|
92
|
+
try {
|
|
93
|
+
bindings = loadBindings(home);
|
|
94
|
+
checks.push(check("bindings", true, `${bindings.bindings.length} binding(s)`));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
checks.push(check("bindings", false, err instanceof Error ? err.message : String(err)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
checks.push(
|
|
100
|
+
check(
|
|
101
|
+
"skills",
|
|
102
|
+
skillsPresent(home),
|
|
103
|
+
skillsPresent(home)
|
|
104
|
+
? "skill present in a user agent dir"
|
|
105
|
+
: `run \`${CMD} install\``,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const gitRoot = resolved.ok ? resolved.data.gitRoot : findGitRoot(cwd, { env });
|
|
110
|
+
const leftover = findLocalMental(cwd, { home, gitRoot });
|
|
111
|
+
if (leftover && !isOptedInLocal(leftover)) {
|
|
112
|
+
const leftoverAbs = resolvePath(leftover);
|
|
113
|
+
const imported = Boolean(
|
|
114
|
+
bindings?.bindings.some(
|
|
115
|
+
(x) => x.legacyImportedFrom && resolvePath(x.legacyImportedFrom) === leftoverAbs,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
checks.push(
|
|
119
|
+
check(
|
|
120
|
+
"legacy-import",
|
|
121
|
+
true,
|
|
122
|
+
imported
|
|
123
|
+
? `leftover ${leftover} still on disk; already imported into ~/.mental/projects (not deleted)`
|
|
124
|
+
: `leftover ${leftover} will import into ~/.mental/projects on next write (status/journal/install)`,
|
|
125
|
+
"warn",
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const problems = checks.filter((c) => !c.ok && c.level === "error");
|
|
132
|
+
const data = {
|
|
133
|
+
checks,
|
|
134
|
+
where: resolved.ok ? resolved.data : null,
|
|
135
|
+
problems: problems.length,
|
|
136
|
+
};
|
|
137
|
+
printResult(
|
|
138
|
+
stdout,
|
|
139
|
+
args.json,
|
|
140
|
+
true,
|
|
141
|
+
data,
|
|
142
|
+
undefined,
|
|
143
|
+
(d) =>
|
|
144
|
+
d.checks
|
|
145
|
+
.map((c) => `${c.ok ? "✓" : "✖"} ${c.id}: ${c.message}`)
|
|
146
|
+
.join("\n") +
|
|
147
|
+
(problems.length ? `\n${problems.length} problem(s)` : `\n${brandMark()} doctor clean`),
|
|
148
|
+
);
|
|
149
|
+
return problems.length ? 3 : 0;
|
|
150
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental` with no args on a TTY, or `mental heartbeat` — print the pulse and exit.
|
|
3
|
+
* Agents: `mental heartbeat --json`.
|
|
4
|
+
*/
|
|
5
|
+
import { collectHeartbeat, formatHeartbeat } from "../lib/heartbeat.mjs";
|
|
6
|
+
import { printResult } from "../lib/output.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {{ json: boolean, dir?: string, cwd?: string, home?: string, env?: NodeJS.ProcessEnv }} args
|
|
10
|
+
* @returns {number}
|
|
11
|
+
*/
|
|
12
|
+
export function cmdHeartbeat(args, io = {}) {
|
|
13
|
+
const stdout = io.stdout ?? process.stdout;
|
|
14
|
+
const collected = collectHeartbeat(args);
|
|
15
|
+
if (!collected.ok) {
|
|
16
|
+
printResult(stdout, args.json, false, undefined, collected.error);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
printResult(stdout, args.json, true, collected.data, undefined, formatHeartbeat);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental hooks on|off` — optional session-start snippets. Default off.
|
|
3
|
+
*/
|
|
4
|
+
import { disableHooks, enableHooks } from "../lib/hooks.mjs";
|
|
5
|
+
import { printResult } from "../lib/output.mjs";
|
|
6
|
+
|
|
7
|
+
export function cmdHooks(args, io = {}) {
|
|
8
|
+
const stdout = io.stdout ?? process.stdout;
|
|
9
|
+
const home = args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null;
|
|
10
|
+
if (!home) {
|
|
11
|
+
printResult(stdout, args.json, false, undefined, {
|
|
12
|
+
code: "no-home",
|
|
13
|
+
message: "HOME is unset; Mental will not write hooks.",
|
|
14
|
+
});
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const action = (args.rest[0] || "").toLowerCase();
|
|
19
|
+
if (action !== "on" && action !== "off") {
|
|
20
|
+
printResult(stdout, args.json, false, undefined, {
|
|
21
|
+
code: "usage",
|
|
22
|
+
message: "mental hooks on|off (default off; install does not enable hooks)",
|
|
23
|
+
});
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = action === "on" ? enableHooks(home) : disableHooks(home);
|
|
28
|
+
if (!result.ok) {
|
|
29
|
+
printResult(stdout, args.json, false, undefined, result.error);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
printResult(
|
|
33
|
+
stdout,
|
|
34
|
+
args.json,
|
|
35
|
+
true,
|
|
36
|
+
{ action, ...result },
|
|
37
|
+
undefined,
|
|
38
|
+
() => `hooks ${action}: ${result.written.join(", ") || "nothing to change"}`,
|
|
39
|
+
);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental install` — copy skill + tiny rule to user agent dirs; ~/.mental skeleton.
|
|
3
|
+
* `--hooks` and `--mcp` are optional and default off.
|
|
4
|
+
*/
|
|
5
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
6
|
+
import { userMentalDir } from "../lib/bindings.mjs";
|
|
7
|
+
import { ensureSkeleton } from "../lib/okf.mjs";
|
|
8
|
+
import { installSkills } from "../lib/install-skills.mjs";
|
|
9
|
+
import { installGlobalCli } from "../lib/install-cli.mjs";
|
|
10
|
+
import { enableHooks } from "../lib/hooks.mjs";
|
|
11
|
+
import { enableMcp } from "../lib/mcp.mjs";
|
|
12
|
+
import { CMD } from "../lib/pkg.mjs";
|
|
13
|
+
import { printResult, brandLine } from "../lib/output.mjs";
|
|
14
|
+
|
|
15
|
+
export function cmdInstall(args, io = {}) {
|
|
16
|
+
const stdout = io.stdout ?? process.stdout;
|
|
17
|
+
const home = args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null;
|
|
18
|
+
if (!home) {
|
|
19
|
+
printResult(stdout, args.json, false, undefined, {
|
|
20
|
+
code: "no-home",
|
|
21
|
+
message: "HOME is unset; refusing to install. Mental fails open — continue coding.",
|
|
22
|
+
});
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const project = Boolean(args.flags?.project);
|
|
27
|
+
const hooks = Boolean(args.flags?.hooks);
|
|
28
|
+
const mcp = Boolean(args.flags?.mcp);
|
|
29
|
+
const cwd = args.cwd ?? process.cwd();
|
|
30
|
+
|
|
31
|
+
const installed = installSkills({
|
|
32
|
+
home,
|
|
33
|
+
projectDir: project ? cwd : null,
|
|
34
|
+
});
|
|
35
|
+
const cli = installGlobalCli({ home, env: args.env ?? process.env });
|
|
36
|
+
const personal = userMentalDir(home);
|
|
37
|
+
ensureSkeleton(personal, { name: "personal" });
|
|
38
|
+
|
|
39
|
+
let hookResult = null;
|
|
40
|
+
if (hooks) hookResult = enableHooks(home);
|
|
41
|
+
|
|
42
|
+
let mcpResult = null;
|
|
43
|
+
if (mcp) mcpResult = enableMcp(home);
|
|
44
|
+
|
|
45
|
+
const resolved = resolveBundle({
|
|
46
|
+
cwd,
|
|
47
|
+
home,
|
|
48
|
+
env: args.env ?? process.env,
|
|
49
|
+
dir: args.dir ?? null,
|
|
50
|
+
write: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const imported = resolved.ok ? resolved.data.imported : null;
|
|
54
|
+
const data = {
|
|
55
|
+
home,
|
|
56
|
+
personalRoot: personal,
|
|
57
|
+
skills: installed.written,
|
|
58
|
+
cli,
|
|
59
|
+
project: project ? `${cwd}/.github/skills/mental` : null,
|
|
60
|
+
hooks: hookResult,
|
|
61
|
+
mcp: mcpResult,
|
|
62
|
+
where: resolved.ok ? resolved.data : null,
|
|
63
|
+
imported: imported || null,
|
|
64
|
+
};
|
|
65
|
+
const importLine =
|
|
66
|
+
imported?.copied?.length
|
|
67
|
+
? `\nimported ${imported.copied.length} leftover file(s) from ${imported.from}`
|
|
68
|
+
: "";
|
|
69
|
+
const cliLine = cli.bin ? `\nCLI: ${cli.bin}` : "";
|
|
70
|
+
const hookLine = hooks ? "\nhooks: enabled (session-start → mental status --json)" : "";
|
|
71
|
+
const mcpLine = mcp
|
|
72
|
+
? mcpResult?.ok
|
|
73
|
+
? `\nMCP: ${CMD} serve registered in ${mcpResult.written.join(", ")}`
|
|
74
|
+
: `\nMCP: config write failed (${mcpResult?.error?.message ?? "unknown"}) — add \`${CMD} serve\` manually`
|
|
75
|
+
: "";
|
|
76
|
+
printResult(
|
|
77
|
+
stdout,
|
|
78
|
+
args.json,
|
|
79
|
+
true,
|
|
80
|
+
data,
|
|
81
|
+
undefined,
|
|
82
|
+
() =>
|
|
83
|
+
`${brandLine(`installed skill + rule (${installed.written.length} paths)`)}\n~/.mental skeleton: ${personal}${cliLine}${hookLine}${mcpLine}${importLine}`,
|
|
84
|
+
);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental journal` — append one section to today's journal (task boundary).
|
|
3
|
+
*/
|
|
4
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
5
|
+
import { appendJournal, bundleName, ensureSkeleton, repoRelativePath } from "../lib/okf.mjs";
|
|
6
|
+
import { refreshIndex } from "../lib/index.mjs";
|
|
7
|
+
import { printResult, kindLine } from "../lib/output.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ json: boolean, dir?: string, flags?: Record<string, string | boolean>, cwd?: string, home?: string, env?: NodeJS.ProcessEnv }} args
|
|
11
|
+
* @returns {number}
|
|
12
|
+
*/
|
|
13
|
+
export function cmdJournal(args, io = {}) {
|
|
14
|
+
const stdout = io.stdout ?? process.stdout;
|
|
15
|
+
const title = typeof args.flags?.title === "string" ? args.flags.title : null;
|
|
16
|
+
if (!title) {
|
|
17
|
+
printResult(stdout, args.json, false, undefined, {
|
|
18
|
+
code: "usage",
|
|
19
|
+
message: "mental journal requires --title (agents also pass --body --resume --json)",
|
|
20
|
+
});
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const resolved = resolveBundle({
|
|
24
|
+
cwd: args.cwd ?? process.cwd(),
|
|
25
|
+
home: args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null,
|
|
26
|
+
env: args.env ?? process.env,
|
|
27
|
+
dir: args.dir ?? null,
|
|
28
|
+
write: true,
|
|
29
|
+
});
|
|
30
|
+
if (!resolved.ok) {
|
|
31
|
+
printResult(stdout, args.json, false, undefined, resolved.error);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
const body = typeof args.flags?.body === "string" ? args.flags.body : "";
|
|
35
|
+
const resume = typeof args.flags?.resume === "string" ? args.flags.resume : "Continue. — open loops: none";
|
|
36
|
+
const againstRaw = typeof args.flags?.against === "string" ? args.flags.against : undefined;
|
|
37
|
+
const against = againstRaw != null ? repoRelativePath(againstRaw) : undefined;
|
|
38
|
+
if (againstRaw != null && against === null) {
|
|
39
|
+
printResult(stdout, args.json, false, undefined, {
|
|
40
|
+
code: "usage",
|
|
41
|
+
message: "--against must be a repo-relative path (no ..)",
|
|
42
|
+
});
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
ensureSkeleton(resolved.data.root, {
|
|
46
|
+
name: bundleName(resolved.data.root, resolved.data.id || "project"),
|
|
47
|
+
});
|
|
48
|
+
const written = appendJournal(resolved.data.root, { title, body, resume, against });
|
|
49
|
+
const home = args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null;
|
|
50
|
+
const indexed = refreshIndex(resolved.data, home, args.env ?? process.env);
|
|
51
|
+
const data = { ...resolved.data, ...written, indexed };
|
|
52
|
+
printResult(stdout, args.json, true, data, undefined, () => kindLine("journal", `appended ${written.path}`));
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental link` — point this cwd at an existing UUID (second clone of same project).
|
|
3
|
+
*/
|
|
4
|
+
import { printResult } from "../lib/output.mjs";
|
|
5
|
+
import { cmdRemap } from "./remap.mjs";
|
|
6
|
+
|
|
7
|
+
export function cmdLink(args, io = {}) {
|
|
8
|
+
const stdout = io.stdout ?? process.stdout;
|
|
9
|
+
const to = typeof args.flags?.to === "string" ? args.flags.to : null;
|
|
10
|
+
if (!to) {
|
|
11
|
+
printResult(stdout, args.json, false, undefined, {
|
|
12
|
+
code: "usage",
|
|
13
|
+
message: "mental link requires --to <uuid> (or run mental remap --to <uuid>)",
|
|
14
|
+
});
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
return cmdRemap({ ...args, flags: { ...args.flags, to } }, io);
|
|
18
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental list` — concepts in the active bundle (filters: --type --status --tag --kind).
|
|
3
|
+
*/
|
|
4
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
5
|
+
import { filterConcepts, listConcepts } from "../lib/index.mjs";
|
|
6
|
+
import { printResult } from "../lib/output.mjs";
|
|
7
|
+
|
|
8
|
+
function summarize(c) {
|
|
9
|
+
return {
|
|
10
|
+
path: c.path,
|
|
11
|
+
type: c.type,
|
|
12
|
+
title: c.title,
|
|
13
|
+
description: c.description || "",
|
|
14
|
+
status: c.status,
|
|
15
|
+
kind: c.kind || "",
|
|
16
|
+
tags: c.tags,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function cmdList(args, io = {}) {
|
|
21
|
+
const stdout = io.stdout ?? process.stdout;
|
|
22
|
+
const resolved = resolveBundle({
|
|
23
|
+
cwd: args.cwd ?? process.cwd(),
|
|
24
|
+
home: args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null,
|
|
25
|
+
env: args.env ?? process.env,
|
|
26
|
+
dir: args.dir ?? null,
|
|
27
|
+
write: false,
|
|
28
|
+
});
|
|
29
|
+
if (!resolved.ok) {
|
|
30
|
+
printResult(stdout, args.json, false, undefined, resolved.error);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const type = typeof args.flags?.type === "string" ? args.flags.type : undefined;
|
|
34
|
+
const status = typeof args.flags?.status === "string" ? args.flags.status : undefined;
|
|
35
|
+
const tag = typeof args.flags?.tag === "string" ? args.flags.tag : undefined;
|
|
36
|
+
const kind = typeof args.flags?.kind === "string" ? args.flags.kind : undefined;
|
|
37
|
+
const items = filterConcepts(listConcepts(resolved.data.root), { type, status, tag, kind }).map(summarize);
|
|
38
|
+
const data = {
|
|
39
|
+
...resolved.data,
|
|
40
|
+
items,
|
|
41
|
+
type: type ?? null,
|
|
42
|
+
status: status ?? null,
|
|
43
|
+
tag: tag ?? null,
|
|
44
|
+
kind: kind ?? null,
|
|
45
|
+
};
|
|
46
|
+
printResult(stdout, args.json, true, data, undefined, (d) => {
|
|
47
|
+
if (d.items.length === 0) return "(none)";
|
|
48
|
+
return d.items.map((i) => `[${i.type}] ${i.title} (${i.path})`).join("\n");
|
|
49
|
+
});
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental local` — opt in to a project-local bundle.
|
|
3
|
+
* Refuse unless git will ignore `./.mental/` (v1 always private).
|
|
4
|
+
*
|
|
5
|
+
* Leftover Balakit `./.mental` is snapshotted into the home UUID store first,
|
|
6
|
+
* then this folder becomes the write root.
|
|
7
|
+
*
|
|
8
|
+
* `--import` copies the home UUID slice into `./.mental`.
|
|
9
|
+
* `--move` does the same and marks the binding store=local (home files stay
|
|
10
|
+
* unless `--delete-home`).
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, rmSync } from "node:fs";
|
|
13
|
+
import { basename, join } from "node:path";
|
|
14
|
+
import { findGitRoot } from "../lib/git.mjs";
|
|
15
|
+
import { assertLocalIgnorable } from "../lib/ignore.mjs";
|
|
16
|
+
import { bundleName, copyOkfTree, ensureSkeleton } from "../lib/okf.mjs";
|
|
17
|
+
import { printResult } from "../lib/output.mjs";
|
|
18
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
19
|
+
import { projectSliceDir, setBindingStore } from "../lib/bindings.mjs";
|
|
20
|
+
import { isOptedInLocal, markOptedInLocal } from "../lib/import-legacy.mjs";
|
|
21
|
+
|
|
22
|
+
export function cmdLocal(args, io = {}) {
|
|
23
|
+
const stdout = io.stdout ?? process.stdout;
|
|
24
|
+
const cwd = args.cwd ?? process.cwd();
|
|
25
|
+
const env = args.env ?? process.env;
|
|
26
|
+
const home = args.home ?? env.HOME ?? env.USERPROFILE ?? null;
|
|
27
|
+
const doImport = Boolean(args.flags?.import);
|
|
28
|
+
const doMove = Boolean(args.flags?.move);
|
|
29
|
+
const deleteHome = Boolean(args.flags?.["delete-home"]);
|
|
30
|
+
|
|
31
|
+
const gitRoot = findGitRoot(cwd, { env });
|
|
32
|
+
const destRoot = gitRoot || cwd;
|
|
33
|
+
const dest = join(destRoot, ".mental");
|
|
34
|
+
|
|
35
|
+
const gate = assertLocalIgnorable(destRoot, { env });
|
|
36
|
+
if (!gate.ok) {
|
|
37
|
+
printResult(stdout, args.json, false, undefined, gate.error);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const leftoverExists = existsSync(dest);
|
|
42
|
+
const alreadyOpted = leftoverExists && isOptedInLocal(dest);
|
|
43
|
+
|
|
44
|
+
if (leftoverExists && !alreadyOpted) {
|
|
45
|
+
resolveBundle({
|
|
46
|
+
cwd,
|
|
47
|
+
home,
|
|
48
|
+
env,
|
|
49
|
+
dir: args.dir ?? null,
|
|
50
|
+
write: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const homeWhere = resolveBundle({
|
|
55
|
+
cwd,
|
|
56
|
+
home,
|
|
57
|
+
env,
|
|
58
|
+
dir: args.dir ?? null,
|
|
59
|
+
write: true,
|
|
60
|
+
});
|
|
61
|
+
if (!homeWhere.ok) {
|
|
62
|
+
printResult(stdout, args.json, false, undefined, homeWhere.error);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const created = !leftoverExists;
|
|
67
|
+
ensureSkeleton(dest, { name: bundleName(dest, basename(destRoot)) });
|
|
68
|
+
|
|
69
|
+
let copied = [];
|
|
70
|
+
if ((doImport || doMove) && homeWhere.data.id && homeWhere.data.mode !== "local") {
|
|
71
|
+
const slice = projectSliceDir(home, homeWhere.data.id);
|
|
72
|
+
copied = copyOkfTree(slice, dest);
|
|
73
|
+
} else if ((doImport || doMove) && homeWhere.data.mode === "home" && homeWhere.data.root) {
|
|
74
|
+
copied = copyOkfTree(homeWhere.data.root, dest);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
markOptedInLocal(dest);
|
|
78
|
+
|
|
79
|
+
const where = resolveBundle({
|
|
80
|
+
cwd,
|
|
81
|
+
home,
|
|
82
|
+
env,
|
|
83
|
+
dir: args.dir ?? null,
|
|
84
|
+
write: true,
|
|
85
|
+
});
|
|
86
|
+
if (where.ok && where.data.id && home) {
|
|
87
|
+
setBindingStore(home, where.data.id, "local");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let purged = null;
|
|
91
|
+
if (doMove && deleteHome && homeWhere.data.id && home) {
|
|
92
|
+
const slice = projectSliceDir(home, homeWhere.data.id);
|
|
93
|
+
if (existsSync(slice) && slice !== dest) {
|
|
94
|
+
rmSync(slice, { recursive: true, force: true });
|
|
95
|
+
purged = slice;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const data = {
|
|
100
|
+
root: dest,
|
|
101
|
+
created,
|
|
102
|
+
imported: doImport || doMove,
|
|
103
|
+
copied,
|
|
104
|
+
purged,
|
|
105
|
+
gitRoot: gitRoot || null,
|
|
106
|
+
where: where.ok ? where.data : null,
|
|
107
|
+
};
|
|
108
|
+
printResult(
|
|
109
|
+
stdout,
|
|
110
|
+
args.json,
|
|
111
|
+
true,
|
|
112
|
+
data,
|
|
113
|
+
undefined,
|
|
114
|
+
() =>
|
|
115
|
+
`${created ? "created" : "already present"} ${dest}${copied.length ? `\ncopied ${copied.join(", ")}` : ""}`,
|
|
116
|
+
);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental note` — scaffold a durable note (only if it will save future time).
|
|
3
|
+
*/
|
|
4
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
5
|
+
import { bundleName, ensureSkeleton, writeNote } from "../lib/okf.mjs";
|
|
6
|
+
import { refreshIndex } from "../lib/index.mjs";
|
|
7
|
+
import { printResult, kindLine } from "../lib/output.mjs";
|
|
8
|
+
|
|
9
|
+
const STATUSES = new Set(["draft", "active", "superseded"]);
|
|
10
|
+
|
|
11
|
+
export function cmdNote(args, io = {}) {
|
|
12
|
+
const stdout = io.stdout ?? process.stdout;
|
|
13
|
+
const title = typeof args.flags?.title === "string" ? args.flags.title : null;
|
|
14
|
+
if (!title) {
|
|
15
|
+
printResult(stdout, args.json, false, undefined, {
|
|
16
|
+
code: "usage",
|
|
17
|
+
message: "mental note requires --title",
|
|
18
|
+
});
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const status = typeof args.flags?.status === "string" ? args.flags.status : "active";
|
|
22
|
+
if (!STATUSES.has(status)) {
|
|
23
|
+
printResult(stdout, args.json, false, undefined, {
|
|
24
|
+
code: "usage",
|
|
25
|
+
message: `status must be ${[...STATUSES].join("|")}`,
|
|
26
|
+
});
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
const resolved = resolveBundle({
|
|
30
|
+
cwd: args.cwd ?? process.cwd(),
|
|
31
|
+
home: args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null,
|
|
32
|
+
env: args.env ?? process.env,
|
|
33
|
+
dir: args.dir ?? null,
|
|
34
|
+
write: true,
|
|
35
|
+
});
|
|
36
|
+
if (!resolved.ok) {
|
|
37
|
+
printResult(stdout, args.json, false, undefined, resolved.error);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
ensureSkeleton(resolved.data.root, {
|
|
41
|
+
name: bundleName(resolved.data.root, resolved.data.id || "project"),
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
const written = writeNote(resolved.data.root, {
|
|
45
|
+
title,
|
|
46
|
+
status,
|
|
47
|
+
description: typeof args.flags?.description === "string" ? args.flags.description : "",
|
|
48
|
+
body: typeof args.flags?.body === "string" ? args.flags.body : "",
|
|
49
|
+
slug: typeof args.flags?.slug === "string" ? args.flags.slug : undefined,
|
|
50
|
+
});
|
|
51
|
+
const home = args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null;
|
|
52
|
+
const indexed = refreshIndex(resolved.data, home, args.env ?? process.env);
|
|
53
|
+
printResult(stdout, args.json, true, { ...resolved.data, ...written, indexed }, undefined, () => kindLine("note", `wrote ${written.path}`));
|
|
54
|
+
return 0;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
57
|
+
const code = /** @type {{ code?: string }} */ (err).code || "write";
|
|
58
|
+
printResult(stdout, args.json, false, undefined, { code, message });
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental reindex` — rebuild the derived sqlite index from OKF files.
|
|
3
|
+
*/
|
|
4
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
5
|
+
import { reindexBundle } from "../lib/index.mjs";
|
|
6
|
+
import { printResult, brandLine } from "../lib/output.mjs";
|
|
7
|
+
|
|
8
|
+
export function cmdReindex(args, io = {}) {
|
|
9
|
+
const stdout = io.stdout ?? process.stdout;
|
|
10
|
+
const resolved = resolveBundle({
|
|
11
|
+
cwd: args.cwd ?? process.cwd(),
|
|
12
|
+
home: args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null,
|
|
13
|
+
env: args.env ?? process.env,
|
|
14
|
+
dir: args.dir ?? null,
|
|
15
|
+
write: true,
|
|
16
|
+
});
|
|
17
|
+
if (!resolved.ok) {
|
|
18
|
+
printResult(stdout, args.json, false, undefined, resolved.error);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
if (!resolved.data.id) {
|
|
22
|
+
printResult(stdout, args.json, false, undefined, {
|
|
23
|
+
code: "no-id",
|
|
24
|
+
message: "No project UUID yet; reindex needs a git binding. Run `mental where` in a repo.",
|
|
25
|
+
});
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const indexed =
|
|
29
|
+
resolved.data.indexed ??
|
|
30
|
+
reindexBundle({
|
|
31
|
+
root: resolved.data.root,
|
|
32
|
+
id: resolved.data.id,
|
|
33
|
+
home: args.home ?? process.env.HOME ?? process.env.USERPROFILE,
|
|
34
|
+
env: args.env ?? process.env,
|
|
35
|
+
});
|
|
36
|
+
printResult(
|
|
37
|
+
stdout,
|
|
38
|
+
args.json,
|
|
39
|
+
indexed.ok,
|
|
40
|
+
{ ...resolved.data, indexed },
|
|
41
|
+
indexed.ok ? undefined : { code: "index", message: indexed.error || "reindex failed" },
|
|
42
|
+
(d) =>
|
|
43
|
+
d.indexed.ok
|
|
44
|
+
? brandLine(`indexed ${d.indexed.concepts} concept(s) → ${d.indexed.path}`)
|
|
45
|
+
: d.indexed.error || "reindex failed",
|
|
46
|
+
);
|
|
47
|
+
return indexed.ok ? 0 : 1;
|
|
48
|
+
}
|