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
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
1
9
|
// dexterm — `dexterm record` (task-1414): a HEADLESS collector that keeps the history recorder fed when
|
|
2
10
|
// nobody has the TUI open. The recorder (task-1329) only grows while the interactive app runs; the backtest
|
|
3
11
|
// (task-1401) is only worth re-running on fresh data, so this command runs the exact same recorder jobs
|
|
@@ -20,13 +28,17 @@
|
|
|
20
28
|
// re-installed with them: `--retention-mb N` replaces that pause with eviction of the oldest history (observation
|
|
21
29
|
// never pauses), `--retention-days D` is the age rule for snapshot rows, and `--focus-ticks` runs the autotrade
|
|
22
30
|
// tick lane (5–10 s DexScreener samples of watchlist / held / candidate / open coins, plus 1m candles).
|
|
31
|
+
// task-1786 (winner-recall DATA-GAPS items 1-2), opt-in the same way: `--focus-module PATH` loads an ESM module whose
|
|
32
|
+
// `focus(now)` adds focus coins to the tick lane (the challenge / control coins live outside the app; the module that
|
|
33
|
+
// reads them lives with them), and `--exit-probes` adds a keyless Jupiter sell quote for every held focus coin once a
|
|
34
|
+
// minute (exit-probes.ts). Both imply --focus-ticks.
|
|
23
35
|
import { mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
|
24
36
|
import { spawnSync } from 'node:child_process';
|
|
25
37
|
import { dirname, join, resolve } from 'node:path';
|
|
26
|
-
import { fileURLToPath } from 'node:url';
|
|
38
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
39
|
import { defaultDbPath, resolveRpcUrl, extraRpcUrls, redactSecrets } from '../../data/src/index.js';
|
|
28
40
|
import { PumpPortalClient } from '../../data/src/index.js';
|
|
29
|
-
import { Recorder, recorderEnabled, dbFocus, focusFilePath, readFocusFile, DEFAULT_TICKS } from '../../recorder/src/index.js';
|
|
41
|
+
import { Recorder, recorderEnabled, dbFocus, focusFilePath, readFocusFile, exitProbeDeps, DEFAULT_TICKS } from '../../recorder/src/index.js';
|
|
30
42
|
import { dextermHome } from "./boot-state.js";
|
|
31
43
|
import { createLiveFeed } from "./feed.js";
|
|
32
44
|
import { NewPairsLive } from "./live/index.js";
|
|
@@ -50,13 +62,30 @@ export function buildLiveEngine(env, opts) {
|
|
|
50
62
|
// Focus coins for the tick lane: the watchlist + paper positions in this DB, and whatever the autotrade runner /
|
|
51
63
|
// the app publish in the focus file. Read on every poll (two indexed reads + one small file).
|
|
52
64
|
const focusFile = focusFilePath(dextermHome(env));
|
|
65
|
+
// task-1786: the focus module's coins. A throwing module costs its own coins only (logged at most every 10 min).
|
|
66
|
+
let extraFailedAt = -Infinity;
|
|
67
|
+
const extra = () => {
|
|
68
|
+
if (!opts.extraFocus)
|
|
69
|
+
return [];
|
|
70
|
+
try {
|
|
71
|
+
return [...opts.extraFocus()];
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
if (Date.now() - extraFailedAt > 600_000) {
|
|
75
|
+
extraFailedAt = Date.now();
|
|
76
|
+
opts.onLog?.(`record: focus module failed: ${redactSecrets(e.message)}\n`);
|
|
77
|
+
}
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
};
|
|
53
81
|
const recorder = new Recorder({
|
|
54
82
|
path: opts.dbPath, onLog: opts.verbose ? opts.onLog : undefined,
|
|
55
83
|
// The cap reclaims the data layer's expired cache entries (its own rule: a day past their TTL) before it evicts
|
|
56
84
|
// any history; `feed` is built above, so the hook can reach it.
|
|
57
85
|
...(opts.retentionBytes ? { cap: { maxBytes: opts.retentionBytes, reclaim: () => feed.dl.cache.prune() } } : {}),
|
|
58
86
|
...(opts.retentionDays ? { retention: { columnsDays: opts.retentionDays } } : {}),
|
|
59
|
-
...(opts.focusTicks ? { ticks: { focus: () => [...dbFocus(recorder.database), ...readFocusFile(focusFile, Date.now())] } } : {}),
|
|
87
|
+
...(opts.focusTicks ? { ticks: { focus: () => [...dbFocus(recorder.database), ...readFocusFile(focusFile, Date.now()), ...extra()] } } : {}),
|
|
88
|
+
...(opts.focusTicks && opts.exitProbes ? { exitProbes: {} } : {}),
|
|
60
89
|
});
|
|
61
90
|
recorder.attach(feed.dl.store);
|
|
62
91
|
let newpairs = null;
|
|
@@ -71,6 +100,7 @@ export function buildLiveEngine(env, opts) {
|
|
|
71
100
|
feed.extraMints = () => [...np.rows.keys()];
|
|
72
101
|
}
|
|
73
102
|
const dl = feed.dl;
|
|
103
|
+
const probeDeps = exitProbeDeps(dl);
|
|
74
104
|
return {
|
|
75
105
|
async observe() {
|
|
76
106
|
const before = recorder.counters.written;
|
|
@@ -83,7 +113,14 @@ export function buildLiveEngine(env, opts) {
|
|
|
83
113
|
}
|
|
84
114
|
catch { /* logged inside; never fatal */ } },
|
|
85
115
|
enforceCap() { return recorder.enforceCap(); },
|
|
86
|
-
...(opts.focusTicks ? { pollTicks: () =>
|
|
116
|
+
...(opts.focusTicks ? { pollTicks: async () => {
|
|
117
|
+
const r = await recorder.pollTicks(dl);
|
|
118
|
+
// Detached: a quote waiting on the Jupiter bucket must never delay the next 5 s tick (the lane's own in-flight
|
|
119
|
+
// guard keeps one probe pass at a time; it never throws).
|
|
120
|
+
if (opts.exitProbes)
|
|
121
|
+
void recorder.probeExits(probeDeps).catch(() => { });
|
|
122
|
+
return r;
|
|
123
|
+
} } : {}),
|
|
87
124
|
dbBytes() {
|
|
88
125
|
try {
|
|
89
126
|
const db = recorder.database;
|
|
@@ -261,6 +298,19 @@ function parseRetention(t, next, a) {
|
|
|
261
298
|
a.focusTicks = true;
|
|
262
299
|
return null;
|
|
263
300
|
}
|
|
301
|
+
if (t === '--exit-probes') {
|
|
302
|
+
a.exitProbes = true;
|
|
303
|
+
a.focusTicks = true;
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
if (t === '--focus-module') {
|
|
307
|
+
const v = next();
|
|
308
|
+
if (!v || v.startsWith('--'))
|
|
309
|
+
return '--focus-module needs a PATH';
|
|
310
|
+
a.focusModule = resolve(v);
|
|
311
|
+
a.focusTicks = true;
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
264
314
|
if (t === '--retention-mb') {
|
|
265
315
|
const n = Number(next());
|
|
266
316
|
if (!Number.isFinite(n) || n < 1)
|
|
@@ -278,7 +328,7 @@ function parseRetention(t, next, a) {
|
|
|
278
328
|
return undefined;
|
|
279
329
|
}
|
|
280
330
|
export function parseRun(argv) {
|
|
281
|
-
const a = { intervalMs: DEFAULT_INTERVAL_MS, ticks: Infinity, maintainMs: DEFAULT_MAINTAIN_MS, maxDbBytes: DEFAULT_MAX_DB_MB * 1_000_000, noNewpairs: false, db: null, verbose: false, retentionBytes: null, retentionDays: null, focusTicks: false };
|
|
331
|
+
const a = { intervalMs: DEFAULT_INTERVAL_MS, ticks: Infinity, maintainMs: DEFAULT_MAINTAIN_MS, maxDbBytes: DEFAULT_MAX_DB_MB * 1_000_000, noNewpairs: false, db: null, verbose: false, retentionBytes: null, retentionDays: null, focusTicks: false, focusModule: null, exitProbes: false };
|
|
282
332
|
for (let i = 0; i < argv.length; i++) {
|
|
283
333
|
const t = argv[i];
|
|
284
334
|
const r = parseRetention(t, () => argv[++i], a);
|
|
@@ -329,8 +379,9 @@ export function parseRun(argv) {
|
|
|
329
379
|
export const RECORD_USAGE = `dexterm record: keep the history recorder fed with no TUI open (task-1414).
|
|
330
380
|
|
|
331
381
|
dexterm record [run] [--interval S] [--ticks N | --once] [--maintain S] [--max-db-mb N] [--retention-mb N] [--retention-days D]
|
|
332
|
-
[--focus-ticks] [--no-newpairs] [--db PATH] [--verbose]
|
|
333
|
-
dexterm record install [--interval S] [--max-db-mb N] [--retention-mb N] [--retention-days D] [--focus-ticks]
|
|
382
|
+
[--focus-ticks] [--focus-module PATH] [--exit-probes] [--no-newpairs] [--db PATH] [--verbose]
|
|
383
|
+
dexterm record install [--interval S] [--max-db-mb N] [--retention-mb N] [--retention-days D] [--focus-ticks]
|
|
384
|
+
[--focus-module PATH] [--exit-probes] [--load] [--dry-run]
|
|
334
385
|
dexterm record uninstall [--unload]
|
|
335
386
|
dexterm record status [--db PATH]
|
|
336
387
|
|
|
@@ -341,7 +392,10 @@ run the collector loop: one keyless feed refresh per --interval (default
|
|
|
341
392
|
to stay under N MB and observation never pauses. --retention-days D drops snapshot rows (sweep rows
|
|
342
393
|
too) older than D days (default 90). --focus-ticks samples watchlist / held / candidate / open coins
|
|
343
394
|
every 5-10 s (DexScreener batches of 30, a daily call ceiling) with 1m candles; autotrade backtests
|
|
344
|
-
read them. --
|
|
395
|
+
read them. --focus-module PATH adds the coins an ESM module's focus(now) returns
|
|
396
|
+
([{ mint, role }], role held | open | candidate | watch) to that lane. --exit-probes asks Jupiter for
|
|
397
|
+
a keyless $50 sell quote of every held focus coin once a minute (exit_probes; quotes only).
|
|
398
|
+
Both imply --focus-ticks. --no-newpairs drops the PumpPortal stream. Ctrl-C stops cleanly
|
|
345
399
|
(labels whatever is due). Requests honour the data layer's rate limits, health gate and sweep budgets.
|
|
346
400
|
install write a user-level launchd LaunchAgent (no sudo) that runs \`record run\`; prints the launchctl
|
|
347
401
|
command. --load also runs launchctl (off by default). --dry-run prints the plist, writes nothing.
|
|
@@ -404,7 +458,22 @@ async function run(argv, io, deps) {
|
|
|
404
458
|
};
|
|
405
459
|
})();
|
|
406
460
|
const log = (s) => io.stderr.write(s);
|
|
407
|
-
|
|
461
|
+
// task-1786: the focus module is loaded once, up front; a module that does not load is a startup error (exit 2).
|
|
462
|
+
let extraFocus;
|
|
463
|
+
if (parsed.focusModule) {
|
|
464
|
+
try {
|
|
465
|
+
const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(parsed.focusModule).href));
|
|
466
|
+
if (typeof mod.focus !== 'function')
|
|
467
|
+
throw new Error('it exports no focus(now) function');
|
|
468
|
+
const fn = mod.focus;
|
|
469
|
+
extraFocus = () => fn(Date.now());
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
io.stderr.write(`dexterm record: --focus-module ${parsed.focusModule}: ${redactSecrets(e.message)}\n`);
|
|
473
|
+
return 2;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const engine = deps.engine ?? buildLiveEngine(io.env, { dbPath, noNewpairs: parsed.noNewpairs, verbose: parsed.verbose, onLog: log, retentionBytes: parsed.retentionBytes, retentionDays: parsed.retentionDays, focusTicks: parsed.focusTicks, extraFocus, exitProbes: parsed.exitProbes });
|
|
408
477
|
const own = !deps.engine;
|
|
409
478
|
const onSig = () => { io.stderr.write('\ndexterm record: stopping…\n'); stop.abort(); };
|
|
410
479
|
if (own) {
|
|
@@ -415,7 +484,7 @@ async function run(argv, io, deps) {
|
|
|
415
484
|
// alive between passes and node would exit mid-run (task-1504 smoke). Hold it open until the loops finish.
|
|
416
485
|
const hold = own ? setInterval(() => { }, 1 << 30) : null;
|
|
417
486
|
if (parsed.verbose || parsed.ticks !== Infinity)
|
|
418
|
-
io.stderr.write(`dexterm record: db ${dbPath} · interval ${Math.round(parsed.intervalMs / 1000)}s · maintain ${Math.round(parsed.maintainMs / 1000)}s · cap ${parsed.maxDbBytes === null ? 'off' : `${Math.round(parsed.maxDbBytes / 1e6)} MB`} · newpairs ${parsed.noNewpairs ? 'off' : 'on'}${parsed.retentionBytes !== null ? ` · retention ${Math.round(parsed.retentionBytes / 1e6)} MB (evict, never pause)` : ''}${parsed.retentionDays !== null ? ` · rows ${parsed.retentionDays} d` : ''} · focus ticks ${parsed.focusTicks ? 'on' : 'off'}\n`);
|
|
487
|
+
io.stderr.write(`dexterm record: db ${dbPath} · interval ${Math.round(parsed.intervalMs / 1000)}s · maintain ${Math.round(parsed.maintainMs / 1000)}s · cap ${parsed.maxDbBytes === null ? 'off' : `${Math.round(parsed.maxDbBytes / 1e6)} MB`} · newpairs ${parsed.noNewpairs ? 'off' : 'on'}${parsed.retentionBytes !== null ? ` · retention ${Math.round(parsed.retentionBytes / 1e6)} MB (evict, never pause)` : ''}${parsed.retentionDays !== null ? ` · rows ${parsed.retentionDays} d` : ''} · focus ticks ${parsed.focusTicks ? 'on' : 'off'}${parsed.focusModule ? ` · focus module ${parsed.focusModule}` : ''}${parsed.exitProbes ? ' · exit probes on' : ''}\n`);
|
|
419
488
|
let result;
|
|
420
489
|
let tickRes = { polls: 0, calls: 0, written: 0 };
|
|
421
490
|
try {
|
|
@@ -446,7 +515,7 @@ async function run(argv, io, deps) {
|
|
|
446
515
|
}
|
|
447
516
|
function install(argv, io) {
|
|
448
517
|
let intervalMs = DEFAULT_INTERVAL_MS, maxDbMb = DEFAULT_MAX_DB_MB, load = false, dry = false;
|
|
449
|
-
const ret = { retentionBytes: null, retentionDays: null, focusTicks: false };
|
|
518
|
+
const ret = { retentionBytes: null, retentionDays: null, focusTicks: false, focusModule: null, exitProbes: false };
|
|
450
519
|
for (let i = 0; i < argv.length; i++) {
|
|
451
520
|
const t = argv[i];
|
|
452
521
|
const r = parseRetention(t, () => argv[++i], ret);
|
|
@@ -488,6 +557,8 @@ function install(argv, io) {
|
|
|
488
557
|
...(ret.retentionBytes !== null ? ['--retention-mb', String(Math.round(ret.retentionBytes / 1e6))] : []),
|
|
489
558
|
...(ret.retentionDays !== null ? ['--retention-days', String(ret.retentionDays)] : []),
|
|
490
559
|
...(ret.focusTicks ? ['--focus-ticks'] : []),
|
|
560
|
+
...(ret.focusModule ? ['--focus-module', ret.focusModule] : []),
|
|
561
|
+
...(ret.exitProbes ? ['--exit-probes'] : []),
|
|
491
562
|
];
|
|
492
563
|
const plist = renderCollectorPlist({ label: COLLECTOR_LABEL, nodePath: process.execPath, binPath: bin, intervalMs, maxDbMb, extraArgs, logPath, workingDir: resolve(dirname(bin), '..', '..', '..') });
|
|
493
564
|
if (dry) {
|
|
@@ -119,8 +119,9 @@ export async function rulesCommand(argv, io) {
|
|
|
119
119
|
case 'fields': {
|
|
120
120
|
io.stdout.write(`fields: ${FIELD_NAMES.join(' ')}\n`);
|
|
121
121
|
io.stdout.write(`aliases: vol volume liquidity cap mc holder organic top locked cluster% clusterpct\n`);
|
|
122
|
+
io.stdout.write(`narrative (EXPERIMENTAL, measured heat, not advice): narrative_rank narrative_score narrative:spark|ignition|peak|fading|dead|saturated (null until N → n builds the tracker)\n`);
|
|
122
123
|
io.stdout.write(`signals: <field>.velocity (current ÷ mean of earlier samples in the window) · <field>.delta (current − oldest in the window)\n`);
|
|
123
|
-
io.stdout.write(`tags: ${TAG_NAMES.map((t) => t + ':').join(' ')} (e.g. launchpad:pump flag:mint flag:clean verified:yes sus:no; prefix ! to negate)\n`);
|
|
124
|
+
io.stdout.write(`tags: ${TAG_NAMES.map((t) => t + ':').join(' ')} (e.g. launchpad:pump flag:mint flag:clean verified:yes sus:no narrative:ignition narrative:saturated; prefix ! to negate)\n`);
|
|
124
125
|
io.stdout.write(`values: $1.5M 25K 2B 20% 2h 30m 3d · ops > >= < <= = != · and or not ( )\n`);
|
|
125
126
|
io.stdout.write(`actions: ${ACTIONS.join(' ')} alert(level, "text") ticket(buy|sell, size ≤ ${TICKET_SIZE_CAP}, sol|usd|pct) ask(skill) popup(${RULE_POPUP_KINDS.join('|')}) tag("label") sound(name) watch\n`);
|
|
126
127
|
io.stdout.write(`limits: cooldown (default 10m) · max n/h (default 12, cap ${MAX_PER_HOUR_CAP}) · scope ${SCOPES.join('|')}|mint:<addr> · window (default 10m)\n`);
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
// dexterm — the rules engine's app adapter (task-1358 Tier 3). Pure helpers over @dexterm/custom's RulesEngine
|
|
2
|
+
// that the key handler, the interactive loop and the Rules screen call: load `config.rules` into an engine on
|
|
3
|
+
// `state.rulesUi`, feed it the screener rows every few seconds (`rulesTick`), turn its outputs into things the
|
|
4
|
+
// app can show (toasts, the screen's log, PROPOSAL cards, pending UI actions the loop performs), and persist edits
|
|
5
|
+
// through the config journal (so `dexterm config undo` / the journal cover rules too).
|
|
6
|
+
//
|
|
7
|
+
// SAFETY: a rule's outputs are alerts, annotations (watch/tag on the user's own watchlist), UI actions (popup /
|
|
8
|
+
// terminal bell) and PROPOSALS. Acting on a ticket proposal opens the PAPER ticket in its editing phase (the user
|
|
9
|
+
// sizes and confirms there, signer caps apply); acting on an ask proposal sends a question to the analyst. Nothing
|
|
10
|
+
// in this module can arm, sign, confirm, open a live ticket or touch a mandate — there is no code path for it.
|
|
11
|
+
import { classifierLiveTick } from "./classifier-live.js";
|
|
1
12
|
import { devBookOf } from "./live/dev-history.js";
|
|
13
|
+
import { narrativeRuleFields } from "./narratives.js";
|
|
2
14
|
import { RulesEngine, ruleToJson, ruleToText, slug, applyAndJournal, describeActions, } from '../../custom/src/index.js';
|
|
3
15
|
import { alertToDelivery, proposalToDelivery } from "./alerts/format.js";
|
|
4
16
|
export const RULE_INPUT_MAX = 400;
|
|
@@ -56,6 +68,11 @@ export function larpGrade(verdict) {
|
|
|
56
68
|
* `mintable > 0` fires on the real streaming values, not the on-open snapshot. A token opened by `--mint` that is
|
|
57
69
|
* not in the screener feed is appended as its own row so rules still see it.
|
|
58
70
|
*/
|
|
71
|
+
/** task-1822: the classifier's drain estimate for the `rugp` field (null when unscored or the scoring failed). */
|
|
72
|
+
function rugpOf(state, mint) {
|
|
73
|
+
const p = state.classifierByMint?.get(mint)?.p;
|
|
74
|
+
return typeof p === 'number' && Number.isFinite(p) ? p : null;
|
|
75
|
+
}
|
|
59
76
|
export function ruleRowsOf(state) {
|
|
60
77
|
const out = [];
|
|
61
78
|
const seen = new Set();
|
|
@@ -70,11 +87,13 @@ export function ruleRowsOf(state) {
|
|
|
70
87
|
// 0.1.5 dev history: the creator's recorded rugs as the `devrugs` field (local recorder only; null = unknown).
|
|
71
88
|
const book = devBookOf(state);
|
|
72
89
|
const devRugsOf = (mint) => book ? book.rugsFor(mint) : null;
|
|
90
|
+
// task-1818: narrative_rank / narrative_score / narrative:<state> from the ALREADY-built narratives tracker (never builds one)
|
|
91
|
+
const narr = (mint) => narrativeRuleFields(state, mint);
|
|
73
92
|
for (const r of state.screener.rows) {
|
|
74
93
|
if (!r.mint || r.skeleton || seen.has(r.mint))
|
|
75
94
|
continue;
|
|
76
95
|
seen.add(r.mint);
|
|
77
|
-
const row = Object.assign(Object.create(null), r, { cluster: clusterOf(r.mint), larp: larpGrade(state.larpByMint?.get(r.mint)?.verdict), devRugs: devRugsOf(r.mint) });
|
|
96
|
+
const row = Object.assign(Object.create(null), r, { cluster: clusterOf(r.mint), larp: larpGrade(state.larpByMint?.get(r.mint)?.verdict), devRugs: devRugsOf(r.mint), rugP: rugpOf(state, r.mint) }, narr(r.mint));
|
|
78
97
|
if (r.mint === openMint)
|
|
79
98
|
overlaySignalFields(row, state, r.mint);
|
|
80
99
|
out.push(row);
|
|
@@ -85,9 +104,9 @@ export function ruleRowsOf(state) {
|
|
|
85
104
|
const row = Object.assign(Object.create(null), {
|
|
86
105
|
mint: v.mint, symbol: v.symbol, price: v.price, mcap: v.mcap, fdv: v.fdv, liq: v.liq, vol24: v.vol24,
|
|
87
106
|
holders: v.holders, org: v.org, top10: v.top10, dev: v.dev, lpLocked: v.lpLocked,
|
|
88
|
-
cluster: clusterOf(v.mint), larp: larpGrade(state.larpByMint?.get(v.mint)?.verdict), devRugs: devRugsOf(v.mint),
|
|
107
|
+
cluster: clusterOf(v.mint), larp: larpGrade(state.larpByMint?.get(v.mint)?.verdict), devRugs: devRugsOf(v.mint), rugP: rugpOf(state, v.mint),
|
|
89
108
|
ageMin: v.ageMin, chg: v.chg, launchpad: v.launchpad, flags: v.flags, verified: v.verified, sus: v.sus,
|
|
90
|
-
});
|
|
109
|
+
}, narr(v.mint));
|
|
91
110
|
overlaySignalFields(row, state, v.mint);
|
|
92
111
|
out.push(row);
|
|
93
112
|
}
|
|
@@ -107,7 +126,7 @@ export function ruleRowsOf(state) {
|
|
|
107
126
|
out.push(Object.assign(Object.create(null), {
|
|
108
127
|
mint: p.mint, symbol: p.symbol, mcap: p.mcap, liq: p.liq, holders: p.holders, launchpad: p.launchpad,
|
|
109
128
|
ageMin: p.createdAt ? Math.max(0, (now - p.createdAt) / 60_000) : null, bonding: p.bonding, devRugs,
|
|
110
|
-
}));
|
|
129
|
+
}, narr(p.mint)));
|
|
111
130
|
}
|
|
112
131
|
}
|
|
113
132
|
return out;
|
|
@@ -153,6 +172,7 @@ export function rulesTick(state, now) {
|
|
|
153
172
|
const rows = ruleRowsOf(state);
|
|
154
173
|
if (!rows.length)
|
|
155
174
|
return null;
|
|
175
|
+
classifierLiveTick(state, rows, now); // task-1822: fills classifierByMint for `rugp` rules (a no-op without one)
|
|
156
176
|
ui.engine.observe(rows, now);
|
|
157
177
|
const watch = state.live?.watch.store ?? null;
|
|
158
178
|
const positions = state.live?.positions ?? null;
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
// new launches / runners / pace / "what is working" score. No model in the hot path; the board carries
|
|
4
4
|
// its own limits (name-based, X/TG-only narratives missed, honest unclassified share). w flips 1h/24h,
|
|
5
5
|
// M opens the HUD popup. The board is computed in state.ts (computeNarrativeView) on entry and the tick.
|
|
6
|
+
// task-1818: `n` flips to the EXPERIMENTAL heat table (@dexterm/narratives: clustered by what the name says,
|
|
7
|
+
// lifecycle + traction score per narrative); ⏎ drills into one narrative's member coins, esc returns to the table.
|
|
6
8
|
import { Canvas, header, footer, badge, breakpoint } from '../../../tui/src/index.js';
|
|
7
9
|
import { headerModel } from "../state.js";
|
|
10
|
+
import { clean } from '../../../packs/src/index.js';
|
|
11
|
+
import { HEAT_NOTE } from '../../../narratives/src/index.js';
|
|
12
|
+
import { heatRows, drillDetail, currentTracker, narrativesSource, UNTRUSTED_NOTE } from "../narratives.js";
|
|
8
13
|
const fmtUsd = (v) => { if (v == null)
|
|
9
14
|
return '—'; const a = Math.abs(v); if (a >= 1e9)
|
|
10
15
|
return (v / 1e9).toFixed(2) + 'B'; if (a >= 1e6)
|
|
@@ -12,6 +17,13 @@ const fmtUsd = (v) => { if (v == null)
|
|
|
12
17
|
return (v / 1e3).toFixed(1) + 'K'; return v.toFixed(0); };
|
|
13
18
|
const fmtPct = (v) => (v == null ? '—' : (v > 0 ? '+' : '') + v.toFixed(1) + '%');
|
|
14
19
|
export function renderNarrative(c, state) {
|
|
20
|
+
if (state.narrativesUi?.mode === 'heat') {
|
|
21
|
+
if (state.narrativesUi.drill)
|
|
22
|
+
renderDrill(c, state);
|
|
23
|
+
else
|
|
24
|
+
renderHeat(c, state);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
15
27
|
const t = c.theme;
|
|
16
28
|
const bp = breakpoint(c.w);
|
|
17
29
|
header(c, headerModel(state));
|
|
@@ -27,7 +39,7 @@ export function renderNarrative(c, state) {
|
|
|
27
39
|
if (!board || board.narratives.every((n) => n.tokens === 0)) {
|
|
28
40
|
c.textC(box.x, box.y + 1, box.w, 'collecting narratives — run the app with the recorder on, or open the screener first', { fg: t.muted, bg, i: true });
|
|
29
41
|
c.textC(box.x, box.y + 3, box.w, view?.error ? `(${view.error})` : 'clusters come from token names against a lexicon (owner: maintainers); X/Telegram-only narratives are missed', { fg: t.dim, bg });
|
|
30
|
-
footer(c, [['w', '1h/24h'], ['esc', 'back'], ['q', 'quit']], t.name);
|
|
42
|
+
footer(c, [['w', '1h/24h'], ['n', 'narratives (exp)'], ['esc', 'back'], ['q', 'quit']], t.name);
|
|
31
43
|
return;
|
|
32
44
|
}
|
|
33
45
|
const shown = board.narratives.filter((n) => n.tokens > 0);
|
|
@@ -86,5 +98,145 @@ export function renderNarrative(c, state) {
|
|
|
86
98
|
const ly = box.y + box.h - 1;
|
|
87
99
|
if (wide)
|
|
88
100
|
c.textC(box.x, ly, box.w, `model output: none (deterministic lexicon v${board.meta.lexicon_version}) · ${board.meta.copycat_tokens} copycat symbols · name-based · pace '*' = all tokens · compute ${board.meta.compute_ms}ms`, { fg: t.dim, bg });
|
|
89
|
-
footer(c, [['↑↓', 'move'], ['w', win === '1h' ? '→24h' : '→1h'], ['M', 'popup'], ['esc', 'back'], ['q', 'quit']], t.name);
|
|
101
|
+
footer(c, [['↑↓', 'move'], ['w', win === '1h' ? '→24h' : '→1h'], ['n', 'narratives (exp)'], ['M', 'popup'], ['esc', 'back'], ['q', 'quit']], t.name);
|
|
102
|
+
}
|
|
103
|
+
// ---- task-1818 EXPERIMENTAL heat table + drill-down ------------------------------------------------------------
|
|
104
|
+
const heatUsd = (v) => (v == null ? '—' : '$' + fmtUsd(v));
|
|
105
|
+
const fmtShare = (v) => (v == null ? '—' : (v * 100).toFixed(1) + '%');
|
|
106
|
+
const fmtX = (v) => (v == null || !Number.isFinite(v) ? '—' : v.toFixed(2) + 'x');
|
|
107
|
+
function lcColor(t, lc) {
|
|
108
|
+
// states are measured heat, so none is painted as a market up/down colour (no green "go")
|
|
109
|
+
return lc === 'ignition' ? t.accent : lc === 'peak' ? t.text : lc === 'fading' ? t.warn : lc === 'dead' ? t.dim : t.muted;
|
|
110
|
+
}
|
|
111
|
+
/** The limits line: the full HEAT_NOTE when it fits, else the short form (the note must never be the part that is cut). */
|
|
112
|
+
function limitsLine(w, extra) {
|
|
113
|
+
const full = `${HEAT_NOTE} · ${UNTRUSTED_NOTE} · ${extra}`;
|
|
114
|
+
return full.length <= w ? full : `measured heat, not advice · ${UNTRUSTED_NOTE} · ${extra}`;
|
|
115
|
+
}
|
|
116
|
+
/** The status line under an empty heat table: building (with the one-time stall warning), failed, or nothing yet. */
|
|
117
|
+
function heatPlaceholder(state) {
|
|
118
|
+
const ui = state.narrativesUi;
|
|
119
|
+
const rec = narrativesSource(state) === 'recorded';
|
|
120
|
+
if (ui?.error)
|
|
121
|
+
return [`narratives failed: ${clean(ui.error, 120)}`, 'the theme board (v) still works'];
|
|
122
|
+
if (!currentTracker(state))
|
|
123
|
+
return [rec ? 'building narratives from recorded history…' : 'building narratives from the visible rows…',
|
|
124
|
+
rec ? 'first build reads the last 26 h of the recorder: tens of seconds, the screen stays live (once per run)' : 'no recorder DB here (fixtures / replay): no history, so every narrative starts as a spark'];
|
|
125
|
+
return ['no narrative with 2+ coins yet', 'a narrative needs at least two coins whose names share a theme · measured heat, not advice'];
|
|
126
|
+
}
|
|
127
|
+
function renderHeat(c, state) {
|
|
128
|
+
const t = c.theme;
|
|
129
|
+
const bp = breakpoint(c.w);
|
|
130
|
+
header(c, headerModel(state));
|
|
131
|
+
const ui = state.narrativesUi;
|
|
132
|
+
const rows = heatRows(state);
|
|
133
|
+
const tr = currentTracker(state);
|
|
134
|
+
const src = narrativesSource(state) === 'recorded' ? 'recorded history' : 'visible rows';
|
|
135
|
+
const top = 2, bottom = c.h - 1;
|
|
136
|
+
const title = `NARRATIVES · EXPERIMENTAL · ${tr ? `${rows.length} narratives · ${tr.coins.size} coins` : 'building'} · ${src}${ui.busy && tr ? ' · refreshing' : ''}`;
|
|
137
|
+
const caption = bp === 'compact' ? undefined : 'measured heat, not advice';
|
|
138
|
+
const box = c.box(0, top, c.w, bottom - top, { title, caption, focus: true });
|
|
139
|
+
const bg = box.bg;
|
|
140
|
+
const keys = [['↑↓', 'move'], ['⏎', 'drill'], ['n', 'theme board'], ['esc', 'back'], ['q', 'quit']];
|
|
141
|
+
if (!rows.length) {
|
|
142
|
+
const [a, b] = heatPlaceholder(state);
|
|
143
|
+
c.textC(box.x, box.y + 1, box.w, a, { fg: t.muted, bg, i: true });
|
|
144
|
+
c.textC(box.x, box.y + 3, box.w, b, { fg: t.dim, bg });
|
|
145
|
+
c.textC(box.x, box.y + 5, box.w, HEAT_NOTE, { fg: t.dim, bg });
|
|
146
|
+
footer(c, keys, t.name);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const sel = Math.max(0, Math.min(rows.length - 1, ui.selected));
|
|
150
|
+
// columns: [x, header]; a column that does not fit the box is dropped (the runner first)
|
|
151
|
+
const cols = [[1, '#', 3], [5, 'NARRATIVE', 18], [24, 'THEME', 12], [37, 'STATE', 10], [48, 'COINS', 5], [54, 'NEW/h', 5], [60, 'VOL 1h', 9], [70, 'SHARE', 6], [77, 'TRADERS', 7], [85, 'SCORE', 5], [91, 'TOP RUNNER (x · drawdown)', 28]];
|
|
152
|
+
const fits = (i) => cols[i][0] + Math.min(cols[i][2], 12) <= box.w - 1;
|
|
153
|
+
let cy = box.y;
|
|
154
|
+
cols.forEach(([x, h], i) => { if (fits(i))
|
|
155
|
+
c.text(box.x + x, cy, h, { fg: t.dim, bg }, box.w - x - 1); });
|
|
156
|
+
cy += 1;
|
|
157
|
+
c.hline(box.x, cy, box.w, { fg: t.border, bg });
|
|
158
|
+
cy += 1;
|
|
159
|
+
const scroll = Math.max(0, sel - (box.h - 6));
|
|
160
|
+
for (let i = scroll; i < rows.length; i++) {
|
|
161
|
+
if (cy >= box.y + box.h - 2)
|
|
162
|
+
break;
|
|
163
|
+
const s = rows[i];
|
|
164
|
+
const w1 = s.windows['1h'];
|
|
165
|
+
const selected = i === sel;
|
|
166
|
+
const rb = selected ? t.selBg : bg;
|
|
167
|
+
c.fill(box.x, cy, box.w, 1, ' ', { bg: rb });
|
|
168
|
+
if (selected)
|
|
169
|
+
c.put(box.x, cy, '▎', { fg: t.accent, bg: rb, soft: true });
|
|
170
|
+
const cell = (k, v, st) => { if (fits(k))
|
|
171
|
+
c.text(box.x + cols[k][0], cy, v, { ...st, bg: rb }, Math.min(cols[k][2], box.w - cols[k][0] - 1)); };
|
|
172
|
+
cell(0, s.rank == null ? '·' : String(s.rank), { fg: s.rank == null ? t.dim : t.accent, b: true });
|
|
173
|
+
cell(1, clean(s.label, 18), { fg: selected ? t.borderFocus : t.text, b: s.rank != null });
|
|
174
|
+
cell(2, s.theme ? clean(s.theme, 12) : '—', { fg: t.muted });
|
|
175
|
+
cell(3, s.lifecycle + (s.saturated ? '*' : ''), { fg: lcColor(t, s.lifecycle), b: s.lifecycle === 'ignition' });
|
|
176
|
+
cell(4, String(s.members), { fg: t.text });
|
|
177
|
+
cell(5, w1.newPerHour.toFixed(0), { fg: t.muted });
|
|
178
|
+
cell(6, heatUsd(w1.volume), { fg: t.text });
|
|
179
|
+
cell(7, fmtShare(w1.volumeShare), { fg: t.muted });
|
|
180
|
+
cell(8, String(w1.traders), { fg: t.muted });
|
|
181
|
+
cell(9, String(s.score), { fg: t.accent, b: true });
|
|
182
|
+
const r = s.topRunner;
|
|
183
|
+
cell(10, r ? `${clean(r.symbol, 10)} ${fmtX(r.x)} · ${r.drawdownPct.toFixed(0)}%` : '—', { fg: r ? t.text : t.dim });
|
|
184
|
+
cy += 1;
|
|
185
|
+
}
|
|
186
|
+
c.textC(box.x, box.y + box.h - 1, box.w, limitsLine(box.w, '* = copy-cat saturated'), { fg: t.dim, bg });
|
|
187
|
+
footer(c, keys, t.name);
|
|
188
|
+
}
|
|
189
|
+
function renderDrill(c, state) {
|
|
190
|
+
const t = c.theme;
|
|
191
|
+
header(c, headerModel(state));
|
|
192
|
+
const ui = state.narrativesUi;
|
|
193
|
+
const d = drillDetail(state);
|
|
194
|
+
const top = 2, bottom = c.h - 1;
|
|
195
|
+
const keys = [['↑↓', 'move'], ['⏎', 'open coin'], ['esc', 'table'], ['q', 'quit']];
|
|
196
|
+
if (!d) {
|
|
197
|
+
const box = c.box(0, top, c.w, bottom - top, { title: 'NARRATIVE · EXPERIMENTAL', focus: true });
|
|
198
|
+
c.textC(box.x, box.y + 1, box.w, 'this narrative is no longer tracked (esc returns to the table)', { fg: t.muted, bg: box.bg, i: true });
|
|
199
|
+
footer(c, keys, t.name);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const w1 = d.windows['1h'];
|
|
203
|
+
const title = `NARRATIVE · ${clean(d.label, 24)} · ${d.lifecycle}${d.saturated ? ' · copy-cat saturated' : ''} · ${d.memberList.length} coin${d.memberList.length === 1 ? '' : 's'} · EXPERIMENTAL`;
|
|
204
|
+
const box = c.box(0, top, c.w, bottom - top, { title, caption: 'measured heat, not advice · members in spawn order', focus: true });
|
|
205
|
+
const bg = box.bg;
|
|
206
|
+
let cy = box.y;
|
|
207
|
+
c.text(box.x + 1, cy, `lifecycle rule (${d.lifecycle}): ${d.lifecycleRule}`, { fg: lcColor(t, d.lifecycle), bg }, box.w - 2);
|
|
208
|
+
cy += 1;
|
|
209
|
+
c.text(box.x + 1, cy, `1h: vol ${heatUsd(w1.volume)} · share ${fmtShare(w1.volumeShare)} · new/h ${w1.newPerHour.toFixed(0)} · traders ${w1.traders} · score ${d.score}${d.rank ? ` · rank ${d.rank}` : ''}${d.theme ? ` · theme ${clean(d.theme, 16)}` : ''}`, { fg: t.muted, bg }, box.w - 2);
|
|
210
|
+
cy += 1;
|
|
211
|
+
const cols = [[1, '#', 4], [6, 'SYMBOL', 12], [19, 'NAME', 26], [46, 'X SINCE 1ST', 11], [58, 'LIQ', 9], [68, 'VOL 1h', 9]];
|
|
212
|
+
const fits = (i) => cols[i][0] + Math.min(cols[i][2], 8) <= box.w - 1;
|
|
213
|
+
cols.forEach(([x, h], i) => { if (fits(i))
|
|
214
|
+
c.text(box.x + x, cy, h, { fg: t.dim, bg }); });
|
|
215
|
+
cy += 1;
|
|
216
|
+
c.hline(box.x, cy, box.w, { fg: t.border, bg });
|
|
217
|
+
cy += 1;
|
|
218
|
+
const n = d.memberList.length;
|
|
219
|
+
const sel = Math.max(0, Math.min(n - 1, ui.drill.selected));
|
|
220
|
+
const scroll = Math.max(0, sel - (box.h - 8));
|
|
221
|
+
for (let i = scroll; i < n; i++) {
|
|
222
|
+
if (cy >= box.y + box.h - 1)
|
|
223
|
+
break;
|
|
224
|
+
const m = d.memberList[i];
|
|
225
|
+
const selected = i === sel;
|
|
226
|
+
const rb = selected ? t.selBg : bg;
|
|
227
|
+
c.fill(box.x, cy, box.w, 1, ' ', { bg: rb });
|
|
228
|
+
if (selected)
|
|
229
|
+
c.put(box.x, cy, '▎', { fg: t.accent, bg: rb, soft: true });
|
|
230
|
+
const cell = (k, v, fg) => { if (fits(k))
|
|
231
|
+
c.text(box.x + cols[k][0], cy, v, { fg, bg: rb }, Math.min(cols[k][2], box.w - cols[k][0] - 1)); };
|
|
232
|
+
cell(0, `#${m.spawn}`, t.dim);
|
|
233
|
+
cell(1, clean(m.symbol, 12), selected ? t.borderFocus : t.text);
|
|
234
|
+
cell(2, clean(m.name, 26), t.muted);
|
|
235
|
+
cell(3, fmtX(m.x), m.x == null ? t.dim : m.x >= 1 ? t.up : t.down);
|
|
236
|
+
cell(4, heatUsd(m.liq), t.text);
|
|
237
|
+
cell(5, heatUsd(m.vol1h), t.text);
|
|
238
|
+
cy += 1;
|
|
239
|
+
}
|
|
240
|
+
c.textC(box.x, box.y + box.h - 1, box.w, limitsLine(box.w, 'x = now vs first $10K+ pool price'), { fg: t.dim, bg });
|
|
241
|
+
footer(c, keys, t.name);
|
|
90
242
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// symbol is written (the name and symbol are read only to compute the domain-match boolean).
|
|
12
12
|
import { linkFeatures } from '../../data/src/index.js';
|
|
13
13
|
import { LABELER_VERSION } from '../../recorder/src/index.js';
|
|
14
|
-
import { SKILL_EVAL_SCHEMA } from '../../packs/src/index.js';
|
|
14
|
+
import { SKILL_EVAL_SCHEMA, cutoffIso } from '../../packs/src/index.js';
|
|
15
15
|
function readCache(db, ns, mint) {
|
|
16
16
|
const row = db.prepare('SELECT json, fetched_at FROM cache WHERE ns = ? AND key = ?').get(ns, mint);
|
|
17
17
|
if (!row)
|
|
@@ -47,6 +47,11 @@ export function buildSkillEvalSet(db, opts = {}) {
|
|
|
47
47
|
let picked = null;
|
|
48
48
|
for (const o of outs) {
|
|
49
49
|
const t0 = Number(o.t0);
|
|
50
|
+
// A t0 that is not a valid time can never anchor point-in-time evidence (and would poison the split). task-1982
|
|
51
|
+
if (Number.isNaN(new Date(t0).getTime())) {
|
|
52
|
+
excluded.bad_t0 = (excluded.bad_t0 ?? 0) + 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
50
55
|
const srcs = [['jupiter', jup], ['dexscreener', ds]]
|
|
51
56
|
.filter((x) => !!x[1] && x[1].fetched_at >= t0 && x[1].fetched_at <= t0 + lagMs);
|
|
52
57
|
if (srcs.length) {
|
|
@@ -101,7 +106,7 @@ export function buildSkillEvalSet(db, opts = {}) {
|
|
|
101
106
|
horizon_sec: horizon,
|
|
102
107
|
label_rule: `recorder outcome at ${horizon}s (keyless proxy, labeler ${LABELER_VERSION}); bad = rug ∪ dead, ok = flat ∪ fade ∪ runner; unobserved dropped`,
|
|
103
108
|
evidence_rule: `link features from the cached DexScreener/Jupiter token record only when it was fetched within ${lagMs / 1000}s after the row's t0 (latest such t0 per coin); rug-check features from that t0 snapshot`,
|
|
104
|
-
split_rule: `by time: the latest ${Math.round(testFrac * 100)}% of rows by t0 (t0 ≥ ${
|
|
109
|
+
split_rule: `by time: the latest ${Math.round(testFrac * 100)}% of rows by t0 (t0 ≥ ${cutoffIso(cutoff)}) are the held-out TEST split, fixed before tuning`,
|
|
105
110
|
cutoff_t0: cutoff,
|
|
106
111
|
counts: { rows: rows.length, train: train.length, test: test.length, bad_train: train.filter((r) => r.bad).length, bad_test: test.filter((r) => r.bad).length, excluded },
|
|
107
112
|
rows,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The one place the app's version lives (task-1397). --version, the help banner and `dexterm update` read it;
|
|
2
2
|
// cli.test.ts keeps it equal to packages/cli/package.json (release.yml refuses a tag that differs).
|
|
3
|
-
export const VERSION = '0.2.
|
|
3
|
+
export const VERSION = '0.2.2';
|
|
@@ -3,7 +3,8 @@ export const FIELDS = {
|
|
|
3
3
|
vol5m: (r) => r.vol5m, vol1h: (r) => r.vol1h, vol24: (r) => r.vol24,
|
|
4
4
|
holders: (r) => r.holders, org: (r) => r.org, top10: (r) => r.top10, dev: (r) => r.dev,
|
|
5
5
|
lplocked: (r) => r.lpLocked, bundled: (r) => r.bundled, snipers: (r) => r.snipers,
|
|
6
|
-
cluster: (r) => r.cluster, larp: (r) => r.larp, devrugs: (r) => r.devRugs,
|
|
6
|
+
cluster: (r) => r.cluster, larp: (r) => r.larp, devrugs: (r) => r.devRugs, rugp: (r) => r.rugP,
|
|
7
|
+
narrative_rank: (r) => r.narrativeRank, narrative_score: (r) => r.narrativeScore,
|
|
7
8
|
fresh: (r) => r.fresh, mintable: (r) => r.mintable, freezable: (r) => r.freezable,
|
|
8
9
|
age: (r) => r.ageMin, m5: (r) => r.chg?.m5, h1: (r) => r.chg?.h1, h6: (r) => r.chg?.h6, h24: (r) => r.chg?.h24,
|
|
9
10
|
buys: (r) => r.buys, sells: (r) => r.sells, bonding: (r) => r.bonding,
|
|
@@ -16,6 +17,9 @@ const TAGS = {
|
|
|
16
17
|
flag: (r, v) => (v === 'clean' ? (r.flags ?? []).length === 0 : (r.flags ?? []).map((f) => f.toLowerCase()).includes(v.toLowerCase())),
|
|
17
18
|
verified: (r, v) => (v === 'yes') === !!r.verified,
|
|
18
19
|
sus: (r, v) => (v === 'yes') === !!r.sus,
|
|
20
|
+
// task-1818: `narrative:ignition` (or spark / peak / fading / dead) = the coin's narrative is in that lifecycle state;
|
|
21
|
+
// `narrative:saturated` = copy-cat saturated. An untracked coin matches nothing (so `!narrative:dead` is true for it).
|
|
22
|
+
narrative: (r, v) => { const k = v.toLowerCase(); return k === 'saturated' ? r.narrativeSaturated === true : !!r.narrativeLifecycle && r.narrativeLifecycle === k; },
|
|
19
23
|
};
|
|
20
24
|
export const TAG_NAMES = Object.keys(TAGS);
|
|
21
25
|
export const ACTIONS = ['alert', 'ticket', 'ask', 'popup', 'tag', 'sound', 'watch'];
|
|
@@ -35,6 +35,6 @@ sources:
|
|
|
35
35
|
date: 2026-10-16
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
-
Create a bot with @BotFather, then run `dexterm telegram setup`: paste the token at the hidden prompt (never as an argument), dexterm checks it and keeps it in your keychain (or the system keyring, or a 0600 file), then shows a 6-digit code; send `/pair <code>` to your bot from your own private chat and confirm the account it shows (@username and id) with `y` on your computer. Pairing is refused while a webhook or another program polls the bot. Only that account in that chat is answered. `dexterm autotrade run <id> --paper` then starts the bot by itself (`--no-telegram` keeps it off): entries, exits, PnL cards, halts and a daily summary reach your phone; `/status`, `/positions`, `/pause`, `/resume` (a pause only), `/kill`, `/flatten` and `/use` (each after a confirm button, `/use` only to an equal-or-tighter strategy; a change it cannot rank counts as looser) and `/approvals on` work from chat, which can make things safer and never less safe. With `entry.decide: "agent+approve"` or approvals on, every entry is a card with [Approve] [Half size] [Skip]: only a button approves, once, for that proposal, your account and your private chat; the price and your limits are checked again at the tap; no answer is a skip. Typed messages are never trades. `dexterm telegram status|test|audit --verify|revoke`. Nothing is hosted; paper only; never send keys or seed phrases in chat. Not financial advice. Full docs: https://dexterm.xyz/docs/telegram/
|
|
38
|
+
Create a bot with @BotFather, then run `dexterm telegram setup`: paste the token at the hidden prompt (never as an argument), dexterm checks it and keeps it in your keychain (or the system keyring, or a 0600 file), then shows a 6-digit code; send `/pair <code>` to your bot from your own private chat and confirm the account it shows (@username and id) with `y` on your computer. Pairing is refused while a webhook or another program polls the bot. Only that account in that chat is answered. `dexterm autotrade run <id> --paper` then starts the bot by itself (`--no-telegram` keeps it off): entries, exits, PnL cards, halts and a daily summary reach your phone; `/status`, `/positions`, `/pause`, `/resume` (a pause only), `/kill`, `/flatten` and `/use` (each after a confirm button, `/use` only to an equal-or-tighter strategy; a change it cannot rank counts as looser) and `/approvals on` work from chat, which can make things safer and never less safe. Read-only from chat: `/check <CA>` (one coin's LARP score and risk facts: authorities, top-10 holders, dev share, LP lock, liquidity, each with the age of its data), `/top [N]` (the screener's trending list, 1 to 10, the runner's own coins marked) and `/signals [N]` (the last 1 to 20 signals the runner raised: screen matches, safety refusals, the drain guard, stuck bags, halts, fills); data, not advice, and they cannot reach any write. With `entry.decide: "agent+approve"` or approvals on, every entry is a card with [Approve] [Half size] [Skip]: only a button approves, once, for that proposal, your account and your private chat; the price and your limits are checked again at the tap; no answer is a skip. Typed messages are never trades. `dexterm telegram status|test|audit --verify|revoke`. Nothing is hosted; paper only; never send keys or seed phrases in chat. Not financial advice. Full docs: https://dexterm.xyz/docs/telegram/
|
|
39
39
|
|
|
40
40
|
CLI: `dexterm telegram`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: classification-models
|
|
3
|
+
title: Pick a classification model (Kev, hosted Jev, or the built-in scores)
|
|
4
|
+
kind: app-help
|
|
5
|
+
screen: all
|
|
6
|
+
feature: classifier
|
|
7
|
+
tags:
|
|
8
|
+
- app-help
|
|
9
|
+
- classifier
|
|
10
|
+
- classification
|
|
11
|
+
- kev
|
|
12
|
+
- jev
|
|
13
|
+
- model
|
|
14
|
+
- probability
|
|
15
|
+
- rug
|
|
16
|
+
- drain
|
|
17
|
+
- llama
|
|
18
|
+
- calibrated
|
|
19
|
+
- typesafe
|
|
20
|
+
- experimental
|
|
21
|
+
commands:
|
|
22
|
+
- classifier
|
|
23
|
+
tools:
|
|
24
|
+
- classify_coin
|
|
25
|
+
- classify_batch
|
|
26
|
+
- classifier_status
|
|
27
|
+
confidence: high
|
|
28
|
+
owner: maintainers
|
|
29
|
+
origin: app-help-generated
|
|
30
|
+
reviewed_by: generated@2026-10-16
|
|
31
|
+
generated_from: dexterm/app keymap + overlay + mcp tools + cli (task-1413)
|
|
32
|
+
sources:
|
|
33
|
+
- packages/tui/src/keymap.ts
|
|
34
|
+
- packages/cli/src/main.ts
|
|
35
|
+
- packages/mcp/src/server.ts
|
|
36
|
+
date: 2026-10-16
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
EXPERIMENTAL. A classification model answers typed questions about one coin with a probability per answer: p(drain within 6 h) and RUG / WIN / DUD over 6 h, or a narrative category. `dexterm classifier list` shows the choices: `heuristic` (the built-in H1 rug score and LARP link score; the default, no download), `kev-local` (an open Kev decision model on your own machine through llama.cpp: `dexterm classifier pull kev-4b-q4` shows the size and source and asks first, verifies the sha256, then `dexterm classifier pick kev-local kev-4b-q4` and `dexterm classifier serve`), `jev-hosted` (TypeSafe's hosted Jev with your own API key: `dexterm classifier key set`; per-call cost and a daily call cap), `jev-open` (the open JEV students on a vLLM server), and `claude` / `ollama-llm` zero-shot baselines. `dexterm classifier doctor` reads RAM, CPU and GPU/VRAM and recommends a tier. If the picked model is unreachable the built-in scores answer and say so. Agents use the `classify_coin`, `classify_batch` (triage: flag / pass / escalate) and `classifier_status` MCP tools; a watch rule can use `rugp` (e.g. `when rugp > 0.8 then alert(warn, "...")`). How each model scored on dexterm's own labelled history is on the docs page. Every answer is a model estimate with the model id, never advice. Docs: https://dexterm.xyz/docs/classifier/
|
|
40
|
+
|
|
41
|
+
CLI: `dexterm classifier`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: narratives-tracker
|
|
3
|
+
title: Track memes and narratives (dexterm narratives, experimental)
|
|
4
|
+
kind: app-help
|
|
5
|
+
screen: narrative
|
|
6
|
+
feature: narratives
|
|
7
|
+
tags:
|
|
8
|
+
- app-help
|
|
9
|
+
- narratives
|
|
10
|
+
- narrative
|
|
11
|
+
- memes
|
|
12
|
+
- meme
|
|
13
|
+
- copycat
|
|
14
|
+
- lifecycle
|
|
15
|
+
- ignition
|
|
16
|
+
- heat
|
|
17
|
+
- saturated
|
|
18
|
+
- experimental
|
|
19
|
+
keys:
|
|
20
|
+
- n
|
|
21
|
+
- enter
|
|
22
|
+
actions:
|
|
23
|
+
- narrative.view
|
|
24
|
+
- narrative.drill
|
|
25
|
+
commands:
|
|
26
|
+
- narratives
|
|
27
|
+
tools:
|
|
28
|
+
- narratives_top
|
|
29
|
+
- narrative_detail
|
|
30
|
+
- narrative_of
|
|
31
|
+
confidence: high
|
|
32
|
+
owner: maintainers
|
|
33
|
+
origin: app-help-generated
|
|
34
|
+
reviewed_by: generated@2026-10-16
|
|
35
|
+
generated_from: dexterm/app keymap + overlay + mcp tools + cli (task-1413)
|
|
36
|
+
sources:
|
|
37
|
+
- packages/tui/src/keymap.ts
|
|
38
|
+
- packages/cli/src/main.ts
|
|
39
|
+
- packages/mcp/src/server.ts
|
|
40
|
+
date: 2026-10-16
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
EXPERIMENTAL. The narratives tracker groups coins by the theme their name and ticker carry (ticker puns, animals, celebrities, AI agents, seasonal events, copy-cats of a runner) and measures each theme per window: coins, new-coin rate, volume share, unique-trader growth, top runner and drawdown, a lifecycle state (spark / ignition / peak / fading / dead, with the rule that set it) and a copy-cat saturation flag. On the N screen, n flips the board into the NARRATIVES table (the first switch reads the last 26 h of recorded history and takes tens of seconds), enter to see a narrative's coins in spawn order, esc to go back. `dexterm narratives [top|detail ID|of MINT|export]` prints the same from the recorder; the analyst uses narratives_top, narrative_detail and narrative_of. Rules can use narrative_rank, narrative_score and the tags narrative:ignition / narrative:saturated. Keyless and local; measured heat, not advice.
|
|
44
|
+
|
|
45
|
+
Keys: `n` (narrative), `⏎` (narrative). CLI: `dexterm narratives`.
|