@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,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental remap` — list bindings, or point this clone at an existing UUID.
|
|
3
|
+
* Flags, not a standing TTY session: `--to <id>` or `--from <id>`.
|
|
4
|
+
*/
|
|
5
|
+
import { findGitRoot, getRemoteUrl } from "../lib/git.mjs";
|
|
6
|
+
import { loadBindings, remapToBinding } from "../lib/bindings.mjs";
|
|
7
|
+
import { printResult } from "../lib/output.mjs";
|
|
8
|
+
|
|
9
|
+
export function formatBindings(home) {
|
|
10
|
+
const data = loadBindings(home);
|
|
11
|
+
if (!data.bindings.length) return "(no bindings)";
|
|
12
|
+
return data.bindings
|
|
13
|
+
.map((b) =>
|
|
14
|
+
[
|
|
15
|
+
`${b.id} ${b.name || ""}`.trim(),
|
|
16
|
+
` origins ${(b.origins || []).join(", ") || "—"}`,
|
|
17
|
+
` paths ${(b.paths || []).join(", ") || "—"}`,
|
|
18
|
+
b.store ? ` store ${b.store}` : null,
|
|
19
|
+
]
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join("\n"),
|
|
22
|
+
)
|
|
23
|
+
.join("\n\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cmdRemap(args, io = {}) {
|
|
27
|
+
const stdout = io.stdout ?? process.stdout;
|
|
28
|
+
const home = args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null;
|
|
29
|
+
const cwd = args.cwd ?? process.cwd();
|
|
30
|
+
const env = args.env ?? process.env;
|
|
31
|
+
if (!home) {
|
|
32
|
+
printResult(stdout, args.json, false, undefined, {
|
|
33
|
+
code: "no-home",
|
|
34
|
+
message: "HOME is unset; Mental will not write.",
|
|
35
|
+
});
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const to =
|
|
40
|
+
(typeof args.flags?.to === "string" && args.flags.to) ||
|
|
41
|
+
(typeof args.flags?.from === "string" && args.flags.from) ||
|
|
42
|
+
null;
|
|
43
|
+
|
|
44
|
+
if (!to) {
|
|
45
|
+
const list = formatBindings(home);
|
|
46
|
+
printResult(stdout, args.json, true, { bindings: loadBindings(home).bindings }, undefined, () =>
|
|
47
|
+
`${list}\n\nPoint this clone: mental remap --to <id>`,
|
|
48
|
+
);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const gitRoot = findGitRoot(cwd, { env });
|
|
53
|
+
if (!gitRoot) {
|
|
54
|
+
printResult(stdout, args.json, false, undefined, {
|
|
55
|
+
code: "not-git",
|
|
56
|
+
message: "mental remap needs a git repository.",
|
|
57
|
+
});
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const origin = getRemoteUrl(gitRoot, "origin", { env });
|
|
62
|
+
const result = remapToBinding({ home, gitRoot, toId: to, origin });
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
printResult(stdout, args.json, false, undefined, { code: result.code, message: result.message });
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
printResult(
|
|
68
|
+
stdout,
|
|
69
|
+
args.json,
|
|
70
|
+
true,
|
|
71
|
+
{ id: result.id, gitRoot, origin },
|
|
72
|
+
undefined,
|
|
73
|
+
() => `this clone → ${result.id}`,
|
|
74
|
+
);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental search` — query the derived index (sqlite) or scan OKF files.
|
|
3
|
+
*/
|
|
4
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
5
|
+
import { searchBundle } from "../lib/index.mjs";
|
|
6
|
+
import { printResult } from "../lib/output.mjs";
|
|
7
|
+
|
|
8
|
+
export function cmdSearch(args, io = {}) {
|
|
9
|
+
const stdout = io.stdout ?? process.stdout;
|
|
10
|
+
const q = args.rest.join(" ").trim();
|
|
11
|
+
if (!q) {
|
|
12
|
+
printResult(stdout, args.json, false, undefined, {
|
|
13
|
+
code: "usage",
|
|
14
|
+
message: "mental search requires a query",
|
|
15
|
+
});
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
const resolved = resolveBundle({
|
|
19
|
+
cwd: args.cwd ?? process.cwd(),
|
|
20
|
+
home: args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null,
|
|
21
|
+
env: args.env ?? process.env,
|
|
22
|
+
dir: args.dir ?? null,
|
|
23
|
+
write: false,
|
|
24
|
+
});
|
|
25
|
+
if (!resolved.ok) {
|
|
26
|
+
printResult(stdout, args.json, false, undefined, resolved.error);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
const type = typeof args.flags?.type === "string" ? args.flags.type : undefined;
|
|
30
|
+
const status = typeof args.flags?.status === "string" ? args.flags.status : undefined;
|
|
31
|
+
const tag = typeof args.flags?.tag === "string" ? args.flags.tag : undefined;
|
|
32
|
+
const kind = typeof args.flags?.kind === "string" ? args.flags.kind : undefined;
|
|
33
|
+
const found = searchBundle({
|
|
34
|
+
root: resolved.data.root,
|
|
35
|
+
id: resolved.data.id,
|
|
36
|
+
home: args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null,
|
|
37
|
+
env: args.env ?? process.env,
|
|
38
|
+
q,
|
|
39
|
+
type,
|
|
40
|
+
status,
|
|
41
|
+
tag,
|
|
42
|
+
kind,
|
|
43
|
+
});
|
|
44
|
+
const data = { ...resolved.data, q, ...found };
|
|
45
|
+
printResult(stdout, args.json, true, data, undefined, (d) => {
|
|
46
|
+
if (d.hits.length === 0) return `no hits for ${d.q} (${d.backend})`;
|
|
47
|
+
return d.hits
|
|
48
|
+
.map((h) => {
|
|
49
|
+
const line = `[${h.type}] ${h.title} (${h.path})`;
|
|
50
|
+
return h.snippet ? `${line}\n ${h.snippet}` : line;
|
|
51
|
+
})
|
|
52
|
+
.join("\n");
|
|
53
|
+
});
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental serve` — optional MCP stdio wrapping the CLI commands
|
|
3
|
+
* (heartbeat/where/status/search/list/show/journal/attention/decide/note).
|
|
4
|
+
*/
|
|
5
|
+
import { serveMcp } from "../lib/mcp.mjs";
|
|
6
|
+
|
|
7
|
+
export function cmdServe(args, io = {}) {
|
|
8
|
+
return serveMcp({
|
|
9
|
+
cwd: args.cwd,
|
|
10
|
+
home: args.home,
|
|
11
|
+
env: args.env,
|
|
12
|
+
dir: args.dir ?? null,
|
|
13
|
+
stdin: io.stdin,
|
|
14
|
+
stdout: io.stdout,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental show <path>` — one OKF file relative to the bundle root.
|
|
3
|
+
*/
|
|
4
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
5
|
+
import { readBundleFile } from "../lib/okf.mjs";
|
|
6
|
+
import { listBacklinks } from "../lib/index.mjs";
|
|
7
|
+
import { printResult, kindLine } from "../lib/output.mjs";
|
|
8
|
+
|
|
9
|
+
export function cmdShow(args, io = {}) {
|
|
10
|
+
const stdout = io.stdout ?? process.stdout;
|
|
11
|
+
const rel = (args.rest[0] || (typeof args.flags?.path === "string" ? args.flags.path : "")).trim();
|
|
12
|
+
if (!rel) {
|
|
13
|
+
printResult(stdout, args.json, false, undefined, {
|
|
14
|
+
code: "usage",
|
|
15
|
+
message: "mental show requires a path relative to the bundle root",
|
|
16
|
+
});
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const resolved = resolveBundle({
|
|
20
|
+
cwd: args.cwd ?? process.cwd(),
|
|
21
|
+
home: args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null,
|
|
22
|
+
env: args.env ?? process.env,
|
|
23
|
+
dir: args.dir ?? null,
|
|
24
|
+
write: false,
|
|
25
|
+
});
|
|
26
|
+
if (!resolved.ok) {
|
|
27
|
+
printResult(stdout, args.json, false, undefined, resolved.error);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
const file = readBundleFile(resolved.data.root, rel);
|
|
31
|
+
if (!file.ok) {
|
|
32
|
+
printResult(stdout, args.json, false, undefined, file.error);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
const home = args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null;
|
|
36
|
+
const backlinks = listBacklinks({
|
|
37
|
+
root: resolved.data.root,
|
|
38
|
+
path: file.data.path,
|
|
39
|
+
id: resolved.data.id,
|
|
40
|
+
home,
|
|
41
|
+
env: args.env ?? process.env,
|
|
42
|
+
});
|
|
43
|
+
const payload = {
|
|
44
|
+
...resolved.data,
|
|
45
|
+
path: file.data.path,
|
|
46
|
+
frontmatter: file.data.data,
|
|
47
|
+
body: file.data.body,
|
|
48
|
+
backlinks,
|
|
49
|
+
};
|
|
50
|
+
printResult(stdout, args.json, true, payload, undefined, (d) => {
|
|
51
|
+
const title = typeof d.frontmatter.title === "string" ? d.frontmatter.title : d.path;
|
|
52
|
+
const type = typeof d.frontmatter.type === "string" ? d.frontmatter.type : "";
|
|
53
|
+
const head = type ? `${title} [${type}]` : title;
|
|
54
|
+
const linked =
|
|
55
|
+
d.backlinks.length === 0
|
|
56
|
+
? ""
|
|
57
|
+
: `\n\nLinked from:\n${d.backlinks.map((b) => ` [${b.type}] ${b.title} (${b.path})`).join("\n")}`;
|
|
58
|
+
return `${kindLine("read", head)}\n${d.path}\n\n${d.body.trim() || "(empty)"}${linked}`;
|
|
59
|
+
});
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental split` — this clone gets a new UUID (empty or `--copy` of current OKF).
|
|
3
|
+
*/
|
|
4
|
+
import { findGitRoot, getRemoteUrl } from "../lib/git.mjs";
|
|
5
|
+
import { projectSliceDir, splitBinding } from "../lib/bindings.mjs";
|
|
6
|
+
import { bundleName, copyOkfTree, ensureSkeleton } from "../lib/okf.mjs";
|
|
7
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
8
|
+
import { printResult } from "../lib/output.mjs";
|
|
9
|
+
|
|
10
|
+
export function cmdSplit(args, io = {}) {
|
|
11
|
+
const stdout = io.stdout ?? process.stdout;
|
|
12
|
+
const home = args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null;
|
|
13
|
+
const cwd = args.cwd ?? process.cwd();
|
|
14
|
+
const env = args.env ?? process.env;
|
|
15
|
+
if (!home) {
|
|
16
|
+
printResult(stdout, args.json, false, undefined, {
|
|
17
|
+
code: "no-home",
|
|
18
|
+
message: "HOME is unset; Mental will not write.",
|
|
19
|
+
});
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const gitRoot = findGitRoot(cwd, { env });
|
|
24
|
+
if (!gitRoot) {
|
|
25
|
+
printResult(stdout, args.json, false, undefined, {
|
|
26
|
+
code: "not-git",
|
|
27
|
+
message: "mental split needs a git repository.",
|
|
28
|
+
});
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const copy = Boolean(args.flags?.copy);
|
|
33
|
+
const prior = resolveBundle({ cwd, home, env, dir: args.dir ?? null, write: true });
|
|
34
|
+
if (!prior.ok) {
|
|
35
|
+
printResult(stdout, args.json, false, undefined, prior.error);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
const fromId = prior.data.id;
|
|
39
|
+
const fromRoot = prior.data.root;
|
|
40
|
+
|
|
41
|
+
const origin = getRemoteUrl(gitRoot, "origin", { env });
|
|
42
|
+
const split = splitBinding({ home, gitRoot, origin });
|
|
43
|
+
const dest = split.dest;
|
|
44
|
+
if (copy && fromRoot) copyOkfTree(fromRoot, dest);
|
|
45
|
+
ensureSkeleton(dest, { name: bundleName(dest, split.id) });
|
|
46
|
+
|
|
47
|
+
printResult(
|
|
48
|
+
stdout,
|
|
49
|
+
args.json,
|
|
50
|
+
true,
|
|
51
|
+
{ id: split.id, fromId, gitRoot, copied: copy, root: dest, dest: projectSliceDir(home, split.id) },
|
|
52
|
+
undefined,
|
|
53
|
+
() => `split ${fromId || "—"} → ${split.id}${copy ? " (copied OKF)" : ""}`,
|
|
54
|
+
);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental status` — derive git + latest Resume + open/deferred decisions + notes.
|
|
3
|
+
* Writes `status/current.md` as a disposable cache. OKF files remain SoT.
|
|
4
|
+
*/
|
|
5
|
+
import { writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
8
|
+
import { gitSnapshot } from "../lib/git.mjs";
|
|
9
|
+
import {
|
|
10
|
+
bundleName,
|
|
11
|
+
ensureSkeleton,
|
|
12
|
+
latestJournalHandoff,
|
|
13
|
+
listNotes,
|
|
14
|
+
listOpenAttention,
|
|
15
|
+
listOpenDecisions,
|
|
16
|
+
localDate,
|
|
17
|
+
renderStatus,
|
|
18
|
+
} from "../lib/okf.mjs";
|
|
19
|
+
import { printResult } from "../lib/output.mjs";
|
|
20
|
+
|
|
21
|
+
function formatGit(git, gitRoot) {
|
|
22
|
+
if (!gitRoot) return "Not a git repository.";
|
|
23
|
+
const branch = git.branch || "(unknown branch)";
|
|
24
|
+
const dirty = git.dirty ? "uncommitted changes" : "clean";
|
|
25
|
+
const recent = git.recent.length ? git.recent.map((l) => ` ${l}`).join("\n") : " (no commits)";
|
|
26
|
+
return `${branch}; ${dirty}\n${recent}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatHuman(data) {
|
|
30
|
+
const air =
|
|
31
|
+
data.attention.length === 0
|
|
32
|
+
? " none"
|
|
33
|
+
: data.attention.map((a) => {
|
|
34
|
+
const tag = a.status === "later" ? "later" : a.kind || a.status;
|
|
35
|
+
return ` - [${tag}] ${a.title} (${a.path})`;
|
|
36
|
+
}).join("\n");
|
|
37
|
+
const dec =
|
|
38
|
+
data.openDecisions.length === 0
|
|
39
|
+
? " none"
|
|
40
|
+
: data.openDecisions.map((d) => ` - [${d.status}] ${d.title} (${d.path})`).join("\n");
|
|
41
|
+
const notes =
|
|
42
|
+
data.notes.length === 0
|
|
43
|
+
? " none"
|
|
44
|
+
: data.notes.map((n) => ` - ${n.title} (${n.path})`).join("\n");
|
|
45
|
+
return [
|
|
46
|
+
`root: ${data.root}`,
|
|
47
|
+
`mode: ${data.mode}`,
|
|
48
|
+
`git: ${data.git.branch || "—"} ${data.git.dirty ? "(dirty)" : "(clean)"}`,
|
|
49
|
+
`resume: ${data.resume || "—"}`,
|
|
50
|
+
`against: ${data.against || "—"}`,
|
|
51
|
+
`now: ${data.latestOutcome || "—"}`,
|
|
52
|
+
`in the air:`,
|
|
53
|
+
air,
|
|
54
|
+
`unsettled:`,
|
|
55
|
+
dec,
|
|
56
|
+
`notes:`,
|
|
57
|
+
notes,
|
|
58
|
+
].join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {{ json: boolean, dir?: string, cwd?: string, home?: string, env?: NodeJS.ProcessEnv }} args
|
|
63
|
+
* @returns {number}
|
|
64
|
+
*/
|
|
65
|
+
export function cmdStatus(args, io = {}) {
|
|
66
|
+
const stdout = io.stdout ?? process.stdout;
|
|
67
|
+
const resolved = resolveBundle({
|
|
68
|
+
cwd: args.cwd ?? process.cwd(),
|
|
69
|
+
home: args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null,
|
|
70
|
+
env: args.env ?? process.env,
|
|
71
|
+
dir: args.dir ?? null,
|
|
72
|
+
write: true,
|
|
73
|
+
});
|
|
74
|
+
if (!resolved.ok) {
|
|
75
|
+
printResult(stdout, args.json, false, undefined, resolved.error);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { data: where } = resolved;
|
|
80
|
+
const now = new Date();
|
|
81
|
+
ensureSkeleton(where.root, { name: bundleName(where.root, where.id || "project"), now });
|
|
82
|
+
const git = gitSnapshot(where.gitRoot, { env: args.env ?? process.env });
|
|
83
|
+
const handoff = latestJournalHandoff(where.root);
|
|
84
|
+
const openDecisions = listOpenDecisions(where.root);
|
|
85
|
+
const attention = listOpenAttention(where.root);
|
|
86
|
+
const notes = listNotes(where.root);
|
|
87
|
+
const name = bundleName(where.root, where.id || "project");
|
|
88
|
+
const inFlight = formatGit(git, where.gitRoot);
|
|
89
|
+
const resume = handoff.resume || "No journal yet — start work, then `mental journal` at the task boundary.";
|
|
90
|
+
const latestOutcome = handoff.outcome || "No journal sections yet.";
|
|
91
|
+
const against = handoff.against || null;
|
|
92
|
+
|
|
93
|
+
writeFileSync(
|
|
94
|
+
join(where.root, "status", "current.md"),
|
|
95
|
+
renderStatus({
|
|
96
|
+
name,
|
|
97
|
+
date: localDate(now),
|
|
98
|
+
ts: now.toISOString(),
|
|
99
|
+
now: latestOutcome,
|
|
100
|
+
inFlight,
|
|
101
|
+
decisions: openDecisions.map((d) => ({
|
|
102
|
+
title: d.title,
|
|
103
|
+
file: d.file,
|
|
104
|
+
status: d.status,
|
|
105
|
+
})),
|
|
106
|
+
attention: attention.map((a) => ({
|
|
107
|
+
title: a.title,
|
|
108
|
+
file: a.file,
|
|
109
|
+
status: a.status,
|
|
110
|
+
kind: a.kind,
|
|
111
|
+
})),
|
|
112
|
+
notes: notes.map((n) => ({
|
|
113
|
+
title: n.title,
|
|
114
|
+
file: n.file,
|
|
115
|
+
status: n.status,
|
|
116
|
+
description: n.description,
|
|
117
|
+
})),
|
|
118
|
+
resume,
|
|
119
|
+
against,
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const payload = {
|
|
124
|
+
...where,
|
|
125
|
+
git: { branch: git.branch, dirty: git.dirty, recent: git.recent },
|
|
126
|
+
resume,
|
|
127
|
+
latestOutcome,
|
|
128
|
+
against,
|
|
129
|
+
attention,
|
|
130
|
+
openDecisions,
|
|
131
|
+
notes,
|
|
132
|
+
statusFile: "status/current.md",
|
|
133
|
+
};
|
|
134
|
+
printResult(stdout, args.json, true, payload, undefined, formatHuman);
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental uninstall` — remove skill/rule/hooks Mental copied into user agent dirs.
|
|
3
|
+
* Does not delete ~/.mental unless `--delete-data DELETE`.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, rmSync } from "node:fs";
|
|
6
|
+
import { userMentalDir } from "../lib/bindings.mjs";
|
|
7
|
+
import { uninstallSkills } from "../lib/uninstall.mjs";
|
|
8
|
+
import { disableHooks } from "../lib/hooks.mjs";
|
|
9
|
+
import { disableMcp } from "../lib/mcp.mjs";
|
|
10
|
+
import { printResult } from "../lib/output.mjs";
|
|
11
|
+
|
|
12
|
+
export function cmdUninstall(args, io = {}) {
|
|
13
|
+
const stdout = io.stdout ?? process.stdout;
|
|
14
|
+
const home = args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null;
|
|
15
|
+
if (!home) {
|
|
16
|
+
printResult(stdout, args.json, false, undefined, {
|
|
17
|
+
code: "no-home",
|
|
18
|
+
message: "HOME is unset; nothing to uninstall.",
|
|
19
|
+
});
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const deleteData = Boolean(args.flags?.["delete-data"]);
|
|
24
|
+
const confirm = typeof args.flags?.confirm === "string" ? args.flags.confirm : args.rest[0] || "";
|
|
25
|
+
if (deleteData && confirm !== "DELETE") {
|
|
26
|
+
printResult(stdout, args.json, false, undefined, {
|
|
27
|
+
code: "usage",
|
|
28
|
+
message: "Refusing to delete OKF. Pass --delete-data --confirm DELETE to wipe ~/.mental.",
|
|
29
|
+
});
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const skills = uninstallSkills({
|
|
34
|
+
home,
|
|
35
|
+
projectDir: args.flags?.project ? args.cwd ?? process.cwd() : null,
|
|
36
|
+
});
|
|
37
|
+
const hooks = disableHooks(home);
|
|
38
|
+
const mcp = disableMcp(home);
|
|
39
|
+
let wiped = null;
|
|
40
|
+
if (deleteData && confirm === "DELETE") {
|
|
41
|
+
const root = userMentalDir(home);
|
|
42
|
+
if (existsSync(root)) {
|
|
43
|
+
rmSync(root, { recursive: true, force: true });
|
|
44
|
+
wiped = root;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
printResult(
|
|
49
|
+
stdout,
|
|
50
|
+
args.json,
|
|
51
|
+
true,
|
|
52
|
+
{ removed: skills.removed, hooks: hooks.written, mcp: mcp.written, wiped },
|
|
53
|
+
undefined,
|
|
54
|
+
() =>
|
|
55
|
+
`removed ${skills.removed.length} skill/rule path(s)${wiped ? `\nwiped ${wiped}` : "\nOKF left in place (~/.mental)"}`,
|
|
56
|
+
);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mental where` — print the active OKF bundle. Agents call this first.
|
|
3
|
+
*/
|
|
4
|
+
import { resolveBundle } from "../lib/resolve.mjs";
|
|
5
|
+
import { formatWhere, printResult } from "../lib/output.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {{ json: boolean, dir?: string, cwd?: string, home?: string, env?: NodeJS.ProcessEnv }} args
|
|
9
|
+
* @param {{ stdout?: NodeJS.WritableStream, stderr?: NodeJS.WritableStream }} [io]
|
|
10
|
+
* @returns {number} exit code
|
|
11
|
+
*/
|
|
12
|
+
export function cmdWhere(args, io = {}) {
|
|
13
|
+
const stdout = io.stdout ?? process.stdout;
|
|
14
|
+
const resolved = resolveBundle({
|
|
15
|
+
cwd: args.cwd ?? process.cwd(),
|
|
16
|
+
home: args.home ?? process.env.HOME ?? process.env.USERPROFILE ?? null,
|
|
17
|
+
env: args.env ?? process.env,
|
|
18
|
+
dir: args.dir ?? null,
|
|
19
|
+
write: false,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!resolved.ok) {
|
|
23
|
+
printResult(stdout, args.json, false, undefined, resolved.error);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
printResult(stdout, args.json, true, resolved.data, undefined, formatWhere);
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
package/bin/lib/args.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI argument helpers and usage text.
|
|
3
|
+
*/
|
|
4
|
+
import { CMD, VERSION } from "./pkg.mjs";
|
|
5
|
+
|
|
6
|
+
export function usage() {
|
|
7
|
+
return `${CMD} v${VERSION} — local-first OKF continuity
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
${CMD} Heartbeat (TTY): resume, last outcome, git, residue, open decisions
|
|
11
|
+
${CMD} heartbeat Same pulse (agents: --json). Cheap reload — not a notes dump
|
|
12
|
+
${CMD} where Active bundle (root, id, mode) — read-only
|
|
13
|
+
${CMD} status Git + latest Resume + residue + open decisions + notes
|
|
14
|
+
${CMD} search <q> Query notes/journal/decisions/attention (sqlite or scan)
|
|
15
|
+
${CMD} list List concepts (--type --status --tag --kind)
|
|
16
|
+
${CMD} show <path> One file, relative to the bundle root (includes backlinks)
|
|
17
|
+
${CMD} journal Append today's journal section (--against PLAN.md)
|
|
18
|
+
${CMD} attention Create or update residue in the air (--kind --status resolved)
|
|
19
|
+
${CMD} decide Scaffold a decision file
|
|
20
|
+
${CMD} note Scaffold a note
|
|
21
|
+
${CMD} local Create ./.mental after ignore check (--import copies home, --move switches store)
|
|
22
|
+
${CMD} remap List UUID bindings, or --to <id> / --from <id> for this clone
|
|
23
|
+
${CMD} split New UUID for this clone (--copy keeps OKF files)
|
|
24
|
+
${CMD} link Point this clone at --to <id>
|
|
25
|
+
${CMD} install Skill + rule + put ${CMD} on PATH (overrides previous)
|
|
26
|
+
${CMD} uninstall Remove installed skill/rule/hooks (OKF stays unless --delete-data DELETE)
|
|
27
|
+
${CMD} hooks on|off Optional session-start hooks (default off)
|
|
28
|
+
${CMD} serve Optional MCP stdio (heartbeat/where/status/search/list/show/journal/attention/decide/note)
|
|
29
|
+
${CMD} doctor PATH, bindings, ignore, skill, index
|
|
30
|
+
${CMD} reindex Rebuild derived sqlite index from OKF files
|
|
31
|
+
|
|
32
|
+
TTY: no args prints a one-shot heartbeat and exits. Named commands are one-shot.
|
|
33
|
+
Non-TTY / agents: always pass --json. Do not grep OKF / YAML.
|
|
34
|
+
|
|
35
|
+
Global flags:
|
|
36
|
+
--json Machine-readable { ok, data } | { ok, error }
|
|
37
|
+
--dir <path> Override resolve (same as MENTAL_DIR)
|
|
38
|
+
-h, --help Show this help
|
|
39
|
+
-v, --version Print version
|
|
40
|
+
|
|
41
|
+
Privacy: default store is ~/.mental (never commit). Project ./.mental
|
|
42
|
+
only after \`${CMD} local\`. Leftover ./.mental is normalized into
|
|
43
|
+
~/.mental/projects/<uuid> and indexed (source is not deleted).
|
|
44
|
+
Never store secrets.
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse argv into a structured args object.
|
|
50
|
+
* Global flags may appear before or after the command.
|
|
51
|
+
*
|
|
52
|
+
* @param {string[]} argv
|
|
53
|
+
*/
|
|
54
|
+
export function parseArgv(argv) {
|
|
55
|
+
const args = {
|
|
56
|
+
command: null,
|
|
57
|
+
json: false,
|
|
58
|
+
dir: undefined,
|
|
59
|
+
help: false,
|
|
60
|
+
version: false,
|
|
61
|
+
/** @type {string[]} */
|
|
62
|
+
rest: [],
|
|
63
|
+
/** @type {Record<string, string | boolean>} */
|
|
64
|
+
flags: {},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const takesValue = new Set([
|
|
68
|
+
"--dir",
|
|
69
|
+
"--title",
|
|
70
|
+
"--body",
|
|
71
|
+
"--resume",
|
|
72
|
+
"--status",
|
|
73
|
+
"--slug",
|
|
74
|
+
"--description",
|
|
75
|
+
"--from",
|
|
76
|
+
"--to",
|
|
77
|
+
"--against",
|
|
78
|
+
"--kind",
|
|
79
|
+
"--path",
|
|
80
|
+
"--type",
|
|
81
|
+
"--tag",
|
|
82
|
+
"--confirm",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < argv.length; i++) {
|
|
86
|
+
const a = argv[i];
|
|
87
|
+
if (a === "-h" || a === "--help") {
|
|
88
|
+
args.help = true;
|
|
89
|
+
} else if (a === "-v" || a === "--version") {
|
|
90
|
+
args.version = true;
|
|
91
|
+
} else if (a === "--json") {
|
|
92
|
+
args.json = true;
|
|
93
|
+
} else if (a === "--dir") {
|
|
94
|
+
const v = argv[++i];
|
|
95
|
+
if (v == null) throw new Error("--dir requires a path");
|
|
96
|
+
args.dir = v;
|
|
97
|
+
} else if (takesValue.has(a)) {
|
|
98
|
+
const v = argv[++i];
|
|
99
|
+
if (v == null) throw new Error(`${a} requires a value`);
|
|
100
|
+
args.flags[a.slice(2)] = v;
|
|
101
|
+
} else if (a.startsWith("--") && a.includes("=")) {
|
|
102
|
+
const eq = a.indexOf("=");
|
|
103
|
+
const key = a.slice(2, eq);
|
|
104
|
+
args.flags[key] = a.slice(eq + 1);
|
|
105
|
+
if (key === "dir") args.dir = a.slice(eq + 1);
|
|
106
|
+
if (key === "json") args.json = true;
|
|
107
|
+
} else if (a.startsWith("--")) {
|
|
108
|
+
args.flags[a.slice(2)] = true;
|
|
109
|
+
} else if (!args.command) {
|
|
110
|
+
args.command = a;
|
|
111
|
+
} else {
|
|
112
|
+
args.rest.push(a);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return args;
|
|
117
|
+
}
|