mnemos-capture 1.2.0 → 1.3.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/README.md +315 -223
- package/dist/cli/config.js +34 -15
- package/dist/cli/hooks.js +3 -3
- package/dist/cli/index.js +22 -20
- package/dist/cli/kos.js +8 -8
- package/dist/cli/mcp-server.js +68 -33
- package/dist/cli/version-check.js +1 -1
- package/package.json +18 -10
- package/dist/cli/__tests__/config.test.js +0 -49
- package/dist/cli/__tests__/hooks.test.js +0 -276
- package/dist/cli/__tests__/kos.test.js +0 -65
package/dist/cli/config.js
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* optional API key, in ~/.
|
|
4
|
-
* user tells Mnemos the exact command to launch their assistant.
|
|
2
|
+
* Xmu CLI config — stores which AI assistant kos should drive, and an
|
|
3
|
+
* optional API key, in ~/.xmu/config.json. Falls back to ~/.mnemos/config.json.
|
|
5
4
|
*/
|
|
6
5
|
import { homedir } from "os";
|
|
7
6
|
import { join } from "path";
|
|
8
7
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
function xmuDir() {
|
|
9
|
+
return join(homedir(), ".xmu");
|
|
10
|
+
}
|
|
11
|
+
function legacyDir() {
|
|
12
|
+
return join(homedir(), ".mnemos");
|
|
13
|
+
}
|
|
14
|
+
function xmuPath() {
|
|
15
|
+
return join(xmuDir(), "config.json");
|
|
16
|
+
}
|
|
17
|
+
function legacyPath() {
|
|
18
|
+
return join(legacyDir(), "config.json");
|
|
19
|
+
}
|
|
20
|
+
/** Path we currently read from: new dir if present, else the mnemos fallback. */
|
|
21
|
+
function readPath() {
|
|
22
|
+
if (existsSync(xmuPath()))
|
|
23
|
+
return xmuPath();
|
|
24
|
+
if (existsSync(legacyPath()))
|
|
25
|
+
return legacyPath();
|
|
26
|
+
return xmuPath();
|
|
27
|
+
}
|
|
11
28
|
const ALLOWED_FIELDS = ["agent", "key"];
|
|
12
29
|
export function readConfig() {
|
|
13
|
-
|
|
30
|
+
const path = readPath();
|
|
31
|
+
if (!existsSync(path))
|
|
14
32
|
return {};
|
|
15
33
|
try {
|
|
16
|
-
return JSON.parse(readFileSync(
|
|
34
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
17
35
|
}
|
|
18
36
|
catch {
|
|
19
37
|
return {};
|
|
20
38
|
}
|
|
21
39
|
}
|
|
22
40
|
export function writeConfig(cfg) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
41
|
+
const dir = xmuDir();
|
|
42
|
+
if (!existsSync(dir))
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
writeFileSync(xmuPath(), JSON.stringify(cfg, null, 2) + "\n");
|
|
26
45
|
}
|
|
27
46
|
export function setConfigField(field, value) {
|
|
28
47
|
if (!ALLOWED_FIELDS.includes(field)) {
|
|
@@ -32,21 +51,21 @@ export function setConfigField(field, value) {
|
|
|
32
51
|
cfg[field] = value;
|
|
33
52
|
writeConfig(cfg);
|
|
34
53
|
}
|
|
35
|
-
/** Route `
|
|
54
|
+
/** Route `xmu config <set|get> ...`. */
|
|
36
55
|
export function runConfigCommand(args) {
|
|
37
56
|
const action = args[0];
|
|
38
57
|
if (action === "set") {
|
|
39
58
|
const field = args[1];
|
|
40
59
|
const value = args.slice(2).join(" ");
|
|
41
60
|
if (!field || !value) {
|
|
42
|
-
console.error(
|
|
43
|
-
console.error(' e.g.
|
|
61
|
+
console.error("Usage: xmu config set <field> <value>");
|
|
62
|
+
console.error(' e.g. xmu config set agent "claude -p"');
|
|
44
63
|
process.exit(1);
|
|
45
64
|
}
|
|
46
65
|
try {
|
|
47
66
|
setConfigField(field, value);
|
|
48
67
|
console.log(`Set ${field} = ${value}`);
|
|
49
|
-
console.log(`Saved to ${
|
|
68
|
+
console.log(`Saved to ${xmuPath()}`);
|
|
50
69
|
}
|
|
51
70
|
catch (err) {
|
|
52
71
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -65,6 +84,6 @@ export function runConfigCommand(args) {
|
|
|
65
84
|
}
|
|
66
85
|
return;
|
|
67
86
|
}
|
|
68
|
-
console.error("Usage:
|
|
87
|
+
console.error("Usage: xmu config set|get <field> [value]");
|
|
69
88
|
process.exit(1);
|
|
70
89
|
}
|
package/dist/cli/hooks.js
CHANGED
|
@@ -108,7 +108,7 @@ export function setupHooks(apiKey, opts = {}) {
|
|
|
108
108
|
settings.hooks = { ...hooks, SessionStart: filtered };
|
|
109
109
|
writeSettings(settings);
|
|
110
110
|
if (!opts.silent) {
|
|
111
|
-
console.log("
|
|
111
|
+
console.log("Xmu inbox hook installed.");
|
|
112
112
|
console.log(`Location: ${CLAUDE_SETTINGS_PATH}`);
|
|
113
113
|
if (opts.briefing) {
|
|
114
114
|
console.log("At the start of each Claude Code session you'll see a full project briefing.");
|
|
@@ -151,7 +151,7 @@ export function setupPreToolHook(apiKey, opts = {}) {
|
|
|
151
151
|
settings.hooks = { ...hooks, PreToolCall: filtered };
|
|
152
152
|
writeSettings(settings);
|
|
153
153
|
if (!opts.silent) {
|
|
154
|
-
console.log("
|
|
154
|
+
console.log("Xmu vault hook installed (PreToolCall).");
|
|
155
155
|
console.log("Your knowledge vault will surface relevant captures as you edit files.");
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -294,7 +294,7 @@ export async function inboxCheck(apiKey, opts = {}) {
|
|
|
294
294
|
return;
|
|
295
295
|
const count = parseInt(match[1], 10);
|
|
296
296
|
if (count > 0) {
|
|
297
|
-
process.stdout.write(`
|
|
297
|
+
process.stdout.write(`Xmu: ${count} capture${count === 1 ? "" : "s"} in inbox — run list_inbox to review\n`);
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
catch {
|
package/dist/cli/index.js
CHANGED
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
const keyIdx = args.indexOf("--key");
|
|
53
53
|
const apiKey = keyIdx !== -1 ? args[keyIdx + 1] : readConfig().key;
|
|
54
54
|
if (!apiKey) {
|
|
55
|
-
console.error("No API key. Pass --key YOUR_KEY or store it:
|
|
55
|
+
console.error("No API key. Pass --key YOUR_KEY or store it: xmu config set key YOUR_KEY");
|
|
56
56
|
process.exit(1);
|
|
57
57
|
}
|
|
58
58
|
const planIdx = args.indexOf("--plan");
|
|
@@ -62,13 +62,13 @@ async function main() {
|
|
|
62
62
|
}
|
|
63
63
|
// Default: open the hosted app
|
|
64
64
|
console.log("");
|
|
65
|
-
console.log("
|
|
65
|
+
console.log(" Xmu — A knowledge graph for your AI workers\n");
|
|
66
66
|
console.log(` Open ${HOSTED_URL} to start capturing.`);
|
|
67
67
|
console.log("");
|
|
68
68
|
console.log(" First time? Sign in with GitHub — setup takes 30 seconds.");
|
|
69
69
|
console.log("");
|
|
70
|
-
console.log(" Connect
|
|
71
|
-
console.log(" claude mcp add
|
|
70
|
+
console.log(" Connect an AI worker:");
|
|
71
|
+
console.log(" claude mcp add xmu -- npx -y mnemos-capture@latest serve-mcp --key YOUR_API_KEY");
|
|
72
72
|
console.log("");
|
|
73
73
|
// Try to open the URL in the default browser
|
|
74
74
|
const { exec } = await import("child_process");
|
|
@@ -77,31 +77,33 @@ async function main() {
|
|
|
77
77
|
}
|
|
78
78
|
function printHelp() {
|
|
79
79
|
console.log(`
|
|
80
|
-
|
|
80
|
+
Xmu — A knowledge graph for your AI workers
|
|
81
81
|
|
|
82
82
|
Usage:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
xmu Open Xmu in your browser
|
|
84
|
+
xmu serve-mcp --key KEY Start the MCP server for your AI worker
|
|
85
|
+
xmu setup-hooks --key KEY Install inbox count hook (fast)
|
|
86
|
+
xmu setup-hooks --key KEY --briefing Install full briefing hook (uses LLM)
|
|
87
|
+
xmu setup-hooks --key KEY --vault Install vault hook (PreToolCall)
|
|
88
|
+
xmu setup-hooks --key KEY --briefing --vault Install both hooks
|
|
89
|
+
xmu config set agent "claude -p" Set which AI worker kos drives
|
|
90
|
+
xmu kos --key KEY [--plan FILE] Implement a plan in an isolated worktree
|
|
91
|
+
xmu help Show this help
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
npx -y mnemos-capture@latest … Same CLI, via the published package
|
|
94
|
+
|
|
95
|
+
MCP tools (via your AI worker):
|
|
94
96
|
briefing — Session-start briefing with ranked insights to apply
|
|
95
97
|
generate_plan — Turn selected captures into a full implementation plan
|
|
96
98
|
list_plans — List or read saved implementation plans
|
|
97
99
|
vault_scan — Scan all captures for relevance to current activity
|
|
98
100
|
curate — Validate URLs and flag stale captures
|
|
99
101
|
|
|
100
|
-
kos — implement plans with your own AI
|
|
101
|
-
1. Tell
|
|
102
|
-
|
|
102
|
+
kos — implement plans with your own AI worker (model-agnostic):
|
|
103
|
+
1. Tell Xmu which assistant to drive (once):
|
|
104
|
+
xmu config set agent "claude -p" # or "codex exec", "aider --yes --message"
|
|
103
105
|
2. Generate a plan via the generate_plan MCP tool.
|
|
104
|
-
3. Run:
|
|
106
|
+
3. Run: xmu kos --key YOUR_KEY
|
|
105
107
|
kos creates an isolated git worktree, hands the plan to your assistant,
|
|
106
108
|
and reports the branch + the plan's Verification Checklist when done.
|
|
107
109
|
|
|
@@ -112,7 +114,7 @@ function printHelp() {
|
|
|
112
114
|
4. Start capturing!
|
|
113
115
|
|
|
114
116
|
Connect to Claude Code:
|
|
115
|
-
claude mcp add
|
|
117
|
+
claude mcp add xmu -- npx -y mnemos-capture@latest serve-mcp --key YOUR_API_KEY
|
|
116
118
|
|
|
117
119
|
The @latest tag ensures every Claude Code session uses the most recent
|
|
118
120
|
published version. serve-mcp also self-upgrades on startup as a safety net
|
package/dist/cli/kos.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* kos —
|
|
2
|
+
* kos — mnemos' Knowledge Operations orchestrator.
|
|
3
3
|
*
|
|
4
|
-
* Drives whatever AI assistant the user configured (`
|
|
5
|
-
* to implement a generated plan in an isolated git worktree.
|
|
4
|
+
* Drives whatever AI assistant the user configured (`xmu config set agent ...`)
|
|
5
|
+
* to implement a generated plan in an isolated git worktree. mnemos owns the
|
|
6
6
|
* harness-independent parts: fetching the plan, the worktree, and reporting the
|
|
7
7
|
* Verification Checklist. The assistant is a black box — model-agnostic by design.
|
|
8
8
|
*/
|
|
@@ -54,10 +54,10 @@ function run(cmd, cwd) {
|
|
|
54
54
|
export async function kos(apiKey, opts = {}) {
|
|
55
55
|
const cfg = readConfig();
|
|
56
56
|
if (!cfg.agent) {
|
|
57
|
-
console.error("No agent configured. Tell
|
|
58
|
-
console.error('
|
|
59
|
-
console.error('
|
|
60
|
-
console.error('
|
|
57
|
+
console.error("No agent configured. Tell Xmu which AI worker to drive:\n");
|
|
58
|
+
console.error(' xmu config set agent "claude -p" # Claude Code');
|
|
59
|
+
console.error(' xmu config set agent "codex exec" # Codex');
|
|
60
|
+
console.error(' xmu config set agent "aider --yes --message" # Aider');
|
|
61
61
|
process.exit(1);
|
|
62
62
|
}
|
|
63
63
|
// 1. Resolve the plan — explicit filename, or the newest saved plan
|
|
@@ -77,7 +77,7 @@ export async function kos(apiKey, opts = {}) {
|
|
|
77
77
|
repoRoot = run("git rev-parse --show-toplevel");
|
|
78
78
|
}
|
|
79
79
|
catch {
|
|
80
|
-
console.error("
|
|
80
|
+
console.error("xmu kos must be run inside a git repository.");
|
|
81
81
|
process.exit(1);
|
|
82
82
|
return;
|
|
83
83
|
}
|
package/dist/cli/mcp-server.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
// MCP server that runs locally via stdio and proxies to the hosted
|
|
2
|
-
// Used by Claude Code: claude mcp add
|
|
3
|
-
const
|
|
1
|
+
// MCP server that runs locally via stdio and proxies to the hosted mnemos API.
|
|
2
|
+
// Used by Claude Code: claude mcp add xmu -- npx mnemos-capture serve-mcp --key <api-key>
|
|
3
|
+
const DEFAULT_HOSTED_URL = "https://mnemos-capture.vercel.app/api/mcp";
|
|
4
|
+
/** Override for pointing the proxy at a local instance during development. */
|
|
5
|
+
const HOSTED_URL = process.env.XMU_API_URL ?? process.env.MNEMOS_API_URL ?? DEFAULT_HOSTED_URL;
|
|
4
6
|
function sendMessage(msg) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
process.stdout.write(`${JSON.stringify(msg)}\n`);
|
|
8
|
+
}
|
|
9
|
+
/** In-flight proxied requests, so shutdown can wait for them. */
|
|
10
|
+
const pending = new Set();
|
|
11
|
+
function rpcError(id, message) {
|
|
12
|
+
sendMessage({ jsonrpc: "2.0", id, error: { code: -32603, message } });
|
|
7
13
|
}
|
|
8
14
|
async function proxyToHosted(apiKey, msg) {
|
|
15
|
+
let res;
|
|
9
16
|
try {
|
|
10
|
-
|
|
17
|
+
res = await fetch(HOSTED_URL, {
|
|
11
18
|
method: "POST",
|
|
12
19
|
headers: {
|
|
13
20
|
"Content-Type": "application/json",
|
|
@@ -15,17 +22,50 @@ async function proxyToHosted(apiKey, msg) {
|
|
|
15
22
|
},
|
|
16
23
|
body: JSON.stringify(msg),
|
|
17
24
|
});
|
|
18
|
-
const data = (await res.json());
|
|
19
|
-
sendMessage(data);
|
|
20
25
|
}
|
|
21
26
|
catch (err) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const detail = err instanceof Error ? err.message : "unknown error";
|
|
28
|
+
rpcError(msg.id, `Could not reach Xmu at ${HOSTED_URL}: ${detail}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const body = await res.text().catch(() => "");
|
|
32
|
+
const result = interpretResponse(res.status, res.headers.get("content-type"), body);
|
|
33
|
+
if ("error" in result) {
|
|
34
|
+
rpcError(msg.id, result.error);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
sendMessage(result.data);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Turn a raw HTTP reply into either parsed JSON-RPC data or a useful message.
|
|
41
|
+
*
|
|
42
|
+
* Parsing straight to JSON turns any non-JSON reply — an HTML error page from a
|
|
43
|
+
* 500, a proxy block page, a login redirect — into "Unexpected token '<'",
|
|
44
|
+
* which says nothing about what actually happened. Reporting the status and a
|
|
45
|
+
* snippet makes the real cause visible in the MCP client.
|
|
46
|
+
*
|
|
47
|
+
* Exported for testing; this is the logic worth covering, not the stdio plumbing.
|
|
48
|
+
*/
|
|
49
|
+
export function interpretResponse(status, contentType, body) {
|
|
50
|
+
const type = contentType ?? "";
|
|
51
|
+
if (!type.includes("application/json")) {
|
|
52
|
+
const snippet = body.trim().slice(0, 200).replace(/\s+/g, " ");
|
|
53
|
+
return {
|
|
54
|
+
error: `Xmu returned a non-JSON response (HTTP ${status}${type ? `, ${type}` : ""})${snippet ? `: ${snippet}` : ""}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return { data: JSON.parse(body) };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { error: `Xmu returned malformed JSON (HTTP ${status}).` };
|
|
27
62
|
}
|
|
28
63
|
}
|
|
64
|
+
/** Track a proxied request so shutdown can await it. */
|
|
65
|
+
function track(p) {
|
|
66
|
+
pending.add(p);
|
|
67
|
+
void p.finally(() => pending.delete(p));
|
|
68
|
+
}
|
|
29
69
|
export async function serveMcp() {
|
|
30
70
|
// Parse --key flag
|
|
31
71
|
const args = process.argv.slice(2);
|
|
@@ -57,38 +97,33 @@ export async function serveMcp() {
|
|
|
57
97
|
catch {
|
|
58
98
|
// Non-fatal — hook setup should never block the MCP server from starting
|
|
59
99
|
}
|
|
60
|
-
process.stderr.write("
|
|
100
|
+
process.stderr.write("Xmu MCP server starting (proxying to hosted instance)...\n");
|
|
61
101
|
let buffer = "";
|
|
62
102
|
process.stdin.setEncoding("utf-8");
|
|
63
103
|
process.stdin.on("data", (chunk) => {
|
|
64
104
|
buffer += chunk;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
71
|
-
if (!match) {
|
|
72
|
-
buffer = buffer.slice(headerEnd + 4);
|
|
105
|
+
let newlineIdx;
|
|
106
|
+
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
|
107
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
108
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
109
|
+
if (!line)
|
|
73
110
|
continue;
|
|
74
|
-
}
|
|
75
|
-
const contentLength = parseInt(match[1], 10);
|
|
76
|
-
const bodyStart = headerEnd + 4;
|
|
77
|
-
if (buffer.length < bodyStart + contentLength)
|
|
78
|
-
break;
|
|
79
|
-
const body = buffer.slice(bodyStart, bodyStart + contentLength);
|
|
80
|
-
buffer = buffer.slice(bodyStart + contentLength);
|
|
81
111
|
try {
|
|
82
|
-
const msg = JSON.parse(
|
|
112
|
+
const msg = JSON.parse(line);
|
|
83
113
|
// Handle notifications/initialized locally (no response needed)
|
|
84
114
|
if (msg.method === "notifications/initialized")
|
|
85
115
|
continue;
|
|
86
|
-
|
|
116
|
+
track(proxyToHosted(apiKey, msg));
|
|
87
117
|
}
|
|
88
118
|
catch {
|
|
89
|
-
process.stderr.write(`Failed to parse: ${
|
|
119
|
+
process.stderr.write(`Failed to parse: ${line}\n`);
|
|
90
120
|
}
|
|
91
121
|
}
|
|
92
122
|
});
|
|
93
|
-
|
|
123
|
+
// Wait for in-flight requests before exiting: exiting immediately on stdin
|
|
124
|
+
// close discards responses that were still being fetched, so the client sees
|
|
125
|
+
// silence rather than an answer or an error.
|
|
126
|
+
process.stdin.on("end", () => {
|
|
127
|
+
void Promise.allSettled([...pending]).then(() => process.exit(0));
|
|
128
|
+
});
|
|
94
129
|
}
|
|
@@ -59,7 +59,7 @@ export async function maybeSelfUpgrade() {
|
|
|
59
59
|
const latest = await getLatestVersion();
|
|
60
60
|
if (!latest || !isNewer(latest, current))
|
|
61
61
|
return false;
|
|
62
|
-
process.stderr.write(`
|
|
62
|
+
process.stderr.write(`Xmu: upgrading to v${latest} (was v${current})...\n`);
|
|
63
63
|
const child = spawn("npx", ["-y", `mnemos-capture@${latest}`, ...process.argv.slice(2)], {
|
|
64
64
|
stdio: "inherit",
|
|
65
65
|
env: { ...process.env, MNEMOS_UPGRADED: "1" },
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mnemos-capture",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Xmu — Turn what you discover into context your AI workers can use. Capture once, then brief, plan, and execute with any MCP-compatible tool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"mnemos-capture": "./dist/cli/index.js"
|
|
7
|
+
"mnemos-capture": "./dist/cli/index.js",
|
|
8
|
+
"xmu": "./dist/cli/index.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"dev": "next dev",
|
|
@@ -13,10 +14,14 @@
|
|
|
13
14
|
"build:cli": "tsc -p cli/tsconfig.json",
|
|
14
15
|
"prepublishOnly": "npm run build:cli",
|
|
15
16
|
"test": "vitest run",
|
|
16
|
-
"test:watch": "vitest"
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"test:e2e": "playwright test",
|
|
19
|
+
"lint": "eslint .",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
17
21
|
},
|
|
18
22
|
"keywords": [
|
|
19
23
|
"ai",
|
|
24
|
+
"xmu",
|
|
20
25
|
"knowledge",
|
|
21
26
|
"capture",
|
|
22
27
|
"claude",
|
|
@@ -40,23 +45,26 @@
|
|
|
40
45
|
"README.md"
|
|
41
46
|
],
|
|
42
47
|
"dependencies": {
|
|
43
|
-
"@anthropic-ai/sdk": "^0.
|
|
48
|
+
"@anthropic-ai/sdk": "^0.122.0",
|
|
44
49
|
"@vercel/postgres": "^0.10.0",
|
|
45
50
|
"next": "^16.1.6",
|
|
46
51
|
"react": "^19.0.0",
|
|
47
52
|
"react-dom": "^19.0.0"
|
|
48
53
|
},
|
|
49
54
|
"devDependencies": {
|
|
50
|
-
"@
|
|
51
|
-
"@types/
|
|
52
|
-
"@types/react
|
|
55
|
+
"@playwright/test": "^1.62.1",
|
|
56
|
+
"@types/node": "^26",
|
|
57
|
+
"@types/react": "^19.2.18",
|
|
58
|
+
"@types/react-dom": "^19.2.5",
|
|
53
59
|
"autoprefixer": "^10.4.21",
|
|
60
|
+
"eslint": "^9.39.5",
|
|
61
|
+
"eslint-config-next": "^16.3.3",
|
|
54
62
|
"postcss": "^8.5.3",
|
|
55
63
|
"tailwindcss": "^3.4.17",
|
|
56
|
-
"typescript": "^
|
|
64
|
+
"typescript": "^6.0.3",
|
|
57
65
|
"vitest": "^4.1.8"
|
|
58
66
|
},
|
|
59
67
|
"engines": {
|
|
60
|
-
"node": ">=
|
|
68
|
+
"node": ">=20.9"
|
|
61
69
|
}
|
|
62
70
|
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtempSync, rmSync, readFileSync, existsSync } from "fs";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
describe("config", () => {
|
|
6
|
-
let tmpDir;
|
|
7
|
-
let configPath;
|
|
8
|
-
let origHome;
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
tmpDir = mkdtempSync(join(tmpdir(), "mnemos-config-test-"));
|
|
11
|
-
configPath = join(tmpDir, ".mnemos", "config.json");
|
|
12
|
-
origHome = process.env["HOME"];
|
|
13
|
-
process.env["HOME"] = tmpDir;
|
|
14
|
-
// Module-level CONFIG_DIR is computed from homedir() at import — reset cache
|
|
15
|
-
vi.resetModules();
|
|
16
|
-
});
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
19
|
-
process.env["HOME"] = origHome;
|
|
20
|
-
});
|
|
21
|
-
it("returns an empty object when no config exists", async () => {
|
|
22
|
-
const { readConfig } = await import("../config.js");
|
|
23
|
-
expect(readConfig()).toEqual({});
|
|
24
|
-
});
|
|
25
|
-
it("writes and reads back the agent field", async () => {
|
|
26
|
-
const { setConfigField, readConfig } = await import("../config.js");
|
|
27
|
-
setConfigField("agent", "claude -p");
|
|
28
|
-
expect(readConfig().agent).toBe("claude -p");
|
|
29
|
-
expect(existsSync(configPath)).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
it("persists the agent command verbatim, including flags", async () => {
|
|
32
|
-
const { setConfigField } = await import("../config.js");
|
|
33
|
-
setConfigField("agent", "aider --yes --message");
|
|
34
|
-
const saved = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
35
|
-
expect(saved.agent).toBe("aider --yes --message");
|
|
36
|
-
});
|
|
37
|
-
it("preserves other fields when updating one", async () => {
|
|
38
|
-
const { setConfigField, readConfig } = await import("../config.js");
|
|
39
|
-
setConfigField("key", "mnemos_abc123");
|
|
40
|
-
setConfigField("agent", "codex exec");
|
|
41
|
-
const cfg = readConfig();
|
|
42
|
-
expect(cfg.key).toBe("mnemos_abc123");
|
|
43
|
-
expect(cfg.agent).toBe("codex exec");
|
|
44
|
-
});
|
|
45
|
-
it("throws on an unknown config field", async () => {
|
|
46
|
-
const { setConfigField } = await import("../config.js");
|
|
47
|
-
expect(() => setConfigField("provider", "openai")).toThrow(/Unknown config field/);
|
|
48
|
-
});
|
|
49
|
-
});
|