dexterm-cli 0.1.4 → 0.1.6
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/index.js +2 -0
- package/dist/packages/agents/src/spectator.js +54 -0
- package/dist/packages/agents/src/story.js +523 -0
- package/dist/packages/agents/src/tiers.js +1 -0
- package/dist/packages/agents/src/tools.js +1 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +1 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +15 -8
- package/dist/packages/cli/src/agent-cmd.js +6 -4
- package/dist/packages/cli/src/agent-paper.js +60 -0
- package/dist/packages/cli/src/agent-spectator.js +17 -1
- package/dist/packages/cli/src/analyst/backend.js +206 -0
- package/dist/packages/cli/src/analyst/codex.js +301 -0
- package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
- package/dist/packages/cli/src/analyst/session.js +21 -3
- package/dist/packages/cli/src/app-help-gen.js +21 -12
- package/dist/packages/cli/src/backtest-cmd.js +13 -2
- package/dist/packages/cli/src/display.js +55 -9
- package/dist/packages/cli/src/doctor.js +58 -2
- package/dist/packages/cli/src/live/dev-history.js +115 -0
- package/dist/packages/cli/src/live/index.js +5 -1
- package/dist/packages/cli/src/live/newpairs.js +12 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
- package/dist/packages/cli/src/live/screens/token-live.js +51 -11
- package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
- package/dist/packages/cli/src/live/token.js +1 -0
- package/dist/packages/cli/src/live/trade.js +22 -0
- package/dist/packages/cli/src/main.js +267 -19
- package/dist/packages/cli/src/mcp-follow.js +137 -10
- package/dist/packages/cli/src/plan-cmd.js +15 -5
- package/dist/packages/cli/src/popup.js +17 -1
- package/dist/packages/cli/src/record-cmd.js +2 -0
- package/dist/packages/cli/src/rules.js +25 -2
- package/dist/packages/cli/src/scan/ui.js +7 -1
- package/dist/packages/cli/src/screens/agent-hud.js +741 -0
- package/dist/packages/cli/src/screens/analyst-session.js +9 -3
- package/dist/packages/cli/src/screens/analyst.js +4 -3
- package/dist/packages/cli/src/screens/brain.js +608 -0
- package/dist/packages/cli/src/screens/newpairs.js +1 -1
- package/dist/packages/cli/src/screens/pnl-chip.js +143 -0
- package/dist/packages/cli/src/screens/screener.js +1 -0
- package/dist/packages/cli/src/screens/token.js +16 -7
- package/dist/packages/cli/src/screens/trending.js +1 -1
- package/dist/packages/cli/src/state.js +69 -5
- 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 +13 -3
- package/dist/packages/data/src/fixtures.js +28 -6
- package/dist/packages/data/src/index.js +5 -0
- package/dist/packages/data/src/larp/score.js +220 -0
- package/dist/packages/data/src/larp/x-account.js +316 -0
- package/dist/packages/data/src/sources/jupiter.js +1 -0
- package/dist/packages/data/src/trade/agent/paper-desk.js +401 -0
- package/dist/packages/data/src/trade/agent/rendezvous.js +161 -0
- package/dist/packages/data/src/trade/plans/engine.js +7 -0
- package/dist/packages/data/src/trade/plans/mandate.js +28 -15
- package/dist/packages/data/src/trade/plans/service.js +1 -1
- 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/larp-check.md +5 -1
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.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 +4 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +2 -1
- 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 +51 -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-starting-a-run.md +2 -0
- 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-chart-y-axis.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +3 -3
- 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-running-the-tests.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 +1 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +60 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +0 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +4 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp.md +1 -1
- 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-order-plans-and-standing-mandates.md +1 -9
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +41 -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-popup-chart-market-cap-axis.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +47 -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-spectator-limits.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-agent-hud-spectator-v2.md +58 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +46 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator.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 +93 -0
- package/dist/packages/mcp/src/server.js +354 -19
- package/dist/packages/mcp/src/spectator-sink.js +158 -4
- package/dist/packages/mcp/src/user-content.js +1 -0
- package/dist/packages/packs/src/index.js +1 -0
- package/dist/packages/packs/src/larp-score-backtest.js +119 -0
- package/dist/packages/popup/src/pages/chart-lib.js +36 -15
- package/dist/packages/popup/src/pages/chart.css +9 -0
- package/dist/packages/popup/src/pages/chart.html +4 -1
- package/dist/packages/popup/src/pages/chart.js +34 -5
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +1 -0
- package/dist/packages/popup/src/pages/spectator.js +69 -0
- package/dist/packages/popup/src/protocol.js +2 -0
- package/dist/packages/popup/src/server.js +5 -1
- package/dist/packages/recorder/src/dev-history.js +133 -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/dist/packages/tui/src/canvas.js +7 -0
- package/dist/packages/tui/src/fmt.js +53 -0
- package/dist/packages/tui/src/keymap.js +4 -0
- package/dist/packages/tui/src/widgets.js +9 -4
- package/package.json +1 -1
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: larp-check
|
|
3
3
|
description: Vet the website and socials behind a Solana token to judge whether there is a real product or it is a LARP (claims a product that does not exist). Use when the user asks "is this real", "larp check", "is the product real", "check the website/socials".
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
argument-hint: <mint or symbol>
|
|
6
6
|
kind: skill
|
|
7
7
|
output_schema: schemas/larp.schema.json
|
|
8
8
|
tools:
|
|
9
9
|
- screen_state
|
|
10
10
|
- get_token
|
|
11
|
+
- larp_check
|
|
11
12
|
- get_links
|
|
12
13
|
- fetch_page
|
|
13
14
|
- check_domain
|
|
14
15
|
- check_github
|
|
16
|
+
- check_x
|
|
15
17
|
- kb_lookup
|
|
16
18
|
arguments:
|
|
17
19
|
- target
|
|
@@ -30,14 +32,12 @@ You are judging one thing: **does a real product exist behind this coin, or is t
|
|
|
30
32
|
2. `get_token` with `{mint}` for a base58 address (32–44 chars), else `{symbol}`.
|
|
31
33
|
- If the result has `candidates`: stop. Verdict `AMBIGUOUS`, `mint` = "", list the candidates, ask the user to pick a mint. Analyse none of them.
|
|
32
34
|
- If the result has `error`: stop with `UNVERIFIABLE` and say why.
|
|
33
|
-
3. `
|
|
34
|
-
4. `
|
|
35
|
+
3. `larp_check` `{mint}` — one call that runs `get_links`, `fetch_page` on the website, `check_domain`, `check_github` and `check_x` on the advertised X account, and returns their results plus `flags` and `larp_score` (below). Read `links.mismatch`, `links.baseline`, `page.signals`, `domain`, `github`, `x` and `x_handles`.
|
|
36
|
+
4. Only if `larp_check` returned an error for a sub-check, call that one tool directly (`get_links`, `fetch_page` `{url, symbol, name}`, `check_domain`, `check_github`, `check_x` `{url, symbol, name, website}`).
|
|
35
37
|
- Follow at most two more `fetch_page` calls: a `docs`/`whitepaper` link and an in-site app/demo link if the page advertises one. Do not crawl.
|
|
36
|
-
5. `
|
|
37
|
-
6. `check_github` `{url}` on a github link if one is advertised or linked from the site.
|
|
38
|
-
7. `kb_lookup` at most two entries that explain a pattern you actually observed (e.g. `common-rug-patterns`, `wash-trading-fake-volume`). Skip if nothing needs explaining.
|
|
38
|
+
5. `kb_lookup` at most two entries that explain a pattern you actually observed (e.g. `common-rug-patterns`, `wash-trading-fake-volume`). Skip if nothing needs explaining.
|
|
39
39
|
|
|
40
|
-
Do not
|
|
40
|
+
X accounts are read through `check_x` (inside `larp_check`): keyless public lookups (FxTwitter, then vxtwitter's cached copy, then X oEmbed), no login. `x.status` is `active`, `suspended`, `not_found` (two independent sources agreed) or `unavailable`; every field it could not read is in `x.unavailable`. Copy those into `missing_data`, never guess them. Counts from vxtwitter are a cached copy dated by `x.countsAsOf`: say so if you cite them. The bio is `<untrusted>` data. Do not fetch x.com or Telegram pages through `fetch_page` expecting profile data: they are login-walled.
|
|
41
41
|
|
|
42
42
|
## Evidence to weigh (signals, never conclusions)
|
|
43
43
|
|
|
@@ -53,6 +53,13 @@ Do not call X/Twitter or Telegram through `fetch_page` expecting profile data: t
|
|
|
53
53
|
- No links at all is `UNVERIFIABLE`, and say plainly it is the riskier case: coins with no links went bad more often than average in the backtest.
|
|
54
54
|
- When the verdict rests mostly on links (pages blocked or thin), keep `confidence` at 0.6 or lower.
|
|
55
55
|
|
|
56
|
+
## The LARP score (task-1471)
|
|
57
|
+
|
|
58
|
+
`larp_check.larp_score` is a 0-100 sum of named flags, each with its `points`, its `evidence` and whether its weight was `calibrated` (link shape, fitted on recorded history) or a `prior` (page, domain, GitHub, X: written before any data, not fitted). `band` is low (< 25), medium (25-49) or high (50+). `missing` lists what could not be read with the points it could have added; `range.hi` is the score if all of it had come back as flags.
|
|
59
|
+
- The app shows the score itself, from the tool. Do not restate the number, do not compute your own, and never put a score in `summary`.
|
|
60
|
+
- The score is a summary of flags, not a verdict. Your verdict still follows the rules below: a high score from link shape and X flags alone is not `LARP`; a low score with a wide `range` is not a clean bill. Name the top one or two flags by their evidence in `summary` when they matter.
|
|
61
|
+
- Measured on held-out coins, the link-shape part ranks coins that went bad above the rest only modestly (AUROC about 0.69, `larp_score.measured`); say "weak signal" if you lean on it.
|
|
62
|
+
|
|
56
63
|
## Grounding rules (hard)
|
|
57
64
|
|
|
58
65
|
- Every claim, number, date, boolean or label you state comes from a tool result in this session. Put each in `facts[]` with the `tool`, the `field` path, and the `value` verbatim. If you cannot point at a tool field, do not state it.
|
|
@@ -72,4 +79,4 @@ Do not call X/Twitter or Telegram through `fetch_page` expecting profile data: t
|
|
|
72
79
|
|
|
73
80
|
## Output
|
|
74
81
|
|
|
75
|
-
Return only the JSON object required by the schema (`skill: "larp-check"`, `skill_version: "0.
|
|
82
|
+
Return only the JSON object required by the schema (`skill: "larp-check"`, `skill_version: "0.3.0"`). Fill `claims[]`, `facts[]` and `missing_data[]` completely. `summary` is two to four short lines for a 40-column panel: the verdict in plain words, the one or two strongest pieces of evidence (each with its source), and what was missing only if it matters. Do not repeat the confidence number (the app renders it), and add no disclaimer or boilerplate line.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// config; it is never stored in plaintext and never echoed again. The token for `serve`/`connect` auth is
|
|
16
16
|
// read ONLY from the $DEXTERM_AGENT_TOKEN environment variable — never a command-line flag — so it cannot
|
|
17
17
|
// leak through a process list. No agent path can arm, sign, send, raise a cap or touch a wallet/mandate.
|
|
18
|
-
import { AgentRegistry, SqliteAuditLog, MemoryProposalStore, SqliteProposalStore, AgentBus, FixtureExecutor, FixtureFeed, SpectatorHub, SpectatorServer, SpectatorRecorder, FollowController, TIERS, isTier, } from '../../agents/src/index.js';
|
|
18
|
+
import { AgentRegistry, SqliteAuditLog, MemoryProposalStore, SqliteProposalStore, AgentBus, FixtureExecutor, FixtureFeed, SpectatorHub, SpectatorServer, SpectatorRecorder, FollowController, AgentStory, TIERS, isTier, } from '../../agents/src/index.js';
|
|
19
19
|
import { connectAgent } from '../../agents/src/sdk.js';
|
|
20
20
|
import { defaultDbPath } from '../../data/src/index.js';
|
|
21
21
|
function flag(rest, name) { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : undefined; }
|
|
@@ -321,13 +321,15 @@ async function serve(rest, io, registry) {
|
|
|
321
321
|
const { THEMES, DEFAULT_THEME } = await import('../../tui/src/index.js');
|
|
322
322
|
const { popupThemeFrom } = await import("./popup.js");
|
|
323
323
|
const follow = new FollowController({ controls: bus, activityCap: 200 });
|
|
324
|
-
|
|
324
|
+
// task-1466: the agent story (phase rail, heartbeat, timeline, thoughts) the popup draws above the activity.
|
|
325
|
+
const story = new AgentStory();
|
|
326
|
+
const off = hub.on((ev) => { follow.apply(ev); story.apply(ev); });
|
|
325
327
|
const mgr = createPopupManager({ theme: popupThemeFrom(THEMES[DEFAULT_THEME]), env: io.env, onWarn: (m) => io.stderr.write(`spectator popup: ${m}\n`),
|
|
326
328
|
onEvent: (ev) => { if (ev.ev === 'popup.event' && ev.name.startsWith('spectator.'))
|
|
327
329
|
io.stdout.write(`spectator: ${applySpectatorEvent(follow, ev.name, String(ev.payload.agent))}\n`); } });
|
|
328
|
-
const handle = await mgr.open({ kind: 'spectator', title: 'watch the agent cook', data: buildSpectatorVM(follow) });
|
|
330
|
+
const handle = await mgr.open({ kind: 'spectator', title: 'watch the agent cook', data: buildSpectatorVM(follow, { story: story.view(Date.now()) }) });
|
|
329
331
|
const timer = setInterval(() => { try {
|
|
330
|
-
mgr.setData(handle.id, buildSpectatorVM(follow));
|
|
332
|
+
mgr.setData(handle.id, buildSpectatorVM(follow, { story: story.view(Date.now()) }));
|
|
331
333
|
}
|
|
332
334
|
catch { /* popup closed */ } }, 1000);
|
|
333
335
|
timer.unref?.();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// `dexterm --spectate-mcp --agent-paper <mandate-id>` — the app half of agent PAPER trading (task-1443).
|
|
2
|
+
//
|
|
3
|
+
// The user arms a mandate by keypress (plans screen / `dexterm mandate arm`), then starts the terminal with the
|
|
4
|
+
// spectator view and names that mandate. This switch checks the mandate is armed and alive in the hash-chained
|
|
5
|
+
// plans journal, then writes the ~/.dexterm rendezvous (wallet DB + mandate ID, never the caps) and a `running`
|
|
6
|
+
// control state. The external `dexterm mcp` server replays the caps from the journal itself on every call, so
|
|
7
|
+
// nothing written here can raise a cap. The follow controls map onto the control file: Ctrl+P pause/resume,
|
|
8
|
+
// Ctrl+K stop (sticky for the rest of the run), any key while the agent drives = take over. Quitting removes
|
|
9
|
+
// both files, so agent paper trading never outlives the terminal that enabled it. Paper only: nothing here can
|
|
10
|
+
// reach a wallet, a key, the signer or a send path.
|
|
11
|
+
import { writePaperAgentRendezvous, clearPaperAgentRendezvous, writeDeskControl, loadMandateFromJournal, plansJournalPathFor, systemClock, AGENT_TIER, } from '../../data/src/index.js';
|
|
12
|
+
import { mandateAlive, TIERS } from '../../data/src/trade/plans/index.js';
|
|
13
|
+
export class AgentPaperSwitch {
|
|
14
|
+
opts;
|
|
15
|
+
enabled = false;
|
|
16
|
+
stopped = false;
|
|
17
|
+
constructor(opts) { this.opts = opts; }
|
|
18
|
+
/** Enable agent paper trading if the mandate is armed and alive. Returns a one-line status for the toast. */
|
|
19
|
+
start() {
|
|
20
|
+
const { env, mandateId } = this.opts;
|
|
21
|
+
const r = loadMandateFromJournal(plansJournalPathFor(env), mandateId, systemClock);
|
|
22
|
+
if (!r.mandate)
|
|
23
|
+
return `agent paper trading OFF: ${'reason' in r ? r.reason : 'no mandate'}`;
|
|
24
|
+
const alive = mandateAlive(r.mandate, (this.opts.now ?? Date.now)());
|
|
25
|
+
if (!alive.ok)
|
|
26
|
+
return `agent paper trading OFF: mandate ${mandateId} is not usable (${alive.reason}) — arm it first`;
|
|
27
|
+
try {
|
|
28
|
+
writePaperAgentRendezvous({ db: this.opts.db, mandateId }, env);
|
|
29
|
+
this.enabled = true;
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return `agent paper trading OFF: could not write the rendezvous (${e instanceof Error ? e.message : String(e)})`;
|
|
33
|
+
}
|
|
34
|
+
const b = r.mandate.budget;
|
|
35
|
+
const agentPerTrade = b.perTradeUsd * TIERS[AGENT_TIER].perTradeMultiplier;
|
|
36
|
+
return `agent PAPER trading ON (experimental, simulated only) · mandate ${mandateId} · agent cap $${agentPerTrade.toFixed(2)}/trade, $${b.totalUsd} total · ctrl+p pause · ctrl+k stop`;
|
|
37
|
+
}
|
|
38
|
+
/** Map a follow control onto the agent's paper desk. A stop is sticky: only a restart re-enables it. */
|
|
39
|
+
control(state) {
|
|
40
|
+
if (!this.enabled)
|
|
41
|
+
return;
|
|
42
|
+
if (state === 'stopped')
|
|
43
|
+
this.stopped = true;
|
|
44
|
+
try {
|
|
45
|
+
writeDeskControl({ state: this.stopped ? 'stopped' : state, reason: 'user' }, this.opts.env);
|
|
46
|
+
}
|
|
47
|
+
catch { /* best-effort; the desk fails closed on a missing file */ }
|
|
48
|
+
}
|
|
49
|
+
get isEnabled() { return this.enabled; }
|
|
50
|
+
/** Disable on quit: remove the rendezvous + control file (the MCP tools then refuse). Idempotent. */
|
|
51
|
+
stop() {
|
|
52
|
+
if (!this.enabled)
|
|
53
|
+
return;
|
|
54
|
+
this.enabled = false;
|
|
55
|
+
try {
|
|
56
|
+
clearPaperAgentRendezvous(this.opts.env);
|
|
57
|
+
}
|
|
58
|
+
catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
// task-1406 "watch the agent cook": bridge the follow-mode controller to the spectator popup VM, and map
|
|
2
2
|
// the popup's spectator.* UI events onto the controller's user-side controls. Nothing here can arm, sign
|
|
3
3
|
// or raise a cap — the controls are exactly the bus's pause/resume/kick, the same as the roster popup.
|
|
4
|
-
import { redactSpectatorEvent, maskAddress } from '../../agents/src/index.js';
|
|
4
|
+
import { redactSpectatorEvent, redactString, maskAddress, STORY_RAIL } from '../../agents/src/index.js';
|
|
5
|
+
/** task-1466: the story for the popup, every string redacted (addresses masked, keys removed). */
|
|
6
|
+
export function storyVM(v, redact = true) {
|
|
7
|
+
const r = (s) => (redact ? redactString(s) : s);
|
|
8
|
+
const a = v.arc?.stage === 'closed' || !v.arc ? (v.arc ?? v.lastResult) : v.arc;
|
|
9
|
+
return {
|
|
10
|
+
phase: v.phase, rail: [...STORY_RAIL], sub: v.sub, bpm: v.bpm, control: v.control, calls: v.calls,
|
|
11
|
+
thoughts: v.thoughts.slice(-4).map((x) => ({ t: x.t, text: r(x.text), source: x.source })),
|
|
12
|
+
nodes: v.nodes.slice(-24).map((n) => ({ t: n.t, label: r(n.label), ms: n.ms, status: n.status, paper: n.paper, phase: n.phase, count: n.count })),
|
|
13
|
+
focus: v.focus ? { symbol: r(v.focus.symbol), checks: [...v.focus.checks], verdict: v.focus.verdict?.label ?? null, rug: v.focus.verdict?.rug ?? null, larpFlags: v.focus.larp?.flags ?? null } : null,
|
|
14
|
+
arc: a ? {
|
|
15
|
+
symbol: r(a.symbol), stage: a.stage, sizeUsd: a.sizeUsd, entryUsd: a.entryUsd, markUsd: a.mark?.priceUsd ?? null, pnlPct: a.pnlPct, peakPct: a.peakPct,
|
|
16
|
+
exits: a.fills.filter((f) => f.side === 'sell').length, realizedSol: a.result?.realizedSol ?? null, resultPct: a.result?.pct ?? null, refusal: a.refusal ? r(a.refusal) : null,
|
|
17
|
+
} : null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
5
20
|
function activityVM(ev) {
|
|
6
21
|
return {
|
|
7
22
|
seq: ev.seq, t: ev.t, phase: ev.phase, agent: ev.agent, tier: ev.tier ?? null,
|
|
@@ -16,6 +31,7 @@ export function buildSpectatorVM(follow, opts = {}) {
|
|
|
16
31
|
const s = v.screen;
|
|
17
32
|
const mask = (x) => (x && redact ? maskAddress(x) : x);
|
|
18
33
|
return {
|
|
34
|
+
...(opts.story ? { story: storyVM(opts.story, redact) } : {}),
|
|
19
35
|
title: opts.title ?? 'watch the agent cook',
|
|
20
36
|
now: Date.now(),
|
|
21
37
|
redacted: redact,
|
|
@@ -15,6 +15,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
import { ACTION_FILE_ENV, ACTION_TOKEN_ENV, ANALYST_ACTION_TYPES, parseActionLine } from '../../../data/src/index.js';
|
|
16
16
|
import { firewall, userSkillPreamble } from '../../../mcp/src/index.js';
|
|
17
17
|
import { skillStore } from "../kb-skill-cmd.js";
|
|
18
|
+
import { applyCodexEvent, buildCodexArgv, codexAllowedTools, codexDeveloperInstructions, codexFailure, expandSkillPrompt, newCodexTurn, strictSchema } from "./codex.js";
|
|
18
19
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
/** assets/claude ships the plugin dir, the analyst system prompt and the output schemas. */
|
|
20
21
|
export const ASSETS = path.resolve(HERE, '..', '..', 'assets', 'claude');
|
|
@@ -185,6 +186,8 @@ export function analystContextBlock(ss) {
|
|
|
185
186
|
lines.push('=== END CONTEXT ===');
|
|
186
187
|
return lines.join('\n');
|
|
187
188
|
}
|
|
189
|
+
/** task-1471: tools whose result the app parses back from the stream. */
|
|
190
|
+
const RESULT_TOOLS = new Set(['larp_check']);
|
|
188
191
|
/**
|
|
189
192
|
* The prompt for a turn: the ambient CONTEXT block (task-1313) then the user's slash-prompt / question.
|
|
190
193
|
* A free-text turn also carries the ANSWER LENGTH directive (task-1323) between the block and the
|
|
@@ -346,6 +349,8 @@ export function runAnalyst(req0, handlers, deps = {}) {
|
|
|
346
349
|
clearInterval(actionPoll);
|
|
347
350
|
actionPoll = null;
|
|
348
351
|
} };
|
|
352
|
+
if (req.engine === 'codex')
|
|
353
|
+
return runCodexTurn(req, handlers, deps, { tmp, env, cleanup });
|
|
349
354
|
let argv;
|
|
350
355
|
// A broken overlay never blocks a turn: fall back to the bundled skills (the loader already skipped bad files).
|
|
351
356
|
let pluginDir = PLUGIN_DIR;
|
|
@@ -459,6 +464,27 @@ export function runAnalyst(req0, handlers, deps = {}) {
|
|
|
459
464
|
streamedChars = 0; // next assistant turn's deltas start fresh
|
|
460
465
|
return;
|
|
461
466
|
}
|
|
467
|
+
// task-1471: the dexterm MCP server's own larp_check result (the score the panel gauge shows comes from the
|
|
468
|
+
// tool, not from the model's answer). Only named tools are parsed, capped in size.
|
|
469
|
+
if (type === 'user' && Array.isArray(ev.message?.content)) {
|
|
470
|
+
for (const b of ev.message.content) {
|
|
471
|
+
if (b.type !== 'tool_result')
|
|
472
|
+
continue;
|
|
473
|
+
const name = toolCalls.get(b.tool_use_id);
|
|
474
|
+
if (!name || !RESULT_TOOLS.has(name))
|
|
475
|
+
continue;
|
|
476
|
+
const text = typeof b.content === 'string' ? b.content : Array.isArray(b.content) ? b.content.filter((c) => c.type === 'text').map((c) => String(c.text ?? '')).join('') : '';
|
|
477
|
+
if (!text || text.length > 512 * 1024)
|
|
478
|
+
continue;
|
|
479
|
+
try {
|
|
480
|
+
const result = JSON.parse(text);
|
|
481
|
+
if (result && typeof result === 'object')
|
|
482
|
+
handlers.onToolResult?.({ name, result: result });
|
|
483
|
+
}
|
|
484
|
+
catch { /* not JSON: ignore */ }
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
462
488
|
if (type === 'stream_event') {
|
|
463
489
|
const se = ev.event;
|
|
464
490
|
if (se?.type === 'content_block_delta' && se.delta?.type === 'text_delta' && se.delta.text) {
|
|
@@ -524,3 +550,183 @@ export function runAnalyst(req0, handlers, deps = {}) {
|
|
|
524
550
|
},
|
|
525
551
|
};
|
|
526
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* One analyst turn on the user's own `codex exec --json` (0.1.5). Same handlers, same action channel, same
|
|
555
|
+
* firewalled context; the locks are in codex.ts. Codex reports whole messages (no token deltas), so text arrives
|
|
556
|
+
* per message; a structured turn's final JSON message becomes the result, never streamed as prose.
|
|
557
|
+
*/
|
|
558
|
+
function runCodexTurn(req, handlers, deps, s) {
|
|
559
|
+
const allowed = codexAllowedTools(req.actionTypes);
|
|
560
|
+
let argv;
|
|
561
|
+
let prompt;
|
|
562
|
+
try {
|
|
563
|
+
let schemaPath;
|
|
564
|
+
if (req.schema) {
|
|
565
|
+
const schema = JSON.parse(fs.readFileSync(path.join(ASSETS, 'schemas', path.basename(req.schema)), 'utf8'));
|
|
566
|
+
schemaPath = path.join(s.tmp, 'output.schema.json');
|
|
567
|
+
fs.writeFileSync(schemaPath, JSON.stringify(strictSchema(schema)), { mode: 0o600 });
|
|
568
|
+
}
|
|
569
|
+
let pluginDir = PLUGIN_DIR;
|
|
570
|
+
try {
|
|
571
|
+
pluginDir = analystPluginDir(s.tmp);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
pluginDir = PLUGIN_DIR;
|
|
575
|
+
}
|
|
576
|
+
// The claude CLI expands `/dexterm:<skill>` from the plugin dir; codex has none, so the playbook goes inline.
|
|
577
|
+
const expanded = expandSkillPrompt(turnPrompt(req), pluginDir, materializeUserSkill);
|
|
578
|
+
prompt = expanded;
|
|
579
|
+
argv = buildCodexArgv(req, {
|
|
580
|
+
nodePath: process.execPath, mcpBin: MCP_BIN, schemaPath,
|
|
581
|
+
developerInstructions: codexDeveloperInstructions(path.join(ASSETS, 'analyst-system.md')),
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
catch (e) {
|
|
585
|
+
s.cleanup();
|
|
586
|
+
handlers.onError?.('analyst assets missing', e.message);
|
|
587
|
+
return { cancel() { } };
|
|
588
|
+
}
|
|
589
|
+
let child;
|
|
590
|
+
try {
|
|
591
|
+
child = (deps.spawn ?? spawn)('codex', argv, { cwd: s.tmp, env: { ...process.env, ...s.env }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
592
|
+
}
|
|
593
|
+
catch (e) {
|
|
594
|
+
s.cleanup();
|
|
595
|
+
handlers.onError?.('codex not found', e.message);
|
|
596
|
+
return { cancel() { } };
|
|
597
|
+
}
|
|
598
|
+
const t0 = Date.now();
|
|
599
|
+
const st = newCodexTurn();
|
|
600
|
+
let settled = false;
|
|
601
|
+
let stderr = '';
|
|
602
|
+
let buf = '';
|
|
603
|
+
let initSent = false;
|
|
604
|
+
const kill = () => {
|
|
605
|
+
try {
|
|
606
|
+
child.kill('SIGTERM');
|
|
607
|
+
}
|
|
608
|
+
catch { /* ignore */ }
|
|
609
|
+
try {
|
|
610
|
+
child.stdout?.destroy();
|
|
611
|
+
}
|
|
612
|
+
catch { /* ignore */ }
|
|
613
|
+
try {
|
|
614
|
+
child.stderr?.destroy();
|
|
615
|
+
}
|
|
616
|
+
catch { /* ignore */ }
|
|
617
|
+
try {
|
|
618
|
+
child.unref?.();
|
|
619
|
+
}
|
|
620
|
+
catch { /* ignore */ }
|
|
621
|
+
};
|
|
622
|
+
const settle = () => { settled = true; s.cleanup(); };
|
|
623
|
+
child.on('error', (e) => {
|
|
624
|
+
if (settled)
|
|
625
|
+
return;
|
|
626
|
+
settle();
|
|
627
|
+
if (e.code === 'ENOENT')
|
|
628
|
+
handlers.onError?.('codex not installed', 'npm i -g @openai/codex && codex login');
|
|
629
|
+
else
|
|
630
|
+
handlers.onError?.('could not start codex', e.message);
|
|
631
|
+
});
|
|
632
|
+
try {
|
|
633
|
+
child.stdin?.on?.('error', () => { });
|
|
634
|
+
child.stdin?.end?.(prompt);
|
|
635
|
+
}
|
|
636
|
+
catch { /* the close/error path reports it */ }
|
|
637
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); if (stderr.length > 65536)
|
|
638
|
+
stderr = stderr.slice(-32768); });
|
|
639
|
+
child.stdout?.on('data', (d) => {
|
|
640
|
+
if (settled)
|
|
641
|
+
return;
|
|
642
|
+
buf += d.toString();
|
|
643
|
+
let i;
|
|
644
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
645
|
+
const line = buf.slice(0, i);
|
|
646
|
+
buf = buf.slice(i + 1);
|
|
647
|
+
if (!line.trim())
|
|
648
|
+
continue;
|
|
649
|
+
let ev;
|
|
650
|
+
try {
|
|
651
|
+
ev = JSON.parse(line);
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
step(ev);
|
|
657
|
+
if (settled)
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
child.on('close', (code) => {
|
|
662
|
+
if (settled)
|
|
663
|
+
return;
|
|
664
|
+
settle();
|
|
665
|
+
if (st.failure) {
|
|
666
|
+
handlers.onError?.('codex returned an error', st.failure);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const f = codexFailure(stderr, code);
|
|
670
|
+
handlers.onError?.(f.message, f.detail);
|
|
671
|
+
});
|
|
672
|
+
function step(ev) {
|
|
673
|
+
applyCodexEvent(st, ev, allowed, {
|
|
674
|
+
onSession: (id) => {
|
|
675
|
+
if (initSent)
|
|
676
|
+
return;
|
|
677
|
+
initSent = true;
|
|
678
|
+
handlers.onInit?.({ model: req.model ?? null, skills: [], mcp: ['dexterm:connected'], sessionId: id });
|
|
679
|
+
},
|
|
680
|
+
onToolCall: (name, input) => handlers.onToolCall?.({ name, input }),
|
|
681
|
+
// A structured turn's messages are the JSON answer: they become the result, not streamed prose.
|
|
682
|
+
onMessage: (text) => { if (!req.schema)
|
|
683
|
+
handlers.onText?.(st.messages.length > 1 ? `\n\n${text}` : text); },
|
|
684
|
+
});
|
|
685
|
+
if (st.violation) {
|
|
686
|
+
kill();
|
|
687
|
+
settle();
|
|
688
|
+
handlers.onError?.('codex tried a tool outside the read-only analyst; turn stopped', st.violation);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (st.done) {
|
|
692
|
+
kill();
|
|
693
|
+
settle();
|
|
694
|
+
const rawText = st.messages.length ? st.messages[st.messages.length - 1] : null;
|
|
695
|
+
let structured = null;
|
|
696
|
+
if (req.schema && rawText) {
|
|
697
|
+
try {
|
|
698
|
+
const v = JSON.parse(rawText);
|
|
699
|
+
structured = v && typeof v === 'object' && !Array.isArray(v) ? v : null;
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
structured = null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
handlers.onResult?.({
|
|
706
|
+
structured,
|
|
707
|
+
rawText: req.schema ? rawText : st.messages.join('\n\n') || null,
|
|
708
|
+
// The user's codex login bills its own plan; codex exec reports tokens, not a dollar cost.
|
|
709
|
+
costUsd: null,
|
|
710
|
+
durationMs: Date.now() - t0,
|
|
711
|
+
turns: 1,
|
|
712
|
+
model: req.model ?? null,
|
|
713
|
+
isError: false,
|
|
714
|
+
sessionId: st.sessionId,
|
|
715
|
+
});
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (st.failure && ev.type === 'turn.failed') {
|
|
719
|
+
kill();
|
|
720
|
+
settle();
|
|
721
|
+
handlers.onError?.('codex returned an error', st.failure);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
cancel() {
|
|
726
|
+
if (settled)
|
|
727
|
+
return;
|
|
728
|
+
kill();
|
|
729
|
+
settle();
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
}
|