dexterm-cli 0.1.2 → 0.1.3

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.
Files changed (118) hide show
  1. package/dist/packages/agents/src/bus.js +37 -1
  2. package/dist/packages/agents/src/follow.js +105 -0
  3. package/dist/packages/agents/src/index.js +5 -0
  4. package/dist/packages/agents/src/spectator-server.js +153 -0
  5. package/dist/packages/agents/src/spectator.js +212 -0
  6. package/dist/packages/cli/assets/claude/analyst-system.md +6 -1
  7. package/dist/packages/cli/src/agent-cmd.js +71 -7
  8. package/dist/packages/cli/src/agent-spectator.js +37 -0
  9. package/dist/packages/cli/src/alerts/format.js +25 -0
  10. package/dist/packages/cli/src/app-help-gen.js +365 -0
  11. package/dist/packages/cli/src/doctor.js +6 -5
  12. package/dist/packages/cli/src/follow-cmd.js +107 -1
  13. package/dist/packages/cli/src/help-cmd.js +48 -0
  14. package/dist/packages/cli/src/live/index.js +64 -3
  15. package/dist/packages/cli/src/live/screens/token-live.js +24 -0
  16. package/dist/packages/cli/src/live/signals.js +109 -0
  17. package/dist/packages/cli/src/live/trade.js +24 -1
  18. package/dist/packages/cli/src/main.js +355 -10
  19. package/dist/packages/cli/src/mcp-cmd.js +2 -7
  20. package/dist/packages/cli/src/pnl-app.js +221 -0
  21. package/dist/packages/cli/src/pnl-card.js +615 -0
  22. package/dist/packages/cli/src/record-cmd.js +455 -0
  23. package/dist/packages/cli/src/rules.js +50 -2
  24. package/dist/packages/cli/src/screens/follows.js +91 -2
  25. package/dist/packages/cli/src/screens/overlay.js +3 -0
  26. package/dist/packages/cli/src/screens/positions.js +1 -1
  27. package/dist/packages/cli/src/screens/rules.js +1 -1
  28. package/dist/packages/cli/src/spectate-cmd.js +104 -0
  29. package/dist/packages/cli/src/state.js +1 -1
  30. package/dist/packages/cli/src/version.js +1 -1
  31. package/dist/packages/cli/src/voice-cmd.js +86 -11
  32. package/dist/packages/cli/src/voice-hud.js +1 -1
  33. package/dist/packages/custom/src/rules.js +2 -1
  34. package/dist/packages/data/src/analyst-actions.js +130 -3
  35. package/dist/packages/data/src/fast-lane.js +16 -2
  36. package/dist/packages/data/src/index.js +1 -0
  37. package/dist/packages/data/src/intel/live-signals.js +185 -0
  38. package/dist/packages/data/src/trade/copy/index.js +1 -1
  39. package/dist/packages/data/src/trade/copy/stats.js +36 -2
  40. package/dist/packages/data/src/trade/copy/tracker.js +3 -2
  41. package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
  42. package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
  43. package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
  44. package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
  45. package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
  46. package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
  47. package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
  48. package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
  49. package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
  50. package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
  51. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
  52. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
  53. package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
  54. package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
  55. package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
  56. package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
  57. package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
  58. package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
  59. package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
  60. package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
  61. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
  62. package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
  63. package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
  64. package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
  65. package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
  66. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
  67. package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
  68. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
  69. package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
  70. package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
  71. package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
  72. package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
  73. package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
  74. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
  75. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
  76. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
  77. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
  78. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
  79. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
  80. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
  81. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
  82. package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
  83. package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
  84. package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
  85. package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
  86. package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +36 -0
  87. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
  88. package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
  89. package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
  90. package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
  91. package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
  92. package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
  93. package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
  94. package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
  95. package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
  96. package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
  97. package/dist/packages/mcp/src/app-help.js +135 -0
  98. package/dist/packages/mcp/src/index.js +3 -0
  99. package/dist/packages/mcp/src/server.js +90 -3
  100. package/dist/packages/mcp/src/spectator-sink.js +72 -0
  101. package/dist/packages/packs/src/index.js +2 -0
  102. package/dist/packages/packs/src/skill-care.js +206 -0
  103. package/dist/packages/popup/src/pages/spectator.css +45 -0
  104. package/dist/packages/popup/src/pages/spectator.html +25 -0
  105. package/dist/packages/popup/src/pages/spectator.js +74 -0
  106. package/dist/packages/popup/src/selftest.js +19 -0
  107. package/dist/packages/popup/src/server.js +7 -1
  108. package/dist/packages/tui/src/keymap.js +10 -0
  109. package/dist/packages/voice/src/controller.js +42 -1
  110. package/dist/packages/voice/src/doctor.js +25 -8
  111. package/dist/packages/voice/src/engines.js +8 -1
  112. package/dist/packages/voice/src/index.js +3 -1
  113. package/dist/packages/voice/src/speakable.js +111 -0
  114. package/dist/packages/voice/src/tts/piper-setup.js +229 -0
  115. package/dist/packages/voice/src/tts/piper.js +309 -66
  116. package/dist/packages/voice/src/tts/say.js +3 -2
  117. package/dist/packages/voice/src/tts/voices.js +8 -5
  118. package/package.json +1 -1
@@ -19,9 +19,11 @@ import { spawn } from 'node:child_process';
19
19
  import { dirname, join } from 'node:path';
20
20
  import { homedir } from 'node:os';
21
21
  import { writeExport, exportViewFor } from "./export.js";
22
+ import { openPnl, loadPnl, pnlKey } from "./pnl-app.js";
23
+ import { shareCard, runLocal } from "./pnl-card.js";
22
24
  import { parseMintTarget, parseCoinTarget, resolveCoinTarget, isBareOpen } from "./coin-target.js";
