@mnemahq/cli 0.11.0 → 0.12.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/package.json +2 -2
- package/src/cli.mjs +8 -1
- package/src/read-commands.mjs +62 -1
- package/src/tui/app.mjs +10 -3
- package/src/tui/screens/flows.mjs +77 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mnemahq/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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
|
"htm": "^3.1.1",
|
|
35
35
|
"ink": "^6.8.0",
|
|
36
36
|
"react": "^19.2.7",
|
|
37
|
-
"@mnemahq/sdk": "0.4.
|
|
37
|
+
"@mnemahq/sdk": "0.4.1"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node -e \"process.exit(0)\"",
|
package/src/cli.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { cmdLogin, cmdLogout, accessToken } from './login.mjs';
|
|
|
15
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
|
+
cmdFlows, cmdFlow,
|
|
18
19
|
} from './read-commands.mjs';
|
|
19
20
|
import {
|
|
20
21
|
DEFAULT_ORIGIN, DEFAULT_APP_URL, c, truncate, width, mark, gitInfo, canonicalRepo, readConfig, writeConfig,
|
|
@@ -519,6 +520,8 @@ Read your workspace:
|
|
|
519
520
|
docs List documents [--limit]
|
|
520
521
|
doc [id] Print one document as markdown; no id opens a picker
|
|
521
522
|
projects List projects
|
|
523
|
+
flows List flows
|
|
524
|
+
flow [slug] One flow; no slug opens a picker
|
|
522
525
|
briefing What deserves attention — pulse, deltas, findings
|
|
523
526
|
|
|
524
527
|
Ask the knowledge graph (paid feature):
|
|
@@ -597,7 +600,7 @@ export function screenFor(cmd, flags, rest = []) {
|
|
|
597
600
|
if (rest.length > 1) return null;
|
|
598
601
|
return rest[0] ? { name: 'node', target: { label: rest[0] } } : { name: 'graph' };
|
|
599
602
|
}
|
|
600
|
-
if (cmd === 'tasks' || cmd === 'docs' || cmd === 'briefing') return { name: cmd };
|
|
603
|
+
if (cmd === 'tasks' || cmd === 'docs' || cmd === 'briefing' || cmd === 'flows') return { name: cmd };
|
|
601
604
|
return null;
|
|
602
605
|
}
|
|
603
606
|
|
|
@@ -667,6 +670,10 @@ export async function run(argv) {
|
|
|
667
670
|
return cmdDocs(flags, resolveContext(flags));
|
|
668
671
|
case 'doc': return cmdDoc(flags, resolveContext(flags), rest);
|
|
669
672
|
case 'projects': return cmdProjects(flags, resolveContext(flags));
|
|
673
|
+
case 'flows':
|
|
674
|
+
if (await maybeInteractive(flags, screenFor('flows', flags))) return 0;
|
|
675
|
+
return cmdFlows(flags, resolveContext(flags));
|
|
676
|
+
case 'flow': return cmdFlow(flags, resolveContext(flags), rest);
|
|
670
677
|
case 'ask': return cmdAsk(flags, resolveContext(flags), rest);
|
|
671
678
|
case 'graph':
|
|
672
679
|
// With a node named, open the walker AT it. With no argument, open the hub
|
package/src/read-commands.mjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { call, hasApiKey, canAuthenticate, renderError } from './client.mjs';
|
|
28
28
|
import { take } from './paging.mjs';
|
|
29
29
|
import { c, truncate, link, mark } from './util.mjs';
|
|
30
|
-
import { heading, empty, rows, more, copyable } from './render/layout.mjs';
|
|
30
|
+
import { heading, entries, empty, rows, more, copyable } from './render/layout.mjs';
|
|
31
31
|
import { pick } from './render/pick.mjs';
|
|
32
32
|
|
|
33
33
|
/** Machine output is the whole object; humans get the formatted view. */
|
|
@@ -274,3 +274,64 @@ export async function cmdBriefing(flags, ctx) {
|
|
|
274
274
|
});
|
|
275
275
|
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'briefing' }); }
|
|
276
276
|
}
|
|
277
|
+
|
|
278
|
+
// ── flows ─────────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* ⚠️ THE PUBLIC FLOW ENDPOINT RETURNS METADATA ONLY — no nodes, no steps, no run
|
|
282
|
+
* history. Rendering a "Steps" heading with nothing under it would be the exact
|
|
283
|
+
* silent-empty failure this codebase is named for: it would read as "this flow
|
|
284
|
+
* has no steps" when it means "this endpoint does not serve them". So the detail
|
|
285
|
+
* shows what is genuinely there and says where the rest lives.
|
|
286
|
+
*/
|
|
287
|
+
export async function cmdFlows(flags, ctx) {
|
|
288
|
+
await guard(ctx.workspaceId);
|
|
289
|
+
const limit = limitOf(flags, 50);
|
|
290
|
+
try {
|
|
291
|
+
const page = await call(ctx, (m) => take(m.flows.list(), limit));
|
|
292
|
+
emit(flags, page.rows, () => {
|
|
293
|
+
heading('Flows', page.rows.length);
|
|
294
|
+
if (!page.rows.length) return empty('No flows yet.', 'Build one in the app and it shows up here.');
|
|
295
|
+
rows(page.rows.map((f) => ({
|
|
296
|
+
cells: [f.slug ?? '', f.name ?? ''],
|
|
297
|
+
style: [c.cyan, undefined],
|
|
298
|
+
})));
|
|
299
|
+
more(page.more, `mnema flows --limit ${limit * 2}`);
|
|
300
|
+
});
|
|
301
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'flows' }); }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function cmdFlow(flags, ctx, rest) {
|
|
305
|
+
await guard(ctx.workspaceId);
|
|
306
|
+
let slug = rest[0];
|
|
307
|
+
const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
308
|
+
if (!slug && !flags.json && interactive) {
|
|
309
|
+
const page = await call(ctx, (m) => take(m.flows.list(), 50)).catch(() => null);
|
|
310
|
+
if (page?.rows.length) {
|
|
311
|
+
const chosen = await pick(page.rows.map((f) => ({ label: `${f.name ?? f.slug}` })), { title: 'Which flow?' });
|
|
312
|
+
if (chosen === null) return;
|
|
313
|
+
slug = page.rows[chosen].slug;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (!slug) {
|
|
317
|
+
console.error(c.red('Usage: mnema flow <slug>'));
|
|
318
|
+
console.error(c.dim(' Run it without a slug in a terminal to pick from a list.'));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const f = await call(ctx, (m) => m.flows.get(slug));
|
|
323
|
+
emit(flags, f, () => {
|
|
324
|
+
heading(f.name ?? slug);
|
|
325
|
+
entries([
|
|
326
|
+
{ label: 'slug', value: f.slug ?? slug },
|
|
327
|
+
{ label: 'published', value: f.publishedVersionId ? 'yes' : 'draft', state: Boolean(f.publishedVersionId) },
|
|
328
|
+
...(f.projectId ? [{ label: 'project', value: f.projectId }] : []),
|
|
329
|
+
{ label: 'updated', value: String(f.updatedAt ?? '').slice(0, 10), dim: true },
|
|
330
|
+
]);
|
|
331
|
+
if (f.description) console.log(`\n ${c.dim(f.description)}`);
|
|
332
|
+
// Saying this is the point: the absence of steps here is the endpoint's
|
|
333
|
+
// shape, not the flow's emptiness.
|
|
334
|
+
console.log(c.dim('\n Steps and runs are not on the public API — open the flow in the app to walk it.'));
|
|
335
|
+
});
|
|
336
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'flow' }); }
|
|
337
|
+
}
|
package/src/tui/app.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { Header } from './components/header.mjs';
|
|
|
23
23
|
import { GraphHome } from './screens/graph-home.mjs';
|
|
24
24
|
import { Tasks, Task } from './screens/tasks.mjs';
|
|
25
25
|
import { Docs, Doc } from './screens/docs.mjs';
|
|
26
|
+
import { Flows, Flow } from './screens/flows.mjs';
|
|
26
27
|
|
|
27
28
|
const HOME = { name: 'briefing' };
|
|
28
29
|
|
|
@@ -71,6 +72,7 @@ export function App({ ctx, initial }) {
|
|
|
71
72
|
if (input === 'g') { setStack([{ name: 'graph' }]); return; }
|
|
72
73
|
if (input === 't') { setStack([{ name: 'tasks' }]); return; }
|
|
73
74
|
if (input === 'd') { setStack([{ name: 'docs' }]); return; }
|
|
75
|
+
if (input === 'f') { setStack([{ name: 'flows' }]); return; }
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
if (fatal?.kind === 'auth') {
|
|
@@ -100,6 +102,11 @@ export function App({ ctx, initial }) {
|
|
|
100
102
|
onOpen=${(d) => push({ name: 'doc', doc: d })} />`;
|
|
101
103
|
} else if (top.name === 'doc') {
|
|
102
104
|
body = html`<${Doc} ctx=${wrapped} doc=${top.doc} focused=${true} />`;
|
|
105
|
+
} else if (top.name === 'flows') {
|
|
106
|
+
body = html`<${Flows} ctx=${wrapped} focused=${true}
|
|
107
|
+
onOpen=${(f) => push({ name: 'flow', flow: f })} />`;
|
|
108
|
+
} else if (top.name === 'flow') {
|
|
109
|
+
body = html`<${Flow} ctx=${wrapped} flow=${top.flow} />`;
|
|
103
110
|
} else if (top.name === 'briefing') {
|
|
104
111
|
body = html`<${Briefing} ctx=${wrapped} focused=${true} onOpen=${(f) => push({ name: 'finding', finding: f })} />`;
|
|
105
112
|
} else if (top.name === 'node') {
|
|
@@ -144,13 +151,13 @@ export function App({ ctx, initial }) {
|
|
|
144
151
|
* which is worse than not mentioning it. The global jumps are appended once, in
|
|
145
152
|
* one place, so they cannot drift per screen.
|
|
146
153
|
*/
|
|
147
|
-
const JUMPS = 'b briefing · g graph · t tasks · d docs · q quit';
|
|
154
|
+
const JUMPS = 'b briefing · g graph · t tasks · d docs · f flows · q quit';
|
|
148
155
|
|
|
149
156
|
function footerFor(top) {
|
|
150
157
|
if (top.name === 'briefing') return `↑↓ move · enter open · r refresh · ${JUMPS}`;
|
|
151
158
|
if (top.name === 'graph') return `↑↓ move · enter walk in · ${JUMPS}`;
|
|
152
|
-
if (top.name === 'tasks' || top.name === 'docs') return `↑↓ move · enter open · ${JUMPS}`;
|
|
153
|
-
if (top.name === 'task') return `esc back · ${JUMPS}`;
|
|
159
|
+
if (top.name === 'tasks' || top.name === 'docs' || top.name === 'flows') return `↑↓ move · enter open · ${JUMPS}`;
|
|
160
|
+
if (top.name === 'task' || top.name === 'flow') return `esc back · ${JUMPS}`;
|
|
154
161
|
if (top.name === 'doc') return `j/k scroll · esc back · ${JUMPS}`;
|
|
155
162
|
if (top.name === 'node') return `↑↓ move · enter walk in · esc back · r refresh · ${JUMPS}`;
|
|
156
163
|
if (top.name === 'finding') {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flows (t-640).
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ THE PUBLIC ENDPOINT SERVES METADATA ONLY — no nodes, no steps, no runs. A
|
|
5
|
+
* "Steps" heading with nothing under it would read as "this flow is empty" when
|
|
6
|
+
* it means "this endpoint does not serve them", which is exactly the silent-empty
|
|
7
|
+
* failure this codebase is named for. So the detail shows what is genuinely there
|
|
8
|
+
* and names where the rest lives.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Box, Text } from 'ink';
|
|
12
|
+
import { html } from '../h.mjs';
|
|
13
|
+
import { useResource, ago } from '../store.mjs';
|
|
14
|
+
import { List } from '../components/list.mjs';
|
|
15
|
+
import { take } from '../../paging.mjs';
|
|
16
|
+
|
|
17
|
+
export function Flows({ ctx, focused, onOpen }) {
|
|
18
|
+
const { status, data, error, elapsed, at } = useResource('flows', () =>
|
|
19
|
+
ctx.call((m) => take(m.flows.list(), 60)));
|
|
20
|
+
|
|
21
|
+
if (status === 'loading') {
|
|
22
|
+
return html`<${Box} paddingX=${1}><${Text} dimColor>Loading flows… ${elapsed > 1 ? `${elapsed}s` : ''}<//><//>`;
|
|
23
|
+
}
|
|
24
|
+
if (error && !data) {
|
|
25
|
+
return html`<${Box} paddingX=${1}><${Text} color="red">${error.message ?? String(error)}<//><//>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const rows = data?.rows ?? [];
|
|
29
|
+
return html`
|
|
30
|
+
<${Box} flexDirection="column" paddingX=${1}>
|
|
31
|
+
<${Box}>
|
|
32
|
+
<${Text} bold>Flows<//>
|
|
33
|
+
<${Text} dimColor> ${rows.length}${data?.more ? '+' : ''} · ${ago(at)}<//>
|
|
34
|
+
<//>
|
|
35
|
+
${rows.length === 0
|
|
36
|
+
? html`<${Text} dimColor>No flows yet.<//>`
|
|
37
|
+
: html`<${List}
|
|
38
|
+
items=${rows.map((f) => ({ key: f.id ?? f.slug, left: f.slug ?? '', label: f.name ?? '' }))}
|
|
39
|
+
focused=${focused}
|
|
40
|
+
height=${12}
|
|
41
|
+
onSelect=${(i) => onOpen(rows[i])}
|
|
42
|
+
/>`}
|
|
43
|
+
<//>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function Flow({ ctx, flow }) {
|
|
47
|
+
const { status, data, error, elapsed } = useResource(`flow:${flow?.slug}`, () =>
|
|
48
|
+
ctx.call((m) => m.flows.get(flow.slug)));
|
|
49
|
+
|
|
50
|
+
if (status === 'loading') {
|
|
51
|
+
return html`<${Box} paddingX=${1}><${Text} dimColor>Opening ${flow?.name ?? ''}… ${elapsed > 1 ? `${elapsed}s` : ''}<//><//>`;
|
|
52
|
+
}
|
|
53
|
+
if (error) {
|
|
54
|
+
return html`<${Box} flexDirection="column" paddingX=${1}>
|
|
55
|
+
<${Text} color="red">${error.message ?? String(error)}<//>
|
|
56
|
+
<${Text} dimColor>esc to go back<//>
|
|
57
|
+
<//>`;
|
|
58
|
+
}
|
|
59
|
+
const f = data ?? flow ?? {};
|
|
60
|
+
|
|
61
|
+
return html`
|
|
62
|
+
<${Box} flexDirection="column" paddingX=${1}>
|
|
63
|
+
<${Text} bold>${f.name ?? f.slug ?? '(unnamed)'}<//>
|
|
64
|
+
<${Box} flexDirection="column" marginTop=${1}>
|
|
65
|
+
<${Box}><${Text} dimColor>slug <//><${Text} color="cyan">${f.slug ?? ''}<//><//>
|
|
66
|
+
<${Box}><${Text} dimColor>published <//>
|
|
67
|
+
<${Text} color=${f.publishedVersionId ? 'green' : undefined}>${f.publishedVersionId ? 'yes' : 'draft'}<//><//>
|
|
68
|
+
<${Box}><${Text} dimColor>updated <//><${Text}>${String(f.updatedAt ?? '').slice(0, 10)}<//><//>
|
|
69
|
+
<//>
|
|
70
|
+
${f.description
|
|
71
|
+
? html`<${Box} marginTop=${1}><${Text} dimColor>${f.description}<//><//>`
|
|
72
|
+
: null}
|
|
73
|
+
<${Box} marginTop=${1}>
|
|
74
|
+
<${Text} dimColor>Steps and runs are not on the public API — open the flow in the app to walk it.<//>
|
|
75
|
+
<//>
|
|
76
|
+
<//>`;
|
|
77
|
+
}
|