@mnemahq/cli 0.3.0 → 0.3.1
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/package.json +2 -2
- package/src/cli.mjs +50 -10
- package/src/client.mjs +93 -4
- package/src/read-commands.mjs +18 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mnemahq/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Mnema CLI — connect a repo to your Mnema workspace: install session capture, sweep past sessions, and search from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "node -e \"process.exit(0)\"",
|
|
37
|
-
"typecheck": "node --check bin/mnema.mjs && node --check src/cli.mjs && node --check src/util.mjs && node --check src/secrets.mjs && node --check src/client.mjs && node --check src/read-commands.mjs",
|
|
37
|
+
"typecheck": "node --check bin/mnema.mjs && node --check src/cli.mjs && node --check src/util.mjs && node --check src/secrets.mjs && node --check src/client.mjs && node --check src/read-commands.mjs && node --check test/version.test.mjs",
|
|
38
38
|
"test": "vitest run"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* mnema uninstall cleanly reverse everything
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync } from 'node:fs';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { cmdLogin, cmdLogout, accessToken } from './login.mjs';
|
|
15
|
-
import { makeClient, canAuthenticate, renderError } from './client.mjs';
|
|
15
|
+
import { makeClient, call, hasApiKey, canAuthenticate, renderError, mintHookToken } from './client.mjs';
|
|
16
16
|
import {
|
|
17
17
|
cmdTasks, cmdNext, cmdDocs, cmdDoc, cmdProjects, cmdAsk, cmdGraph, cmdBriefing,
|
|
18
18
|
} from './read-commands.mjs';
|
|
@@ -27,7 +27,25 @@ import {
|
|
|
27
27
|
import { applyContext, scaffold } from './artifacts.mjs';
|
|
28
28
|
import { execFileSync } from 'node:child_process';
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* ⚠️ READ FROM package.json, NEVER HAND-WRITTEN. This was `const VERSION =
|
|
32
|
+
* '0.1.0'` and stayed that way through 0.1.1 and 0.3.0 — so every release of
|
|
33
|
+
* this CLI reported the same wrong number, and `mnema --version` was useless for
|
|
34
|
+
* the one job it has.
|
|
35
|
+
*
|
|
36
|
+
* The cost was not cosmetic. After 0.3.0 shipped with eight new commands,
|
|
37
|
+
* `mnema --help` printed "mnema 0.1.0" and the old command list, because the
|
|
38
|
+
* global install was stale and npm does not upgrade an installed global when you
|
|
39
|
+
* publish. The right fix was one line — `npm i -g @mnemahq/cli@latest` — but a
|
|
40
|
+
* binary that misreports itself makes that undiagnosable, and the honest reading
|
|
41
|
+
* of the evidence was "you shipped nothing".
|
|
42
|
+
*
|
|
43
|
+
* Resolved relative to import.meta.url, not cwd: the CLI runs from whatever
|
|
44
|
+
* directory the user happens to be in. package.json is always in the tarball.
|
|
45
|
+
*/
|
|
46
|
+
const VERSION = JSON.parse(
|
|
47
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
48
|
+
).version;
|
|
31
49
|
|
|
32
50
|
function parseFlags(argv) {
|
|
33
51
|
const flags = {}; const rest = [];
|
|
@@ -105,6 +123,26 @@ async function cmdInit(flags) {
|
|
|
105
123
|
if (!workspaceId) { console.error(c.red('A workspace id is required.')); process.exit(1); }
|
|
106
124
|
|
|
107
125
|
let hookToken = process.env.MNEMA_HOOK_TOKEN || getSecret(workspaceId, 'hook-token');
|
|
126
|
+
|
|
127
|
+
// ⚠️ ONLY MINT WHEN THERE IS NOTHING STORED. Minting ROTATES — the server keeps
|
|
128
|
+
// a hash, so `regenerate` is the only way to obtain a token and it invalidates
|
|
129
|
+
// the previous one. Re-running `init` on a second machine must not silently
|
|
130
|
+
// kill capture on the first.
|
|
131
|
+
if (!hookToken) {
|
|
132
|
+
process.stdout.write('Getting a hook token… ');
|
|
133
|
+
const minted = await mintHookToken({ origin, workspaceId });
|
|
134
|
+
if (minted) {
|
|
135
|
+
hookToken = minted;
|
|
136
|
+
console.log(c.green('done'));
|
|
137
|
+
console.log(c.dim(' A new token was issued. Any OTHER machine set up for this workspace'));
|
|
138
|
+
console.log(c.dim(' must re-run `mnema init` — the previous token no longer works.'));
|
|
139
|
+
} else {
|
|
140
|
+
// Not logged in, not the owner (the endpoint is owner-only), or offline.
|
|
141
|
+
// Fall through to asking, with the right pointer.
|
|
142
|
+
console.log(c.dim('not available — enter one manually'));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
108
146
|
if (!hookToken) {
|
|
109
147
|
// ⚠️ NOT "Settings → Developer". That page does not exist and never did in
|
|
110
148
|
// this shape — the hook token moved to the Access page when tokens were
|
|
@@ -215,9 +253,8 @@ async function cmdSessions(flags) {
|
|
|
215
253
|
const repo = canonicalRepo(git.remote);
|
|
216
254
|
let rows = [];
|
|
217
255
|
try {
|
|
218
|
-
rows = await
|
|
219
|
-
.sessions.list({ limit, ...(repo ? { repo } : {}) })
|
|
220
|
-
.all();
|
|
256
|
+
rows = await call({ origin, workspaceId }, (m) =>
|
|
257
|
+
m.sessions.list({ limit, ...(repo ? { repo } : {}) }).all());
|
|
221
258
|
} catch (e) {
|
|
222
259
|
// Non-fatal by design: the LOCAL list above is the useful half and has
|
|
223
260
|
// already printed. Degrading here is deliberate, and it says which wall it
|
|
@@ -260,8 +297,8 @@ async function cmdSearch(flags, rest) {
|
|
|
260
297
|
|
|
261
298
|
let results = [];
|
|
262
299
|
try {
|
|
263
|
-
results = await
|
|
264
|
-
} catch (e) { renderError(e, { context: 'search' }); }
|
|
300
|
+
results = await call({ origin, workspaceId }, (m) => m.docs.search(query));
|
|
301
|
+
} catch (e) { renderError(e, { context: 'search', usedApiKey: hasApiKey(workspaceId) }); }
|
|
265
302
|
if (!results.length) { console.log(c.dim('No results.')); return; }
|
|
266
303
|
console.log(c.bold(`${results.length} result(s) for "${query}"`));
|
|
267
304
|
for (const d of results) {
|
|
@@ -285,8 +322,8 @@ async function cmdPull(flags) {
|
|
|
285
322
|
try {
|
|
286
323
|
// `.context`, not the whole result — `.repo` is the URL the server matched.
|
|
287
324
|
// Reading the wrong one of these two is the bug this rewire found in the SDK.
|
|
288
|
-
docs = (await
|
|
289
|
-
} catch (e) { renderError(e, { context: 'pull' }); }
|
|
325
|
+
docs = (await call({ origin, workspaceId }, (m) => m.repos.context(repo))).context ?? [];
|
|
326
|
+
} catch (e) { renderError(e, { context: 'pull', usedApiKey: hasApiKey(workspaceId) }); }
|
|
290
327
|
|
|
291
328
|
scaffold(root);
|
|
292
329
|
const s = applyContext(root, docs);
|
|
@@ -329,6 +366,9 @@ async function cmdDoctor(flags) {
|
|
|
329
366
|
const apiKey = workspaceId ? getSecret(workspaceId, 'api-key') : null;
|
|
330
367
|
if (apiKey) {
|
|
331
368
|
let keyOk = false;
|
|
369
|
+
// Probes the KEY specifically — no fallback — because that is the thing
|
|
370
|
+
// doctor is reporting on. Using call() here would mask a dead key behind a
|
|
371
|
+
// working login and print a green tick for a credential that does not work.
|
|
332
372
|
try { await makeClient({ origin, workspaceId }).docs.list({ limit: 1 }).pages().next(); keyOk = true; } catch { /* */ }
|
|
333
373
|
ok('API key valid', keyOk);
|
|
334
374
|
} else {
|
package/src/client.mjs
CHANGED
|
@@ -28,8 +28,8 @@ import { c, DEFAULT_APP_URL } from './util.mjs';
|
|
|
28
28
|
* request — that is what makes a refresh mid-command work instead of failing on
|
|
29
29
|
* a token that expired thirty seconds ago.
|
|
30
30
|
*/
|
|
31
|
-
export function makeClient({ origin, workspaceId }) {
|
|
32
|
-
const apiKey = (workspaceId && getSecret(workspaceId, 'api-key')) || process.env.MNEMA_API_KEY || null;
|
|
31
|
+
export function makeClient({ origin, workspaceId, forceLogin = false }) {
|
|
32
|
+
const apiKey = forceLogin ? null : ((workspaceId && getSecret(workspaceId, 'api-key')) || process.env.MNEMA_API_KEY || null);
|
|
33
33
|
return new Mnema(
|
|
34
34
|
apiKey
|
|
35
35
|
? { apiKey, baseUrl: origin }
|
|
@@ -37,6 +37,45 @@ export function makeClient({ origin, workspaceId }) {
|
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Run an SDK call, and if a STORED API KEY is rejected, retry once with the login.
|
|
42
|
+
*
|
|
43
|
+
* ⭐ THE TRAP THIS REMOVES. makeClient prefers an explicit API key, which is right
|
|
44
|
+
* — a key is scoped and deliberate. But a DEAD key then shadows a perfectly good
|
|
45
|
+
* login forever, and the failure told the user to do the one thing that could not
|
|
46
|
+
* help:
|
|
47
|
+
*
|
|
48
|
+
* $ mnema docs -> not authenticated. Run `mnema login`.
|
|
49
|
+
* $ mnema login -> ✓ Logged in.
|
|
50
|
+
* $ mnema docs -> not authenticated. Run `mnema login`.
|
|
51
|
+
*
|
|
52
|
+
* The login was never the credential in play. An instruction that cannot work is
|
|
53
|
+
* worse than none: it spends the reader's remaining trust in the tool.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ THE SDK CANNOT DO THIS FOR US. packages/core resolves `Bearer apiKey`
|
|
56
|
+
* whenever apiKey is set and never consults getToken, so `onUnauthorized` can
|
|
57
|
+
* ask for a retry but cannot change which credential the retry uses. The swap has
|
|
58
|
+
* to happen out here, by building a second client.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ AND IT IS ANNOUNCED, not silent. Quietly using a different identity than the
|
|
61
|
+
* one configured is its own confusion — the next question would be "which
|
|
62
|
+
* credential ran this?", and the answer should not require reading source.
|
|
63
|
+
*/
|
|
64
|
+
export async function call(ctx, fn) {
|
|
65
|
+
try {
|
|
66
|
+
return await fn(makeClient(ctx));
|
|
67
|
+
} catch (e) {
|
|
68
|
+
const isAuth = e?.constructor?.name === 'AuthError';
|
|
69
|
+
if (!isAuth || ctx.forceLogin || !hasApiKey(ctx.workspaceId)) throw e;
|
|
70
|
+
let loggedIn = false;
|
|
71
|
+
try { loggedIn = Boolean(await accessToken()); } catch { /* no login */ }
|
|
72
|
+
if (!loggedIn) throw e;
|
|
73
|
+
console.error(c.yellow(' stored API key was rejected — using your login instead.'));
|
|
74
|
+
console.error(c.dim(' Run `mnema init` to replace the key, or ignore this if you meant to use the login.'));
|
|
75
|
+
return await fn(makeClient({ ...ctx, forceLogin: true }));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
/** True when an explicit API key is configured. Cheap; no network. */
|
|
41
80
|
export function hasApiKey(workspaceId) {
|
|
42
81
|
return Boolean((workspaceId && getSecret(workspaceId, 'api-key')) || process.env.MNEMA_API_KEY);
|
|
@@ -73,14 +112,24 @@ export async function canAuthenticate(workspaceId) {
|
|
|
73
112
|
* branch — turning a good message back into a bad one for the exact reason this
|
|
74
113
|
* function exists.
|
|
75
114
|
*/
|
|
76
|
-
export function renderError(e, { context = '' } = {}) {
|
|
115
|
+
export function renderError(e, { context = '', usedApiKey = false } = {}) {
|
|
116
|
+
const opts = { usedApiKey };
|
|
77
117
|
const name = e?.constructor?.name ?? '';
|
|
78
118
|
const where = context ? `${context}: ` : '';
|
|
79
119
|
|
|
80
120
|
switch (name) {
|
|
81
121
|
case 'AuthError':
|
|
82
122
|
console.error(c.red(`${where}not authenticated.`));
|
|
83
|
-
|
|
123
|
+
// ⚠️ NAME THE CREDENTIAL THAT FAILED. The SDK's generic fix is "Run `mnema
|
|
124
|
+
// login`", which is exactly wrong when the thing that was rejected is a
|
|
125
|
+
// stored API KEY and a valid login already exists — the user runs it,
|
|
126
|
+
// succeeds, and nothing changes.
|
|
127
|
+
if (opts.usedApiKey) {
|
|
128
|
+
console.error(' The stored API key was rejected.');
|
|
129
|
+
console.error(' Run `mnema init` to replace it, or `mnema login` to use a login instead.');
|
|
130
|
+
} else {
|
|
131
|
+
console.error(` ${e.fix ?? 'Run `mnema login`.'}`);
|
|
132
|
+
}
|
|
84
133
|
break;
|
|
85
134
|
|
|
86
135
|
case 'PlanRequiredError':
|
|
@@ -126,3 +175,43 @@ export function renderError(e, { context = '' } = {}) {
|
|
|
126
175
|
}
|
|
127
176
|
process.exit(1);
|
|
128
177
|
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Mint a hook token for this workspace, using the login (t-627).
|
|
181
|
+
*
|
|
182
|
+
* ⭐ WHY THIS EXISTS. Setting up a repo meant fetching three things from two
|
|
183
|
+
* settings pages. The workspace id now comes from /api/me and the API key is
|
|
184
|
+
* optional — this was the last piece of copy-paste, and it never needed to be one:
|
|
185
|
+
* the endpoint has existed all along and has accepted a bearer token since t-613.
|
|
186
|
+
*
|
|
187
|
+
* ⚠️ THE HOOK TOKEN CANNOT SIMPLY BE THE LOGIN, and that is deliberate rather than
|
|
188
|
+
* unfinished. installHook writes it in PLAINTEXT to ~/.claude/mnema-hook.json,
|
|
189
|
+
* where an unattended process reads it after every session. A narrow,
|
|
190
|
+
* single-purpose token belongs in a file like that; an OAuth refresh token — which
|
|
191
|
+
* can mint access tokens for the whole account — does not.
|
|
192
|
+
*
|
|
193
|
+
* ⚠️ AND MINTING ROTATES. The server keeps only a HASH, so there is no way to read
|
|
194
|
+
* an existing token back; `regenerate` is the only path and it invalidates the old
|
|
195
|
+
* one. Callers MUST check for a stored token first — minting unconditionally would
|
|
196
|
+
* silently break capture on every other machine the user has set up. That check
|
|
197
|
+
* lives in cmdInit, not here, because this function's job is to mint and it should
|
|
198
|
+
* not pretend otherwise.
|
|
199
|
+
*
|
|
200
|
+
* Returns null rather than throwing: not logged in, not the owner (the endpoint is
|
|
201
|
+
* owner-only), or offline are all ordinary, and the caller falls back to asking.
|
|
202
|
+
*/
|
|
203
|
+
export async function mintHookToken({ origin, workspaceId }) {
|
|
204
|
+
try {
|
|
205
|
+
const token = await accessToken();
|
|
206
|
+
if (!token) return null;
|
|
207
|
+
const res = await fetch(`${origin}/api/workspaces/${workspaceId}/regenerate-hook-token`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
210
|
+
});
|
|
211
|
+
if (!res.ok) return null;
|
|
212
|
+
const body = await res.json();
|
|
213
|
+
return body?.hookToken ?? null;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/read-commands.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* returns the shape worth emitting, so it costs one line each.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import { call, hasApiKey, canAuthenticate, renderError } from './client.mjs';
|
|
24
24
|
import { c } from './util.mjs';
|
|
25
25
|
|
|
26
26
|
/** Machine output is the whole object; humans get the formatted view. */
|
|
@@ -42,11 +42,11 @@ export async function cmdTasks(flags, ctx) {
|
|
|
42
42
|
await guard(ctx.workspaceId);
|
|
43
43
|
const limit = Number(flags.limit) || 20;
|
|
44
44
|
try {
|
|
45
|
-
const rows = await
|
|
45
|
+
const rows = await call(ctx, (m) => m.tasks.list({
|
|
46
46
|
limit,
|
|
47
47
|
...(flags.status ? { status: flags.status } : {}),
|
|
48
48
|
...(flags.project ? { project: flags.project } : {}),
|
|
49
|
-
}).all();
|
|
49
|
+
}).all());
|
|
50
50
|
emit(flags, rows, () => {
|
|
51
51
|
if (!rows.length) return console.log(c.dim('No tasks match.'));
|
|
52
52
|
console.log(c.bold(`${rows.length} task(s)`));
|
|
@@ -55,13 +55,13 @@ export async function cmdTasks(flags, ctx) {
|
|
|
55
55
|
console.log(` ${c.cyan(id)} ${String(t.status).padEnd(12)} ${c.dim(String(t.priority).padEnd(8))} ${trunc(t.title, 70)}`);
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
|
-
} catch (e) { renderError(e, { context: 'tasks' }); }
|
|
58
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'tasks' }); }
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export async function cmdNext(flags, ctx) {
|
|
62
62
|
await guard(ctx.workspaceId);
|
|
63
63
|
try {
|
|
64
|
-
const t = await
|
|
64
|
+
const t = await call(ctx, (m) => m.tasks.next());
|
|
65
65
|
emit(flags, t, () => {
|
|
66
66
|
if (!t) return console.log(c.dim('Nothing queued — the board is clear.'));
|
|
67
67
|
console.log(`${c.bold(t.publicId || t.id)} ${t.title}`);
|
|
@@ -73,7 +73,7 @@ export async function cmdNext(flags, ctx) {
|
|
|
73
73
|
console.log(`\n ${c.dim('branch:')} git checkout -b ${t.publicId}${slug ? `-${slug}` : ''}`);
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
|
-
} catch (e) { renderError(e, { context: 'next' }); }
|
|
76
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'next' }); }
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// ── docs ──────────────────────────────────────────────────────────────────────
|
|
@@ -81,7 +81,7 @@ export async function cmdDocs(flags, ctx) {
|
|
|
81
81
|
await guard(ctx.workspaceId);
|
|
82
82
|
const limit = Number(flags.limit) || 20;
|
|
83
83
|
try {
|
|
84
|
-
const rows = await
|
|
84
|
+
const rows = await call(ctx, (m) => m.docs.list({ limit }).all());
|
|
85
85
|
emit(flags, rows, () => {
|
|
86
86
|
if (!rows.length) return console.log(c.dim('No docs.'));
|
|
87
87
|
console.log(c.bold(`${rows.length} doc(s)`));
|
|
@@ -89,7 +89,7 @@ export async function cmdDocs(flags, ctx) {
|
|
|
89
89
|
console.log(` ${c.dim(d.id.slice(0, 8))} ${trunc(d.title, 60).padEnd(60)} ${c.dim(String(d.updatedAt).slice(0, 10))}`);
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
|
-
} catch (e) { renderError(e, { context: 'docs' }); }
|
|
92
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'docs' }); }
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export async function cmdDoc(flags, ctx, rest) {
|
|
@@ -97,7 +97,7 @@ export async function cmdDoc(flags, ctx, rest) {
|
|
|
97
97
|
const id = rest[0];
|
|
98
98
|
if (!id) { console.error(c.red('Usage: mnema doc <id>')); process.exit(1); }
|
|
99
99
|
try {
|
|
100
|
-
const d = await
|
|
100
|
+
const d = await call(ctx, (m) => m.docs.get(id));
|
|
101
101
|
// Markdown goes to stdout unadorned so `mnema doc <id> > file.md` works and
|
|
102
102
|
// the heading is not silently baked into the file.
|
|
103
103
|
emit(flags, d, () => {
|
|
@@ -105,20 +105,20 @@ export async function cmdDoc(flags, ctx, rest) {
|
|
|
105
105
|
console.error(c.dim(`${d.path} · updated ${String(d.updatedAt).slice(0, 10)}`));
|
|
106
106
|
if (d.markdown) console.log(d.markdown);
|
|
107
107
|
});
|
|
108
|
-
} catch (e) { renderError(e, { context: 'doc' }); }
|
|
108
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'doc' }); }
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// ── projects ──────────────────────────────────────────────────────────────────
|
|
112
112
|
export async function cmdProjects(flags, ctx) {
|
|
113
113
|
await guard(ctx.workspaceId);
|
|
114
114
|
try {
|
|
115
|
-
const rows = await
|
|
115
|
+
const rows = await call(ctx, (m) => m.projects.list().all());
|
|
116
116
|
emit(flags, rows, () => {
|
|
117
117
|
if (!rows.length) return console.log(c.dim('No projects.'));
|
|
118
118
|
console.log(c.bold(`${rows.length} project(s)`));
|
|
119
119
|
for (const p of rows) console.log(` ${c.dim(p.id.slice(0, 8))} ${trunc(p.name, 50)}`);
|
|
120
120
|
});
|
|
121
|
-
} catch (e) { renderError(e, { context: 'projects' }); }
|
|
121
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'projects' }); }
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// ── graph: ask + traverse ─────────────────────────────────────────────────────
|
|
@@ -127,7 +127,7 @@ export async function cmdAsk(flags, ctx, rest) {
|
|
|
127
127
|
const q = rest.join(' ').trim();
|
|
128
128
|
if (!q) { console.error(c.red('Usage: mnema ask "<question>"')); process.exit(1); }
|
|
129
129
|
try {
|
|
130
|
-
const a = await
|
|
130
|
+
const a = await call(ctx, (m) => m.graph.ask(q, { ...(flags.budget ? { budgetMs: Number(flags.budget) } : {}) }));
|
|
131
131
|
emit(flags, a, () => {
|
|
132
132
|
console.log(a.answer);
|
|
133
133
|
// ⚠️ NEVER WITHOUT THE CONFIDENCE. The interpreter deliberately hedges
|
|
@@ -142,7 +142,7 @@ export async function cmdAsk(flags, ctx, rest) {
|
|
|
142
142
|
for (const s of a.sources.slice(0, 5)) console.log(c.dim(` ${trunc(s.title, 70)}`));
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
|
-
} catch (e) { renderError(e, { context: 'ask' }); }
|
|
145
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'ask' }); }
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
export async function cmdGraph(flags, ctx, rest) {
|
|
@@ -150,7 +150,7 @@ export async function cmdGraph(flags, ctx, rest) {
|
|
|
150
150
|
const [from, to] = rest;
|
|
151
151
|
if (!from) { console.error(c.red('Usage: mnema graph <from> [to]')); process.exit(1); }
|
|
152
152
|
try {
|
|
153
|
-
const g = await
|
|
153
|
+
const g = await call(ctx, (m) => m.graph.traverse(from, to));
|
|
154
154
|
emit(flags, g, () => {
|
|
155
155
|
if (to) {
|
|
156
156
|
// connected:false is an ANSWER, not a failure — say so plainly rather
|
|
@@ -166,14 +166,14 @@ export async function cmdGraph(flags, ctx, rest) {
|
|
|
166
166
|
console.log(c.bold(`${g.nodes.length} node(s), ${g.edges.length} edge(s) around "${from}"`));
|
|
167
167
|
for (const n of g.nodes.slice(0, 25)) console.log(` ${trunc(n.label, 60).padEnd(60)} ${c.dim(n.type ?? '')}`);
|
|
168
168
|
});
|
|
169
|
-
} catch (e) { renderError(e, { context: 'graph' }); }
|
|
169
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'graph' }); }
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
// ── briefing ──────────────────────────────────────────────────────────────────
|
|
173
173
|
export async function cmdBriefing(flags, ctx) {
|
|
174
174
|
await guard(ctx.workspaceId);
|
|
175
175
|
try {
|
|
176
|
-
const b = await
|
|
176
|
+
const b = await call(ctx, (m) => m.findings.briefing());
|
|
177
177
|
emit(flags, b, () => {
|
|
178
178
|
const p = b.pulse;
|
|
179
179
|
console.log(c.bold('Pulse'));
|
|
@@ -192,5 +192,5 @@ export async function cmdBriefing(flags, ctx) {
|
|
|
192
192
|
console.log(` ${c.dim(String(f.kind).padEnd(14))} ${trunc(f.headline, 90)}${count}`);
|
|
193
193
|
}
|
|
194
194
|
});
|
|
195
|
-
} catch (e) { renderError(e, { context: 'briefing' }); }
|
|
195
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'briefing' }); }
|
|
196
196
|
}
|