23
25
  import { THEMES, isThemeName, DEFAULT_THEME, THEME_NAMES, Flusher, ENTER_SCREEN, LEAVE_SCREEN, DEFAULT_KEYMAP, Keymap, nextTheme, parseKeys, keyId, detectColorDepth, parseColorDepth, blinkFrame, blinkFrames, stepVfx, pulse, isCalm, fxBusy, blendActivity, ImageLayer, queryTerminal, resolveProtocol, fireMoment, fireInflight, clearInflight, skipMoment, momentKindFor, playMoment, soundOn, breakpoint, fmtUsd as fmtUsdTui, } from '../../tui/src/index.js';
24
- import { resolveRpcUrl, redactSecrets, defaultDbPath, extraRpcUrls, wsUrlFor, SOLANA_PUBLIC_RPC_WS, parseHttpUrl, validateAnalystAction, ACTION_RATE, ImageService, encodePng, SummaryLimiter, buildSummaryWindow, KolStore, Cache, PackError, normalizeHandle, terminalSafe } from '../../data/src/index.js';
26
+ import { resolveRpcUrl, redactSecrets, defaultDbPath, extraRpcUrls, wsUrlFor, SOLANA_PUBLIC_RPC_WS, parseHttpUrl, validateAnalystAction, ACTION_RATE, ACTION_TIER, configChangeForAction, ImageService, encodePng, SummaryLimiter, buildSummaryWindow, KolStore, Cache, PackError, normalizeHandle, terminalSafe } from '../../data/src/index.js';
25
27
  import { Recorder, recorderEnabled, MaintenanceSchedule } from '../../recorder/src/index.js';
26
28
  import { makeFixtureState, makeFeedState, syncFeed, renderFrame, isScreenKey, activityFor, SCREEN_NAMES, analystKey, syncAnalyst, pinNewpairsSelection, ANALYST_TAB_KEY, computeNarrativeView, newPlansUi, plansService, plansRows, newFollowsUi, copyService, copyAutoService, MIRROR_DEFAULT_CAP_USD } from "./state.js";
27
29
  import { mulberry32, tick, spawnPair, shock } from "./fixtures.js";
@@ -39,6 +41,8 @@ import { widgetPage, widgetEnabled, syncWidgetPopups } from "./widgets.js";
39
41
  import { recordLarpVerdict } from "./larp-record.js";
40
42
  import { rulesUiOf, rulesTick, rulesMove, beginRuleInput, saveRuleInput, dryRunNow, removeSelectedRule, toggleSelectedRule, dismissSelectedProposal, actOnSelectedProposal, RULE_INPUT_MAX } from "./rules.js";
41
43
  import { buildNotifier, hasPushChannel } from "./alerts/notifier.js";
44
+ import { copyAlertToDelivery } from "./alerts/format.js";
45
+ import { readTelegramCreds, readDiscordCreds } from "./alerts/creds.js";
42
46
  import { launchIntelCells, launchRetryable } from "./live/intel.js";
43
47
  import { AnalystSession } from "./analyst/session.js";
44
48
  import { resolveAnalystResults } from "./analyst/results.js";
@@ -57,7 +61,7 @@ import { FOLLOW_INPUT_MAX } from "./screens/follows.js";
57
61
  import { isPriceMode, configPriceMode, togglePriceMode, priceModeOf, planLevels } from "./display.js";
58
62
  import { VERSION } from "./version.js";
59
63
  import { initCustom, planWorkspace, planPopupOpens, findWorkspace, nextWorkspaceName, workspaceSlots, saveWorkspace, themeFor, themeByName, cycleTheme, themeNames, CHORD_TTL_MS, MAX_WORKSPACE_NAME } from "./custom.js";
60
- import { DEFAULT_WORKSPACE, MORNING_HUD, applyAndJournal } from '../../custom/src/index.js';
64
+ import { DEFAULT_WORKSPACE, MORNING_HUD, applyAndJournal, policyCheck } from '../../custom/src/index.js';
61
65
  /**
62
66
  * Context the `screen_state` MCP tool returns AND the per-turn context block is rendered from
63
67
  * (task-1313): what the user is looking at right now — selected/hovered coin, screen, the visible
@@ -65,6 +69,37 @@ import { DEFAULT_WORKSPACE, MORNING_HUD, applyAndJournal } from '../../custom/sr
65
69
  * that bounds what the analyst's UI actions may reference. Token-derived text stays raw here; the
66
70
  * context-block renderer wraps it as untrusted before a model sees it.
67
71
  */
72
+ /** Screens the analyst's `switch_screen` action may target (task-1413); the user-facing set. */
73
+ export const SWITCHABLE_SCREENS = ['screener', 'token', 'newpairs', 'watchlist', 'analyst', 'positions', 'trending', 'kol'];
74
+ /**
75
+ * A read-only snapshot of local UI state for the `app_state` MCP tool (task-1413): what the user sees.
76
+ * NEVER includes a secret — alert channels report a boolean "configured", computed from creds presence,
77
+ * never the token/webhook/key itself. `null` when there is no live config (one-shot fixture frames).
78
+ */
79
+ export function appStateBlock(state) {
80
+ const ui = state.custom;
81
+ if (!ui)
82
+ return null;
83
+ const cfg = ui.config;
84
+ const home = ui.home;
85
+ const ch = (cfg.alerts?.channels ?? {});
86
+ const alerts = {
87
+ // toast is on unless explicitly disabled; the rest are off unless enabled. telegram/discord also need creds.
88
+ toast: ch.toast?.enabled !== false,
89
+ desktop: ch.desktop?.enabled === true,
90
+ telegram: ch.telegram?.enabled === true && readTelegramCreds(home) !== null,
91
+ discord: ch.discord?.enabled === true && readDiscordCreds(home) !== null,
92
+ };
93
+ return {
94
+ theme: state.theme.name,
95
+ priceMode: priceModeOf(state),
96
+ intensity: state.vfx.intensity,
97
+ calm: isCalm(state.vfx),
98
+ voice: { name: cfg.voice?.name ?? null, rate: cfg.voice?.rate ?? null },
99
+ scan: { on: state.scan?.engine.on ?? false },
100
+ alerts, // booleans only — never a token, webhook or key
101
+ };
102
+ }
68
103
  export function buildScreenState(state) {
69
104
  const live = state.live;
70
105
  const sc = state.screener;
@@ -127,7 +162,14 @@ export function buildScreenState(state) {
127
162
  watchlist,
128
163
  ticket,
129
164
  positions,
130
- actions: { enabled: !!live, mints: [...mints], feeds, sortKeys: SORT_KEYS.map((k) => k.key) },
165
+ actions: {
166
+ enabled: !!live, mints: [...mints], feeds, sortKeys: SORT_KEYS.map((k) => k.key),
167
+ // task-1413: targets the new tiered actions may reference.
168
+ screens: [...SWITCHABLE_SCREENS],
169
+ workspaces: Object.keys(state.custom?.config.layout?.workspaces ?? {}),
170
+ themes: themeNames(state.custom),
171
+ },
172
+ app: appStateBlock(state),
131
173
  now: Date.now(),
132
174
  };
133
175
  }
