dexterm-cli 0.2.0 → 0.2.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.
Files changed (138) hide show
  1. package/dist/packages/agents/src/spectator.js +28 -2
  2. package/dist/packages/agents/src/tiers.js +3 -0
  3. package/dist/packages/agents/src/tools.js +3 -0
  4. package/dist/packages/autotrade/src/backtest.js +6 -2
  5. package/dist/packages/autotrade/src/fleet.js +2 -2
  6. package/dist/packages/autotrade/src/index.js +2 -0
  7. package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
  8. package/dist/packages/autotrade/src/rugproof.js +231 -0
  9. package/dist/packages/autotrade/src/runner.js +132 -17
  10. package/dist/packages/autotrade/src/service-install.js +6 -4
  11. package/dist/packages/autotrade/src/sessions.js +16 -7
  12. package/dist/packages/autotrade/src/strategy-stats.js +344 -0
  13. package/dist/packages/autotrade/src/strategy.js +12 -1
  14. package/dist/packages/classifier/src/calibrate.js +138 -0
  15. package/dist/packages/classifier/src/classify.js +107 -0
  16. package/dist/packages/classifier/src/config.js +220 -0
  17. package/dist/packages/classifier/src/dossier.js +95 -0
  18. package/dist/packages/classifier/src/hardware.js +63 -0
  19. package/dist/packages/classifier/src/index.js +18 -0
  20. package/dist/packages/classifier/src/models.js +111 -0
  21. package/dist/packages/classifier/src/providers/heuristic.js +77 -0
  22. package/dist/packages/classifier/src/providers/http.js +63 -0
  23. package/dist/packages/classifier/src/providers/jev-open.js +86 -0
  24. package/dist/packages/classifier/src/providers/llm.js +89 -0
  25. package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
  26. package/dist/packages/classifier/src/questions.js +97 -0
  27. package/dist/packages/classifier/src/registry.js +42 -0
  28. package/dist/packages/classifier/src/snapshot.js +31 -0
  29. package/dist/packages/classifier/src/systemone.js +88 -0
  30. package/dist/packages/classifier/src/types.js +18 -0
  31. package/dist/packages/cli/assets/dash/dash.css +139 -0
  32. package/dist/packages/cli/assets/dash/dash.js +200 -0
  33. package/dist/packages/cli/assets/dash/icon.svg +1 -0
  34. package/dist/packages/cli/assets/dash/index.html +36 -0
  35. package/dist/packages/cli/src/app-help-gen.js +12 -0
  36. package/dist/packages/cli/src/autotrade-cmd/fleet.js +9 -2
  37. package/dist/packages/cli/src/autotrade-cmd/run.js +11 -1
  38. package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
  39. package/dist/packages/cli/src/classifier-cmd.js +353 -0
  40. package/dist/packages/cli/src/classifier-live.js +81 -0
  41. package/dist/packages/cli/src/dash/data.js +217 -0
  42. package/dist/packages/cli/src/dash/server.js +92 -0
  43. package/dist/packages/cli/src/dash-cmd.js +113 -0
  44. package/dist/packages/cli/src/doctor.js +34 -0
  45. package/dist/packages/cli/src/live/screens/token-live.js +4 -0
  46. package/dist/packages/cli/src/live/signals.js +8 -0
  47. package/dist/packages/cli/src/main.js +98 -6
  48. package/dist/packages/cli/src/mcp-follow.js +21 -1
  49. package/dist/packages/cli/src/narratives-cmd.js +147 -0
  50. package/dist/packages/cli/src/narratives.js +114 -0
  51. package/dist/packages/cli/src/overlay/model.js +147 -0
  52. package/dist/packages/cli/src/overlay/page.js +146 -0
  53. package/dist/packages/cli/src/overlay/pnl.js +75 -0
  54. package/dist/packages/cli/src/overlay/server.js +170 -0
  55. package/dist/packages/cli/src/overlay-cmd.js +367 -0
  56. package/dist/packages/cli/src/record-cmd.js +82 -11
  57. package/dist/packages/cli/src/rules-cmd.js +2 -1
  58. package/dist/packages/cli/src/rules.js +24 -4
  59. package/dist/packages/cli/src/screens/narrative.js +154 -2
  60. package/dist/packages/cli/src/state.js +1 -0
  61. package/dist/packages/cli/src/version.js +1 -1
  62. package/dist/packages/custom/src/rules.js +5 -1
  63. package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
  64. package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
  65. package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
  66. package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
  67. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
  68. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +3 -1
  69. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
  70. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
  71. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  72. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
  73. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
  74. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
  75. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
  76. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  77. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
  78. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +2 -0
  79. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +2 -0
  80. package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
  81. package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
  82. package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
  83. package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
  84. package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
  85. package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
  86. package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
  87. package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
  88. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
  89. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
  90. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  91. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
  92. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  93. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
  94. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
  95. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
  96. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
  97. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
  98. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
  99. package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
  100. package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
  101. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
  102. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
  103. package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
  104. package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
  105. package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
  106. package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
  107. package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
  108. package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
  109. package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
  110. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
  111. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
  112. package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
  113. package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
  114. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
  115. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +2 -0
  116. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
  117. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
  118. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
  119. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
  120. package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
  121. package/dist/packages/mcp/src/server.js +104 -0
  122. package/dist/packages/mcp/src/spectator-sink.js +27 -0
  123. package/dist/packages/narratives/src/cluster.js +193 -0
  124. package/dist/packages/narratives/src/embed.js +184 -0
  125. package/dist/packages/narratives/src/export.js +60 -0
  126. package/dist/packages/narratives/src/index.js +10 -0
  127. package/dist/packages/narratives/src/label.js +48 -0
  128. package/dist/packages/narratives/src/normalize.js +179 -0
  129. package/dist/packages/narratives/src/recorder.js +93 -0
  130. package/dist/packages/narratives/src/tracker.js +328 -0
  131. package/dist/packages/recorder/src/exit-probes.js +215 -0
  132. package/dist/packages/recorder/src/index.js +1 -0
  133. package/dist/packages/recorder/src/recorder.js +11 -1
  134. package/dist/packages/recorder/src/retention.js +4 -0
  135. package/dist/packages/recorder/src/schema.js +20 -1
  136. package/dist/packages/tui/src/keymap.js +5 -0
  137. package/package.json +3 -1
  138. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
