@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemahq/cli",
3
- "version": "0.11.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.0"
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
@@ -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
+ }