@@ -225,10 +267,11 @@ export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watch
225
267
  dexterm wallet link|unlink|status|panic|resume link a Phantom wallet via a local bridge (pubkey only); panic disables live trading, resume needs a TTY keypress
226
268
  dexterm plan propose|list|arm|cancel|journal conditional order plans (entry/ladder/take-profit/moon bag/stop); arming needs a physical keypress
227
269
  dexterm mandate draft|list|arm|revoke standing capped mandates (fast wallet); arming needs a physical keypress — voice/agents can never arm
228
- dexterm follow add|remove|list|stats|trades|purge copy trading tier 1: follow a wallet (your list, local labels), its decoded swaps + FIFO PnL; alerts in the app
229
- dexterm mcp [install <client>|doctor] read-only MCP server for your own agent; 'install' writes the config for claude-code|claude-desktop|cursor|codex|openclaw|generic (asks first, idempotent, backs up); 'doctor' verifies the connection
270
+ dexterm follow add|remove|list|stats|trades|mirror|purge copy trading tier 1: follow a wallet (your list, local labels), its decoded swaps + FIFO PnL + win rate/early-entry; 'mirror' previews a one-key MIRROR TICKET from the wallet's latest buy (inert; arm in the app)
271
+ dexterm mcp [install <client>|doctor] read-only MCP server for your own agent; 'install' writes the config for claude-code|claude-desktop|cursor|codex|generic (asks first, idempotent, backs up); 'doctor' verifies the connection
230
272
  dexterm alerts status|enable|disable|set-telegram|set-discord|test where rule alerts are delivered (desktop / your Telegram / Discord; creds stay local, never logged)
231
273
  dexterm watch [--interval S] [--once] [--replay FILE] run your rules headless and push alerts when the terminal is closed (no signer, proposals only)
274
+ dexterm record run|install|uninstall|status keep the history recorder fed with no TUI open, as a user-level launchd agent (no network beyond the data layer; 'dexterm record help')
232
275
  dexterm --fixtures interactive demo on seeded fixtures
233
276
  dexterm --screen <name> one-shot frame to stdout (live)
234
277
  dexterm --screen <name> --fixtures one-shot frame from fixtures
@@ -315,9 +358,10 @@ ${X_HELP}
315
358
  --version print the version
316
359
 
317
360
  Keys: 1-4 screens · tab cycle · j/k move · ⏎ open · esc back · a analyst · v voice · t theme · c calm · p pause · q quit
318
- voice (JARVIS, macOS): v push-to-talk (press to talk, press to send) · m mute · , change voice · esc stop speaking
361
+ voice (JARVIS, macOS): v push-to-talk (press to talk, press to send) · m mute · , change voice · < / > slower/faster · esc stop speaking
319
362
  voice asks the analyst or triggers safe actions (open/filter/watch/open a PAPER ticket);
320
363
  a spoken "buy"/"sell" only OPENS a ticket — you still confirm it with a keypress
364
+ spoken replies talk in market cap and round numbers (never a raw per-token price); < / > set the rate (saved to config)
321
365
  screener: / filter · s sort · [ ] presets · r refresh · g/G top/bottom
322
366
  social (task-1330): T trending in the space · K KOL lists · token screen: s social panel · f filter · x hidden spam
323
367
  dexterm kol list | create <name> | add <list> @handle | remove <list> @handle | mute @handle | unmute @handle | enable <list> | disable <list> | import <dir> | import-x
@@ -725,6 +769,12 @@ export async function main(argv, io = process, proc = process) {
725
769
  const { runAgent } = await import("./agent-cmd.js");
726
770
  return runAgent(argv.slice(1), io);
727
771
  }
772
+ // `dexterm spectate --replay FILE | --info` → watch an agent drive dexterm (task-1406, "watch the agent
773
+ // cook"): replay a saved session, or show the live read-only, token-gated, loopback spectator stream.
774
+ if (argv[0] === 'spectate') {
775
+ const { runSpectate } = await import("./spectate-cmd.js");
776
+ return runSpectate(argv.slice(1), io);
777
+ }
728
778
  // `dexterm recorder <cmd>` → the history recorder's tables: stats, label, prune, export (task-1329). No network.
729
779
  if (argv[0] === 'recorder') {
730
780
  const { recorderCli } = await import('../../recorder/src/index.js');
@@ -760,6 +810,12 @@ export async function main(argv, io = process, proc = process) {
760
810
  const { kbSkillCommand } = await import("./kb-skill-cmd.js");
761
811
  return kbSkillCommand(argv[0], argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env, isTTY: io.stdout.isTTY === true });
762
812
  }
813
+ // `dexterm help [topic]` → the app-help KB (task-1413): how the app works, with the exact key/command.
814
+ // Backed by the same generated entries the analyst's `app_help` tool uses. No network, no model.
815
+ if (argv[0] === 'help') {
816
+ const { helpCommand } = await import("./help-cmd.js");
817
+ return helpCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr });
818
+ }
763
819
  // `dexterm theme list|show|init|check` → user themes under ~/.dexterm/themes (task-1358 Tier 2). No network.
