dexterm-cli 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/fleet.js +2 -2
- package/dist/packages/autotrade/src/index.js +2 -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 +132 -17
- package/dist/packages/autotrade/src/service-install.js +6 -4
- package/dist/packages/autotrade/src/sessions.js +16 -7
- package/dist/packages/autotrade/src/strategy-stats.js +344 -0
- package/dist/packages/autotrade/src/strategy.js +12 -1
- 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 +220 -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 +18 -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/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 +12 -0
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +9 -2
- package/dist/packages/cli/src/autotrade-cmd/run.js +11 -1
- package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
- package/dist/packages/cli/src/classifier-cmd.js +353 -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/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/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-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 +34 -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-what-it-is.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 +104 -0
- 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/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,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: docs-overlay
|
|
3
|
+
title: Streamer overlay (OBS)
|
|
4
|
+
kind: docs
|
|
5
|
+
page: overlay
|
|
6
|
+
page_title: Streamer overlay (OBS)
|
|
7
|
+
url: https://dexterm.xyz/docs/overlay
|
|
8
|
+
tags:
|
|
9
|
+
- docs
|
|
10
|
+
- overlay
|
|
11
|
+
- streamer
|
|
12
|
+
- obs
|
|
13
|
+
confidence: high
|
|
14
|
+
owner: maintainers
|
|
15
|
+
origin: docs-generated
|
|
16
|
+
reviewed_by: generated@2026-10-16
|
|
17
|
+
generated_from: dexterm/site content/docs (task-1420)
|
|
18
|
+
sources:
|
|
19
|
+
- site/content/docs/overlay.md
|
|
20
|
+
date: 2026-10-16
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
Put an agent session on your stream. dexterm overlay serves three transparent scenes for an OBS Browser source (agent activity, the coin card, the PAPER or LIVE PnL card) on 127.0.0.1, fed by the read-only spectator stream or a saved replay. Covers the OBS setup step by step, the three themes, what is redacted, the security model, the SAMPLE DATA tag and the limits.
|
|
24
|
+
|
|
25
|
+
`dexterm overlay` turns the spectator view (https://dexterm.xyz/docs/spectator) into scenes you can drop into OBS as a **Browser source**: the agent's recent calls, the coin it is on, and its PnL card, over a transparent background. It watches; it never acts. Nothing on an overlay page can pause, trade, arm, size, sign or raise a cap, and the page sends nothing back to dexterm.
|
|
@@ -26,6 +26,6 @@ The PAPER ticket, the simulated wallet, the confirm gate, order plans on paper,
|
|
|
26
26
|
|
|
27
27
|
Trading in dexterm is **PAPER by default**: a simulated wallet priced from live, keyless Jupiter quotes. Nothing is signed, no key is held, no transaction is sent, and the paper code imports no wallet or signing library of any kind. Every ticket and positions frame says so in a banner that is never hidden.
|
|
28
28
|
|
|
29
|
-
dexterm has no live trading path of its own: it holds no key, has no trading wallet and builds or sends no transaction. Live trading happens in a Telegram trading bot of your choice; see Live trading (https://dexterm.xyz/docs/live-trading).
|
|
29
|
+
dexterm has no live trading path of its own: it holds no key, has no trading wallet and builds or sends no transaction. Live trading happens in a Telegram trading bot of your choice; see Live trading (https://dexterm.xyz/docs/live-trading). Real mode is experimental; signals to your own executor; dexterm never holds a key; not financial advice (see Real mode (https://dexterm.xyz/docs/real-mode)).
|
|
30
30
|
|
|
31
31
|
PAPER positions on sample data
|
|
@@ -28,6 +28,8 @@ date: 2026-10-16
|
|
|
28
28
|
|
|
29
29
|
> **0.2.0, experimental.** Real mode can lead to real trades in a wallet you fund. dexterm never holds a key, never signs and never sends a transaction: your executor does, by its own rules. Not financial advice.
|
|
30
30
|
|
|
31
|
+
arming real mode, stopped at the typed phrase: nothing is armed in this frame
|
|
32
|
+
|
|
31
33
|
Every strategy runs on **paper** by default. In **real** mode the same strategy runs the same way: the same entry rules, the same clamp on every size, the same exits, the same monitor and the same run file as paper. The difference: every buy and sell it fills on its own model of the market also goes out, first, as a **LIVE TRADE FIRE** signal (`dexterm-trade-fire/1`) to the executor you chose.
|
|
32
34
|
|
|
33
35
|
- **Your executor** is a Telegram trading bot you trust, or any script of yours. It holds its own wallet and decides whether to act on a signal.
|
|
@@ -25,7 +25,7 @@ date: 2026-10-16
|
|
|
25
25
|
|
|
26
26
|
`M` on a token opens the holder graph. Each of the top token accounts (merged by owner) is a disc whose **area is its share of supply**, drawn on a half-row pixel grid so it is round in the terminal without any image protocol. **Colour is the rule that labelled the wallet, never a judgement:** yellow dev / dev-linked, magenta bundle, green linked by a shared funder (orange for a direct transfer), red sniper, cyan pool / LP / program-owned vault, grey exchange or multi-token whale, dim unlinked. Linked wallets sit inside a dotted ring with a tag like `B·C1 6w 12.0%`; `j`/`k` select a holder (phosphor, with dotted edges to its cluster mates), `m` flips to a treemap (share, not linkage), `z` zooms the selected cluster, `D` forces a deep scan, `r` rescans, `M` opens the popup window (https://dexterm.xyz/docs/popup-chart#bubble-map-popup).
|
|
27
27
|
|
|
28
|
-
The rules (from the
|
|
28
|
+
The rules (from the wallet-intel spike, replay-tested against its recordings): **P1 pool** (the selling counterparty in ≥ 25 % of launch transactions), **D1 creator** (fee payer of the first transaction referencing the mint), **R1 funder** (two holders share a funding source; a shared exchange funder is a veto, not a link), **R1b** (a holder funded another holder), **R2 bundle** (same-slot buys **and** a shared funder; same-slot alone is a cohort), **R3 transfer** (a direct token move between two holders, pools excluded), **R4 dev-linked** (funded by the creator), **R5 same-slot-created** (weak, shown, never links), **S1 sniper** (bought in the first 3 slots after the first pool trade), **F1 fresh-at-buy** (the wallet's first transactions already move this token), **X1 multi-token-top20**, and **L0** static lists. The **WHY THIS LABEL** panel leads with the holder's class (POOL, CURVE, VAULT, LOCK, CEX, BURN, DEV, LINKED or UNLABELLED) and the on-chain reason, then prints each rule with its confidence (`on-chain`, `program-authority`, `derived`, `seed`, `weak`) and up to three evidence lines; exchange labels come from a typed seed list and always read **unverified**. Cabal-class labels (cabal-candidate, smart, bot, follower, call-group) are hidden until their precision is measured.
|
|
29
29
|
|
|
30
30
|
**HOLDER INSIGHTS** print their sample every time (`sample top 20 accts of 142.5K holders · 55% supply · funders 7/20 · launch 300 txs`): top-10 raw (by token account, the explorer number), **ex-pool** (POOL / CURVE / VAULT / LOCK / BURN owners dropped; the cluster-adjusted figure, with linked owners merged and exchanges dropped too, rides underneath), a red **main risk** line when a counted owner holds more than 30 %, snipers, bundled supply, dev share (an upper bound when the creator is outside the sample) and fresh wallets. The token screen's Snipers and Bundled supply rows, the new-pairs SNIPE / BUNDLE columns and the `I` token popup read the same scan.
|
|
31
31
|
|
|
@@ -29,4 +29,5 @@ date: 2026-10-16
|
|
|
29
29
|
- **Keep secrets off screen.** Don't show `~/.dexterm/spectator.json`, the `DEXTERM_AGENT_TOKEN` line or `dexterm agent add` output (it prints the agent's token once). `agent serve` prints the token's file path, never the token.
|
|
30
30
|
- **Use fixtures for a show.** `agent serve` without `--live` uses canned data, so no real coin names or market numbers appear. With `--live`, a capture shows whatever real coins the agent looks at.
|
|
31
31
|
- **Record and replay.** Add `--record FILE` so you can share the session afterwards. The file is redacted, so you can attach it to a bug report.
|
|
32
|
+
- **Example.** The launch demo clip was made this way: Claude Code, connected with `dexterm mcp install claude-code` against the recorded fixture feed, screened, opened a coin and ran `larp_check`; the watch window showed each call and the user pressed **pause**. Its caption lines are scripted and tagged `✦ claude lines: demo text`, as all marketing footage of Claude lines is.
|
|
32
33
|
- The window and the stream show model and agent activity, not advice. Whatever an agent concludes is its own reading of tool results.
|
|
@@ -30,4 +30,4 @@ It does three things, each one a tier you grant when you pair:
|
|
|
30
30
|
- **Control** (https://dexterm.xyz/docs/telegram#control-the-commands): `/status`, `/positions`, `/pause`, `/resume`, `/kill`, `/flatten`, `/strategies`, `/use` and `/approvals on`. Chat can always make things **safer** and never less safe.
|
|
31
31
|
- **Approve** (https://dexterm.xyz/docs/telegram#approve-one-tap-entries): every new entry waits for your tap, with [Approve] [Half size] [Skip] buttons. Only a button approves; a typed message never does.
|
|
32
32
|
|
|
33
|
-
It runs on **paper**: an approved entry fills on the paper book, with no key, wallet or signer anywhere. This is also dexterm's part in live trading (https://dexterm.xyz/docs/live-trading): dexterm holds no key, and live trades happen in a separate Telegram trading bot of your choice, which holds its own wallet. dexterm sends the alerts and run control to your chat; in this version it does not place orders in that trading bot for you. In real mode (https://dexterm.xyz/docs/real-mode) (
|
|
33
|
+
It runs on **paper**: an approved entry fills on the paper book, with no key, wallet or signer anywhere. This is also dexterm's part in live trading (https://dexterm.xyz/docs/live-trading): dexterm holds no key, and live trades happen in a separate Telegram trading bot of your choice, which holds its own wallet. dexterm sends the alerts and run control to your chat; in this version it does not place orders in that trading bot for you. In real mode (https://dexterm.xyz/docs/real-mode) (0.2.0, experimental) your chat also gets a LIVE TRADE FIRE copy of each signal, and the channels your executor reads are set up in the same file as this bot (`dexterm autotrade signal channels`; `dexterm telegram status` shows them). Experimental, not financial advice.
|
|
@@ -24,9 +24,9 @@ sources:
|
|
|
24
24
|
date: 2026-10-16
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
**Linux is beta (since 0.1.2); Windows
|
|
27
|
+
**Linux is beta (since 0.1.2); Windows is not supported.** macOS is the platform voice mode was built and measured on. Off macOS the engines below exist on main, but no person has yet verified real speaker or microphone I/O on Linux hardware. Treat Linux voice as beta. Every voice action has a keyboard equivalent.
|
|
28
28
|
|
|
29
29
|
- **Speech out (Linux).** The neural Piper engine is the default when its prebuilt binary and the `en_GB-alan-medium` voice are installed (`dexterm voice download piper-voice` fetches the voice from Hugging Face, asks first and checks a pinned checksum). `espeak-ng` is the fallback. Both play their WAV through `aplay`, `paplay` or `ffplay`. Synthesis to WAV is verified in Docker on Linux, but real speaker output is not covered by the test suite. `dexterm doctor` names what is missing. Don't use `apt install piper`, which installs an unrelated tool.
|
|
30
30
|
- **Speech in (Linux).** Apple Speech is macOS-only. The local whisper.cpp engine (`dexterm voice download whisper` fetches its `base.en` model) records with `sox`, `arecord` (alsa-utils) or `ffmpeg -f alsa`. Real microphone capture on Linux is not yet verified on hardware.
|
|
31
|
-
- **Windows.**
|
|
31
|
+
- **Windows.** dexterm does not support Windows, and voice is not built for it: microphone capture has no Windows input (the `ffmpeg` capture uses a macOS or Linux device).
|
|
32
32
|
- **Unchanged everywhere.** The router, the clarify-never-guess rules and the hard limits above are platform-independent. Another platform adds engines, not powers: voice can never confirm, sign or raise a cap.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: narratives-tracker
|
|
3
|
+
title: The memes + narratives tracker (what its numbers mean, how to relay them)
|
|
4
|
+
tags:
|
|
5
|
+
- narrative
|
|
6
|
+
- meme
|
|
7
|
+
- lifecycle
|
|
8
|
+
- copycat
|
|
9
|
+
- heat
|
|
10
|
+
- trending
|
|
11
|
+
date: 2026-10-28
|
|
12
|
+
confidence: medium
|
|
13
|
+
owner: maintainers
|
|
14
|
+
origin: primary-doc
|
|
15
|
+
reviewed_by: none
|
|
16
|
+
status: experimental
|
|
17
|
+
sources:
|
|
18
|
+
- dexterm app packages/narratives (task-1818): normalize.ts, cluster.ts, tracker.ts (the rules below are the code's)
|
|
19
|
+
- dexterm research/narratives-2026-10/REPORT.md (replay study on the recorded history, TRAIN / VALID / HELD-OUT)
|
|
20
|
+
---
|
|
21
|
+
**What it is.** `narratives_top`, `narrative_detail` and `narrative_of` read dexterm's own recorded history (last 26 h)
|
|
22
|
+
and group coins by the theme their name and ticker carry: a ticker pun, an animal, a celebrity or politician, an
|
|
23
|
+
AI-agent craze, a season or event, or a copy of a coin that just ran. A coin needs no real project to belong. The
|
|
24
|
+
grouping is a local model (default: a built-in hashed word / letter-trigram / lexicon-theme vector; optionally a small
|
|
25
|
+
open embedding model on CPU), clustered in first-seen order with stable ids. No network, no model call, no key.
|
|
26
|
+
|
|
27
|
+
**What the numbers mean (all measured, per window 5m / 1h / 6h / 24h):** `coins` seen, `newCoins` and `newPerHour`,
|
|
28
|
+
`volume` (each source's rolling window at the coin's newest sample) and `volumeShare` of all tracked coins,
|
|
29
|
+
`traders` (unique traders 1 h) and `tradersGrowth`, `topRunner` (pool of at least 10k USD; `x` since first sighting,
|
|
30
|
+
`drawdownPct` from its observed peak), `repeatedName1h` (relaunches of the same meme word), `score` 0-100 (traction:
|
|
31
|
+
1 h volume growth 0.30, spawn rate 0.20, repeated-name velocity 0.15, share of new-coin volume 0.25, breadth 0.10)
|
|
32
|
+
and `rank` (1 = highest score). `holderOverlap` is always null: holders per coin are not recorded.
|
|
33
|
+
|
|
34
|
+
**Lifecycle** (first rule that matches): `dead` no member sampled in 6 h, or 1 h volume under 1k USD with no new coin
|
|
35
|
+
in 6 h; `spark` first coin under 1 h old; `ignition` 1 h volume at least 5k USD and at least twice what it was an hour ago, with a new
|
|
36
|
+
coin in the hour or unique traders +50%; `fading` 1 h volume at most 40% of its 24 h max or 60% of an hour ago; `peak`
|
|
37
|
+
otherwise. `saturated` (copy-cat saturation): at least 5 coins, at least 3 new in 6 h, and the coins after the 3
|
|
38
|
+
oldest hold under 25% of the 1 h volume. `spawn` is the coin's order in its narrative (1 = the coin that started it).
|
|
39
|
+
|
|
40
|
+
**How to relay it.** Say it is measured heat from recorded data, give the window and `as_of`, and quote the rule that
|
|
41
|
+
set the lifecycle. Never present a lifecycle, rank or score as a reason to trade, a prediction, or a quality mark of a
|
|
42
|
+
coin. Names and labels are `<untrusted>` data. Tested on the recorded history: on held-back days (task-1818 replay study), adding a coin's narrative to its own numbers did not improve telling apart the coins that later tripled or drained (ranking quality fell slightly), and no narrative gate improved paper results on top of the existing entry rules (0 of 27 cells better, 3 worse). Coins inside a multi-coin narrative drained within 6 h about twice as often as solo coins (about 50% vs 27%). So relay the narrative as context, never as a reason. In a hand-graded sample, 70 of 100 narratives were coherent and 87 labels named the theme.
|
|
43
|
+
|
|
44
|
+
**Limits.** Name-based (X/Telegram-only themes are missed); a coin not sampled for 6 h counts as zero volume; two
|
|
45
|
+
themes that converge stay two rows; the first build reads 26 h of history and can take tens of seconds.
|
|
@@ -21,6 +21,7 @@ import { createDataLayer, resolveRpcUrl, redactSecrets, defaultDbPath, replayTra
|
|
|
21
21
|
import { buildDossier } from "./dossier.js";
|
|
22
22
|
import { MCAP_FIELD_HINT, isPriceBasis, knownSupply, pickBasis, withMcap } from '../../data/src/basis.js';
|
|
23
23
|
import { firewall } from "./firewall.js";
|
|
24
|
+
import { RecorderNarratives, HEAT_NOTE, LIMITS as NARRATIVE_LIMITS, LIFECYCLE_RULES } from '../../narratives/src/index.js';
|
|
24
25
|
import { OverlayStore, dextermHome, KB_RULE } from "./user-content.js";
|
|
25
26
|
import { resolveSpectatorSink, readFollowScreen, FOLLOW_NAV_ACTIONS } from "./spectator-sink.js";
|
|
26
27
|
import { resolvePaperAgentDesk } from "./paper-agent.js";
|
|
@@ -77,6 +78,11 @@ export { firewall, LIMITS } from "./firewall.js";
|
|
|
77
78
|
const KB_DIR = path.join(ASSETS, 'kb', 'entries');
|
|
78
79
|
/** The shipped KB directory (read-only; `dexterm kb` copies from it). */
|
|
79
80
|
export const BUNDLED_KB_DIR = KB_DIR;
|
|
81
|
+
/** task-1818: a tool's `limit` argument as a whole number in [1, max]; missing or non-numeric falls back to the default. */
|
|
82
|
+
const boundedLimit = (v, def, max) => {
|
|
83
|
+
const n = Math.floor(Number(v ?? def));
|
|
84
|
+
return Number.isFinite(n) ? Math.max(1, Math.min(n, max)) : def;
|
|
85
|
+
};
|
|
80
86
|
const logSkip = (kind) => (s) => { process.stderr.write(`dexterm ${kind}: skipped ${s.file}: ${s.reason}\n`); };
|
|
81
87
|
const KB_STORE = new OverlayStore({ kind: 'kb', bundled: [{ dir: KB_DIR, layout: 'flat' }], home: () => dextermHome(process.env), onSkip: logSkip('kb') });
|
|
82
88
|
const kbAll = () => KB_STORE.get().entries;
|
|
@@ -240,6 +246,7 @@ export function resourceRead(uri) {
|
|
|
240
246
|
}
|
|
241
247
|
// ---------------------------------------------------------------------------
|
|
242
248
|
// Tool schemas (advertised to the model). Mirrors the task-1293 surface.
|
|
249
|
+
const CLASSIFIER_NOTE = 'Model estimate from a classification model (see model, calibration). Not advice and not a trade signal; on memecoins its accuracy was measured only on dexterm\'s own labelled history (research/classifier-2026-10).';
|
|
243
250
|
const TOOLS_RAW = [
|
|
244
251
|
{ name: 'screen_state', description: 'What is on screen in dexterm right now: screen name, filter, sort, selected token, visible mints. Call first when the user says "this token".', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
245
252
|
{ name: 'screener_query', description: 'Screener rows (Token + Pair + Stats + Audit summary) with provenance. preset: "new" (youngest), "trending" (volume), "all".', inputSchema: { type: 'object', properties: { preset: { type: 'string', enum: ['new', 'trending', 'all'] }, limit: { type: 'integer', minimum: 1, maximum: 50 } }, additionalProperties: false } },
|
|
@@ -253,6 +260,10 @@ const TOOLS_RAW = [
|
|
|
253
260
|
{ name: 'get_dev_history', description: 'Creator address and prior-launch forensics for this mint. Prior launches and rugs come from dexterm\'s own recorded history when it has the creator (tier "local-history", see dev_history); bundles and snipers need a Helius/Birdeye key and are null on the keyless tier; the response says so.', inputSchema: { type: 'object', properties: { mint: { type: 'string' } }, required: ['mint'], additionalProperties: false } },
|
|
254
261
|
{ name: 'dev_history', description: 'DEV HISTORY ("has this deployer rugged before?"): the creator wallet\'s track record from dexterm\'s OWN recorded history, keyless. Prior launches the recorder linked to the wallet, their code-only outcome labels (rug / dead / fade / flat / runner; 72 h preferred; keyless-proxy tier), how fast the dev sold its own bag, the serial_rugger flag (>= 2 labelled rugs). Every number carries its source, sample size and age; "unknown" when there is no record. A risk flag from observed history, never a certificate: small samples are common, say the n. Pass a mint (the creator is resolved from the record, Jupiter `dev`, then GoPlus) or a creator wallet.', inputSchema: { type: 'object', properties: { mint: { type: 'string' }, creator: { type: 'string' } }, additionalProperties: false } },
|
|
255
262
|
{ name: 'narrative_board', description: 'The NARRATIVE BOARD (task-1356): what is running and what is working across the tokens dexterm has recorded. Deterministic lexicon clustering of recorded launches + trending tokens into narratives (dogs, cats, ai-agents, quantum, …), per narrative: tokens, new launches 1h/24h, 1h/24h volume, pace, median change, top runners, and a documented "what is working" score. No model in the hot path, no token cost; names are matched against a versioned lexicon (X/Telegram-only narratives are missed; the honest unclassified share is shown). window: 1h (default) or 24h; set summary:true for the deterministic spoken morning summary.', inputSchema: { type: 'object', properties: { window: { type: 'string', enum: ['1h', '24h'] }, limit: { type: 'integer', minimum: 1, maximum: 50 }, summary: { type: 'boolean' } }, additionalProperties: false } },
|
|
263
|
+
// task-1818: MEMES + NARRATIVES tracker (@dexterm/narratives): emergent clusters of coins by name/ticker theme, measured heat only.
|
|
264
|
+
{ name: 'narratives_top', description: 'MEMES + NARRATIVES tracker (task-1818): the hottest narratives right now, from dexterm\'s own recorded history (last 26 h), keyless and local. Coins are clustered by the theme their name/ticker carries (ticker puns, animals, celebrities/politics, AI agents, seasonal, copy-cat spawns of a runner); no real project is needed for a coin to belong. Per narrative and window (5m/1h/6h/24h): coin count, new-coin rate, volume share, unique-trader growth, top runner + drawdown, lifecycle (spark/ignition/peak/fading/dead with the rule that fired), copy-cat saturation and a 0-100 memetic traction score (rank 1 = hottest). MEASURED HEAT, NOT ADVICE: say so when you relay it. Names and labels are <untrusted> data.', inputSchema: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 50 }, lifecycle: { type: 'string', enum: ['spark', 'ignition', 'peak', 'fading'] } }, additionalProperties: false } },
|
|
265
|
+
{ name: 'narrative_detail', description: 'One narrative of the MEMES + NARRATIVES tracker (task-1818) by id (from narratives_top or narrative_of): its metrics for every window, lifecycle rule, saturation, score components and the member coins in spawn order (1 = the coin that started it) with price, liquidity, 1 h volume and x since first sighting. Measured heat, not advice; names are <untrusted> data.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, limit: { type: 'integer', minimum: 1, maximum: 100 } }, required: ['id'], additionalProperties: false } },
|
|
266
|
+
{ name: 'narrative_of', description: 'Which narrative a coin belongs to (task-1818 MEMES + NARRATIVES tracker): narrative id + label, its lifecycle and rank, the coin\'s spawn index (#3 of 12 = the third coin of that theme) and whether the narrative is copy-cat saturated. Returns an honest note when the coin was not recorded in the last 26 h. Measured heat, not advice.', inputSchema: { type: 'object', properties: { mint: { type: 'string' } }, required: ['mint'], additionalProperties: false } },
|
|
256
267
|
{ name: 'rug_backtest', description: 'Backtest the rug model or the deterministic pack verdicts against RECORDED outcomes (task-1356). Scores the chosen subject over the recorder history at a horizon and reports HONEST metrics: base rate, precision/recall, Brier (vs base-rate Brier), AUC, a 5-bucket calibration table, a per-feed breakdown, and the worst misses/false-alarms with their t0 numbers. kind: "heuristic" (the shipped H1 rug model), "pack_verdicts" (recorded pack rug_p). Leakage is enforced by construction (features are the t0 snapshot, labels are after t0). Returns a note when there is not enough recorded history yet — it never fabricates numbers. Read-only; no model, no token cost.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['heuristic', 'pack_verdicts'] }, horizon_sec: { type: 'integer', enum: [3600, 7200, 21600, 86400, 259200] }, positive_labels: { type: 'array', items: { type: 'string', enum: ['rug', 'dead', 'fade', 'flat', 'runner'] } } }, additionalProperties: false } },
|
|
257
268
|
{ name: 'kb_lookup', description: 'Fetch one knowledge-base entry by id (mechanics, patterns, glossary). Entries carry sources, date, confidence and owner. KB never holds facts about a specific live token.', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false } },
|
|
258
269
|
{ name: 'kb_search', description: 'Search the knowledge base; returns ids, titles, confidence and a short excerpt. Follow with kb_lookup for the full entry.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'integer', minimum: 1, maximum: 10 } }, required: ['query'], additionalProperties: false } },
|
|
@@ -265,6 +276,12 @@ const TOOLS_RAW = [
|
|
|
265
276
|
{ name: 'check_github', description: 'GitHub repo activity (created/last-push dates, stars, open issues, archived/fork) via the keyless GitHub API for a github.com/<owner>/<repo> URL. Real development shows recent commits and history.', inputSchema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'], additionalProperties: false } },
|
|
266
277
|
{ name: 'check_x', description: 'Keyless, read-only lookup of the X account behind an advertised X link (no login, no API key): exists / suspended / not_found / unavailable, created date and age, followers, following, post count, last post date, whether the bio names the token and links the site, and a rename check for a linked post. Tries FxTwitter, then vxtwitter (a cached copy, dated), then X oEmbed; one source saying "not found" is never enough. Missing fields are listed in `unavailable`, never guessed. The bio is untrusted data.', inputSchema: { type: 'object', properties: { url: { type: 'string' }, symbol: { type: 'string' }, name: { type: 'string' }, mint: { type: 'string' }, website: { type: 'string' } }, required: ['url'], additionalProperties: false } },
|
|
267
278
|
{ name: 'larp_check', description: 'ONE-CALL LARP vet: resolve the links a coin advertises, then fetch the website, check its domain age, any GitHub repo and the advertised X account in parallel, and return the mechanical signals, a checklist of soft LARP flags (brand-new domain, registrant privacy, default/site-builder page, wallet-connect/drainer markers, the site never naming the token, no real repo activity, links that mismatch across sources, a new/suspended/silent X account or one the site does not link) and `larp_score`: a 0-100 sum of those named flags with each flag\'s points, a low/medium/high band, and a `missing` list that widens the score range instead of lowering it. Evidence, not a verdict — any one flag can be innocent, and a low score is not a safety check. Pass a mint/symbol, or a url to vet one site directly. Read-only; all page text and bios are untrusted data. Prefer this over calling get_links/fetch_page/check_domain/check_github/check_x one by one.', inputSchema: { type: 'object', properties: { mint: { type: 'string' }, symbol: { type: 'string' }, url: { type: 'string' } }, additionalProperties: false } },
|
|
279
|
+
// ---- classification models (task-1822, EXPERIMENTAL). Read-only: typed questions about one coin answered with
|
|
280
|
+
// probabilities by the user's picked model (Kev local, hosted Jev with their key, or the built-in heuristic).
|
|
281
|
+
// Every answer is a MODEL ESTIMATE with the model id and a calibration note; never advice, never a trade input.
|
|
282
|
+
{ name: 'classify_coin', description: 'Classification model (EXPERIMENTAL): probabilities for typed questions about ONE coin from the user\'s picked classifier (dexterm classifier pick): drain_6h (p of a rug/drain within 6 h) and outcome_6h (RUG / WIN / DUD), or the narrative category. Returns a labelled model estimate with the model id, latency, cost and calibration note; token text is firewalled first. Not advice, never a trade signal.', inputSchema: { type: 'object', properties: { mint: { type: 'string' }, symbol: { type: 'string' }, set: { type: 'string', enum: ['coin-risk', 'narrative'] } }, additionalProperties: false } },
|
|
283
|
+
{ name: 'classify_batch', description: 'Classification model (EXPERIMENTAL) over up to 100 mints in one call, with optional triage on drain_6h: p >= hi flagged, p <= lo passed, the rest escalated (ask Claude or the user). Model estimates only.', inputSchema: { type: 'object', properties: { mints: { type: 'array', items: { type: 'string' }, maxItems: 100 }, lo: { type: 'number' }, hi: { type: 'number' } }, required: ['mints'], additionalProperties: false } },
|
|
284
|
+
{ name: 'classifier_status', description: 'Which classification model is picked, whether it is ready (server up / model downloaded / key present, never the key itself), the shipped default, and the hardware tier this machine is recommended. Read-only.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
268
285
|
// ---- UI-action tools (task-1313). Non-financial app navigation only. Each is validated against the
|
|
269
286
|
// tokens currently on screen (screen_state.actions) and applied by the running app, which shows
|
|
270
287
|
// "analyst did X". None can size, confirm, sign, or send a trade.
|
|
@@ -439,6 +456,7 @@ export class McpTools {
|
|
|
439
456
|
/** The recorder/cache SQLite file path, so read-only history tools (narrative_board) can open it. */
|
|
440
457
|
dbPath;
|
|
441
458
|
narrativeDb = null;
|
|
459
|
+
narratives = null;
|
|
442
460
|
actionSeq = 0;
|
|
443
461
|
/** Mints this server returned from screener reads (bounded; task-1443 follow-mode nav targets). */
|
|
444
462
|
surfaced = new Set();
|
|
@@ -649,6 +667,17 @@ export class McpTools {
|
|
|
649
667
|
};
|
|
650
668
|
}
|
|
651
669
|
/** Resolve a mint or symbol to one enriched row, or return candidates/error. */
|
|
670
|
+
/** task-1822: the user's picked classifier (classifier.json). Claude is not spawned from here: a `claude` pick falls back. */
|
|
671
|
+
async classifier() {
|
|
672
|
+
const { loadClassifierConfig, providerFromConfig, heuristicProvider } = await import('../../classifier/src/index.js');
|
|
673
|
+
const config = loadClassifierConfig(process.env).config;
|
|
674
|
+
try {
|
|
675
|
+
return { provider: providerFromConfig(config, config.provider, { env: process.env }), config };
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return { provider: heuristicProvider(), config };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
652
681
|
async resolve(ref) {
|
|
653
682
|
if (!ref || typeof ref !== 'string')
|
|
654
683
|
return { error: 'mint or symbol required' };
|
|
@@ -865,6 +894,50 @@ export class McpTools {
|
|
|
865
894
|
return { ...trimmed, summary: summaryText(board) };
|
|
866
895
|
return trimmed;
|
|
867
896
|
}
|
|
897
|
+
case 'narratives_top':
|
|
898
|
+
case 'narrative_detail':
|
|
899
|
+
case 'narrative_of': {
|
|
900
|
+
const db = this.historyDb();
|
|
901
|
+
if (!db)
|
|
902
|
+
return { pack: 'narratives', model_output: false, narratives: [], note: 'No recorded history available (the recorder is off, this is a replay/standalone server, or nothing has been recorded yet). The narratives tracker reads the recorder DB.' };
|
|
903
|
+
this.narratives ??= new RecorderNarratives(db);
|
|
904
|
+
// the first call replays 26 h of history (tens of seconds): wait at most NARRATIVES_FIRST_WAIT_MS, then answer
|
|
905
|
+
// honestly that it is still building (the build carries on; an MCP client's tool timeout never fires on it)
|
|
906
|
+
const cold = !this.narratives.tracker;
|
|
907
|
+
const building = this.narratives.refresh();
|
|
908
|
+
const waitMs = Number(process.env.DEXTERM_NARRATIVES_WAIT_MS ?? 20_000);
|
|
909
|
+
const tr = cold ? await Promise.race([building, new Promise((r) => { const tm = setTimeout(() => r(null), waitMs); tm.unref?.(); })]) : await building;
|
|
910
|
+
building.catch(() => { });
|
|
911
|
+
if (!tr)
|
|
912
|
+
return { pack: 'narratives', model_output: false, building: true, narratives: [], note: `${HEAT_NOTE} The tracker is still building from the last 26 h of recorded history (first call only, tens of seconds). Ask again in about a minute.` };
|
|
913
|
+
const fw = (s) => ({ ...s, label: firewall(s.label, 'narratives', 'name'), modelLabel: s.modelLabel ? firewall(s.modelLabel, 'narratives-model-label', 'name') : null, topRunner: s.topRunner ? { ...s.topRunner, symbol: firewall(s.topRunner.symbol, 'narratives', 'symbol') } : null });
|
|
914
|
+
const base = { pack: 'narratives', model_output: false, as_of: tr.lastTickAt ? new Date(tr.lastTickAt).toISOString() : null, embedder: tr.embedder.id, note: HEAT_NOTE };
|
|
915
|
+
if (name === 'narratives_top') {
|
|
916
|
+
const limit = boundedLimit(args.limit, 15, 50);
|
|
917
|
+
let rows = tr.top(200);
|
|
918
|
+
if (typeof args.lifecycle === 'string')
|
|
919
|
+
rows = rows.filter((r) => r.lifecycle === args.lifecycle);
|
|
920
|
+
rows = rows.slice(0, limit);
|
|
921
|
+
this.surface(rows.flatMap((r) => (r.topRunner ? [r.topRunner.mint] : [])));
|
|
922
|
+
return { ...base, coins_tracked: tr.coins.size, lifecycle_rules: LIFECYCLE_RULES.map(([state, rule]) => ({ state, rule })), limits: NARRATIVE_LIMITS, narratives: rows.map(fw) };
|
|
923
|
+
}
|
|
924
|
+
if (name === 'narrative_detail') {
|
|
925
|
+
const d = tr.detail(String(args.id ?? ''));
|
|
926
|
+
if (!d)
|
|
927
|
+
return { ...base, error: 'unknown narrative id (ids come from narratives_top / narrative_of; the tracker covers the last 26 h)' };
|
|
928
|
+
const limit = boundedLimit(args.limit, 40, 100);
|
|
929
|
+
const members = d.memberList.slice(-limit).map((m) => ({ ...m, symbol: firewall(m.symbol, 'narratives', 'symbol'), name: firewall(m.name, 'narratives', 'name') }));
|
|
930
|
+
this.surface(members.map((m) => m.mint));
|
|
931
|
+
const { memberList: _all, ...rest } = d;
|
|
932
|
+
return { ...base, ...fw(rest), members, members_shown: members.length };
|
|
933
|
+
}
|
|
934
|
+
const mint = String(args.mint ?? '');
|
|
935
|
+
const n = tr.narrativeOf(mint);
|
|
936
|
+
if (!n)
|
|
937
|
+
return { ...base, mint, narrative: null, note: `${HEAT_NOTE} This coin has no recorded sighting in the last 26 h, so it has no narrative yet.` };
|
|
938
|
+
this.surface([mint]);
|
|
939
|
+
return { ...base, ...n, label: firewall(n.label, 'narratives', 'name') };
|
|
940
|
+
}
|
|
868
941
|
case 'rug_backtest': {
|
|
869
942
|
const db = this.historyDb();
|
|
870
943
|
if (!db)
|
|
@@ -1202,6 +1275,37 @@ export class McpTools {
|
|
|
1202
1275
|
const rep = await McpTools.xLookup.lookup(url, { symbol: args.symbol, name: args.name, mint: args.mint, website: args.website ?? null });
|
|
1203
1276
|
return xView(rep);
|
|
1204
1277
|
}
|
|
1278
|
+
case 'classify_coin': {
|
|
1279
|
+
const ref = args.mint ?? args.symbol;
|
|
1280
|
+
const r = await this.resolve(ref);
|
|
1281
|
+
if (!r.row)
|
|
1282
|
+
return { error: r.error, candidates: r.candidates };
|
|
1283
|
+
const { classify, dossierFromSnapshot, COIN_RISK, NARRATIVE } = await import('../../classifier/src/index.js');
|
|
1284
|
+
const { provider, config } = await this.classifier();
|
|
1285
|
+
const out = await classify(provider, args.set === 'narrative' ? NARRATIVE : COIN_RISK, dossierFromSnapshot(r.row), { calibration: config.calibration, fallback: config.fallback ? undefined : null });
|
|
1286
|
+
return { ...out, symbol: firewall(r.row.token.symbol, 'jupiter:symbol', 'symbol', { keepAddresses: false }), note: CLASSIFIER_NOTE };
|
|
1287
|
+
}
|
|
1288
|
+
case 'classify_batch': {
|
|
1289
|
+
const mints = (Array.isArray(args.mints) ? args.mints : []).filter((m) => typeof m === 'string' && BASE58.test(m)).slice(0, 100);
|
|
1290
|
+
if (!mints.length)
|
|
1291
|
+
return { error: 'classify_batch: mints must be base58 mint addresses (max 100)' };
|
|
1292
|
+
const lo = typeof args.lo === 'number' ? args.lo : 0.2, hi = typeof args.hi === 'number' ? args.hi : 0.8;
|
|
1293
|
+
if (!(lo >= 0 && hi <= 1 && lo < hi))
|
|
1294
|
+
return { error: 'classify_batch: need 0 <= lo < hi <= 1' };
|
|
1295
|
+
const { classifyBatch, dossierFromSnapshot, COIN_RISK } = await import('../../classifier/src/index.js');
|
|
1296
|
+
const rows = (await this.data.store.enrich(mints, { price: false })).rows;
|
|
1297
|
+
const { provider, config } = await this.classifier();
|
|
1298
|
+
const b = await classifyBatch(provider, COIN_RISK, rows.map((row) => dossierFromSnapshot(row)), { calibration: config.calibration, fallback: config.fallback ? undefined : null, triage: { question: 'drain_6h', lo, hi } });
|
|
1299
|
+
const results = b.results.map((x) => ('error' in x ? x : { subject: x.subject, provider: x.provider, model: x.model, drain_6h: x.answers.drain_6h?.probs.yes ?? null, outcome_6h: x.answers.outcome_6h?.probs ?? null, injection_suspected: x.injection_suspected, fallback_from: x.fallback_from ?? null }));
|
|
1300
|
+
return { label: 'model-estimate', scored: rows.length, missing: mints.length - rows.length, ms: b.ms, triage: b.triage, results, note: CLASSIFIER_NOTE };
|
|
1301
|
+
}
|
|
1302
|
+
case 'classifier_status': {
|
|
1303
|
+
const { detectHardware, recommendTier, describeHardware, SHIPPED_DEFAULT } = await import('../../classifier/src/index.js');
|
|
1304
|
+
const { provider, config } = await this.classifier();
|
|
1305
|
+
const st = await provider.status();
|
|
1306
|
+
const hw = await detectHardware();
|
|
1307
|
+
return { provider: provider.id, model: provider.model(), ready: st.ready, detail: st.detail, shipped_default: SHIPPED_DEFAULT, hardware: describeHardware(hw), recommendation: recommendTier(hw), fallback: config.fallback, note: CLASSIFIER_NOTE };
|
|
1308
|
+
}
|
|
1205
1309
|
case 'larp_check': {
|
|
1206
1310
|
// Compose the LARP sub-tools into one read-only pass. Every sub-result is already firewalled.
|
|
1207
1311
|
const directUrl = typeof args.url === 'string' ? args.url : undefined;
|
|
@@ -152,6 +152,15 @@ export function detailFor(tool, args, result) {
|
|
|
152
152
|
if (mint)
|
|
153
153
|
d.coin = { mint, symbol: str(args.symbol) };
|
|
154
154
|
}
|
|
155
|
+
// task-1427: the holder concentration a streamer overlay draws as a bar, copied from the result (never computed here)
|
|
156
|
+
const hold = holdersOf(tool, result);
|
|
157
|
+
if (hold)
|
|
158
|
+
d.holders = hold;
|
|
159
|
+
if (tool === 'get_dossier') {
|
|
160
|
+
const mint = mintOf(result.mint) ?? mintOf(args.mint);
|
|
161
|
+
if (mint && !d.coin)
|
|
162
|
+
d.coin = { mint, symbol: str(args.symbol) };
|
|
163
|
+
}
|
|
155
164
|
if (tool === 'paper_open_ticket' || tool === 'paper_size_ticket') {
|
|
156
165
|
const t = (result.ticket ?? {});
|
|
157
166
|
const mint = mintOf(t.mint);
|
|
@@ -176,6 +185,24 @@ export function detailFor(tool, args, result) {
|
|
|
176
185
|
}
|
|
177
186
|
return Object.keys(d).length ? d : null;
|
|
178
187
|
}
|
|
188
|
+
/** task-1427: the top-10 share a holder read reported: get_holders / get_dossier (pools left out), get_token (Jupiter audit, raw). */
|
|
189
|
+
function holdersOf(tool, result) {
|
|
190
|
+
const pct = (x) => { const n = num(x); return n !== null && n >= 0 && n <= 100 ? n : null; };
|
|
191
|
+
if (tool === 'get_holders' || tool === 'get_dossier') {
|
|
192
|
+
const h = (tool === 'get_holders' ? result : result.concentration);
|
|
193
|
+
if (!h || typeof h !== 'object' || typeof h.error === 'string')
|
|
194
|
+
return null;
|
|
195
|
+
const p = pct(h.top10PctExLp);
|
|
196
|
+
return p === null ? null : { top10Pct: p, count: num(h.holderCount), basis: 'ex-pools', ageSec: num(h.ageSec) };
|
|
197
|
+
}
|
|
198
|
+
if (tool === 'get_token') {
|
|
199
|
+
const a = (result.audit ?? null);
|
|
200
|
+
const p = a ? pct(a.top10Pct) : null;
|
|
201
|
+
const stats = (result.stats ?? null);
|
|
202
|
+
return p === null ? null : { top10Pct: p, count: stats ? num(stats.holderCount) : null, basis: 'raw', ageSec: num(a.ageSec) };
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
179
206
|
export function fileSpectatorSinkFromEnv(env) {
|
|
180
207
|
const file = env.DEXTERM_SPECTATOR_FILE;
|
|
181
208
|
if (!file)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// dexterm narratives (task-1818): incremental clustering of coins into narratives with STABLE ids.
|
|
2
|
+
//
|
|
3
|
+
// Leader clustering in first-seen order (online, one pass, point-in-time: a coin only ever joins clusters that
|
|
4
|
+
// existed when it was first seen). Candidates come from blocking (shared squashed meme words) plus the currently
|
|
5
|
+
// hot clusters, so a semantic embedder can still link "puppy" to a dog narrative without a shared word.
|
|
6
|
+
// A coin joins the most similar candidate whose CENTROID cosine >= joinSim; the seed vector also has to stay
|
|
7
|
+
// within seedSim so a cluster cannot drift away from what started it. Otherwise it seeds a new cluster.
|
|
8
|
+
// Ids never change: `nar_<hash of the seed coin's core word + mint>`. Clusters are never merged or split (an
|
|
9
|
+
// honest limit: two narratives that converge later stay two rows).
|
|
10
|
+
import { cosine, unit } from "./embed.js";
|
|
11
|
+
import { AFFIXES, squash } from "./normalize.js";
|
|
12
|
+
function hashId(s) {
|
|
13
|
+
let h = 0x811c9dc5;
|
|
14
|
+
for (let i = 0; i < s.length; i++) {
|
|
15
|
+
h ^= s.charCodeAt(i);
|
|
16
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
17
|
+
}
|
|
18
|
+
let h2 = 0x9e3779b9;
|
|
19
|
+
for (let i = s.length - 1; i >= 0; i--) {
|
|
20
|
+
h2 ^= s.charCodeAt(i);
|
|
21
|
+
h2 = Math.imul(h2, 0x85ebca6b) >>> 0;
|
|
22
|
+
}
|
|
23
|
+
return (h >>> 0).toString(36).padStart(7, '0') + (h2 >>> 0).toString(36).slice(0, 3);
|
|
24
|
+
}
|
|
25
|
+
/** Display label: the top bigram when it is nearly as common as the top word, else the top word. */
|
|
26
|
+
export function clusterLabel(c) {
|
|
27
|
+
let best = null, bestBi = null;
|
|
28
|
+
for (const [g, n] of c.grams) {
|
|
29
|
+
if (g.includes(' ')) {
|
|
30
|
+
if (!bestBi || n > bestBi[1] || (n === bestBi[1] && g < bestBi[0]))
|
|
31
|
+
bestBi = [g, n];
|
|
32
|
+
}
|
|
33
|
+
else if (!best || n > best[1] || (n === best[1] && g.length > best[0].length))
|
|
34
|
+
best = [g, n];
|
|
35
|
+
}
|
|
36
|
+
const pick = bestBi && best && bestBi[1] >= 2 && bestBi[1] >= 0.6 * best[1] ? bestBi[0] : best?.[0] ?? c.seedCore ?? '?';
|
|
37
|
+
return pick.split(' ').map((w) => {
|
|
38
|
+
const forms = c.forms.get(w);
|
|
39
|
+
if (!forms)
|
|
40
|
+
return w;
|
|
41
|
+
return [...forms.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
42
|
+
}).join(' ');
|
|
43
|
+
}
|
|
44
|
+
export class Clusterer {
|
|
45
|
+
opts;
|
|
46
|
+
clusters = new Map();
|
|
47
|
+
byMint = new Map();
|
|
48
|
+
/** blocking key -> cluster ids in join order (may repeat; read newest-first, compacted when long). */
|
|
49
|
+
block = new Map();
|
|
50
|
+
hot = [];
|
|
51
|
+
hotDirty = 0;
|
|
52
|
+
constructor(opts = {}) {
|
|
53
|
+
this.opts = { joinSim: opts.joinSim ?? 0.62, seedSim: opts.seedSim ?? 0.45, hotMax: opts.hotMax ?? 300, perKey: opts.perKey ?? 80, activeMs: opts.activeMs ?? 72 * 3_600_000 };
|
|
54
|
+
}
|
|
55
|
+
/** Assign one coin (first sighting at t). Returns the cluster id and the coin's spawn index (1 = seed). */
|
|
56
|
+
assign(mint, t, norm, vec) {
|
|
57
|
+
const known = this.byMint.get(mint);
|
|
58
|
+
if (known) {
|
|
59
|
+
const c = this.clusters.get(known);
|
|
60
|
+
return { id: known, spawn: c.members.find((m) => m.mint === mint)?.spawn ?? 0, joined: false, sim: 1 };
|
|
61
|
+
}
|
|
62
|
+
const cand = new Set();
|
|
63
|
+
for (const k of norm.keys) {
|
|
64
|
+
const ids = this.block.get(k);
|
|
65
|
+
if (!ids)
|
|
66
|
+
continue;
|
|
67
|
+
const local = new Set();
|
|
68
|
+
for (let i = ids.length - 1; i >= 0 && local.size < this.opts.perKey; i--)
|
|
69
|
+
local.add(ids[i]);
|
|
70
|
+
for (const id of local)
|
|
71
|
+
cand.add(id);
|
|
72
|
+
}
|
|
73
|
+
if (this.opts.hotMax > 0) {
|
|
74
|
+
if (++this.hotDirty >= 200)
|
|
75
|
+
this.refreshHot(t);
|
|
76
|
+
for (const id of this.hot)
|
|
77
|
+
cand.add(id);
|
|
78
|
+
}
|
|
79
|
+
let bestId = null, bestSim = -1;
|
|
80
|
+
for (const id of cand) {
|
|
81
|
+
const c = this.clusters.get(id);
|
|
82
|
+
if (t - c.lastJoinT > this.opts.activeMs)
|
|
83
|
+
continue;
|
|
84
|
+
const s = cosine(vec, c.sum);
|
|
85
|
+
if (s > bestSim && s >= this.opts.joinSim && cosine(vec, c.seed ?? c.sum) >= this.opts.seedSim) {
|
|
86
|
+
bestSim = s;
|
|
87
|
+
bestId = id;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let c;
|
|
91
|
+
if (bestId)
|
|
92
|
+
c = this.clusters.get(bestId);
|
|
93
|
+
else {
|
|
94
|
+
const id = 'nar_' + hashId(`${norm.core ?? ''}|${mint}`);
|
|
95
|
+
c = { id, seedMint: mint, seedCore: norm.core, firstT: t, lastJoinT: t, members: [], grams: new Map(), forms: new Map(), themes: new Map(), cores: new Map(), sum: new Float32Array(vec.length), seed: null };
|
|
96
|
+
this.clusters.set(id, c);
|
|
97
|
+
}
|
|
98
|
+
if (c.members.length === 1 && !c.seed)
|
|
99
|
+
c.seed = Float32Array.from(c.sum);
|
|
100
|
+
const spawn = c.members.length + 1;
|
|
101
|
+
c.members.push({ mint, t, spawn, core: norm.core, symbol: norm.symbol, name: norm.name });
|
|
102
|
+
c.lastJoinT = Math.max(c.lastJoinT, t);
|
|
103
|
+
for (let i = 0; i < vec.length; i++)
|
|
104
|
+
c.sum[i] += vec[i];
|
|
105
|
+
this.count(c, norm);
|
|
106
|
+
for (const k of norm.keys) {
|
|
107
|
+
let ids = this.block.get(k);
|
|
108
|
+
if (!ids)
|
|
109
|
+
this.block.set(k, (ids = []));
|
|
110
|
+
if (ids[ids.length - 1] !== c.id)
|
|
111
|
+
ids.push(c.id);
|
|
112
|
+
if (ids.length > 6 * this.opts.perKey) { // compact: keep the newest occurrence of each id, newest perKey*2 ids
|
|
113
|
+
const keep = [];
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
for (let i = ids.length - 1; i >= 0 && keep.length < 2 * this.opts.perKey; i--)
|
|
116
|
+
if (!seen.has(ids[i])) {
|
|
117
|
+
seen.add(ids[i]);
|
|
118
|
+
keep.push(ids[i]);
|
|
119
|
+
}
|
|
120
|
+
this.block.set(k, keep.reverse());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.byMint.set(mint, c.id);
|
|
124
|
+
return { id: c.id, spawn, joined: !!bestId, sim: bestId ? bestSim : 1 };
|
|
125
|
+
}
|
|
126
|
+
count(c, n) {
|
|
127
|
+
const ws = n.tokens.filter((t) => !t.affix && t.from !== 'description' && t.t.length >= 2).map((t) => t.t);
|
|
128
|
+
const uniq = [...new Set(ws)];
|
|
129
|
+
const grams = new Set(uniq);
|
|
130
|
+
const nameWords = n.name.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w && !AFFIXES.has(w));
|
|
131
|
+
for (let i = 0; i + 1 < nameWords.length; i++)
|
|
132
|
+
grams.add(nameWords[i] + ' ' + nameWords[i + 1]);
|
|
133
|
+
for (const g of grams)
|
|
134
|
+
c.grams.set(g, (c.grams.get(g) ?? 0) + 1);
|
|
135
|
+
// display forms from the raw (firewalled) text
|
|
136
|
+
for (const raw of [n.symbol, ...n.name.split(/[^\p{L}\p{N}]+/u)]) {
|
|
137
|
+
const lw = raw.toLowerCase();
|
|
138
|
+
if (!grams.has(lw))
|
|
139
|
+
continue;
|
|
140
|
+
let f = c.forms.get(lw);
|
|
141
|
+
if (!f)
|
|
142
|
+
c.forms.set(lw, (f = new Map()));
|
|
143
|
+
f.set(raw, (f.get(raw) ?? 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
for (const th of n.themes.slice(0, 1))
|
|
146
|
+
c.themes.set(th, (c.themes.get(th) ?? 0) + 1);
|
|
147
|
+
if (n.core) {
|
|
148
|
+
const k = squash(n.core);
|
|
149
|
+
c.cores.set(k, (c.cores.get(k) ?? 0) + 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
refreshHot(t) {
|
|
153
|
+
this.hotDirty = 0;
|
|
154
|
+
const live = [...this.clusters.values()].filter((c) => c.members.length >= 3 && t - c.lastJoinT <= 24 * 3_600_000);
|
|
155
|
+
live.sort((a, b) => b.lastJoinT - a.lastJoinT || b.members.length - a.members.length);
|
|
156
|
+
this.hot = live.slice(0, this.opts.hotMax).map((c) => c.id);
|
|
157
|
+
}
|
|
158
|
+
/** Drop inactive singleton clusters from the blocking index (memory bound for long runs). Ids stay valid. */
|
|
159
|
+
prune(t, idleMs = 7 * 86_400_000) {
|
|
160
|
+
let n = 0;
|
|
161
|
+
for (const [k, ids] of this.block) {
|
|
162
|
+
const keep = ids.filter((id) => { const c = this.clusters.get(id); return !(c.members.length === 1 && t - c.lastJoinT > idleMs); });
|
|
163
|
+
n += ids.length - keep.length;
|
|
164
|
+
if (keep.length)
|
|
165
|
+
this.block.set(k, keep);
|
|
166
|
+
else
|
|
167
|
+
this.block.delete(k);
|
|
168
|
+
}
|
|
169
|
+
return n;
|
|
170
|
+
}
|
|
171
|
+
/** The lexicon theme most members carry, if enough do (>= 2 coins and >= 30%; a solo coin keeps its own). Display only. */
|
|
172
|
+
topTheme(c) {
|
|
173
|
+
let best = null, bn = 0;
|
|
174
|
+
for (const [th, n] of c.themes)
|
|
175
|
+
if (n > bn) {
|
|
176
|
+
best = th;
|
|
177
|
+
bn = n;
|
|
178
|
+
}
|
|
179
|
+
const m = c.members.length;
|
|
180
|
+
return m === 1 || (bn >= 2 && bn >= 0.3 * m) ? best : null;
|
|
181
|
+
}
|
|
182
|
+
/** Mean member-to-centroid cosine (cohesion) for an audit; needs the member vectors. */
|
|
183
|
+
static cohesion(vecs) {
|
|
184
|
+
if (vecs.length < 2)
|
|
185
|
+
return 1;
|
|
186
|
+
const s = new Float32Array(vecs[0].length);
|
|
187
|
+
for (const v of vecs)
|
|
188
|
+
for (let i = 0; i < v.length; i++)
|
|
189
|
+
s[i] += v[i];
|
|
190
|
+
unit(s);
|
|
191
|
+
return vecs.reduce((a, v) => a + cosine(v, s), 0) / vecs.length;
|
|
192
|
+
}
|
|
193
|
+
}
|