@@ -0,0 +1,92 @@
1
+ // `dexterm dash` (task-1749): the loopback server and the static export of the strategy performance dashboard.
2
+ // Binds 127.0.0.1 only. GET/HEAD only. Serves four files from assets/dash plus /api/dash.json (recomputed at most every
3
+ // few seconds). A Host header outside the allow list is refused (DNS-rebinding guard: a web page cannot read the
4
+ // dashboard through a hostname that resolves to 127.0.0.1); a private proxy in front (e.g. `tailscale serve`) is
5
+ // allowed by naming its host with --allow-host. No external request is made or referenced: the page has no CDN, font
6
+ // or analytics URL, and the CSP forbids any.
7
+ import { createServer } from 'node:http';
8
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { collectDash } from "./data.js";
12
+ const HERE = dirname(fileURLToPath(import.meta.url));
13
+ export const ASSET_DIR = resolve(HERE, '..', '..', 'assets', 'dash');
14
+ export const ASSETS = { '/': 'index.html', '/index.html': 'index.html', '/dash.css': 'dash.css', '/dash.js': 'dash.js', '/icon.svg': 'icon.svg' };
15
+ const TYPES = { html: 'text/html; charset=utf-8', css: 'text/css; charset=utf-8', js: 'text/javascript; charset=utf-8', svg: 'image/svg+xml', json: 'application/json; charset=utf-8' };
16
+ export const CSP = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
17
+ /** Is this Host header one we serve? Loopback names on any port, plus the hosts allowed explicitly (with or without a port). */
18
+ export function hostAllowed(host, allow) {
19
+ if (!host)
20
+ return false;
21
+ const h = host.toLowerCase();
22
+ const name = h.startsWith('[') ? h.slice(0, h.indexOf(']') + 1) : h.replace(/:\d+$/, '');
23
+ if (name === '127.0.0.1' || name === 'localhost' || name === '[::1]')
24
+ return true;
25
+ return allow.some((a) => { const x = a.toLowerCase(); return x === h || x === name; });
26
+ }
27
+ export function startDashServer(opts, port) {
28
+ let cache = null;
29
+ const data = () => {
30
+ const now = Date.now();
31
+ if (!cache || now - cache.at > (opts.cacheMs ?? 5000))
32
+ cache = { at: now, body: JSON.stringify(collectDash({ homes: opts.homes, books: opts.books, baseline: opts.baseline, now })) };
33
+ return cache.body;
34
+ };
35
+ const server = createServer((req, res) => {
36
+ const base = { 'Content-Security-Policy': CSP, 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', 'Cache-Control': 'no-store' };
37
+ if (!hostAllowed(req.headers.host, opts.allowHosts)) {
38
+ res.writeHead(421, { ...base, 'Content-Type': 'text/plain' });
39
+ res.end('misdirected request: this host name is not allowed (dexterm dash --allow-host NAME)\n');
40
+ return;
41
+ }
42
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
43
+ res.writeHead(405, { ...base, Allow: 'GET, HEAD' });
44
+ res.end();
45
+ return;
46
+ }
47
+ const path = (req.url ?? '/').split('?')[0];
48
+ try {
49
+ if (path === '/api/dash.json') {
50
+ const body = data();
51
+ res.writeHead(200, { ...base, 'Content-Type': TYPES.json });
52
+ res.end(req.method === 'HEAD' ? undefined : body);
53
+ return;
54
+ }
55
+ const file = ASSETS[path];
56
+ if (!file) {
57
+ res.writeHead(404, { ...base, 'Content-Type': 'text/plain' });
58
+ res.end('not found\n');
59
+ return;
60
+ }
61
+ const body = readFileSync(join(ASSET_DIR, file));
62
+ res.writeHead(200, { ...base, 'Content-Type': TYPES[file.split('.').pop()] ?? 'application/octet-stream' });
63
+ res.end(req.method === 'HEAD' ? undefined : body);
64
+ }
65
+ catch (e) {
66
+ res.writeHead(500, { ...base, 'Content-Type': 'text/plain' });
67
+ res.end(`dashboard error: ${String(e?.message ?? e).slice(0, 200)}\n`);
68
+ }
69
+ });
70
+ return new Promise((ok, fail) => { server.once('error', fail); server.listen(port, '127.0.0.1', () => { server.off('error', fail); ok(server); }); });
71
+ }
72
+ /**
73
+ * A static, read-only SNAPSHOT: the same page + the data as data.json (the page reads it instead of /api/dash.json and
74
+ * shows "snapshot" with its time, no auto-refresh). `redactMints` drops the mints (coin symbols stay).
75
+ */
76
+ export function exportSnapshot(d, outDir, { redactMints = false } = {}) {
77
+ mkdirSync(outDir, { recursive: true });
78
+ const data = redactMints ? JSON.parse(JSON.stringify(d, (k, v) => (k === 'mint' ? null : v))) : d;
79
+ const written = [];
80
+ for (const f of new Set(Object.values(ASSETS))) {
81
+ let body = readFileSync(join(ASSET_DIR, f), 'utf8');
82
+ if (f === 'index.html')
83
+ body = body.replace('content="api/dash.json"', 'content="data.json"').replace('data-mode="live"', 'data-mode="snapshot"');
84
+ writeFileSync(join(outDir, f), body);
85
+ written.push(f);
86
+ }
87
+ writeFileSync(join(outDir, 'data.json'), JSON.stringify({ ...data, snapshot: true }));
88
+ written.push('data.json');
89
+ writeFileSync(join(outDir, '_headers'), `/*\n Content-Security-Policy: ${CSP}\n X-Content-Type-Options: nosniff\n Referrer-Policy: no-referrer\n`);
90
+ written.push('_headers');
91
+ return written;
92
+ }
@@ -0,0 +1,113 @@
1
+ // `dexterm dash` (task-1749): the STRATEGY PERFORMANCE DASHBOARD. Local-first: it reads your own autotrade run ledgers
2
+ // ($DEXTERM_HOME/autotrade/runs) and serves a page on 127.0.0.1 only; no account, no cloud, nothing leaves the machine.
3
+ // Read-only: it never writes into the engine's state. PAPER results are simulated; not financial advice.
4
+ import { existsSync } from 'node:fs';
5
+ import { basename, dirname, join, resolve } from 'node:path';
6
+ import { dextermHome } from '../../custom/src/index.js';
7
+ import { defaultDbPath } from '../../data/src/index.js';
8
+ import { collectDash, homeLabel } from "./dash/data.js";
9
+ import { exportSnapshot, startDashServer } from "./dash/server.js";
10
+ export const DASH_PORT = 4317;
11
+ export const DASH_HELP = `dexterm dash: track how your autotrade strategies perform, in a browser, on this machine only. Read-only.
12
+
13
+ dexterm dash serve the dashboard on http://127.0.0.1:${DASH_PORT}/ (Ctrl-C stops it)
14
+ dexterm dash --json print the numbers the page shows and exit
15
+ dexterm dash --export DIR [--redact-mints] write a static read-only snapshot (the page + data.json) and exit
16
+
17
+ --port N the loopback port (default ${DASH_PORT}; 0 = any free port)
18
+ --home DIR a dexterm home to read (default $DEXTERM_HOME or ~/.dexterm); repeat for several
19
+ --db PATH the market cache DB for pool reads (default: dexterm.db next to a .dexterm home, else DEXTERM_DB_PATH or the default DB)
20
+ --books FILE add books from a dexterm-dash-books/1 JSON file (your own script's output); repeat for several
21
+ --baseline ID the strategy every other one is compared against
22
+ --allow-host NAME also answer requests for this host name (a private proxy in front, e.g. tailscale serve)
23
+
24
+ Overview, strategy detail, rugs & stuck, compare (vs the baseline, with the 95% interval) and live status, refreshed
25
+ every 30 s. The server binds 127.0.0.1 only, makes no network request and loads nothing from the internet.
26
+ PAPER results are simulated. Not financial advice.`;
27
+ const VALUE = new Set(['port', 'home', 'db', 'books', 'baseline', 'allow-host', 'export']);
28
+ const BOOL = new Set(['json', 'help', 'redact-mints']);
29
+ function parse(argv) {
30
+ const o = { _: [], flags: new Map(), bools: new Set() };
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const a = argv[i];
33
+ if (a === '-h') {
34
+ o.bools.add('help');
35
+ continue;
36
+ }
37
+ if (!a.startsWith('--')) {
38
+ o._.push(a);
39
+ continue;
40
+ }
41
+ const [k, inline] = a.slice(2).split(/=(.*)/s, 2);
42
+ if (BOOL.has(k)) {
43
+ o.bools.add(k);
44
+ continue;
45
+ }
46
+ if (!VALUE.has(k))
47
+ return `unknown flag --${k}`;
48
+ const v = inline ?? argv[++i];
49
+ if (v === undefined || v === '')
50
+ return `--${k} needs a value`;
51
+ o.flags.set(k, [...(o.flags.get(k) ?? []), v]);
52
+ }
53
+ return o;
54
+ }
55
+ /** A home given as DIR: the .dexterm folder itself, or a folder holding one (the kit's layout: <dir>/.dexterm + <dir>/dexterm.db). */
56
+ export function resolveHome(dir, env, db) {
57
+ let d = resolve(dir);
58
+ if (basename(d) !== '.dexterm' && !existsSync(join(d, 'autotrade')) && existsSync(join(d, '.dexterm')))
59
+ d = join(d, '.dexterm');
60
+ const sibling = basename(d) === '.dexterm' ? join(dirname(d), 'dexterm.db') : null;
61
+ return { dir: d, label: homeLabel(d), db: db ?? (sibling && existsSync(sibling) ? sibling : env.DEXTERM_DB_PATH || defaultDbPath(env)) };
62
+ }
63
+ export async function dashCommand(argv, io, { wait = true } = {}) {
64
+ const o = parse(argv);
65
+ if (typeof o === 'string') {
66
+ io.stderr.write(`dexterm dash: ${o}\n\n${DASH_HELP}\n`);
67
+ return 2;
68
+ }
69
+ if (o.bools.has('help') || o._[0] === 'help') {
70
+ io.stdout.write(`${DASH_HELP}\n`);
71
+ return 0;
72
+ }
73
+ if (o._.length) {
74
+ io.stderr.write(`dexterm dash: unexpected argument ${o._[0]}\n\n${DASH_HELP}\n`);
75
+ return 2;
76
+ }
77
+ const db = o.flags.get('db')?.[0];
78
+ const homes = (o.flags.get('home') ?? [dextermHome(io.env)]).map((h) => resolveHome(h, io.env, db));
79
+ const books = (o.flags.get('books') ?? []).map((b) => resolve(b));
80
+ const baseline = o.flags.get('baseline')?.[0] ?? null;
81
+ if (o.bools.has('json')) {
82
+ io.stdout.write(`${JSON.stringify(collectDash({ homes, books, baseline }), null, 1)}\n`);
83
+ return 0;
84
+ }
85
+ const out = o.flags.get('export')?.[0];
86
+ if (out) {
87
+ const files = exportSnapshot(collectDash({ homes, books, baseline }), resolve(out), { redactMints: o.bools.has('redact-mints') });
88
+ io.stdout.write(`snapshot written to ${resolve(out)}: ${files.join(', ')}\nIt is a static read-only copy of what this machine shows now. Publishing it is your decision; check it first. Not financial advice.\n`);
89
+ return 0;
90
+ }
91
+ const port = Number(o.flags.get('port')?.[0] ?? DASH_PORT);
92
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
93
+ io.stderr.write(`dexterm dash: --port must be 0-65535\n`);
94
+ return 2;
95
+ }
96
+ let server;
97
+ try {
98
+ server = await startDashServer({ homes, books, baseline, allowHosts: o.flags.get('allow-host') ?? [] }, port);
99
+ }
100
+ catch (e) {
101
+ io.stderr.write(`dexterm dash: cannot listen on 127.0.0.1:${port} (${e.code ?? e}); try --port N\n`);
102
+ return 1;
103
+ }
104
+ const addr = server.address();
105
+ const p = typeof addr === 'object' && addr ? addr.port : port;
106
+ io.stdout.write(`dexterm dash: http://127.0.0.1:${p}/ (reading ${homes.map((h) => h.label).join(', ')}${books.length ? ` + ${books.length} books file(s)` : ''}; loopback only, read-only; Ctrl-C stops)\nPaper trading, not financial advice.\n`);
107
+ if (!wait) {
108
+ server.close();
109
+ return 0;
110
+ }
111
+ await new Promise((done) => { const stop = () => { server.close(() => done()); }; process.once('SIGINT', stop); process.once('SIGTERM', stop); });
112
+ return 0;
113
+ }
@@ -204,6 +204,7 @@ export function defaultDeps(env = process.env, isTTY = process.stdout.isTTY ===
204
204
  claude: () => claudeProbe(),
205
205
  codex: () => codexProbe(4000, env),
206
206
  ollama: () => ollamaFacts(env),
207
+ classifier: () => classifierFacts(env),
207
208
  analystEngine: env.DEXTERM_ANALYST_ENGINE ?? loadConfig(dextermHome(env)).config.analyst?.engine ?? null,
208
209
  probe: (url) => livingProbe(url),
209
210
  checkWritable: writableProbe,
@@ -325,6 +326,12 @@ export async function runDoctor(deps) {
325
326
  // 3c. Local engine (task-1543): the user's own Ollama on this machine, the model, tool calling, context, profile.
326
327
  if (ol)
327
328
  checks.push(ollamaCheck(ol, claude !== 'missing' || codexUsable));
329
+ // 3d. Classification model (task-1822, EXPERIMENTAL): hardware tier + the picked model. Never fails the doctor.
330
+ if (deps.classifier) {
331
+ const cf = await deps.classifier().catch(() => null);
332
+ if (cf)
333
+ checks.push(classifierCheck(cf));
334
+ }
328
335
  // 4b. Login with X (optional, task-1357): the user's own X app + login unlock X posts in the SOCIAL panel / TRENDING.
329
336
  {
330
337
  const xs = new XTokenStore({ env: deps.env });
@@ -471,6 +478,33 @@ export function renderDoctor(result, opts = {}) {
471
478
  lines.push(paint(`${fails} check${fails === 1 ? '' : 's'} failed — fix the ✗ lines above, then re-run \`dexterm doctor\`.`, color ? ANSI.fail : ''));
472
479
  return lines.join('\n');
473
480
  }
481
+ /** task-1822: what `dexterm classifier doctor` says, as one doctor line. */
482
+ export async function classifierFacts(env = process.env) {
483
+ const { describeHardware, detectHardware, loadClassifierConfig, providerFromConfig, recommendTier } = await import('../../classifier/src/index.js');
484
+ const hw = await detectHardware();
485
+ const rec = recommendTier(hw);
486
+ const c = loadClassifierConfig(env).config;
487
+ let st = { ready: false, detail: '' };
488
+ let model = c.provider;
489
+ try {
490
+ const p = providerFromConfig(c, c.provider, { env });
491
+ model = p.model();
492
+ st = await p.status();
493
+ }
494
+ catch (e) {
495
+ st = { ready: false, detail: e.message };
496
+ }
497
+ return { hardware: describeHardware(hw), recommended: rec.model, bestQuality: rec.bestQuality, why: rec.why, provider: c.provider, model, ready: st.ready, detail: st.detail };
498
+ }
499
+ export function classifierCheck(f) {
500
+ const tier = f.recommended ? `recommended model here: ${f.recommended}${f.bestQuality ? ` (best quality: ${f.bestQuality})` : ''}` : 'recommended: the built-in scores';
501
+ const name = 'Classification model (experimental)';
502
+ if (f.provider === 'heuristic')
503
+ return { name, status: 'pass', detail: `built-in scores (no download) · ${f.hardware} · ${tier}` };
504
+ if (f.ready)
505
+ return { name, status: 'pass', detail: `${f.provider} ${f.model} ready · ${tier}` };
506
+ return { name, status: 'warn', detail: `${f.provider} ${f.model} not ready: ${f.detail}`, fix: f.provider === 'kev-local' ? `dexterm classifier pull ${f.model}, then dexterm classifier serve (answers fall back to the built-in scores meanwhile)` : 'dexterm classifier doctor' };
507
+ }
474
508
  /**
475
509
  * The Ollama line (task-1543). Optional while claude/codex can run the analyst; a warning with the 2-minute
476
510
  * setup when nothing can, or when the user chose ollama and it is not ready.
@@ -19,6 +19,7 @@ import { launchIntelCells, holderTags } from "../intel.js";
19
19
  import { devBookOf, devRows } from "../dev-history.js";
20
20
  import { EX_POOL_TAGS } from '../../../../data/src/index.js';
21
21
  import { layoutFor } from "../../custom.js";
22
+ import { narrativeCell } from "../../narratives.js";
22
23
  import { chartMode, chartTitle, priceModeOf, chartAxisOf, chartMarkersOn, mcapAt, positionPriceCells, levelCell, supplyRatio } from "../../display.js";
23
24
  const dash = '—';
24
25
  const EX_POOL_TAG_SET = EX_POOL_TAGS;
@@ -211,6 +212,9 @@ export function renderTokenLive(c, state, v) {
211
212
  ['RugCheck score', ...(v.rug === null || v.rug.score === null ? na(v.panels.rugcheck.state === 'error' ? `unavailable: ${v.panels.rugcheck.reason ?? ''}`.trim() : dash) : [`${Math.round(v.rug.score)}/100`, v.rug.score < 30 ? 'ok' : v.rug.score < 60 ? 'warn' : 'bad'])],
212
213
  ['GoPlus', ...goplusRow(v)],
213
214
  ...devTail,
215
+ // task-1818 (EXPERIMENTAL): the coin's narrative from the already-built tracker (measured heat, not advice). Never builds
216
+ // one here (the 26 h replay runs only from N → n); warn = copy-cat saturated or fading/dead, else neutral na (never a green ok).
217
+ ['Narrative', ...narrativeCell(state, v.mint)],
214
218
  ];
215
219
  for (const [label, val0, lvl] of fitRows(rows, sb.y + sb.h - sy)) {
216
220
  if (sy >= sb.y + sb.h)
@@ -69,6 +69,8 @@ export class LiveSignalsLive {
69
69
  logByMint = new Map();
70
70
  /** Signals emitted this session (all mints), newest last — a cheap counter/feed for the smoke script + tests. */
71
71
  emitted = [];
72
+ /** task-1427: extra read-only listeners (the MCP follow session forwards each signal to the spectator hub). */
73
+ listeners = new Set();
72
74
  constructor(opts = {}) {
73
75
  this.engine = new LiveSignalsEngine({ thresholds: opts.thresholds });
74
76
  this.now = opts.now ?? (() => Date.now());
@@ -86,6 +88,12 @@ export class LiveSignalsLive {
86
88
  this.logByMint.set(s.mint, arr);
87
89
  this.emitted.push(s);
88
90
  this.onSignal?.(s);
91
+ for (const l of this.listeners) {
92
+ try {
93
+ l(s);
94
+ }
95
+ catch { /* a listener never breaks the engine */ }
96
+ }
89
97
  }
90
98
  return sigs;
91
99
  }
@@ -47,6 +47,7 @@ import { recordLarpVerdict } from "./larp-record.js";
47
47
  import { newAutotradeUi, defaultDeps as autotradeDeps, autotradeCapturing, autotradeTick, reloadList as autotradeReload, refreshRunner as autotradeRefreshRunner } from "./screens/autotrade-ui.js";
48
48
  import { autotradeAction, autotradeKey } from "./screens/autotrade-keys.js";
49
49
  import { workerJobs as autotradeWorkerJobs } from "./screens/autotrade-worker.js";
50
+ import { narrativesUiOf, kickNarratives, heatRows, drillDetail } from "./narratives.js";
50
51
  import { rulesUiOf, rulesTick, rulesMove, beginRuleInput, saveRuleInput, dryRunNow, removeSelectedRule, toggleSelectedRule, dismissSelectedProposal, actOnSelectedProposal, RULE_INPUT_MAX } from "./rules.js";
51
52
  import { buildNotifier, hasPushChannel } from "./alerts/notifier.js";
52
53
  import { copyAlertToDelivery } from "./alerts/format.js";
@@ -306,12 +307,16 @@ export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watch
306
307
  dexterm kb list|show|new|edit|check|reset|path your knowledge-base entries (~/.dexterm/kb/entries) over the bundled 18
307
308
  dexterm skill list|show|new|edit|check|reset|path your analyst skills (~/.dexterm/skills) over the bundled ones (read tools only)
308
309
  dexterm narrative [--window 1h|24h] [--summary] the narrative board over recorded history (no network, no model)
310
+ dexterm narratives [top|detail ID|of MINT|export] EXPERIMENTAL memes + narratives tracker over recorded history: themes, heat, lifecycle (measured heat, not advice)
309
311
  dexterm backtest [--subject heuristic|pack_verdicts] [--horizon SEC] honest rug-model metrics over recorded outcomes (no network, no model)
310
312
  dexterm ledger trades|positions|summary|equity|compare|export|card|sync|verify the unified trade ledger: paper, agent desk, autotrade runs (no network; 'dexterm ledger help')
313
+ dexterm dash [--port N] [--home DIR] [--baseline ID] [--export DIR] your strategies' performance in a browser on 127.0.0.1 (read-only, no network; 'dexterm dash --help')
311
314
  dexterm wallet link|unlink|status|panic|resume link a Phantom wallet via a local bridge (pubkey only); panic halts autotrade + the paper desk, resume needs a TTY keypress
312
315
  dexterm plan propose|list|arm|cancel|journal conditional order plans (entry/ladder/take-profit/moon bag/stop); arming needs a physical keypress
313
316
  dexterm mandate draft|list|arm|revoke standing capped mandates (paper fills); arming needs a physical keypress — voice/agents can never arm
314
317
  dexterm follow add|remove|list|stats|trades|mirror|purge copy trading tier 1: follow a wallet (your list, local labels), its decoded swaps + FIFO PnL + win rate/early-entry; 'mirror' previews a one-key MIRROR TICKET from the wallet's latest buy (inert; arm in the app)
318
+ dexterm classifier [list|pick|pull|serve|doctor|bench|try|key] classification models (EXPERIMENTAL): Kev on your own machine
319
+ (llama.cpp), hosted Jev with your own key, or the built-in heuristic; model estimates, never advice
315
320
  dexterm ai [status|engine|model|host|restrictions|probe|ask|agent] the AI engine: your claude / codex CLI or a free LOCAL model on
316
321
  your own Ollama (localhost); restriction profiles observe|propose|paper|full ('dexterm ai help')
317
322
  dexterm mcp [install <client>|doctor] read-only MCP server for your own agent; 'install' writes the config for claude-code|claude-desktop|cursor|codex|generic (asks first, idempotent, backs up); 'doctor' verifies the connection
@@ -849,6 +854,12 @@ export async function main(argv, io = process, proc = process) {
849
854
  const { runSpectate } = await import("./spectate-cmd.js");
850
855
  return runSpectate(argv.slice(1), io);
851
856
  }
857
+ // `dexterm overlay` → the streamer overlay (task-1427): OBS browser-source scenes (agent activity, the coin card,
858
+ // the PAPER / LIVE PnL card) fed by the read-only spectator stream or a replay; loopback, single-use ticket → token.
859
+ if (argv[0] === 'overlay') {
860
+ const { runOverlay } = await import("./overlay-cmd.js");
861
+ return runOverlay(argv.slice(1), io);
862
+ }
852
863
  // `dexterm recorder <cmd>` → the history recorder's tables: stats, label, prune, export (task-1329). No network.
853
864
  if (argv[0] === 'recorder') {
854
865
  const { recorderCli } = await import('../../recorder/src/index.js');
@@ -859,11 +870,21 @@ export async function main(argv, io = process, proc = process) {
859
870
  const { narrativeCommand } = await import("./narrative-cmd.js");
860
871
  return narrativeCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
861
872
  }
873
+ // `dexterm narratives [top|detail ID|of MINT|export]` → the MEMES + NARRATIVES tracker (task-1818, EXPERIMENTAL). No network, no model.
874
+ if (argv[0] === 'narratives') {
875
+ const { narrativesCommand } = await import("./narratives-cmd.js");
876
+ return narrativesCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
877
+ }
862
878
  // `dexterm ledger …` → the unified trade ledger (task-1530): reads fold the file + the sources; only `sync` writes. No network, no model, no keys.
863
879
  if (argv[0] === 'ledger') {
864
880
  const { ledgerCommand } = await import("./ledger-cmd.js");
865
881
  return ledgerCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
866
882
  }
883
+ // `dexterm dash` → the strategy performance dashboard (task-1749): run ledgers → a page on 127.0.0.1 only. Read-only, no network, no model, no keys.
884
+ if (argv[0] === 'dash') {
885
+ const { dashCommand } = await import("./dash-cmd.js");
886
+ return dashCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
887
+ }
867
888
  // `dexterm autotrade validate|list|backtest|sweep …` and `dexterm backtest --subject strategy` → the paper autotrade engine (task-1503). No network, no model, no keys.
868
889
  if (argv[0] === 'autotrade' || argv[0] === 'backtest') {
869
890
  const at = await import("./autotrade-cmd.js");
@@ -1001,6 +1022,11 @@ export async function main(argv, io = process, proc = process) {
1001
1022
  const { aiCommand } = await import("./ai-cmd.js");
1002
1023
  return aiCommand(argv[0] === 'ask' ? ['ask', ...argv.slice(1)] : argv.slice(1), { stdin: io.stdin, stdout: io.stdout, stderr: io.stderr, env: io.env });
1003
1024
  }
1025
+ // task-1822: `dexterm classifier …` (pick / pull / serve the classification model; hardware tiers; hosted key).
1026
+ if (argv[0] === 'classifier') {
1027
+ const { classifierCommand } = await import("./classifier-cmd.js");
1028
+ return classifierCommand(argv.slice(1), { stdin: io.stdin, stdout: io.stdout, stderr: io.stderr, env: io.env });
1029
+ }
1004
1030
  // `dexterm doctor` → a pre-flight that says whether dexterm will run here and how to fix what won't (task-1305).
1005
1031
  if (argv[0] === 'doctor') {
1006
1032
  const { defaultDeps, runDoctor, renderDoctor } = await import("./doctor.js");
@@ -1433,6 +1459,20 @@ function rulesScreenMove(state, delta) {
1433
1459
  rulesMove(state.rulesUi, delta);
1434
1460
  return true;
1435
1461
  }
1462
+ // task-1818: j/k on the N screen's EXPERIMENTAL heat table move its cursor, or the member cursor while drilled in. The theme
1463
+ // board (the default mode) keeps the shared state.selected cursor.
1464
+ function narrativesMove(state, delta) {
1465
+ const ui = state.narrativesUi;
1466
+ if (state.screen !== 'narrative' || ui?.mode !== 'heat')
1467
+ return false;
1468
+ if (ui.drill) {
1469
+ const n = drillDetail(state)?.memberList.length ?? 0;
1470
+ ui.drill.selected = Math.max(0, Math.min(n - 1, ui.drill.selected + delta));
1471
+ return true;
1472
+ }
1473
+ ui.selected = Math.max(0, Math.min(heatRows(state).length - 1, ui.selected + delta));
1474
+ return true;
1475
+ }
1436
1476
  function plansMove(state, delta) {
1437
1477
  if (state.screen !== 'plans' || !state.plansUi)
1438
1478
  return false;
@@ -1519,7 +1559,7 @@ export function applyAction(state, action, stack) {
1519
1559
  // updates the context injected next turn); PgUp/PgDn scroll the chat transcript. On the token
1520
1560
  // screen (QA M12) the movement keys scroll the answer. Keys reach here only when input is NOT focused.
1521
1561
  case 'move.down':
1522
- if (plansMove(state, 1) || followsMove(state, 1) || rulesScreenMove(state, 1))
1562
+ if (narrativesMove(state, 1) || plansMove(state, 1) || followsMove(state, 1) || rulesScreenMove(state, 1))
1523
1563
  break;
1524
1564
  if (bubbleMove(state, 1))
1525
1565
  break;
@@ -1528,7 +1568,7 @@ export function applyAction(state, action, stack) {
1528
1568
  state.selected = Math.min(rows().length - 1, (state.selected || 0) + 1);
1529
1569
  break;
1530
1570
  case 'move.up':
1531
- if (plansMove(state, -1) || followsMove(state, -1) || rulesScreenMove(state, -1))
1571
+ if (narrativesMove(state, -1) || plansMove(state, -1) || followsMove(state, -1) || rulesScreenMove(state, -1))
1532
1572
  break;
1533
1573
  if (bubbleMove(state, -1))
1534
1574
  break;
@@ -1537,7 +1577,7 @@ export function applyAction(state, action, stack) {
1537
1577
  state.selected = Math.max(0, (state.selected || 0) - 1);
1538
1578
  break;
1539
1579
  case 'move.pagedown':
1540
- if (plansMove(state, PAGE_ROWS) || followsMove(state, PAGE_ROWS) || rulesScreenMove(state, PAGE_ROWS))
1580
+ if (narrativesMove(state, PAGE_ROWS) || plansMove(state, PAGE_ROWS) || followsMove(state, PAGE_ROWS) || rulesScreenMove(state, PAGE_ROWS))
1541
1581
  break;
1542
1582
  if (analystChatScroll(state, PAGE_ROWS))
1543
1583
  break;
@@ -1546,7 +1586,7 @@ export function applyAction(state, action, stack) {
1546
1586
  state.selected = Math.min(rows().length - 1, (state.selected || 0) + PAGE_ROWS);
1547
1587
  break;
1548
1588
  case 'move.pageup':
1549
- if (plansMove(state, -PAGE_ROWS) || followsMove(state, -PAGE_ROWS) || rulesScreenMove(state, -PAGE_ROWS))
1589
+ if (narrativesMove(state, -PAGE_ROWS) || plansMove(state, -PAGE_ROWS) || followsMove(state, -PAGE_ROWS) || rulesScreenMove(state, -PAGE_ROWS))
1550
1590
  break;
1551
1591
  if (analystChatScroll(state, -PAGE_ROWS))
1552
1592
  break;
@@ -1555,14 +1595,14 @@ export function applyAction(state, action, stack) {
1555
1595
  state.selected = Math.max(0, (state.selected || 0) - PAGE_ROWS);
1556
1596
  break;
1557
1597
  case 'move.top':
1558
- if (plansMove(state, -1e9) || followsMove(state, -1e9))
1598
+ if (narrativesMove(state, -1e9) || plansMove(state, -1e9) || followsMove(state, -1e9))
1559
1599
  break;
1560
1600
  if (chatScroll(state, -1e9))
1561
1601
  break;
1562
1602
  state.selected = 0;
1563
1603
  break;
1564
1604
  case 'move.bottom':
1565
- if (plansMove(state, 1e9) || followsMove(state, 1e9))
1605
+ if (narrativesMove(state, 1e9) || plansMove(state, 1e9) || followsMove(state, 1e9))
1566
1606
  break;
1567
1607
  if (chatScroll(state, 1e9))
1568
1608
  break;
@@ -1715,6 +1755,10 @@ export function applyAction(state, action, stack) {
1715
1755
  state.copyUi.detail = null;
1716
1756
  break;
1717
1757
  } // task-1399: esc closes the wallet detail view before leaving the screen
1758
+ if (state.screen === 'narrative' && state.narrativesUi?.drill) {
1759
+ state.narrativesUi.drill = null;
1760
+ break;
1761
+ } // task-1818: esc in a drill-down returns to the heat table
1718
1762
  const prev = stack.pop();
1719
1763
  if (prev)
1720
1764
  state.screen = prev;
@@ -1982,6 +2026,46 @@ export function applyAction(state, action, stack) {
1982
2026
  stack.push(state.screen);
1983
2027
  state.screen = 'narrative';
1984
2028
  state.narrative = computeNarrativeView(state, state.narrative?.window ?? '1h');
2029
+ if (state.narrativesUi?.mode === 'heat')
2030
+ kickNarratives(state); // task-1818: back on the heat table → refresh if it is older than 60 s
2031
+ break;
2032
+ }
2033
+ // task-1818: n flips the theme board ⇄ the EXPERIMENTAL heat table. The first flip starts the tracker build (the recorder's
2034
+ // last 26 h, ~30 s; never at app start); the table shows "building…" until it lands, later refreshes come from the tick.
2035
+ case 'narrative.view': {
2036
+ if (state.screen !== 'narrative')
2037
+ break;
2038
+ const ui = narrativesUiOf(state);
2039
+ ui.mode = ui.mode === 'heat' ? 'board' : 'heat';
2040
+ ui.drill = null;
2041
+ if (ui.mode === 'heat')
2042
+ kickNarratives(state);
2043
+ break;
2044
+ }
2045
+ // task-1818: ⏎ on the heat table drills into the selected narrative; ⏎ on a member opens that coin's token screen (live
2046
+ // mode; fixtures say so). On the theme board ⏎ keeps its old meaning (the shared `open`).
2047
+ case 'narrative.drill': {
2048
+ if (state.screen !== 'narrative')
2049
+ break;
2050
+ const ui = state.narrativesUi;
2051
+ if (ui?.mode !== 'heat')
2052
+ return applyAction(state, 'open', stack);
2053
+ if (!ui.drill) {
2054
+ const r = heatRows(state)[ui.selected];
2055
+ if (r)
2056
+ ui.drill = { id: r.id, selected: 0 };
2057
+ break;
2058
+ }
2059
+ const m = drillDetail(state)?.memberList[ui.drill.selected];
2060
+ if (!m)
2061
+ break;
2062
+ if (!live) {
2063
+ state.pendingToast = `token detail needs live mode (${m.symbol} ${m.mint.slice(0, 8)}…)`;
2064
+ break;
2065
+ }
2066
+ live.openToken(seedForMint(state, live, m.mint, m.symbol));
2067
+ stack.push(state.screen);
2068
+ state.screen = 'token';
1985
2069
  break;
1986
2070
  }
1987
2071
  case 'narrative.window': {
@@ -4283,6 +4367,11 @@ export function interactive(state, io, depth, args, proc = process) {
4283
4367
  // execution moment, pause / kill) draws over this terminal while it follows. Calm mode = reduced motion.
4284
4368
  state.agentHud = { story: followSession.story, clock: () => Date.now(), dataLabel: args.replay ? 'REPLAY DATA' : null };
4285
4369
  void followSession.start();
4370
+ // task-1427: the open coin's live holder/rug signals ride the spectator stream too (the streamer overlay's coin card).
4371
+ {
4372
+ const fs = followSession;
4373
+ state.live?.signals.listeners.add((sig) => fs.liveFlag(sig));
4374
+ }
4286
4375
  if (size()[1] < HUD_MIN_H)
4287
4376
  say('following agents over MCP · ctrl+p pause · ctrl+k stop · any key takes over', 'ai', 6);
4288
4377
  if (agentPaper)
@@ -6169,6 +6258,9 @@ export function interactive(state, io, depth, args, proc = process) {
6169
6258
  say(state.updateNotice, 'info', 8);
6170
6259
  state.updateNotice = null;
6171
6260
  }
6261
+ // task-1818: refresh the EXPERIMENTAL narratives tracker at most every 60 s while its heat table is on screen.
6262
+ if (state.screen === 'narrative' && state.narrativesUi?.mode === 'heat' && !state.paused)
6263
+ kickNarratives(state);
6172
6264
  // task-1508: the autotrade screen re-reads the paper runner's status file every 2 s while it is open.
6173
6265
  if (state.screen === 'autotrade' && state.autotradeUi)
6174
6266
  autotradeTick(state.autotradeUi);
@@ -116,7 +116,10 @@ export class McpFollowSession {
116
116
  };
117
117
  this.follow = new FollowController({ controls, activityCap: Math.max(1, opts.activityCap ?? 200) });
118
118
  this.story = new AgentStory(this.agentLabel);
119
- this.hub.on((ev) => { this.follow.apply(ev); this.story.apply(ev); });
119
+ // task-1427: a live signal the TERMINAL fired (liveFlag) rides the hub for the stream only; it is not an agent call
120
+ // or an agent thought, so the follow view and the story never fold it (the story would show a note as a thought).
121
+ this.hub.on((ev) => { if (ev.phase === 'note' && ev.tool === 'live_signal')
122
+ return; this.follow.apply(ev); this.story.apply(ev); });
120
123
  }
121
124
  /** Arm the rendezvous (so a `dexterm mcp` server starts emitting) and begin tailing. Idempotent. */
122
125
  async start() {
@@ -304,6 +307,23 @@ export class McpFollowSession {
304
307
  }
305
308
  catch { /* ignore */ }
306
309
  }
310
+ /**
311
+ * task-1427: a live holder / rug signal the owner's terminal fired for a coin (the task-1404 engine) → one `note`
312
+ * event in the hub, so the streamer overlay's coin card can show it with its age. Only while armed (an agent is
313
+ * being followed); it describes data the terminal already read, and carries nothing anyone can act on.
314
+ */
315
+ liveFlag(s) {
316
+ if (!this.armed)
317
+ return;
318
+ const detail = sanitizeDetail({ coin: { mint: s.mint }, liveFlags: [{ kind: s.kind, level: s.level, title: s.title, text: s.text, source: s.source, at: s.at, ageSec: Math.max(0, s.dataAgeMs / 1000), stale: s.stale }] });
319
+ if (!detail?.liveFlags)
320
+ return;
321
+ this.hub.emit({ phase: 'note', agent: this.currentAgent, transport: 'mcp', tool: 'live_signal', summary: `${s.title}: ${s.text}`.slice(0, 140), detail, ok: true, t: s.at });
322
+ try {
323
+ this.opts.onEvent?.();
324
+ }
325
+ catch { /* ignore */ }
326
+ }
307
327
  /** True when the terminal should mirror the agent: the agent still has control and is active. */
308
328
  canFollow() {
309
329
  if (this.follow.controlState !== 'agent')