dexterm-cli 0.1.4 → 0.1.5
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/cli/src/agent-paper.js +60 -0
- package/dist/packages/cli/src/analyst/backend.js +183 -0
- package/dist/packages/cli/src/analyst/codex.js +301 -0
- package/dist/packages/cli/src/analyst/session.js +9 -2
- package/dist/packages/cli/src/app-help-gen.js +15 -6
- package/dist/packages/cli/src/doctor.js +58 -2
- package/dist/packages/cli/src/live/dev-history.js +99 -0
- package/dist/packages/cli/src/live/newpairs.js +12 -0
- package/dist/packages/cli/src/live/screens/token-live.js +11 -1
- package/dist/packages/cli/src/live/token.js +1 -0
- package/dist/packages/cli/src/main.js +122 -8
- package/dist/packages/cli/src/mcp-follow.js +37 -7
- package/dist/packages/cli/src/record-cmd.js +2 -0
- package/dist/packages/cli/src/rules.js +25 -2
- package/dist/packages/cli/src/screens/analyst-session.js +5 -3
- package/dist/packages/cli/src/screens/analyst.js +4 -3
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +2 -2
- package/dist/packages/custom/src/schema.js +5 -1
- package/dist/packages/data/src/fixtures.js +28 -6
- package/dist/packages/data/src/index.js +3 -0
- package/dist/packages/data/src/sources/jupiter.js +1 -0
- package/dist/packages/data/src/trade/agent/paper-desk.js +336 -0
- package/dist/packages/data/src/trade/agent/rendezvous.js +161 -0
- package/dist/packages/mcp/assets/kb/app-help/agent-paper-trade.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/dev-history.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +4 -1
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +1 -0
- package/dist/packages/mcp/assets/kb/app-help/use-codex-analyst.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +50 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-directory-metadata-prepared-not-submitted.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-install-in-one-command.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-codex-instead-of-claude.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-or-linux-npm.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-rules-engine.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-analyst-s-launch.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +13 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +0 -2
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-how-well-it-predicts-measured-held-out.md +46 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +2 -0
- package/dist/packages/mcp/src/index.js +5 -2
- package/dist/packages/mcp/src/paper-agent.js +92 -0
- package/dist/packages/mcp/src/server.js +206 -12
- package/dist/packages/mcp/src/spectator-sink.js +73 -4
- package/dist/packages/mcp/src/user-content.js +1 -0
- package/dist/packages/recorder/src/dev-history.js +129 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +23 -0
- package/dist/packages/recorder/src/schema.js +11 -0
- package/package.json +1 -1
|
@@ -85,7 +85,7 @@ export function appTools() {
|
|
|
85
85
|
const SCREENS = [
|
|
86
86
|
{ screen: 'screener', title: 'The screener screen', feature: 'screener', tools: ['screener_query'],
|
|
87
87
|
blurb: 'The live table of Solana tokens (DexScreener + Jupiter, refreshed every ~10 s): price, 5m/1h/6h/24h change, volume, liquidity, market cap, age, holders, organic score, flags and a sparkline, with a p(rug) column. Cells whose source is down show a dash and the caption says why — never a fabricated number.' },
|
|
88
|
-
{ screen: 'token', title: 'The token screen', feature: 'token-intel', tools: ['get_dossier', 'get_token', 'get_holders', 'get_trades', 'get_candles', 'get_dev_history', 'rugcheck'],
|
|
88
|
+
{ screen: 'token', title: 'The token screen', feature: 'token-intel', tools: ['get_dossier', 'get_token', 'get_holders', 'get_trades', 'get_candles', 'get_dev_history', 'dev_history', 'rugcheck'],
|
|
89
89
|
blurb: 'One coin in depth: market stats, a candle chart, the trades tape, mint/freeze authority, top holders with their class (pool/dev/linked/…), RugCheck + GoPlus, snipers/bundled from the wallet-intel scan, and the ANALYST panel. Open it with ⏎ on any live row or `--mint <CA|link>`.' },
|
|
90
90
|
{ screen: 'newpairs', title: 'The new pairs screen', feature: 'new-pairs', tools: ['screener_query'],
|
|
91
91
|
blurb: 'A live feed of just-launched tokens (PumpPortal new-token + migration push, plus Jupiter/RugCheck/DexScreener polls). Bonding progress is a create-time estimate and says so; SNIPE/BUNDLE columns fill in from the wallet-intel scan of the row the cursor rests on (Helius-keyed).' },
|
|
@@ -93,8 +93,8 @@ const SCREENS = [
|
|
|
93
93
|
blurb: 'The coins you watch, persisted in SQLite, priced live every ~10 s with a LIVE/POLL badge. Add with `w` on any coin; remove with `d`.' },
|
|
94
94
|
{ screen: 'analyst', title: 'The analyst screen', feature: 'analyst', tools: ['get_dossier', 'kb_search', 'app_help'],
|
|
95
95
|
blurb: 'Your own `claude` CLI as the dexterm analyst: it reads the read-only dexterm MCP tools, grounds every fact in a tool result, and answers in a few terse lines. ⏎ on a coin runs a full dossier; `a` runs a rug-check. Facts never come from model memory; token text passes the prompt firewall.' },
|
|
96
|
-
{ screen: 'positions', title: 'The paper positions screen', feature: 'positions', tools: [],
|
|
97
|
-
blurb: 'Your simulated (PAPER) positions and their P&L. dexterm holds no keys and signs nothing; paper trading is a simulation for learning the flow.' },
|
|
96
|
+
{ screen: 'positions', title: 'The paper positions screen', feature: 'positions', tools: ['paper_positions', 'paper_pnl'],
|
|
97
|
+
blurb: 'Your simulated (PAPER) positions and their P&L. dexterm holds no keys and signs nothing; paper trading is a simulation for learning the flow. An agent you enabled can read these over MCP with the read-only paper_positions and paper_pnl tools.' },
|
|
98
98
|
{ screen: 'trending', title: 'The trending screen', feature: 'trending-board', tools: ['narrative_board'],
|
|
99
99
|
blurb: 'What is trending in the space — narratives and movers aggregated from the feed.' },
|
|
100
100
|
{ screen: 'kol', title: 'The KOL lists screen', feature: 'kol-lists', tools: [],
|
|
@@ -179,10 +179,16 @@ const TOPICS = [
|
|
|
179
179
|
{ id: 'ask-the-analyst', title: 'Ask the analyst a question', screen: 'analyst', tags: ['analyst', 'ask', 'claude', 'ai', 'question', 'dossier', 'chat'],
|
|
180
180
|
actions: [{ action: 'analyst.toggle' }, { action: 'open', screen: 'screener' }], tools: ['get_dossier', 'kb_search', 'app_help', 'case_lookup'], feature: 'analyst',
|
|
181
181
|
flags: ['--no-ai'],
|
|
182
|
-
body: 'The analyst is your own `claude` login. ⏎ on a coin runs a full dossier; `a` runs a rug-check (or a trench-scan of the screener when nothing is selected). Type a follow-up and ⏎ to send. If `claude` is missing or logged out the panel shows the one-line fix and the rest of dexterm keeps working. Start without AI using `--no-ai`.' },
|
|
182
|
+
body: 'The analyst is your own `claude` login (or your own `codex` login: `/engine codex`, see "Run the analyst on Codex"). ⏎ on a coin runs a full dossier; `a` runs a rug-check (or a trench-scan of the screener when nothing is selected). Type a follow-up and ⏎ to send. If `claude` is missing or logged out the panel shows the one-line fix and the rest of dexterm keeps working. Start without AI using `--no-ai`.' },
|
|
183
183
|
{ id: 'connect-an-agent', title: 'Connect your own agent (MCP)', screen: 'all', tags: ['mcp', 'agent', 'agents', 'connect', 'claude', 'tool', 'server', 'bot'],
|
|
184
184
|
commands: ['mcp'], tools: ['screen_state', 'get_dossier', 'kb_search', 'app_help', 'app_state'], feature: 'mcp',
|
|
185
|
-
body: '`dexterm mcp` is a plain stdio MCP server exposing the same read-only tools the analyst uses. Add it to any Claude Code with `claude mcp add dexterm -- node packages/mcp/bin/dexterm-mcp.mjs
|
|
185
|
+
body: '`dexterm mcp` is a plain stdio MCP server exposing the same read-only tools the analyst uses. Add it to any Claude Code with `claude mcp add dexterm -- node packages/mcp/bin/dexterm-mcp.mjs`, or set up Claude Code, Claude Desktop, Cursor or Codex in one command with `dexterm mcp install <client>`. Agents are untrusted: every tool is read-only and an agent can never sign, arm, raise a cap or touch keys. Use `app_help` for "how do I" questions and `app_state` to see what the user sees.' },
|
|
186
|
+
{ id: 'use-codex-analyst', title: 'Run the analyst on Codex instead of Claude', screen: 'all', tags: ['codex', 'openai', 'chatgpt', 'engine', 'analyst', 'claude', 'switch', 'backend'],
|
|
187
|
+
flags: ['--analyst-engine'], feature: 'analyst',
|
|
188
|
+
body: 'The analyst can run on your own Codex CLI login instead of Claude (0.1.5). Pick it with `--analyst-engine codex`, `DEXTERM_ANALYST_ENGINE=codex`, `"analyst": {"engine": "codex"}` in `~/.dexterm/config.json`, or type `/engine codex` in the chat box (new conversations; `/engine` alone shows the engine and what is installed). The default `auto` uses claude when it is installed, else codex. dexterm stores no credentials: your `codex login` does the work. The Codex run is locked read-only: only the read-only dexterm tools (plus the UI actions the session allows, none in scan mode), no shell, no file edits, no web search, no other MCP server; a turn that tries anything else is stopped. `dexterm doctor` shows a Codex CLI line (installed, version, logged in). Codex is also supported as an MCP client: `dexterm mcp install codex`.' },
|
|
189
|
+
{ id: 'dev-history', title: 'Check if a coin\'s creator rugged before (dev history)', screen: 'token', tags: ['dev', 'deployer', 'creator', 'serial', 'rugger', 'rugged', 'history', 'launches'],
|
|
190
|
+
tools: ['dev_history', 'get_dev_history'], feature: 'token-intel',
|
|
191
|
+
body: 'The token screen\'s ON-CHAIN panel shows the creator\'s recorded track record (0.1.5): `Dev rugs (recorded)` (rugs out of labelled prior launches, with the sample n), `Dev launches seen`, `Dev sold its bag` (median minutes to the dev\'s exit) and `Dev linked` (funder or cluster from the wallet-intel scan). It comes from dexterm\'s own local history recorder (keyless creator links + code-only outcome labels), so it says `unknown` when there is no record; it is a risk flag from observed history, never a certificate. Alert on it with the rule field `devrugs`, e.g. `when devrugs >= 2 then alert(warn, "new coin from a wallet that rugged before") scope newpairs`. Agents and the analyst use the `dev_history` MCP tool.' },
|
|
186
192
|
{ id: 'configure-rpc-key', title: 'Configure a Helius / RPC key for holders', screen: 'all', tags: ['helius', 'rpc', 'key', 'holders', 'free', 'api', 'ranked'],
|
|
187
193
|
feature: 'keyless',
|
|
188
194
|
body: 'dexterm is free-forever and keyless by default. The one real keyless gap is the ranked top-holder list: add a free RPC key for it. Save a Helius key ONLY at `~/.dexterm/helius.key` (mode 600, never a repo or command line), or set `DEXTERM_RPC_URL` to any read-only RPC. `dexterm doctor` prints the exact command. Without a key the holders panel shows count + top-10% via Jupiter.' },
|
|
@@ -194,7 +200,10 @@ const TOPICS = [
|
|
|
194
200
|
body: '`config.keymap` in `~/.dexterm/config.json` re-binds actions to keys; `dexterm keymap [screen]` shows the effective bindings including your overrides. Reserved keys (ctrl+c, esc, enter, y, !) and the confirm/dismiss/panic/quit actions cannot be rebound, and collisions refuse the whole keymap.' },
|
|
195
201
|
{ id: 'paper-trade', title: 'Paper trade a coin', screen: 'token', tags: ['paper', 'trade', 'buy', 'sell', 'ticket', 'simulate', 'practice'],
|
|
196
202
|
actions: [{ action: 'trade.buy', screen: 'token' }, { action: 'trade.sell', screen: 'token' }], feature: 'paper-ticket',
|
|
197
|
-
body: 'Press `B` / `S` on the token screen to open a PAPER buy/sell ticket in its editing phase. dexterm holds no keys and signs nothing — paper trading is a simulation. Confirming a ticket needs your explicit keypress (the gate); a stray key never confirms. The analyst can open a PAPER ticket but can never size, confirm or send it.' },
|
|
203
|
+
body: 'Press `B` / `S` on the token screen to open a PAPER buy/sell ticket in its editing phase. dexterm holds no keys and signs nothing — paper trading is a simulation. Confirming a ticket needs your explicit keypress (the gate); a stray key never confirms. The in-app analyst can open a PAPER ticket but can never size, confirm or send it. If you separately arm a mandate and enable agent paper trading, a connected agent can open, size, confirm and close PAPER trades inside your caps (never raising them, never signing) — see "Let an agent paper trade".' },
|
|
204
|
+
{ id: 'agent-paper-trade', title: 'Let an agent paper trade', screen: 'all', tags: ['agent', 'mcp', 'paper', 'autonomous', 'experimental', 'confirm', 'close', 'simulated'],
|
|
205
|
+
flags: ['--agent-paper'], tools: ['paper_open_ticket', 'paper_size_ticket', 'paper_confirm', 'paper_close', 'paper_positions', 'paper_pnl'], feature: 'mcp',
|
|
206
|
+
body: 'Experimental, paper only, off by default. Arm a mandate yourself (its caps, by keypress), then start `dexterm --spectate-mcp --agent-paper <mandate-id>`: an agent connected through `dexterm mcp` can then open, size, confirm and close SIMULATED (paper) trades inside those caps with paper_open_ticket / paper_size_ticket / paper_confirm / paper_close, and read paper_positions / paper_pnl. The agent is untrusted: it can never sign, send a real transaction, touch keys, or raise a cap; its per-trade cap is a quarter of the mandate\'s, and a size over any cap is refused. Caps are re-read from your armed mandate on every call, so revoking it or the panic key stops the agent. Every call is journaled and shows in this terminal\'s agent activity; Ctrl+P pauses, Ctrl+K stops for the rest of the run, any key takes over, and quitting turns it off. Figures are simulated and are not a result.' },
|
|
198
207
|
{ id: 'run-doctor', title: 'Diagnose problems with doctor', screen: 'all', tags: ['doctor', 'diagnose', 'check', 'troubleshoot', 'broken', 'fix', 'problem'],
|
|
199
208
|
commands: ['doctor'], feature: 'doctor',
|
|
200
209
|
body: '`dexterm doctor` is the first stop for any problem: non-interactive, never touches your keys, exits 0 when dexterm will run and 1 when something must be fixed. Every non-pass line prints the exact fix. `dexterm doctor --offline` skips the network probes.' },
|
|
@@ -17,6 +17,7 @@ import { XTokenStore } from '../../data/src/index.js';
|
|
|
17
17
|
import { SayTts, voiceDoctor, resolveVoice, WHISPER_MODEL, resolvePiperRuntime, pickPlayer, PIPER_MODEL } from '../../voice/src/index.js';
|
|
18
18
|
import { loadConfig, dextermHome } from '../../custom/src/index.js';
|
|
19
19
|
import { kbStore, skillStore } from "./kb-skill-cmd.js";
|
|
20
|
+
import { analystCliOnPath, CODEX_MIN_VERSION, parseEngineChoice, resolveEngine, versionAtLeast } from "./analyst/codex.js";
|
|
20
21
|
/** The minimum Node that runs dexterm (type-stripping of the .ts entry needs 22.18). */
|
|
21
22
|
export const MIN_NODE = [22, 18, 0];
|
|
22
23
|
/** Cheap PATH lookup (no spawn). Windows (task-1423): PATH is `;`-separated and commands carry an extension
|
|
@@ -141,6 +142,32 @@ export async function claudeProbe(timeoutMs = 4000, platform = process.platform,
|
|
|
141
142
|
}
|
|
142
143
|
});
|
|
143
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Default codex probe (0.1.5): PATH (spawnable form only), then `codex --version` and `codex login status`. The
|
|
147
|
+
* status command is the user's own CLI reporting on its own login; dexterm never reads the credential itself.
|
|
148
|
+
*/
|
|
149
|
+
export async function codexProbe(timeoutMs = 4000, env = process.env) {
|
|
150
|
+
if (!analystCliOnPath('codex', env))
|
|
151
|
+
return { state: 'missing' };
|
|
152
|
+
const { execFile } = await import('node:child_process');
|
|
153
|
+
const run = (args) => new Promise((resolve) => {
|
|
154
|
+
try {
|
|
155
|
+
const child = execFile('codex', args, { timeout: timeoutMs, env }, (err, stdout, stderr) => {
|
|
156
|
+
const code = err ? (typeof err.code === 'number' ? err.code : null) : 0;
|
|
157
|
+
resolve({ code, out: `${stdout ?? ''}${stderr ?? ''}` });
|
|
158
|
+
});
|
|
159
|
+
child.on('error', () => resolve({ code: null, out: '' }));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
resolve({ code: null, out: '' });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
const v = await run(['--version']);
|
|
166
|
+
const version = /(\d+\.\d+\.\d+[\w.-]*)/.exec(v.out)?.[1] ?? null;
|
|
167
|
+
const st = await run(['login', 'status']);
|
|
168
|
+
const loggedIn = st.code === 0 ? !/not logged in/i.test(st.out) : st.code === null ? null : false;
|
|
169
|
+
return { state: 'present', version, loggedIn };
|
|
170
|
+
}
|
|
144
171
|
/** Default writable check: create the directory, write and remove a temp file. Returns an error message or null. */
|
|
145
172
|
export function writableProbe(dir) {
|
|
146
173
|
try {
|
|
@@ -162,6 +189,8 @@ export function defaultDeps(env = process.env, isTTY = process.stdout.isTTY ===
|
|
|
162
189
|
nodeVersion: process.versions.node,
|
|
163
190
|
platform: process.platform,
|
|
164
191
|
claude: () => claudeProbe(),
|
|
192
|
+
codex: () => codexProbe(4000, env),
|
|
193
|
+
analystEngine: env.DEXTERM_ANALYST_ENGINE ?? loadConfig(dextermHome(env)).config.analyst?.engine ?? null,
|
|
165
194
|
probe: (url) => livingProbe(url),
|
|
166
195
|
checkWritable: writableProbe,
|
|
167
196
|
offline: env.DEXTERM_OFFLINE === '1' || env.DEXTERM_OFFLINE === 'true',
|
|
@@ -230,11 +259,20 @@ export async function runDoctor(deps) {
|
|
|
230
259
|
}
|
|
231
260
|
// 3. Claude CLI (the analyst). Not required to browse; required for the `a` analyst.
|
|
232
261
|
const claude = await deps.claude();
|
|
233
|
-
|
|
262
|
+
// 0.1.5: codex is the optional second engine; a missing claude is fine when codex can run the analyst.
|
|
263
|
+
const codex = deps.codex ? await deps.codex() : null;
|
|
264
|
+
const codexUsable = codex?.state === 'present';
|
|
265
|
+
if (claude === 'missing' && codexUsable) {
|
|
266
|
+
checks.push({
|
|
267
|
+
name: 'Claude CLI', status: 'pass',
|
|
268
|
+
detail: 'not found on PATH (optional now): the analyst runs on your codex CLI instead',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else if (claude === 'missing') {
|
|
234
272
|
checks.push({
|
|
235
273
|
name: 'Claude CLI', status: 'warn',
|
|
236
274
|
detail: 'not found on PATH; the analyst (a) is disabled, the rest of dexterm works',
|
|
237
|
-
fix: 'Install Claude Code (https://claude.com/claude-code) and run `claude` once to log in
|
|
275
|
+
fix: 'Install Claude Code (https://claude.com/claude-code) and run `claude` once to log in, or use Codex: `npm i -g @openai/codex && codex login`.',
|
|
238
276
|
});
|
|
239
277
|
}
|
|
240
278
|
else {
|
|
@@ -243,6 +281,24 @@ export async function runDoctor(deps) {
|
|
|
243
281
|
detail: 'found on PATH (the analyst runs your local `claude`; log in once with `claude` if the analyst errors)',
|
|
244
282
|
});
|
|
245
283
|
}
|
|
284
|
+
// 3b. Codex CLI (0.1.5): optional second analyst engine, the user's own codex login. Read-only lock needs >= CODEX_MIN_VERSION.
|
|
285
|
+
if (codex) {
|
|
286
|
+
const choice = parseEngineChoice(deps.analystEngine) ?? 'auto';
|
|
287
|
+
const engine = resolveEngine(choice, (b) => b === 'claude' ? claude !== 'missing' : codexUsable);
|
|
288
|
+
const eng = `analyst engine: ${engine}${choice === 'auto' ? ' (auto)' : ''}`;
|
|
289
|
+
if (codex.state === 'missing') {
|
|
290
|
+
checks.push({ name: 'Codex CLI', status: engine === 'codex' ? 'warn' : 'pass', detail: `not installed (optional second analyst engine) · ${eng}`, ...(engine === 'codex' ? { fix: 'npm i -g @openai/codex && codex login, or set analyst.engine to claude' } : {}) });
|
|
291
|
+
}
|
|
292
|
+
else if (codex.version && !versionAtLeast(codex.version, CODEX_MIN_VERSION)) {
|
|
293
|
+
checks.push({ name: 'Codex CLI', status: 'warn', detail: `codex-cli ${codex.version} is older than ${CODEX_MIN_VERSION}; the read-only analyst lock needs ${CODEX_MIN_VERSION}+ · ${eng}`, fix: 'npm i -g @openai/codex@latest' });
|
|
294
|
+
}
|
|
295
|
+
else if (codex.loggedIn === false) {
|
|
296
|
+
checks.push({ name: 'Codex CLI', status: 'warn', detail: `found${codex.version ? ` (codex-cli ${codex.version})` : ''}, not logged in · ${eng}`, fix: 'codex login' });
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
checks.push({ name: 'Codex CLI', status: 'pass', detail: `found${codex.version ? ` (codex-cli ${codex.version})` : ''}${codex.loggedIn ? ', logged in' : ''}; runs the analyst read-only on your own codex login (/engine codex) · ${eng}` });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
246
302
|
// 4b. Login with X (optional, task-1357): the user's own X app + login unlock X posts in the SOCIAL panel / TRENDING.
|
|
247
303
|
{
|
|
248
304
|
const xs = new XTokenStore({ env: deps.env });
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { creatorOf, devHistoryLine, queryDevHistory } from '../../../recorder/src/index.js';
|
|
2
|
+
export const DEV_TTL_MS = 60_000;
|
|
3
|
+
/** Short creator/funder address for the panel (never a full address in a 40-column cell). */
|
|
4
|
+
export const shortAddr = (a) => (a ? `${a.slice(0, 4)}…${a.slice(-4)}` : '—');
|
|
5
|
+
export class DevHistoryBook {
|
|
6
|
+
cache = new Map();
|
|
7
|
+
db;
|
|
8
|
+
now;
|
|
9
|
+
ttlMs;
|
|
10
|
+
constructor(db, now = () => Date.now(), ttlMs = DEV_TTL_MS) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.now = now;
|
|
13
|
+
this.ttlMs = ttlMs;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The entry for `mint` (cached for `ttlMs`). `creatorHint` is the dev wallet a source named on screen (Jupiter
|
|
17
|
+
* `dev`) for a coin the recorder has not linked yet; `intel` is the coin's settled wallet-intel scan, if any.
|
|
18
|
+
*/
|
|
19
|
+
forMint(mint, creatorHint, intel) {
|
|
20
|
+
const now = this.now();
|
|
21
|
+
const hit = this.cache.get(mint);
|
|
22
|
+
if (hit && now - hit.at < this.ttlMs && (hit.creator || !creatorHint) && (hit.linked || !intel))
|
|
23
|
+
return hit;
|
|
24
|
+
const db = this.db() ?? null;
|
|
25
|
+
let creator = db ? creatorOf(db, mint) : null;
|
|
26
|
+
if (!creator && creatorHint && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(creatorHint))
|
|
27
|
+
creator = { mint, creator: creatorHint, source: 'jupiter-tokens', firstT: now, createdAt: null };
|
|
28
|
+
let report = null;
|
|
29
|
+
let linked = null;
|
|
30
|
+
if (creator && db) {
|
|
31
|
+
try {
|
|
32
|
+
report = queryDevHistory(db, { creator: creator.creator, mint, asOf: now });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
report = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (creator && intel)
|
|
39
|
+
linked = linkedFrom(intel, creator.creator, db);
|
|
40
|
+
const line = !db ? 'unknown: the history recorder is off (--no-recorder / DEXTERM_RECORDER=0)'
|
|
41
|
+
: !creator ? 'unknown: no source named the creator yet'
|
|
42
|
+
: report ? devHistoryLine(report) : 'unknown: dev history unreadable';
|
|
43
|
+
const e = { mint, creator, report, linked, line, at: now };
|
|
44
|
+
this.cache.set(mint, e);
|
|
45
|
+
if (this.cache.size > 2000)
|
|
46
|
+
this.cache.delete(this.cache.keys().next().value);
|
|
47
|
+
return e;
|
|
48
|
+
}
|
|
49
|
+
/** The `devrugs` rule field: labelled prior rugs by this coin's creator; null = unknown (clauses on it are false). */
|
|
50
|
+
rugsFor(mint) {
|
|
51
|
+
const e = this.forMint(mint);
|
|
52
|
+
return e.report && e.report.labelled.n > 0 ? e.report.labelled.rug : null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Linked wallets from a settled wallet-intel scan: the creator's funder and any multi-wallet cluster it sits in. */
|
|
56
|
+
export function linkedFrom(a, creator, db) {
|
|
57
|
+
const h = a.holders.find((x) => x.owner === creator);
|
|
58
|
+
const fundedBy = h?.funder?.address ?? a.funding[creator]?.funder ?? null;
|
|
59
|
+
const c = a.clusters.find((x) => x.members.includes(creator) && x.members.length > 1) ?? null;
|
|
60
|
+
let linkedCreators = 0, linkedRugs = 0;
|
|
61
|
+
if (c && db) {
|
|
62
|
+
try {
|
|
63
|
+
const others = c.members.filter((m) => m !== creator).slice(0, 50);
|
|
64
|
+
const launched = db.prepare('SELECT COUNT(*) AS n FROM creators WHERE creator = ?');
|
|
65
|
+
for (const w of others) {
|
|
66
|
+
if (Number(launched.get(w).n) <= 0)
|
|
67
|
+
continue;
|
|
68
|
+
linkedCreators++;
|
|
69
|
+
linkedRugs += queryDevHistory(db, { creator: w, devExit: false }).labelled.rug;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch { /* an old DB without the table: nothing linked */ }
|
|
73
|
+
}
|
|
74
|
+
return { fundedBy, cluster: c ? { name: c.name, kind: c.kind, members: c.members.length, pct: c.pct } : null, linkedCreators, linkedRugs, source: h || c || a.funding[creator] ? 'wallet-intel scan' : null };
|
|
75
|
+
}
|
|
76
|
+
/** Rows for the token screen's DEV block: [label, value, level]. Every value names its source/sample/age or says unknown. */
|
|
77
|
+
export function devRows(e) {
|
|
78
|
+
const rows = [];
|
|
79
|
+
const r = e.report;
|
|
80
|
+
const rugLevel = () => !r || r.labelled.n === 0 ? 'na' : r.serialRugger ? 'bad' : r.labelled.rug > 0 || (r.badRate ?? 0) >= 0.5 ? 'warn' : 'ok';
|
|
81
|
+
rows.push(['Dev rugs (recorded)', r && r.labelled.n ? `${r.labelled.rug}/${r.labelled.n} · n=${r.labelled.n}` : 'unknown', rugLevel()]);
|
|
82
|
+
rows.push(['Dev launches seen', r && r.status === 'ok' ? `${r.launches.n} prior · ${e.creator?.source === 'jupiter-tokens' ? 'jup' : e.creator?.source ?? ''}` : 'unknown', 'na']);
|
|
83
|
+
if (r && r.devExit.n)
|
|
84
|
+
rows.push(['Dev sold its bag', `median ${Math.round(r.devExit.medianMin)}m · n=${r.devExit.n}`, r.devExit.medianMin <= 60 ? 'warn' : 'ok']);
|
|
85
|
+
const l = e.linked;
|
|
86
|
+
if (l && (l.fundedBy || l.cluster))
|
|
87
|
+
rows.push(['Dev linked', l.cluster ? `${l.cluster.kind} ${l.cluster.members}w${l.linkedCreators ? ` · ${l.linkedCreators} devs ${l.linkedRugs} rugs` : ''}` : `funded by ${shortAddr(l.fundedBy)}`, l.linkedRugs > 0 ? 'bad' : 'na']);
|
|
88
|
+
return rows;
|
|
89
|
+
}
|
|
90
|
+
/** The app's one book (created on first use), reading the live recorder's DB. Null when there is no recorder. */
|
|
91
|
+
export function devBookOf(state) {
|
|
92
|
+
if (!state.live?.recorder)
|
|
93
|
+
return null;
|
|
94
|
+
if (!state.devBook) {
|
|
95
|
+
const live = state.live;
|
|
96
|
+
state.devBook = new DevHistoryBook(() => live?.recorder?.database ?? null);
|
|
97
|
+
}
|
|
98
|
+
return state.devBook;
|
|
99
|
+
}
|
|
@@ -20,6 +20,8 @@ export class NewPairsLive {
|
|
|
20
20
|
dl;
|
|
21
21
|
now;
|
|
22
22
|
onChange;
|
|
23
|
+
/** 0.1.5 dev history: called once per row when its creator becomes known (the recorder stores the link). */
|
|
24
|
+
onCreator;
|
|
23
25
|
limit;
|
|
24
26
|
polls;
|
|
25
27
|
rows = new Map();
|
|
@@ -84,6 +86,12 @@ export class NewPairsLive {
|
|
|
84
86
|
this.mode = 'replay';
|
|
85
87
|
return this.client.replay(rec, speed);
|
|
86
88
|
}
|
|
89
|
+
tellCreator(mint, creator, source, createdAt) {
|
|
90
|
+
try {
|
|
91
|
+
this.onCreator?.(mint, creator, source, createdAt);
|
|
92
|
+
}
|
|
93
|
+
catch { /* the recorder must never break the feed */ }
|
|
94
|
+
}
|
|
87
95
|
addCreate(c) {
|
|
88
96
|
const rowAt = this.now();
|
|
89
97
|
const existing = this.rows.get(c.mint);
|
|
@@ -105,6 +113,8 @@ export class NewPairsLive {
|
|
|
105
113
|
}
|
|
106
114
|
else {
|
|
107
115
|
this.rows.set(c.mint, row);
|
|
116
|
+
if (row.creator)
|
|
117
|
+
this.tellCreator(c.mint, row.creator, c.creator ? 'pumpportal' : 'rugcheck', row.createdAt);
|
|
108
118
|
this.latencies.push(row.latencyMs);
|
|
109
119
|
if (this.latencies.length > 500)
|
|
110
120
|
this.latencies.shift();
|
|
@@ -227,6 +237,8 @@ export class NewPairsLive {
|
|
|
227
237
|
const row = this.rows.get(t.mint);
|
|
228
238
|
if (!row)
|
|
229
239
|
continue;
|
|
240
|
+
if (!row.creator && t.creator)
|
|
241
|
+
this.tellCreator(t.mint, t.creator, 'rugcheck', row.createdAt);
|
|
230
242
|
row.creator = row.creator ?? t.creator;
|
|
231
243
|
if (t.mintAuthority !== null)
|
|
232
244
|
row.mintAuth = t.mintAuthority;
|
|
@@ -15,6 +15,7 @@ import { drawCoin, imagesOn } from "../../screens/coin.js";
|
|
|
15
15
|
import { COIN_HERO_W, COIN_HERO_H } from '../../../../tui/src/index.js';
|
|
16
16
|
import { renderSocialPanel } from "../../screens/social-panel.js";
|
|
17
17
|
import { launchIntelCells, holderTags } from "../intel.js";
|
|
18
|
+
import { devBookOf, devRows } from "../dev-history.js";
|
|
18
19
|
import { EX_POOL_TAGS } from '../../../../data/src/index.js';
|
|
19
20
|
import { layoutFor } from "../../custom.js";
|
|
20
21
|
import { chartMode, priceModeOf, mcapAt, positionPriceCells } from "../../display.js";
|
|
@@ -178,6 +179,12 @@ export function renderTokenLive(c, state, v) {
|
|
|
178
179
|
const top10Rows = tags.top10
|
|
179
180
|
? [['Top 10 ex-pool', `${tags.top10.exPool.toFixed(1)}%`, t10lvl(tags.top10.exPool)], ['Top 10 raw', `${tags.top10.raw.toFixed(1)}%`, 'na']]
|
|
180
181
|
: [['Top 10 holders', ...(v.top10 === null ? na(v.panels.holders.state === 'needs-key' ? 'needs key' : dash) : [`${v.top10.toFixed(1)}%${v.panels.holders.state === 'ok' ? '' : ' (jup)'}`, t10lvl(v.top10)])]];
|
|
182
|
+
// 0.1.5 dev history (task-1425): the creator's recorded track record from the local recorder (keyless). The rugs
|
|
183
|
+
// row sits with the other rug signals; launches / bag-exit / linked wallets follow the safety list.
|
|
184
|
+
const book = devBookOf(state);
|
|
185
|
+
const devAll = book ? devRows(book.forMint(v.mint, v.devAddress ?? null, state.live?.intel?.analysis(v.mint) ?? null)) : [];
|
|
186
|
+
const devHead = devAll.slice(0, 1);
|
|
187
|
+
const devTail = devAll.slice(1);
|
|
181
188
|
const sniperRow = [cells.snipers.text, cells.snipers.level];
|
|
182
189
|
const bundleRow = [cells.bundle.text, cells.bundle.level];
|
|
183
190
|
const rows = [
|
|
@@ -189,8 +196,10 @@ export function renderTokenLive(c, state, v) {
|
|
|
189
196
|
['Mint authority', ...(v.mintAuth === null ? na(dash) : [v.mintAuth ? 'ENABLED' : 'revoked', v.mintAuth ? 'bad' : 'ok'])],
|
|
190
197
|
['Freeze authority', ...(v.freezeAuth === null ? na(dash) : [v.freezeAuth ? 'ENABLED' : 'revoked', v.freezeAuth ? 'bad' : 'ok'])],
|
|
191
198
|
['Dev history', ...(v.devMints === null ? needsHistory : [`${v.devMints} mints by dev (jup)`, v.devMints > 3 ? 'warn' : 'ok'])],
|
|
199
|
+
...devHead,
|
|
192
200
|
['RugCheck score', ...(v.rug === null || v.rug.score === null ? na(v.panels.rugcheck.state === 'error' ? `unavailable: ${v.panels.rugcheck.reason ?? ''}`.trim() : dash) : [`${Math.round(v.rug.score)}/100`, v.rug.score < 30 ? 'ok' : v.rug.score < 60 ? 'warn' : 'bad'])],
|
|
193
201
|
['GoPlus', ...goplusRow(v)],
|
|
202
|
+
...devTail,
|
|
194
203
|
];
|
|
195
204
|
for (const [label, val0, lvl] of rows) {
|
|
196
205
|
if (sy >= sb.y + sb.h)
|
|
@@ -435,7 +444,8 @@ function analystPlaceholder(c, x, y, w, h, v, state) {
|
|
|
435
444
|
// task-1311 (QA M5): the pre-run state. Opening a token no longer spends a paid dossier; this panel
|
|
436
445
|
// waits until you ask for one with `a` (dossier) or type a question with `i`. No fake verdicts.
|
|
437
446
|
const t = c.theme;
|
|
438
|
-
const
|
|
447
|
+
const eng = state.analystEngine ?? 'claude'; // 0.1.5: the user's own CLI that will answer
|
|
448
|
+
const box = c.box(x, y, w, h, { title: `✦ ANALYST · ${eng}`, titleFg: t.ai, caption: `read-only · local ${eng} · on demand`, captionFg: t.dim, border: mix(t.border, t.ai, 0.35) });
|
|
439
449
|
const bg = box.bg;
|
|
440
450
|
const inner = box.w - 2;
|
|
441
451
|
let cy = box.y;
|
|
@@ -266,6 +266,7 @@ export class TokenLive {
|
|
|
266
266
|
v.top10 = audit?.top10Pct ?? v.top10;
|
|
267
267
|
v.dev = audit?.devPct ?? null;
|
|
268
268
|
v.devMints = audit?.devMints ?? null;
|
|
269
|
+
v.devAddress = audit?.devAddress ?? v.devAddress ?? null;
|
|
269
270
|
v.sources = [...new Set([...v.sources, ...r.sources])];
|
|
270
271
|
const SHORT = { dexscreener: 'ds', 'jupiter-tokens': 'jup', 'jupiter-price': 'jup-price' };
|
|
271
272
|
this.ok('market', r.sources.map((x) => SHORT[x] ?? x).join('+'), pair.fetchedAt, r.stale);
|
|
@@ -29,7 +29,9 @@ import { makeFixtureState, makeFeedState, syncFeed, renderFrame, isScreenKey, ac
|
|
|
29
29
|
import { mulberry32, tick, spawnPair, shock } from "./fixtures.js";
|
|
30
30
|
import { createLiveFeed, createReplayFeed } from "./feed.js";
|
|
31
31
|
import { LiveContext, INTERVALS, WATCH_REFRESH_MS, POSITIONS_REFRESH_MS, popupBubbleVM } from "./live/index.js";
|
|
32
|
+
import { FOLLOW_NAV_ACTIONS } from '../../mcp/src/index.js';
|
|
32
33
|
import { McpFollowSession } from "./mcp-follow.js";
|
|
34
|
+
import { AgentPaperSwitch } from "./agent-paper.js";
|
|
33
35
|
import { TOKEN_REFRESH_MS, CandlePrefetcher } from "./live/token.js";
|
|
34
36
|
import { PRESETS, presetSpec } from "./screener/presets.js";
|
|
35
37
|
import { parseSortArg, SORT_KEYS } from "./screener/sort.js";
|
|
@@ -46,6 +48,7 @@ import { copyAlertToDelivery } from "./alerts/format.js";
|
|
|
46
48
|
import { readTelegramCreds, readDiscordCreds } from "./alerts/creds.js";
|
|
47
49
|
import { launchIntelCells, launchRetryable } from "./live/intel.js";
|
|
48
50
|
import { AnalystSession } from "./analyst/session.js";
|
|
51
|
+
import { analystCliOnPath, parseEngineChoice, resolveEngine } from "./analyst/codex.js";
|
|
49
52
|
import { resolveAnalystResults } from "./analyst/results.js";
|
|
50
53
|
import { parseScanCommand, SCAN_FLAGS, checkScanFlag, SCAN_HELP } from "./scan/command.js";
|
|
51
54
|
import { parseWalletCommand, WALLET_HELP } from "./wallet-command.js";
|
|
@@ -101,6 +104,21 @@ export function appStateBlock(state) {
|
|
|
101
104
|
alerts, // booleans only — never a token, webhook or key
|
|
102
105
|
};
|
|
103
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* task-1443: the reduced screen snapshot a terminal following an EXTERNAL agent (`--spectate-mcp`) publishes for
|
|
109
|
+
* that agent's `dexterm mcp` server: what is on screen plus a VIEW-ONLY nav allow-list. Never the watchlist,
|
|
110
|
+
* tickets, positions or app settings, and only the four follow nav action types (no watchlist add, no ticket).
|
|
111
|
+
*/
|
|
112
|
+
export function followSnapshot(state) {
|
|
113
|
+
const ss = buildScreenState(state);
|
|
114
|
+
const a = ss.actions;
|
|
115
|
+
return {
|
|
116
|
+
screen: ss.screen, selected: ss.selected, selectedDetail: ss.selectedDetail, filter: ss.filter, sort: ss.sort,
|
|
117
|
+
visible: ss.visible, visibleMints: ss.visibleMints,
|
|
118
|
+
actions: { enabled: a.enabled, mints: a.mints, feeds: a.feeds, sortKeys: a.sortKeys, screens: a.screens, workspaces: [], themes: [], types: [...FOLLOW_NAV_ACTIONS] },
|
|
119
|
+
now: ss.now,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
104
122
|
export function buildScreenState(state) {
|
|
105
123
|
const live = state.live;
|
|
106
124
|
const sc = state.screener;
|
|
@@ -288,6 +306,9 @@ export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watch
|
|
|
288
306
|
--record FILE save every live response (and the PumpPortal session) to FILE (replay it with --replay)
|
|
289
307
|
--spectate-mcp follow an MCP agent's allowed navigation in this terminal; Ctrl+P pauses, Ctrl+K stops,
|
|
290
308
|
and any normal user keypress takes control (refused actions never move the UI)
|
|
309
|
+
--agent-paper MANDATE_ID (experimental, with --spectate-mcp) let that MCP agent PAPER-trade inside a mandate you
|
|
310
|
+
armed: simulated fills only, never signs or sends, never raises a cap; Ctrl+P / Ctrl+K /
|
|
311
|
+
any key also pause, stop or take over its paper trading
|
|
291
312
|
--max-requests N hard cap on live requests for this run (default: none)
|
|
292
313
|
--max-credits N hard cap on Helius credits for this run (default: 5000)
|
|
293
314
|
--mint MINT|LINK token to open on the token screen (a CA or a dexscreener / pump.fun / solscan / birdeye link)
|
|
@@ -302,6 +323,8 @@ export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watch
|
|
|
302
323
|
--selected N highlighted row / token index
|
|
303
324
|
--seed N fixture PRNG seed (default 7)
|
|
304
325
|
--no-ai hide the analyst panel on the token screen
|
|
326
|
+
--analyst-engine E auto | claude | codex: which of YOUR installed CLIs runs the analyst (default auto = claude
|
|
327
|
+
if installed, else codex; DEXTERM_ANALYST_ENGINE or config analyst.engine too; /engine in chat)
|
|
305
328
|
|
|
306
329
|
--intensity LEVEL off | normal | fever (default fever: rain, trails, glitch, blink, scanlines at 30 fps;
|
|
307
330
|
normal: 15 fps, sparser rain, no idle aberration; off = calm: neon theme, no motion)
|
|
@@ -389,14 +412,14 @@ export function parseArgs(argv) {
|
|
|
389
412
|
screen: null, fixtures: false, replay: null, record: null, filter: null, sort: null, preset: null, maxRequests: 0, maxCredits: 0, db: null,
|
|
390
413
|
mint: null, interval: '1h', listen: 20, verbose: false,
|
|
391
414
|
width: null, height: null, html: null, export: null, theme: DEFAULT_THEME, selected: 0, seed: 7,
|
|
392
|
-
noAi: false, title: null, plain: false, ansi: false, color: null, tz: null,
|
|
415
|
+
noAi: false, analystEngine: null, title: null, plain: false, ansi: false, color: null, tz: null,
|
|
393
416
|
calm: false, intensity: null, fps: null, frame: 0, t: 1, event: null, ai: null, mood: null, activity: null, noBoot: false, fit: null, fontPx: null, asciiRain: false, noImages: false,
|
|
394
417
|
scan: false, scanConfig: {},
|
|
395
418
|
noRecorder: false,
|
|
396
419
|
sound: false,
|
|
397
420
|
trade: [], live: true,
|
|
398
421
|
workspace: null, priceMode: null,
|
|
399
|
-
spectateMcp: false,
|
|
422
|
+
spectateMcp: false, agentPaper: null,
|
|
400
423
|
help: false, version: false, unknown: [],
|
|
401
424
|
};
|
|
402
425
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -485,6 +508,13 @@ export function parseArgs(argv) {
|
|
|
485
508
|
}
|
|
486
509
|
else if (k === '--no-ai')
|
|
487
510
|
a.noAi = true;
|
|
511
|
+
else if (k === '--analyst-engine') {
|
|
512
|
+
const s = str();
|
|
513
|
+
const e = parseEngineChoice(s);
|
|
514
|
+
if (!e)
|
|
515
|
+
throw new Error(`unknown analyst engine ${s} (expected auto|claude|codex)`);
|
|
516
|
+
a.analystEngine = e;
|
|
517
|
+
}
|
|
488
518
|
else if (k === '--calm')
|
|
489
519
|
a.calm = true;
|
|
490
520
|
else if (k === '--intensity') {
|
|
@@ -581,6 +611,13 @@ export function parseArgs(argv) {
|
|
|
581
611
|
// task-1421: follow an agent driving over MCP (the running terminal moves with it; a read-only stream opens).
|
|
582
612
|
else if (k === '--spectate-mcp')
|
|
583
613
|
a.spectateMcp = true;
|
|
614
|
+
// task-1443: agent PAPER trading inside a mandate the user armed (validated against the plans journal at start).
|
|
615
|
+
else if (k === '--agent-paper') {
|
|
616
|
+
const s = str();
|
|
617
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(s))
|
|
618
|
+
throw new Error(`--agent-paper expects a mandate id (see \`dexterm mandate list\`), got ${JSON.stringify(s.slice(0, 20))}`);
|
|
619
|
+
a.agentPaper = s;
|
|
620
|
+
}
|
|
584
621
|
else if (k === '--workspace') {
|
|
585
622
|
const s = str();
|
|
586
623
|
if (!/^[a-z][a-z0-9-]{0,39}(\/[a-z][a-z0-9-]{0,39})?$/.test(s))
|
|
@@ -618,6 +655,10 @@ export function parseArgs(argv) {
|
|
|
618
655
|
throw new Error('--scan needs live data or a recording (drop --fixtures)');
|
|
619
656
|
if (a.scan && a.screen)
|
|
620
657
|
throw new Error('--scan is interactive (drop --screen)');
|
|
658
|
+
if (a.agentPaper && !a.spectateMcp)
|
|
659
|
+
throw new Error('--agent-paper needs --spectate-mcp (every agent paper trade must be visible in this terminal)');
|
|
660
|
+
if (a.agentPaper && a.db === ':memory:')
|
|
661
|
+
throw new Error('--agent-paper needs an on-disk --db (the agent books into the same paper wallet)');
|
|
621
662
|
return a;
|
|
622
663
|
}
|
|
623
664
|
/**
|
|
@@ -1019,6 +1060,8 @@ export async function main(argv, io = process, proc = process) {
|
|
|
1019
1060
|
});
|
|
1020
1061
|
recorder.attach(feed.dl.store);
|
|
1021
1062
|
ctx.recorder = recorder;
|
|
1063
|
+
// 0.1.5 dev history: PumpPortal / RugCheck creators of new pairs go into the creator index too.
|
|
1064
|
+
ctx.newpairs.onCreator = (mint, creator, source, createdAt) => { recorder.recordCreator(mint, creator, source, undefined, createdAt); };
|
|
1022
1065
|
}
|
|
1023
1066
|
if (args.verbose)
|
|
1024
1067
|
io.stderr.write(`dexterm: rpc ${rpc.provider} (${rpc.basis}) · db ${dbPath} · recorder ${ctx.recorder ? 'on' : 'off'} · live trading ${args.live && !io.env.DEXTERM_OFFLINE ? 'available' : 'off'}\n`);
|
|
@@ -2639,6 +2682,50 @@ export function applyScanAnalystAction(state, raw) {
|
|
|
2639
2682
|
const res = validateAnalystAction(raw, { ...allow, types: SCAN_ACTION_TYPES });
|
|
2640
2683
|
return { ok: false, error: res.ok ? 'AI scan mode is observe-only' : res.error };
|
|
2641
2684
|
}
|
|
2685
|
+
/**
|
|
2686
|
+
* The analyst engine for a run (0.1.5): `--analyst-engine`, then DEXTERM_ANALYST_ENGINE, then config
|
|
2687
|
+
* `analyst.engine`, then auto (claude when installed, else codex). Only the user picks it; models cannot.
|
|
2688
|
+
*/
|
|
2689
|
+
export function analystEngineFor(flag, env, configured, has = (b) => analystCliOnPath(b, env)) {
|
|
2690
|
+
const choice = parseEngineChoice(flag) ?? parseEngineChoice(env.DEXTERM_ANALYST_ENGINE) ?? parseEngineChoice(configured) ?? 'auto';
|
|
2691
|
+
return resolveEngine(choice, has);
|
|
2692
|
+
}
|
|
2693
|
+
/** `/engine [auto|claude|codex]` in the chat box (0.1.5). Null when the input is not this command. */
|
|
2694
|
+
export function parseEngineCommand(q) {
|
|
2695
|
+
const m = /^\/engine(?:\s+(\S+))?\s*$/i.exec(q.trim());
|
|
2696
|
+
if (!m)
|
|
2697
|
+
return null;
|
|
2698
|
+
if (!m[1])
|
|
2699
|
+
return { show: true };
|
|
2700
|
+
const c = parseEngineChoice(m[1]);
|
|
2701
|
+
return c ? { choice: c } : { error: `unknown analyst engine "${m[1].slice(0, 16)}" (use /engine auto|claude|codex)` };
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Apply `/engine`: switch the CLI for NEW conversations this run (cached per-coin chats are dropped so the next
|
|
2705
|
+
* `a` starts on the chosen engine; a running turn finishes where it started). Returns the line to show.
|
|
2706
|
+
*/
|
|
2707
|
+
export function engineCommand(state, cmd, env, has = (b) => analystCliOnPath(b, env)) {
|
|
2708
|
+
const installed = ['claude', 'codex'].filter((b) => has(b));
|
|
2709
|
+
const list = installed.length ? installed.join(' + ') : 'neither claude nor codex';
|
|
2710
|
+
if ('error' in cmd)
|
|
2711
|
+
return cmd.error;
|
|
2712
|
+
const cur = state.analystEngine ?? 'claude';
|
|
2713
|
+
if ('show' in cmd)
|
|
2714
|
+
return `analyst engine: ${cur} · installed: ${list} · /engine claude|codex|auto switches (config analyst.engine keeps it)`;
|
|
2715
|
+
const next = resolveEngine(cmd.choice, has);
|
|
2716
|
+
if (cmd.choice !== 'auto' && !has(next))
|
|
2717
|
+
return `${next} is not installed (installed: ${list}); ${next === 'codex' ? 'npm i -g @openai/codex && codex login' : 'install Claude Code, then run claude once to log in'}`;
|
|
2718
|
+
if (next !== cur) {
|
|
2719
|
+
for (const [k, sess] of state.analystByMint ?? []) {
|
|
2720
|
+
if (!sess.busy)
|
|
2721
|
+
state.analystByMint.delete(k);
|
|
2722
|
+
}
|
|
2723
|
+
if (state.analyst && !state.analyst.busy && state.analyst.engine !== next)
|
|
2724
|
+
state.analyst = null;
|
|
2725
|
+
}
|
|
2726
|
+
state.analystEngine = next;
|
|
2727
|
+
return `analyst engine: ${next} (your own ${next} CLI and login) for new conversations · set analyst.engine in config.json to keep it`;
|
|
2728
|
+
}
|
|
2642
2729
|
/**
|
|
2643
2730
|
* The ONE way a scan-driven analyst conversation is built (task-1324; the loop and the safety tests both
|
|
2644
2731
|
* use it): the locked coin as the selected coin, `actionTypes: []` (observe-only at the validator, the
|
|
@@ -2652,7 +2739,7 @@ export function createScanSession(state, mint, symbol, hooks = {}, deps = {}) {
|
|
|
2652
2739
|
const type = typeof raw.type === 'string' ? raw.type.replace(/[^a-z_]/gi, '').slice(0, 24) : '?';
|
|
2653
2740
|
hooks.onBlocked?.(type, r.error ?? 'refused');
|
|
2654
2741
|
};
|
|
2655
|
-
return new AnalystSession({ skill: 'token-dossier', mint, symbol, screenState: ss, onAction, actionTypes: SCAN_ACTION_TYPES, scan: true }, hooks.onChange ?? (() => { }), deps);
|
|
2742
|
+
return new AnalystSession({ skill: 'token-dossier', mint, symbol, screenState: ss, onAction, actionTypes: SCAN_ACTION_TYPES, scan: true, ...(state.analystEngine === 'codex' ? { engine: 'codex' } : {}) }, hooks.onChange ?? (() => { }), deps);
|
|
2656
2743
|
}
|
|
2657
2744
|
/**
|
|
2658
2745
|
* Resolve a mint to an openable seed from what the app already knows (screener view → new pairs →
|
|
@@ -3476,7 +3563,14 @@ export function terminalGuard(io, onFatal = () => { }, proc = process, hooks = {
|
|
|
3476
3563
|
proc.exit(1);
|
|
3477
3564
|
};
|
|
3478
3565
|
const onExit = () => restore();
|
|
3479
|
-
const onSignal = (sig) => {
|
|
3566
|
+
const onSignal = (sig) => {
|
|
3567
|
+
restore();
|
|
3568
|
+
try {
|
|
3569
|
+
hooks.onSignal?.();
|
|
3570
|
+
}
|
|
3571
|
+
catch { /* cleanup is best-effort; the exit still happens */ }
|
|
3572
|
+
proc.exit(sig === 'SIGTERM' ? 143 : 129);
|
|
3573
|
+
};
|
|
3480
3574
|
const onTerm = () => onSignal('SIGTERM');
|
|
3481
3575
|
const onHup = () => onSignal('SIGHUP');
|
|
3482
3576
|
let installed = false;
|
|
@@ -3513,6 +3607,8 @@ export function fpsFor(args, intensity) {
|
|
|
3513
3607
|
const SYNC_ON = '\x1b[?2026h';
|
|
3514
3608
|
const SYNC_OFF = '\x1b[?2026l';
|
|
3515
3609
|
export function interactive(state, io, depth, args, proc = process) {
|
|
3610
|
+
// 0.1.5: the analyst engine for new conversations (the user's own claude or codex CLI; never a shipped key).
|
|
3611
|
+
state.analystEngine ??= analystEngineFor(args.analystEngine, io.env, state.custom?.config.analyst?.engine);
|
|
3516
3612
|
const out = io.stdout;
|
|
3517
3613
|
const inp = io.stdin;
|
|
3518
3614
|
const rnd = mulberry32(99);
|
|
@@ -3816,6 +3912,8 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
3816
3912
|
// task-1421: the MCP follow session (set below when `--spectate-mcp` is on). Declared here so the quit /
|
|
3817
3913
|
// terminal-guard teardown closures can stop it.
|
|
3818
3914
|
let followSession = null;
|
|
3915
|
+
// task-1443: `--agent-paper <mandate-id>` (with --spectate-mcp) lets that agent PAPER-trade inside the mandate.
|
|
3916
|
+
const agentPaper = args.spectateMcp && args.agentPaper ? new AgentPaperSwitch({ env: io.env, mandateId: args.agentPaper, db: args.db ?? defaultDbPath(io.env) }) : null;
|
|
3819
3917
|
// Idle economy (task-1303, retuned for task-1319 AC3): between events the ambient rain redraws at
|
|
3820
3918
|
// IDLE_FPS instead of the full frame cap, cutting idle CPU. A keypress opens a KEY_WAKE_S full-rate
|
|
3821
3919
|
// window (`wakeUntil`) so input and transitions stay smooth; data landings paint at once on their own
|
|
@@ -3844,10 +3942,13 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
3844
3942
|
say(`trade moment failed: ${describeError(e)}`, 'warn');
|
|
3845
3943
|
}
|
|
3846
3944
|
};
|
|
3847
|
-
const
|
|
3848
|
-
clearInterval(timer); void followSession?.stop(); state.analyst?.cancel(); if (state.analystByMint)
|
|
3945
|
+
const teardown = () => { done = true; prefetcher?.close(); if (timer)
|
|
3946
|
+
clearInterval(timer); void followSession?.stop(); agentPaper?.stop(); state.analyst?.cancel(); if (state.analystByMint)
|
|
3849
3947
|
for (const s of state.analystByMint.values())
|
|
3850
|
-
s.cancel(); state.popups?.shutdown(); voice?.disable(); images?.close(); live?.close(); feed?.close(); }
|
|
3948
|
+
s.cancel(); state.popups?.shutdown(); voice?.disable(); images?.close(); live?.close(); feed?.close(); };
|
|
3949
|
+
// task-1443: SIGTERM/SIGHUP (a closed terminal window) runs the same teardown as a fatal error, so the agent-paper
|
|
3950
|
+
// rendezvous, the spectator rendezvous and the --record file never outlive the terminal that owns them.
|
|
3951
|
+
const guard = terminalGuard(io, teardown, proc, { beforeLeave: () => layer?.teardown() ?? '', onSignal: teardown });
|
|
3851
3952
|
let lastTokenRefresh = 0;
|
|
3852
3953
|
let lastWatchRefresh = 0;
|
|
3853
3954
|
let lastPositionsRefresh = 0;
|
|
@@ -4043,9 +4144,13 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4043
4144
|
},
|
|
4044
4145
|
onStatus: (line) => { say(line, 'ai', 4); scheduleDraw(); },
|
|
4045
4146
|
onEvent: () => scheduleDraw(),
|
|
4147
|
+
onControl: agentPaper ? (st) => agentPaper.control(st) : undefined,
|
|
4148
|
+
snapshot: () => followSnapshot(state),
|
|
4046
4149
|
});
|
|
4047
4150
|
void followSession.start();
|
|
4048
4151
|
say('following agents over MCP · ctrl+p pause · ctrl+k stop · any key takes over', 'ai', 6);
|
|
4152
|
+
if (agentPaper)
|
|
4153
|
+
say(agentPaper.start(), 'ai', 8);
|
|
4049
4154
|
}
|
|
4050
4155
|
// Launch a real Claude analyst session when a key asked for one (applyAction cannot spawn). A
|
|
4051
4156
|
// `question` means start it as a chat turn, not a paid structured skill run (task-1311, QA B3).
|
|
@@ -4113,7 +4218,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4113
4218
|
maybeSpeakVoiceAnswer(session); // task-1282: speak a voice-initiated answer when it settles
|
|
4114
4219
|
scheduleDraw();
|
|
4115
4220
|
};
|
|
4116
|
-
const session = new AnalystSession({ skill: p.skill, mint: p.mint, symbol: p.symbol, screenState: buildScreenState(state), model: p.model, onAction }, onChange);
|
|
4221
|
+
const session = new AnalystSession({ skill: p.skill, mint: p.mint, symbol: p.symbol, screenState: buildScreenState(state), model: p.model, onAction, ...(state.analystEngine === 'codex' ? { engine: 'codex' } : {}) }, onChange);
|
|
4117
4222
|
if (p.question)
|
|
4118
4223
|
session.ask(p.question, buildScreenState(state));
|
|
4119
4224
|
else
|
|
@@ -5037,6 +5142,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5037
5142
|
if (timer)
|
|
5038
5143
|
clearInterval(timer);
|
|
5039
5144
|
void followSession?.stop();
|
|
5145
|
+
agentPaper?.stop();
|
|
5040
5146
|
guard.restore();
|
|
5041
5147
|
guard.uninstall();
|
|
5042
5148
|
cancelAllAnalysts();
|
|
@@ -5635,6 +5741,14 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5635
5741
|
draw();
|
|
5636
5742
|
continue;
|
|
5637
5743
|
}
|
|
5744
|
+
// 0.1.5: `/engine [auto|claude|codex]` switches the analyst CLI for new conversations; never sent to a model.
|
|
5745
|
+
const en0 = q.startsWith('/') ? parseEngineCommand(q) : null;
|
|
5746
|
+
if (en0) {
|
|
5747
|
+
state.aiInput = '';
|
|
5748
|
+
say(engineCommand(state, en0, io.env), typeof en0 === 'object' && 'error' in en0 ? 'warn' : 'info', 6);
|
|
5749
|
+
draw();
|
|
5750
|
+
continue;
|
|
5751
|
+
}
|
|
5638
5752
|
// task-1324: `/scan …` in the chat input is a command, never sent to claude.
|
|
5639
5753
|
const sc0 = q.startsWith('/') ? parseScanCommand(q) : null;
|
|
5640
5754
|
if (sc0) {
|