764
820
  if (argv[0] === 'theme') {
765
821
  const { themeCommand } = await import("./theme-cmd.js");
@@ -792,6 +848,13 @@ export async function main(argv, io = process, proc = process) {
792
848
  const { watchCommand } = await import("./watch-cmd.js");
793
849
  return watchCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
794
850
  }
851
+ // `dexterm record [run|install|uninstall|status]` → the headless collector that keeps the history recorder
852
+ // fed with no TUI open (task-1414), to be driven by a user-level launchd LaunchAgent. No signer, no trading;
853
+ // requests honour the data layer limits / health gate / sweep budgets, and the Helius key is never printed.
854
+ if (argv[0] === 'record') {
855
+ const { recordCommand } = await import("./record-cmd.js");
856
+ return recordCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
857
+ }
795
858
  // `dexterm pack list|verify|add|remove|export|lock|sign|trust|signers|keymap|widget` → shareable packs (task-1358 Tier 4):
796
859
  // plain files + sha256 lock + optional ssh signature. No network, no model; a pack never runs code (widgets land
797
860
  // disabled, unsigned rules load disabled, a keymap merges only on the explicit command).
@@ -1228,6 +1291,10 @@ function followsMove(state, delta) {
1228
1291
  return false;
1229
1292
  if (state.copyUi.input !== null || state.copyUi.mirror || state.copyUi.autoModal)
1230
1293
  return true;
1294
+ if (state.copyUi.detail) {
1295
+ state.copyUi.detail.scroll = Math.max(0, state.copyUi.detail.scroll + (delta > 0 ? 1 : -1));
1296
+ return true;
1297
+ } // task-1399: j/k scroll the detail view
1231
1298
  const n = state.copy?.follows().length ?? 0;
1232
1299
  if (n <= 0) {
1233
1300
  state.copyUi.selected = 0;
@@ -1513,6 +1580,10 @@ export function applyAction(state, action, stack) {
1513
1580
  state.copyUi.notice = 'mirror preview closed';
1514
1581
  break;
1515
1582
  } // task-1360: esc closes the mirror preview first
1583
+ if (state.screen === 'follows' && state.copyUi?.detail) {
1584
+ state.copyUi.detail = null;
1585
+ break;
1586
+ } // task-1399: esc closes the wallet detail view before leaving the screen
1516
1587
  const prev = stack.pop();
1517
1588
  if (prev)
1518
1589
  state.screen = prev;
@@ -2337,6 +2408,10 @@ export function applyAction(state, action, stack) {
2337
2408
  case 'export.view':
2338
2409
  state.pendingExport = true;
2339
2410
  break;
2411
+ // task-1407 PNL CARD: the loop opens the preview (recorded fills only) and runs the share keys.
2412
+ case 'pnl.card':
2413
+ state.pendingPnl = true;
2414
+ break;
2340
2415
  case 'display.priceMode':
2341
2416
  state.pendingToast = togglePriceMode(state);
2342
2417
  break; // task-1366: display only, persisted
@@ -2708,6 +2783,12 @@ export function applyAnalystAction(state, raw, stack) {
2708
2783
  if (!res.ok)
2709
2784
  return { ok: false, error: res.error };
2710
2785
  const action = res.action;
2786
+ // task-1413 tier (b): a persisted-config action is never applied by the model — return it as a PROPOSAL
2787
+ // the user accepts with a keypress (applyAnalystConfig then journals it as actor 'analyst').
2788
+ if (ACTION_TIER[action.type] === 'config') {
2789
+ times.push(now);
2790
+ return { ok: true, label: res.label, proposal: { action, summary: res.label } };
2791
+ }
2711
2792
  // Resolve a mint to a row object from what is on screen (allow-list guarantees it was visible).
2712
2793
  const findSeed = (mint) => seedForMint(state, live, mint);
2713
2794
  const toScreen = (next) => { if (state.screen !== next) {
@@ -2747,6 +2828,10 @@ export function applyAnalystAction(state, raw, stack) {
2747
2828
  }
2748
2829
  case 'show_in_screener': {
2749
2830
  toScreen('screener');
2831
+ if (action.preset && !setPreset(sc, action.preset))
2832
+ return { ok: false, error: `preset "${action.preset}" is unavailable` };
2833
+ if (action.preset && state.feed)
2834
+ state.feed.setPreset(sc.preset);
2750
2835
  if (action.filter) {
2751
2836
  const err = setFilter(sc, action.filter);
2752
2837
  if (err)
@@ -2760,12 +2845,101 @@ export function applyAnalystAction(state, raw, stack) {
2760
2845
  state.selected = i;
2761
2846
  }
2762
2847
  state.selected = clampSelection(sc, state.selected);
2848
+ if (action.preset) {
2849
+ times.push(now);
2850
+ return { ok: true, label: res.label, refresh: true };
2851
+ }
2852
+ break;
2853
+ }
2854
+ // --- task-1413 view-only actions: applied immediately, shown as "analyst did X".
2855
+ case 'switch_screen': {
2856
+ toScreen(action.screen);
2857
+ state.selected = 0;
2858
+ break;
2859
+ }
2860
+ case 'switch_workspace': {
2861
+ const r = switchWorkspace(state, action.name, stack);
2862
+ if (!r.ok)
2863
+ return { ok: false, error: r.reason };
2864
+ times.push(now);
2865
+ return { ok: true, label: res.label, refresh: r.refresh };
2866
+ }
2867
+ case 'chart_popup': {
2868
+ // Open the chart popup (the loop performs the open/close); needs a selected coin to chart.
2869
+ state.pendingPopup = action.open ? 'chart' : null;
2870
+ break;
2871
+ }
2872
+ case 'toggle_calm': {
2873
+ if (action.on) {
2874
+ if (!isCalm(state.vfx)) {
2875
+ state.vfx.intensity = 'off';
2876
+ state.vfx.trail.clear();
2877
+ }
2878
+ }
2879
+ else if (isCalm(state.vfx)) {
2880
+ const back = 'fever';
2881
+ state.vfx.intensity = back;
2882
+ state.vfx.baseline = 0.12;
2883
+ }
2884
+ break;
2885
+ }
2886
+ case 'set_scan': {
2887
+ scanCommand(state, action.on ? 'on' : 'off', stack);
2888
+ break;
2889
+ }
2890
+ case 'export_view': {
2891
+ state.pendingExport = true;
2763
2892
  break;
2764
2893
  }
2765
2894
  }
2766
2895
  times.push(now);
2767
2896
  return { ok: true, label: res.label };
2768
2897
  }
2898
+ /**
2899
+ * task-1413: apply an accepted tier-(b) config proposal through the journaled path as actor `analyst`
2900
+ * (so `ctrl+z` undoes it), creating the parent section when absent (exactly like the `$` / `,` handlers),
2901
+ * then reflect it in the live UI where cheap. Throws on a refusal (schema / policy); the caller toasts it.
2902
+ * Returns the one-line label for the confirmation toast.
2903
+ */
2904
+ export function applyAnalystConfig(state, proposal) {
2905
+ const ui = state.custom;
2906
+ if (!ui)
2907
+ throw new Error('config changes need a loaded config (not available on this run)');
2908
+ const change = configChangeForAction(proposal.action);
2909
+ if (!change)
2910
+ throw new Error('not a persisted-config action');
2911
+ const parent = ui.config[change.section];
2912
+ const op = parent && typeof parent === 'object'
2913
+ ? { op: 'add', path: change.path, value: change.value }
2914
+ : { op: 'add', path: `/${change.section}`, value: { [change.key]: change.value } };
2915
+ // THE GATE (task-1413 r2): re-run the model-actor policy on the exact journal op BEFORE writing. The
2916
+ // analyst is not the user, so `DENIED_FOR_MODELS` applies — a config action can never reach a signer /
2917
+ // trading / keys / caps / mandates / wallet / alerts / VOICE path, even behind the keypress confirm.
2918
+ // CONFIG_ACTION_PATHS is already a closed non-denied allow-list; this is the server-side belt-and-braces.
2919
+ const denied = policyCheck([op], 'claude');
2920
+ if (denied.length)
2921
+ throw new Error(`refused: ${denied.map((e) => e.msg).join('; ')}`);
2922
+ const { config } = applyAndJournal(ui.home, [op], { actor: 'analyst', why: `analyst: ${proposal.summary}`, now: state.now.getTime() });
2923
+ ui.config = config;
2924
+ // Reflect the change in the running UI where it is cheap and local.
2925
+ switch (proposal.action.type) {
2926
+ case 'set_theme': {
2927
+ const t = themeByName(ui, proposal.action.name);
2928
+ if (t)
2929
+ state.theme = t;
2930
+ break;
2931
+ }
2932
+ case 'set_price_mode':
2933
+ state.priceMode = proposal.action.mode;
2934
+ break;
2935
+ case 'set_intensity':
2936
+ state.vfx.intensity = proposal.action.intensity;
2937
+ state.vfx.baseline = proposal.action.intensity === 'fever' ? 0.12 : 0;
2938
+ break;
2939
+ default: break;
2940
+ }
2941
+ return proposal.summary;
2942
+ }
2769
2943
  /**
2770
2944
  * Route one decoded key: the screener's filter editor and sort menu swallow keys while open,
2771
2945
  * everything else goes through the keymap. Exported for tests.
@@ -2993,6 +3167,22 @@ export function applyKey(state, key, stack) {
2993
3167
  ui.input = cur + key.ch;
2994
3168
  return;
2995
3169
  }
3170
+ // task-1399: on the follows screen (not typing, no modal), enter opens/closes the selected wallet's DETAIL view.
3171
+ if (state.screen === 'follows' && state.copyUi && state.copyUi.input === null && !state.copyUi.mirror && !state.copyUi.autoModal && key.name === 'enter') {
3172
+ const ui = state.copyUi;
3173
+ if (ui.detail) {
3174
+ ui.detail = null;
3175
+ return;
3176
+ }
3177
+ const svc = state.copy;
3178
+ const rows = svc?.follows() ?? [];
3179
+ const f = rows[Math.max(0, Math.min(rows.length - 1, ui.selected))];
3180
+ if (f)
3181
+ ui.detail = { wallet: f.addr, scroll: 0 };
3182
+ else
3183
+ ui.notice = 'follow a wallet first (i)';
3184
+ return;
3185
+ }
2996
3186
  if (state.screen === 'kol' && state.kolInput !== null && state.kolInput !== undefined) {
2997
3187
  const kol = kolOf(state);
2998
3188
  if (key.name === 'esc') {
@@ -3623,6 +3813,10 @@ export function interactive(state, io, depth, args, proc = process) {
3623
3813
  // Live bindings redraw as soon as something lands (one frame per macrotask at most).
3624
3814
  let drawPending = false;
3625
3815
  let lastFastDraw = 0;
3816
+ // task-1399: a followed wallet's buy/sell also PUSHES through the alerts engine (desktop / Telegram / Discord)
3817
+ // when the user enabled a push channel. Forward-declared; the real impl is set once the Notifier exists below.
3818
+ // Read-only, like a rule alert — nothing here trades, arms or signs.
3819
+ let pushCopyAlert = () => { };
3626
3820
  if (live) {
3627
3821
  // task-1360: poll the followed wallets for the whole run (live only; replay never spends credits) and toast
3628
3822
  // every new buy/sell a followed wallet made. Information only: no path from an alert to a trade exists here.
@@ -3635,6 +3829,8 @@ export function interactive(state, io, depth, args, proc = process) {
3635
3829
  const a = as[as.length - 1];
3636
3830
  const who = a.name ?? shortAddr(a.wallet);
3637
3831
  say(`${who} ${a.trade.side === 'buy' ? 'BOUGHT' : 'SOLD'} ${shortAddr(a.trade.mint)}${a.trade.usd !== null ? ` ≈ $${a.trade.usd.toFixed(0)}` : ''}${a.lagMs !== null ? ` · lag ${Math.round(a.lagMs / 1000)}s` : ''}${as.length > 1 ? ` (+${as.length - 1} more · U)` : ' (U)'}`, a.trade.side === 'buy' ? 'up' : 'down', 6);
3832
+ for (const ca of as)
3833
+ pushCopyAlert(ca); // task-1399: push each one out through the alerts engine too
3638
3834
  }
3639
3835
  }
3640
3836
  if (done || drawPending)
@@ -3755,6 +3951,15 @@ export function interactive(state, io, depth, args, proc = process) {
3755
3951
  const r = applyAnalystAction(state, raw, stack);
3756
3952
  if (!r.ok || !r.label)
3757
3953
  return;
3954
+ // task-1413 tier (b): a persisted-config request is a PROPOSAL — show it and wait for the user's key.
3955
+ if (r.proposal) {
3956
+ // The proposal expires with its 8s toast (task-1413 defect c): a 'y' typed after it fades never
3957
+ // persists a stale change. `now` is the frame clock so it stays deterministic under replay.
3958
+ state.analystConfigProposal = { ...r.proposal, expiresAt: state.now.getTime() + 8000 };
3959
+ say(`analyst suggests: ${r.proposal.summary} — press y to accept, any other key to dismiss`, 'ai', 8);
3960
+ scheduleDraw();
3961
+ return;
3962
+ }
3758
3963
  const log = (state.analystActions ??= []);
3759
3964
  log.push(r.label);
3760
3965
  if (log.length > 6)
@@ -4232,6 +4437,10 @@ export function interactive(state, io, depth, args, proc = process) {
4232
4437
  const alertNotifier = state.custom && hasPushChannel(state.custom.config.alerts)
4233
4438
  ? buildNotifier(state.custom.home, state.custom.config.alerts, { toastSink: () => { } })
4234
4439
  : null;
4440
+ // task-1399: now the Notifier exists, a followed wallet's buy/sell can be pushed. Fire-and-forget; a slow
4441
+ // channel never blocks the loop; routing's `levels` filter lets the user scope (a buy is `up`, a sell `down`).
4442
+ if (alertNotifier)
4443
+ pushCopyAlert = (ca) => { void alertNotifier.deliver(copyAlertToDelivery(ca)).catch(() => { }); };
4235
4444
  const drainDeliveries = () => {
4236
4445
  const ui = state.rulesUi;
4237
4446
  if (!ui || !ui.pendingDeliveries.length)
@@ -4402,6 +4611,47 @@ export function interactive(state, io, depth, args, proc = process) {
4402
4611
  return { price: np.price ?? null, mcap: np.mcap ?? null };
4403
4612
  return { price: null, mcap: null };
4404
4613
  };
4614
+ // task-1411: a short instruction appended to a VOICE analyst question so the answer is built for the
4615
+ // ear, not the screen — market cap not a per-token price, round numbers, short.
4616
+ const VOICE_ANALYST_SPEECH_HINT = 'Answer out loud for text-to-speech: talk about price as market cap, never a raw per-token price; '
4617
+ + 'round big numbers like "340 thousand" or "1.2 million"; no long addresses; keep it to two or three short sentences.';
4618
+ // A spoken pronoun that points at the highlighted row ("this one", "this coin", "it").
4619
+ const VOICE_SELECTION_PRONOUN = /\b(?:this (?:one|coin|token)|that (?:one|coin|token)|this|it)\b/i;
4620
+ // A friendly, complete spoken ack for each navigable screen (the router's `say` is the terse key).
4621
+ const NAV_ACK = {
4622
+ screener: 'the screener', newpairs: 'new pairs', watchlist: 'your watchlist', analyst: 'the analyst', positions: 'your positions',
4623
+ };
4624
+ /**
4625
+ * Run a VOICE-originated analyst question (task-1411). Fixes the screener silence: "what do you think
4626
+ * of THIS ONE" aimed at a highlighted screener row used to start a context-free `trench-scan` with no
4627
+ * mint, so the analyst never learned which coin "this one" was and answered with a short, useless
4628
+ * fragment (the tester heard nothing useful). We now attach the SELECTED coin as a token-dossier target —
4629
+ * exactly the context a token screen already gives — so it answers for real. The question also carries
4630
+ * a short "answer for speech" instruction (market cap, round numbers, short).
4631
+ */
4632
+ const askVoiceAnalyst = (question) => {
4633
+ const q = question.trim().slice(0, 400);
4634
+ const instructed = `${q}\n\n${VOICE_ANALYST_SPEECH_HINT}`;
4635
+ // A warm session: follow up on it so the model keeps context (the screen is already analyst/token).
4636
+ if (state.analyst) {
4637
+ state.analyst.ask(instructed, buildScreenState(state));
4638
+ return;
4639
+ }
4640
+ const tv = live?.token?.view;
4641
+ const sel = voiceSelected();
4642
+ if (state.screen === 'token' && live && tv?.mint) {
4643
+ state.pendingAnalyst = { skill: 'token-dossier', mint: tv.mint, symbol: tv.symbol, target: 'token', question: instructed };
4644
+ }
4645
+ else if (live && sel && VOICE_SELECTION_PRONOUN.test(q)) {
4646
+ // the screener/analyst/watchlist/new-pairs highlighted coin IS "this one" (task-1411)
4647
+ state.pendingAnalyst = { skill: 'token-dossier', mint: sel.mint, symbol: sel.symbol, target: 'analyst', question: instructed };
4648
+ }
4649
+ else {
4650
+ state.pendingAnalyst = { skill: 'trench-scan', target: 'analyst', question: instructed };
4651
+ }
4652
+ if (state.pendingAnalyst)
4653
+ beginAnalyst();
4654
+ };
4405
4655
  /**
4406
4656
  * Perform a voice decision. Returns `false` when the app spoke its own reply (an ask / clarify), so
4407
4657
  * the controller does not read the decision's `say` line over it (task-1339, re-verify N1: the
@@ -4417,6 +4667,7 @@ export function interactive(state, io, depth, args, proc = process) {
4417
4667
  const a = SCREEN_ACTION[d.screen];
4418
4668
  if (a)
4419
4669
  applyAction(state, a, stack);
4670
+ ask(NAV_ACK[d.screen] ?? d.say);
4420
4671
  break;
4421
4672
  }
4422
4673
  case 'open-token': {
@@ -4541,14 +4792,14 @@ export function interactive(state, io, depth, args, proc = process) {
4541
4792
  case 'analyst': {
4542
4793
  // Route the question to the analyst, surface the chat, and let the answer be spoken when it lands.
4543
4794
  // Capped like typed chat (400 chars): a runaway transcript never becomes a 200 KB argv (N7).
4795
+ // task-1411: askVoiceAnalyst attaches the highlighted coin as context so a screener "this one"
4796
+ // question is answered for real, and appends the "answer for speech" instruction.
4544
4797
  if (state.screen !== 'token' && state.screen !== 'analyst') {
4545
4798
  stack.push(state.screen);
4546
4799
  state.screen = 'analyst';
4547
4800
  }
4548
4801
  syncAnalyst(state);
4549
- sendChat(d.question.slice(0, 400));
4550
- if (state.pendingAnalyst)
4551
- beginAnalyst();
4802
+ askVoiceAnalyst(d.question);
4552
4803
  break;
4553
4804
  }
4554
4805
  case 'workspace': {
@@ -4615,6 +4866,21 @@ export function interactive(state, io, depth, args, proc = process) {
4615
4866
  }
4616
4867
  catch { /* keep the voice for this run; nothing to surface */ }
4617
4868
  };
4869
+ // task-1411: persist the < / > speaking-rate nudge to config.voice.rate (actor 'user', journaled →
4870
+ // undo works). Best-effort: a failure keeps the rate for this run and never breaks the UI.
4871
+ const persistVoiceRate = (rate) => {
4872
+ const ui = state.custom;
4873
+ if (!ui)
4874
+ return;
4875
+ try {
4876
+ const op = ui.config.voice
4877
+ ? { op: 'add', path: '/voice/rate', value: rate }
4878
+ : { op: 'add', path: '/voice', value: { rate } };
4879
+ const { config } = applyAndJournal(ui.home, [op], { actor: 'user', why: `voice rate ${rate}`, now: state.now.getTime() });
4880
+ ui.config = config;
4881
+ }
4882
+ catch { /* keep the rate for this run; nothing to surface */ }
4883
+ };
4618
4884
  const initVoice = async () => {
4619
4885
  if (voiceInitStarted)
4620
4886
  return;
@@ -4643,6 +4909,7 @@ export function interactive(state, io, depth, args, proc = process) {
4643
4909
  } },
4644
4910
  onDecision: handleVoiceDecision,
4645
4911
  onVoiceChange: persistVoiceChoice,
4912
+ onRateChange: persistVoiceRate,
4646
4913
  });
4647
4914
  voice.enable();
4648
4915
  await voice.toggleTalk(); // the first `v` press both enables and starts listening
@@ -4706,6 +4973,35 @@ export function interactive(state, io, depth, args, proc = process) {
4706
4973
  quit();
4707
4974
  return;
4708
4975
  }
4976
+ // task-1413: a pending analyst config PROPOSAL owns the next key. `y` accepts and journals it as
4977
+ // actor 'analyst' (ctrl+z undoes); any other key dismisses it. A stray key never persists a change.
4978
+ // Two guards (defect c): the proposal does NOT steal a key while a TEXT INPUT is open — the GO-TO
4979
+ // prompt or a focused analyst chat line — so a 'y' being typed goes into the field, not the accept;
4980
+ // and an EXPIRED proposal (its 8s toast has faded) is silently dropped, never accepted by a late 'y'.
4981
+ if (state.analystConfigProposal) {
4982
+ const inputOpen = !!state.goto || (state.aiFocused === true);
4983
+ const expired = state.now.getTime() >= state.analystConfigProposal.expiresAt;
4984
+ if (expired) {
4985
+ state.analystConfigProposal = null; // let the key fall through to its normal handler below
4986
+ }
4987
+ else if (!inputOpen) {
4988
+ const prop = state.analystConfigProposal;
4989
+ state.analystConfigProposal = null;
4990
+ if (key.ch === 'y' || key.ch === 'Y') {
4991
+ try {
4992
+ say(`analyst ${applyAnalystConfig(state, prop)} · ctrl+z undoes`, 'ai', 5);
4993
+ }
4994
+ catch (e) {
4995
+ say(`change refused: ${describeError(e)}`, 'warn', 4);
4996
+ }
4997
+ }
4998
+ else {
4999
+ say('dismissed the analyst change', 'ai', 3);
5000
+ }
5001
+ draw();
5002
+ continue;
5003
+ }
5004
+ }
4709
5005
  // task-1388 GO-TO prompt: while the "open any coin" input is open it owns the keyboard. Route the key
4710
5006
  // straight to applyKey (which does the capture / open) and skip the voice, calm, demo and keymap
4711
5007
  // handlers below — otherwise a base58 char like v / m / c would toggle voice or calm instead of typing.
@@ -4740,7 +5036,7 @@ export function interactive(state, io, depth, args, proc = process) {
4740
5036
  // `micEngaged` also covers the window while the mic is still OPENING (listen() in flight), so a key
4741
5037
  // pressed before the engine came up closes the session the moment it arrives (task-1339, N3/N8).
4742
5038
  if (voice?.micEngaged) {
4743
- const isVoiceCtl = key.name === 'char' && !key.ctrl && !key.alt && (key.ch === 'v' || key.ch === 'm' || key.ch === ',');
5039
+ const isVoiceCtl = key.name === 'char' && !key.ctrl && !key.alt && (key.ch === 'v' || key.ch === 'm' || key.ch === ',' || key.ch === '<' || key.ch === '>');
4744
5040
  if (key.name === 'esc') {
4745
5041
  voice.cancelListen();
4746
5042
  draw();
@@ -4782,6 +5078,16 @@ export function interactive(state, io, depth, args, proc = process) {
4782
5078
  draw();
4783
5079
  continue;
4784
5080
  }
5081
+ if (voice?.isEnabled && key.ch === '<') {
5082
+ voice.nudgeRate(-1);
5083
+ draw();
5084
+ continue;
5085
+ } // slower (task-1411)
5086
+ if (voice?.isEnabled && key.ch === '>') {
5087
+ voice.nudgeRate(1);
5088
+ draw();
5089
+ continue;
5090
+ } // faster (task-1411)
4785
5091
  }
4786
5092
  }
4787
5093
  // A modal overlay (welcome/help) is open: any key dismisses it; `?` switches welcome→help (task-1305).
@@ -4800,6 +5106,32 @@ export function interactive(state, io, depth, args, proc = process) {
4800
5106
  draw();
4801
5107
  continue;
4802
5108
  }
5109
+ // task-1407: the PnL card preview owns the keys while open (share / save / toggles / other coin / esc).
5110
+ if (state.overlay === 'pnl' && state.pnl) {
5111
+ const ui = state.pnl;
5112
+ const eff = pnlKey(ui, key);
5113
+ if (eff.kind === 'close') {
5114
+ state.overlay = null;
5115
+ state.pnl = null;
5116
+ }
5117
+ else if (eff.kind === 'reload') {
5118
+ ui.stats = null;
5119
+ ui.error = null;
5120
+ void loadPnl(state).then(() => { if (!done)
5121
+ draw(); });
5122
+ }
5123
+ else if (eff.kind === 'share' && ui.stats) {
5124
+ ui.busy = true;
5125
+ ui.notice = null;
5126
+ void shareCard(ui.stats, { ...ui.opts }, eff.action, { platform: process.platform, env: io.env, run: runLocal, openUrl: (u) => openInBrowser(u), copyText: copyToClipboard })
5127
+ .then((r) => { ui.notice = r.message; say(r.message, 'info', 10); })
5128
+ .catch((e) => { ui.notice = `card failed: ${describeError(e)}`; })
5129
+ .finally(() => { ui.busy = false; if (!done)
5130
+ draw(); });
5131
+ }
5132
+ draw();
5133
+ continue;
5134
+ }
4803
5135
  // task-1358: the WORKSPACES card — a digit picks a slot, j/k move, ⏎ activates, esc/q/ctrl+w w close.
4804
5136
  if (state.overlay === 'workspaces' && state.custom) {
4805
5137
  const ui = state.custom;
@@ -5449,6 +5781,16 @@ export function interactive(state, io, depth, args, proc = process) {
5449
5781
  say(`export failed: ${describeError(e)}`, 'warn', 6);
5450
5782
  }
5451
5783
  }
5784
+ if (state.pendingPnl) {
5785
+ // task-1407: open the PnL card preview for this screen (recorded fills only), then build the card.
5786
+ state.pendingPnl = false;
5787
+ const why = openPnl(state);
5788
+ if (why)
5789
+ say(why, 'warn', 6);
5790
+ else
5791
+ void loadPnl(state).then(() => { if (!done)
5792
+ draw(); });
5793
+ }
5452
5794
  if (state.pendingToast) {
5453
5795
  // task-1315: surface the link list / "no link" reply as a durable toast. Drained BEFORE
5454
5796
  // pendingOpen so that on a successful open the "opened <url>" toast overwrites the "open <url>" one.
@@ -5528,6 +5870,9 @@ export function interactive(state, io, depth, args, proc = process) {
5528
5870
  lastTokenRefresh = Date.now();
5529
5871
  void live.token.load();
5530
5872
  }
5873
+ // task-1404: keep the open token's credit-metered holder/rug intel LIVE (slow cadence, budget-gated inside).
5874
+ if (state.screen === 'token' && live.token)
5875
+ live.refreshOpenIntel(Date.now());
5531
5876
  if (state.screen === 'watchlist' && Date.now() - lastWatchRefresh >= WATCH_REFRESH_MS) {
5532
5877
  lastWatchRefresh = Date.now();
5533
5878
  void live.watch.refresh();
@@ -3,7 +3,7 @@
3
3
  // dexterm mcp run the read-only MCP server on stdio (what the analyst uses and
4
4
  // what `claude mcp add dexterm -- dexterm mcp` wires up)
5
5
  // dexterm mcp install <client> write/print the MCP config for a client (claude-code, claude-desktop,
6
- // cursor, codex, openclaw, generic). Shows the exact change, asks first,
6
+ // cursor, codex, generic). Shows the exact change, asks first,
7
7
  // is idempotent, and backs the file up before editing.
8
8
  // dexterm mcp doctor spawn the server over stdio, do the MCP handshake, and list the tools,
9
9
  // prompts and resources the agent sees — proof the connection works.
@@ -60,15 +60,10 @@ export const CLIENTS = [
60
60
  path: (env) => join(env.CODEX_HOME || join(home(env), '.codex'), 'config.toml'),
61
61
  note: 'Codex reads MCP servers from the [mcp_servers.*] tables in ~/.codex/config.toml.',
62
62
  },
63
- {
64
- id: 'openclaw', label: 'OpenClaw / generic agent', format: 'print', keyPath: 'mcpServers',
65
- path: () => null,
66
- note: 'OpenClaw and most agents take a standard mcpServers entry; paste this where your agent reads MCP servers, or pass --config FILE to write a JSON file.',
67
- },
68
63
  {
69
64
  id: 'generic', label: 'Generic (print the snippet)', format: 'print', keyPath: 'mcpServers',
70
65
  path: () => null,
71
- note: 'the standard stdio MCP server entry; add it to your client\'s MCP config.',
66
+ note: 'the standard stdio mcpServers entry most agents take; paste it where your agent reads MCP servers, or pass --config FILE to write a JSON file.',
72
67
  },
73
68
  ];
74
69
  export function clientById(id) { return CLIENTS.find((c) => c.id === id); }