dexterm-cli 0.2.0 → 0.2.2
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/dist/packages/agents/src/spectator.js +28 -2
- package/dist/packages/agents/src/tiers.js +3 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/autotrade/src/backtest.js +6 -2
- package/dist/packages/autotrade/src/chat/connector.js +5 -4
- package/dist/packages/autotrade/src/chat/control.js +3 -0
- package/dist/packages/autotrade/src/chat/hub.js +37 -2
- package/dist/packages/autotrade/src/chat/index.js +3 -2
- package/dist/packages/autotrade/src/chat/readonly.js +530 -0
- package/dist/packages/autotrade/src/chat/service.js +12 -1
- package/dist/packages/autotrade/src/engine.js +75 -29
- package/dist/packages/autotrade/src/fleet.js +2 -2
- package/dist/packages/autotrade/src/index.js +4 -0
- package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
- package/dist/packages/autotrade/src/rugproof.js +231 -0
- package/dist/packages/autotrade/src/runner.js +203 -30
- package/dist/packages/autotrade/src/service-install.js +6 -4
- package/dist/packages/autotrade/src/sessions.js +16 -7
- package/dist/packages/autotrade/src/store.js +1 -1
- package/dist/packages/autotrade/src/strategy-stats.js +344 -0
- package/dist/packages/autotrade/src/strategy.js +29 -1
- package/dist/packages/autotrade/src/tiers.js +194 -0
- package/dist/packages/classifier/src/calibrate.js +138 -0
- package/dist/packages/classifier/src/classify.js +107 -0
- package/dist/packages/classifier/src/config.js +298 -0
- package/dist/packages/classifier/src/dossier.js +95 -0
- package/dist/packages/classifier/src/hardware.js +63 -0
- package/dist/packages/classifier/src/index.js +19 -0
- package/dist/packages/classifier/src/models.js +111 -0
- package/dist/packages/classifier/src/providers/heuristic.js +77 -0
- package/dist/packages/classifier/src/providers/http.js +63 -0
- package/dist/packages/classifier/src/providers/jev-open.js +86 -0
- package/dist/packages/classifier/src/providers/llm.js +89 -0
- package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
- package/dist/packages/classifier/src/questions.js +97 -0
- package/dist/packages/classifier/src/registry.js +42 -0
- package/dist/packages/classifier/src/snapshot.js +31 -0
- package/dist/packages/classifier/src/systemone.js +88 -0
- package/dist/packages/classifier/src/types.js +18 -0
- package/dist/packages/classifier/src/win-acl.js +71 -0
- package/dist/packages/cli/assets/dash/dash.css +139 -0
- package/dist/packages/cli/assets/dash/dash.js +200 -0
- package/dist/packages/cli/assets/dash/icon.svg +1 -0
- package/dist/packages/cli/assets/dash/index.html +36 -0
- package/dist/packages/cli/src/app-help-gen.js +13 -1
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +12 -3
- package/dist/packages/cli/src/autotrade-cmd/run.js +14 -2
- package/dist/packages/cli/src/autotrade-cmd/service.js +3 -1
- package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
- package/dist/packages/cli/src/autotrade-cmd/strategies.js +1 -1
- package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +2 -0
- package/dist/packages/cli/src/classifier-cmd.js +361 -0
- package/dist/packages/cli/src/classifier-live.js +81 -0
- package/dist/packages/cli/src/dash/data.js +217 -0
- package/dist/packages/cli/src/dash/server.js +92 -0
- package/dist/packages/cli/src/dash-cmd.js +113 -0
- package/dist/packages/cli/src/doctor.js +34 -0
- package/dist/packages/cli/src/live/screens/token-live.js +4 -0
- package/dist/packages/cli/src/live/signals.js +8 -0
- package/dist/packages/cli/src/main.js +98 -6
- package/dist/packages/cli/src/mcp-follow.js +21 -1
- package/dist/packages/cli/src/narratives-cmd.js +147 -0
- package/dist/packages/cli/src/narratives.js +114 -0
- package/dist/packages/cli/src/overlay/model.js +147 -0
- package/dist/packages/cli/src/overlay/page.js +146 -0
- package/dist/packages/cli/src/overlay/pnl.js +75 -0
- package/dist/packages/cli/src/overlay/server.js +170 -0
- package/dist/packages/cli/src/overlay-cmd.js +367 -0
- package/dist/packages/cli/src/record-cmd.js +82 -11
- package/dist/packages/cli/src/rules-cmd.js +2 -1
- package/dist/packages/cli/src/rules.js +24 -4
- package/dist/packages/cli/src/screens/narrative.js +154 -2
- package/dist/packages/cli/src/skill-eval-build.js +7 -2
- package/dist/packages/cli/src/state.js +1 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +5 -1
- package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-confidence-tiers-experimental-unproven-paper-only.md +56 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-read-only-data-check-top-and-signals-experimental.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
- package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
- package/dist/packages/mcp/src/server.js +106 -1
- package/dist/packages/mcp/src/spectator-sink.js +27 -0
- package/dist/packages/narratives/src/cluster.js +193 -0
- package/dist/packages/narratives/src/embed.js +184 -0
- package/dist/packages/narratives/src/export.js +60 -0
- package/dist/packages/narratives/src/index.js +10 -0
- package/dist/packages/narratives/src/label.js +48 -0
- package/dist/packages/narratives/src/normalize.js +179 -0
- package/dist/packages/narratives/src/recorder.js +93 -0
- package/dist/packages/narratives/src/tracker.js +328 -0
- package/dist/packages/packs/src/skill-care.js +10 -2
- package/dist/packages/recorder/src/exit-probes.js +215 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +11 -1
- package/dist/packages/recorder/src/retention.js +4 -0
- package/dist/packages/recorder/src/schema.js +20 -1
- package/dist/packages/tui/src/keymap.js +5 -0
- package/package.json +3 -1
- 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
|
-
|
|
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')
|