dexterm-cli 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/packages/agents/src/spectator.js +28 -2
  2. package/dist/packages/agents/src/tiers.js +3 -0
  3. package/dist/packages/agents/src/tools.js +3 -0
  4. package/dist/packages/autotrade/src/backtest.js +6 -2
  5. package/dist/packages/autotrade/src/chat/connector.js +5 -4
  6. package/dist/packages/autotrade/src/chat/control.js +3 -0
  7. package/dist/packages/autotrade/src/chat/hub.js +37 -2
  8. package/dist/packages/autotrade/src/chat/index.js +3 -2
  9. package/dist/packages/autotrade/src/chat/readonly.js +530 -0
  10. package/dist/packages/autotrade/src/chat/service.js +12 -1
  11. package/dist/packages/autotrade/src/engine.js +75 -29
  12. package/dist/packages/autotrade/src/fleet.js +2 -2
  13. package/dist/packages/autotrade/src/index.js +4 -0
  14. package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
  15. package/dist/packages/autotrade/src/rugproof.js +231 -0
  16. package/dist/packages/autotrade/src/runner.js +203 -30
  17. package/dist/packages/autotrade/src/service-install.js +6 -4
  18. package/dist/packages/autotrade/src/sessions.js +16 -7
  19. package/dist/packages/autotrade/src/store.js +1 -1
  20. package/dist/packages/autotrade/src/strategy-stats.js +344 -0
  21. package/dist/packages/autotrade/src/strategy.js +29 -1
  22. package/dist/packages/autotrade/src/tiers.js +194 -0
  23. package/dist/packages/classifier/src/calibrate.js +138 -0
  24. package/dist/packages/classifier/src/classify.js +107 -0
  25. package/dist/packages/classifier/src/config.js +298 -0
  26. package/dist/packages/classifier/src/dossier.js +95 -0
  27. package/dist/packages/classifier/src/hardware.js +63 -0
  28. package/dist/packages/classifier/src/index.js +19 -0
  29. package/dist/packages/classifier/src/models.js +111 -0
  30. package/dist/packages/classifier/src/providers/heuristic.js +77 -0
  31. package/dist/packages/classifier/src/providers/http.js +63 -0
  32. package/dist/packages/classifier/src/providers/jev-open.js +86 -0
  33. package/dist/packages/classifier/src/providers/llm.js +89 -0
  34. package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
  35. package/dist/packages/classifier/src/questions.js +97 -0
  36. package/dist/packages/classifier/src/registry.js +42 -0
  37. package/dist/packages/classifier/src/snapshot.js +31 -0
  38. package/dist/packages/classifier/src/systemone.js +88 -0
  39. package/dist/packages/classifier/src/types.js +18 -0
  40. package/dist/packages/classifier/src/win-acl.js +71 -0
  41. package/dist/packages/cli/assets/dash/dash.css +139 -0
  42. package/dist/packages/cli/assets/dash/dash.js +200 -0
  43. package/dist/packages/cli/assets/dash/icon.svg +1 -0
  44. package/dist/packages/cli/assets/dash/index.html +36 -0
  45. package/dist/packages/cli/src/app-help-gen.js +13 -1
  46. package/dist/packages/cli/src/autotrade-cmd/fleet.js +12 -3
  47. package/dist/packages/cli/src/autotrade-cmd/run.js +14 -2
  48. package/dist/packages/cli/src/autotrade-cmd/service.js +3 -1
  49. package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
  50. package/dist/packages/cli/src/autotrade-cmd/strategies.js +1 -1
  51. package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +2 -0
  52. package/dist/packages/cli/src/classifier-cmd.js +361 -0
  53. package/dist/packages/cli/src/classifier-live.js +81 -0
  54. package/dist/packages/cli/src/dash/data.js +217 -0
  55. package/dist/packages/cli/src/dash/server.js +92 -0
  56. package/dist/packages/cli/src/dash-cmd.js +113 -0
  57. package/dist/packages/cli/src/doctor.js +34 -0
  58. package/dist/packages/cli/src/live/screens/token-live.js +4 -0
  59. package/dist/packages/cli/src/live/signals.js +8 -0
  60. package/dist/packages/cli/src/main.js +98 -6
  61. package/dist/packages/cli/src/mcp-follow.js +21 -1
  62. package/dist/packages/cli/src/narratives-cmd.js +147 -0
  63. package/dist/packages/cli/src/narratives.js +114 -0
  64. package/dist/packages/cli/src/overlay/model.js +147 -0
  65. package/dist/packages/cli/src/overlay/page.js +146 -0
  66. package/dist/packages/cli/src/overlay/pnl.js +75 -0
  67. package/dist/packages/cli/src/overlay/server.js +170 -0
  68. package/dist/packages/cli/src/overlay-cmd.js +367 -0
  69. package/dist/packages/cli/src/record-cmd.js +82 -11
  70. package/dist/packages/cli/src/rules-cmd.js +2 -1
  71. package/dist/packages/cli/src/rules.js +24 -4
  72. package/dist/packages/cli/src/screens/narrative.js +154 -2
  73. package/dist/packages/cli/src/skill-eval-build.js +7 -2
  74. package/dist/packages/cli/src/state.js +1 -0
  75. package/dist/packages/cli/src/version.js +1 -1
  76. package/dist/packages/custom/src/rules.js +5 -1
  77. package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +1 -1
  78. package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
  79. package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
  80. package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
  81. package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
  82. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-confidence-tiers-experimental-unproven-paper-only.md +56 -0
  83. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
  84. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +3 -1
  85. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
  86. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
  87. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  88. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
  89. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
  90. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
  91. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
  92. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  93. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
  94. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +2 -0
  95. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +2 -0
  96. package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +36 -0
  97. package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
  98. package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
  99. package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
  100. package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
  101. package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
  102. package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
  103. package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
  104. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
  105. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
  106. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  107. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
  108. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  109. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
  110. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
  111. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
  112. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
  113. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
  114. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
  115. package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
  116. package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
  117. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
  118. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
  119. package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
  120. package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
  121. package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
  122. package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
  123. package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
  124. package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
  125. package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
  126. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
  127. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
  128. package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
  129. package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
  130. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
  131. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +2 -0
  132. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
  133. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
  134. package/dist/packages/mcp/assets/kb/docs/docs-telegram-read-only-data-check-top-and-signals-experimental.md +48 -0
  135. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +3 -1
  136. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +1 -1
  137. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
  138. package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
  139. package/dist/packages/mcp/src/server.js +106 -1
  140. package/dist/packages/mcp/src/spectator-sink.js +27 -0
  141. package/dist/packages/narratives/src/cluster.js +193 -0
  142. package/dist/packages/narratives/src/embed.js +184 -0
  143. package/dist/packages/narratives/src/export.js +60 -0
  144. package/dist/packages/narratives/src/index.js +10 -0
  145. package/dist/packages/narratives/src/label.js +48 -0
  146. package/dist/packages/narratives/src/normalize.js +179 -0
  147. package/dist/packages/narratives/src/recorder.js +93 -0
  148. package/dist/packages/narratives/src/tracker.js +328 -0
  149. package/dist/packages/packs/src/skill-care.js +10 -2
  150. package/dist/packages/recorder/src/exit-probes.js +215 -0
  151. package/dist/packages/recorder/src/index.js +1 -0
  152. package/dist/packages/recorder/src/recorder.js +11 -1
  153. package/dist/packages/recorder/src/retention.js +4 -0
  154. package/dist/packages/recorder/src/schema.js +20 -1
  155. package/dist/packages/tui/src/keymap.js +5 -0
  156. package/package.json +3 -1
  157. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
@@ -95,13 +95,18 @@ const finiteOr = (x, d = null) => (typeof x === 'number' && Number.isFinite(x) ?
95
95
  // eslint-disable-next-line no-control-regex
96
96
  const UNSAFE_RE = /[\u0000-\u001f\u007f-\u009f​-‏‪-‮⁦-⁩]/g;
97
97
  const shortStr = (x, max) => (typeof x === 'string' ? x.replace(UNSAFE_RE, '').slice(0, max) : '');
98
- const isMintish = (x) => typeof x === 'string' && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(x);
98
+ const isFullMint = (x) => typeof x === 'string' && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(x);
99
+ /** A mint as the redactor leaves it (maskAddress: head4…tail4). */
100
+ const isMaskedMint = (x) => typeof x === 'string' && /^[1-9A-HJ-NP-Za-km-z]{4}…[1-9A-HJ-NP-Za-km-z]{4}$/.test(x);
99
101
  /**
100
102
  * task-1466: re-validate a detail read off the local channel (shape, types, lengths). The channel is our own
101
103
  * server's 0600 file and token-checked, but the viewer still treats it as data: anything malformed is dropped,
102
104
  * strings are stripped of control/bidi characters and capped, numbers must be finite. Returns null when empty.
103
105
  */
104
- export function sanitizeDetail(raw) {
106
+ export function sanitizeDetail(raw, opts = {}) {
107
+ // task-1427: a detail read off the SHARED stream (or a saved file) is already redacted, so its mints are head…tail;
108
+ // `masked` accepts that shape (and only that shape) instead of a full mint. The local channel keeps the strict check.
109
+ const isMintish = (x) => (opts.masked ? isMaskedMint(x) : isFullMint(x));
105
110
  if (!raw || typeof raw !== 'object' || Array.isArray(raw))
106
111
  return null;
107
112
  const r = raw;
@@ -140,6 +145,27 @@ export function sanitizeDetail(raw) {
140
145
  out.caps = { perTradeUsd: cp.perTradeUsd, totalUsd: cp.totalUsd, maxPriceImpactPct: finiteOr(cp.maxPriceImpactPct) };
141
146
  if (typeof r.refusal === 'string' && r.refusal)
142
147
  out.refusal = shortStr(r.refusal, 40);
148
+ const h = r.holders;
149
+ const top10 = h ? finiteOr(h.top10Pct) : null;
150
+ if (h && top10 !== null && top10 >= 0 && top10 <= 100 && (h.basis === 'ex-pools' || h.basis === 'raw')) {
151
+ const count = finiteOr(h.count), ageSec = finiteOr(h.ageSec);
152
+ out.holders = { top10Pct: top10, count: count !== null && count >= 0 ? Math.round(count) : null, basis: h.basis, ageSec: ageSec !== null && ageSec >= 0 ? ageSec : null };
153
+ }
154
+ if (Array.isArray(r.liveFlags)) {
155
+ const LEVELS = ['up', 'down', 'info', 'warn'];
156
+ const sigs = r.liveFlags.slice(0, 8).flatMap((x) => {
157
+ const o = x;
158
+ if (!o || typeof o !== 'object')
159
+ return [];
160
+ const kind = shortStr(o.kind, 16), title = shortStr(o.title, 24), text = shortStr(o.text, 120);
161
+ const at = finiteOr(o.at), ageSec = finiteOr(o.ageSec);
162
+ if (!kind || !title || at === null || ageSec === null || ageSec < 0 || !LEVELS.includes(o.level))
163
+ return [];
164
+ return [{ kind, level: o.level, title, text, source: shortStr(o.source, 24) || '?', at, ageSec, stale: o.stale === true }];
165
+ });
166
+ if (sigs.length)
167
+ out.liveFlags = sigs;
168
+ }
143
169
  return Object.keys(out).length ? out : null;
144
170
  }
145
171
  // ---------------------------------------------------------------------------------------------------
@@ -46,6 +46,9 @@ export const TOOL_TIERS = Object.freeze({
46
46
  // agents in task-1405 so a bring-your-own-agent gets the same code-stage analysis the built-in analyst does.
47
47
  analyze_token: 'observe',
48
48
  narrative_board: 'observe',
49
+ narratives_top: 'observe',
50
+ narrative_detail: 'observe',
51
+ narrative_of: 'observe',
49
52
  rug_backtest: 'observe',
50
53
  kb_lookup: 'observe',
51
54
  kb_search: 'observe',
@@ -35,6 +35,9 @@ const TOOLS_DEF = [
35
35
  { name: 'get_dev_history', tier: 'observe', description: 'Creator address and prior-launch forensics. Tier-1 fields need a key and are null on the keyless tier.', inputSchema: { type: 'object', properties: { mint: MINT }, required: ['mint'], additionalProperties: false } },
36
36
  { name: 'analyze_token', tier: 'observe', description: 'Deterministic token-dossier analysis pack: numeric features, named rule flags, a WATCH/SPECULATIVE/AVOID verdict, a p(rug) heuristic and data ages. No model, no token cost; a reasoning stage may only tighten the verdict, never loosen it.', inputSchema: { type: 'object', properties: { mint: MINT, symbol: SYMBOL, interval: { type: 'string', enum: ['1m', '5m', '15m', '1h', '4h', '1d'] } }, additionalProperties: false } },
37
37
  { name: 'narrative_board', tier: 'observe', description: 'The narrative board over recorded + trending tokens: per-narrative volume, new launches, runners and a "what is working" score. No model; returns a note when there is no recorded history yet.', inputSchema: { type: 'object', properties: { window: { type: 'string', enum: ['1h', '24h'] }, limit: { type: 'integer', minimum: 1, maximum: 50 }, summary: { type: 'boolean' } }, additionalProperties: false } },
38
+ { name: 'narratives_top', tier: 'observe', description: 'MEMES + NARRATIVES tracker: the hottest name/ticker-theme clusters of the last 26 h with lifecycle (spark/ignition/peak/fading/dead), copy-cat saturation, volume share and a traction score. Measured heat, not advice.', inputSchema: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 50 }, lifecycle: { type: 'string', enum: ['spark', 'ignition', 'peak', 'fading'] } }, additionalProperties: false } },
39
+ { name: 'narrative_detail', tier: 'observe', description: 'One narrative by id: per-window metrics, lifecycle rule and member coins in spawn order. Measured heat, not advice.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, limit: { type: 'integer', minimum: 1, maximum: 100 } }, required: ['id'], additionalProperties: false } },
40
+ { name: 'narrative_of', tier: 'observe', description: 'Which narrative a coin belongs to: label, lifecycle, rank, spawn index, saturation. Measured heat, not advice.', inputSchema: { type: 'object', properties: { mint: { type: 'string' } }, required: ['mint'], additionalProperties: false } },
38
41
  { name: 'rug_backtest', tier: 'observe', description: 'Backtest the rug model or recorded pack verdicts against RECORDED outcomes: honest precision/recall, Brier, AUC and calibration. Leakage-free by construction; returns a note when there is not enough history. Read-only.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['heuristic', 'pack_verdicts'] }, horizon_sec: { type: 'integer', enum: [3600, 7200, 21600, 86400, 259200] }, positive_labels: { type: 'array', items: { type: 'string', enum: ['rug', 'dead', 'fade', 'flat', 'runner'] }, maxItems: 5 } }, additionalProperties: false } },
39
42
  { name: 'kb_lookup', tier: 'observe', description: 'One knowledge-base entry by id (mechanics / patterns / glossary). KB never holds facts about a live token.', inputSchema: { type: 'object', properties: { id: { type: 'string', maxLength: 64 } }, required: ['id'], additionalProperties: false } },
40
43
  { name: 'kb_search', tier: 'observe', description: 'Search the knowledge base; returns ids, titles and an excerpt.', inputSchema: { type: 'object', properties: { query: { type: 'string', maxLength: 200 }, limit: { type: 'integer', minimum: 1, maximum: 10 } }, required: ['query'], additionalProperties: false } },
@@ -19,6 +19,7 @@
19
19
  import { SKILL_MIN_N, wilson } from '../../packs/src/index.js';
20
20
  import { EntryRules } from "./engine.js";
21
21
  import { runAutotrade } from "./runner.js";
22
+ import { rugproofTapeVet } from "./rugproof.js";
22
23
  import { validateStrategy, round } from "./strategy.js";
23
24
  export const DATA_LIMITS = [
24
25
  'Price path = recorder snapshots (~2-min cadence while listed, written on >= 5% moves or every 30 min), not candles or trades: intrabar spikes and dips are invisible, so stops and take-profits fill at the next recorded price.',
@@ -31,9 +32,12 @@ export const DATA_LIMITS = [
31
32
  ];
32
33
  /** A closed trade at or below this is counted as a rug (the challenge's rugAtPct). */
33
34
  export const RUG_PCT = -80;
34
- export async function backtest(strategy, tape, sessions = null) {
35
+ export async function backtest(strategy, tape, sessions = null, opts = {}) {
36
+ // task-1748: a rug-proof strategy replays its tick gates + the pump.fun-mint proxy (the chain gates need live data).
37
+ const rp = strategy.entry.safety.rugproof;
38
+ const vet = opts.vet === null ? undefined : opts.vet ?? (rp ? rugproofTapeVet(rp) : undefined);
35
39
  // task-1645: a strategy in mode real backtests as its paper twin (a backtest never emits a trade-fire signal).
36
- const run = await runAutotrade({ strategy: strategy.mode === 'real' ? { ...strategy, mode: 'paper' } : strategy, tape, sessions, pacing: { mode: 'lockstep' } });
40
+ const run = await runAutotrade({ strategy: strategy.mode === 'real' ? { ...strategy, mode: 'paper' } : strategy, tape, sessions, pacing: { mode: 'lockstep' }, ...(vet ? { vet } : {}) });
37
41
  return { report: report(strategy, tape, run), run };
38
42
  }
39
43
  export function report(s, tape, run) {
@@ -8,8 +8,8 @@
8
8
  // callback data come from engine state only.
9
9
  //
10
10
  // Transports: telegram (long poll from the user's machine, telegram.ts) and fake (in memory, fake.ts, every test).
11
- /** The closed command list (§8.4 tier 2). Anything else is `help`. */
12
- export const COMMANDS = ['status', 'positions', 'pause', 'resume', 'kill', 'stop', 'flatten', 'strategies', 'use', 'approvals', 'help', 'pair', 'start'];
11
+ /** The closed command list (§8.4 tier 2; task-1426 adds the read-only check, top and signals). Anything else is `help`. */
12
+ export const COMMANDS = ['status', 'positions', 'pause', 'resume', 'kill', 'stop', 'flatten', 'strategies', 'use', 'approvals', 'help', 'pair', 'start', 'check', 'top', 'signals'];
13
13
  /** "/Status@my_bot arg" → {command, args}; null when it is not a slash command. Unknown names map to help. */
14
14
  export function parseCommand(text) {
15
15
  const t = text.trim();
@@ -18,7 +18,8 @@ export function parseCommand(text) {
18
18
  const [head, ...rest] = t.split(/\s+/);
19
19
  const name = head.slice(1).split('@')[0].toLowerCase();
20
20
  const command = COMMANDS.includes(name) ? name : 'help';
21
- // Args are short tokens only (a preset id, a pairing code, on/off): never prose.
22
- const args = rest.slice(0, 3).map((a) => a.slice(0, 40));
21
+ // Args are short tokens only (a preset id, a pairing code, on/off, a count, a mint): never prose. 48 characters
22
+ // keeps a whole mint (44 at most) and leaves an over-long one too long to pass as one (task-1426).
23
+ const args = rest.slice(0, 3).map((a) => a.slice(0, 48));
23
24
  return { command, args };
24
25
  }
@@ -10,6 +10,8 @@
10
10
  // /use <id> an equal-or-tighter validated strategy only, after a [Yes, switch] button;
11
11
  // never changes paper/live; a looser one says "change it locally"
12
12
  // /approvals on every entry waits for a tap (safer). `off` is local only
13
+ // /check <CA> · /top [N] · /signals [N] read-only data (task-1426): not routed here at all; the hub sends them
14
+ // to the ReadOnlyRouter (readonly.ts), which never sees this port
13
15
  // Not from chat, ever: live mode, looser caps or strategies, approvals off, resuming after a kill or a panic.
14
16
  // The router only calls the ControlPort the host gives it; the port writes the same control file the CLI and the
15
17
  // Q screen write, so chat is a remote control for the same local engine and adds no path around its fences.
@@ -22,6 +24,7 @@ export const HELP_LINES = [
22
24
  '/flatten (sell everything, asks first)',
23
25
  '/use <strategy> (equal or tighter only, asks first)',
24
26
  '/approvals on (every entry waits for your tap)',
27
+ '/check <CA> · /top [N] · /signals [N] (read-only: data, not advice)',
25
28
  'Trades are approved only with buttons. I never ask for keys or seed phrases.',
26
29
  ];
27
30
  export const NEVER_RATE_LIMITED = ['kill', 'stop', 'pause', 'flatten'];
@@ -6,15 +6,21 @@
6
6
  // pairing (only while a local pairing window is open: `/pair <code>` from a private chat);
7
7
  // the allowlist: an unknown sender gets NO reply and nothing happens (five in an hour raise a local alert:
8
8
  // "possible token leak"); a paired sender needs the tier the command needs, in the chat it was paired in;
9
- // rate limit: 10 commands a minute per sender, except /kill, /stop, /pause and /flatten (never limited);
9
+ // rate limit: 10 commands a minute per sender, except /kill, /stop, /pause and /flatten (never limited); a
10
+ // read-only data command that reaches the data sources also counts against READS_PER_MIN per chat (task-2146,
11
+ // readonly.ts): over it, the chat is told once a minute, then gets silence;
10
12
  // commands → the ControlRouter; button callbacks → the ApprovalBroker (the only trade authority);
13
+ // the read-only data commands (/check /top /signals, task-1426) → the ReadOnlyRouter, built from the ReadOnlyPort
14
+ // alone: it never sees the ControlPort or the broker and answers with lines only (no confirm, no button);
11
15
  // free text → the help text, never parsed.
12
16
  // Outbound: notify messages go to every chat paired with the notify tier; replies go to the asking chat only.
13
17
  import { allowed } from "./config.js";
14
18
  import { ControlRouter, HELP_LINES, NEVER_RATE_LIMITED } from "./control.js";
19
+ import { READ_COMMANDS, ReadOnlyRouter } from "./readonly.js";
15
20
  export const COMMANDS_PER_MIN = 10;
16
21
  export const UNKNOWN_ALERT_PER_HOUR = 5;
17
- const READ_ONLY = ['status', 'positions', 'strategies', 'help', 'start'];
22
+ const READ_ONLY = ['status', 'positions', 'strategies', 'help', 'start', ...READ_COMMANDS];
23
+ const isReadCommand = (c) => READ_COMMANDS.includes(c);
18
24
  export class ChatHub {
19
25
  o;
20
26
  now;
@@ -23,12 +29,14 @@ export class ChatHub {
23
29
  unknownTimes = [];
24
30
  lastUnknownAlert = 0;
25
31
  router;
32
+ reads;
26
33
  pairing = null;
27
34
  inflight = new Set();
28
35
  constructor(o) {
29
36
  this.o = o;
30
37
  this.now = o.now ?? Date.now;
31
38
  this.router = o.port ? new ControlRouter(o.port) : null;
39
+ this.reads = o.reads ? new ReadOnlyRouter(o.reads, this.now) : null;
32
40
  o.connector.onCommand((c) => this.track(this.onCommand(c)));
33
41
  o.connector.onAction((a) => this.track(this.onAction(a)));
34
42
  o.connector.onText((t) => this.track(this.onText(t)));
@@ -41,6 +49,7 @@ export class ChatHub {
41
49
  async idle() { while (this.inflight.size)
42
50
  await Promise.allSettled([...this.inflight]); }
43
51
  setPort(port) { this.router = port ? new ControlRouter(port) : null; }
52
+ setReads(reads) { this.reads = reads ? new ReadOnlyRouter(reads, this.now) : null; }
44
53
  /** Open a local pairing window: `/pair <code>` from a private chat is checked against it. */
45
54
  openPairing(window, onResult) { this.pairing = { window, onResult }; }
46
55
  closePairing() { this.pairing = null; }
@@ -124,6 +133,10 @@ export class ChatHub {
124
133
  await this.reply(c.chatId, ['Too many commands: wait a minute. /kill and /pause always work.'], 'rate_limited');
125
134
  return;
126
135
  }
136
+ if (isReadCommand(c.command)) {
137
+ await this.onRead(c);
138
+ return;
139
+ }
127
140
  if (!this.router) {
128
141
  await this.reply(c.chatId, ['No runner is connected right now.'], 'no_runner');
129
142
  return;
@@ -139,6 +152,28 @@ export class ChatHub {
139
152
  await this.o.broker.confirm(cf.kind, { senderId: c.senderId, chatId: c.chatId }, cf.lines, cf.yes, cf.payload);
140
153
  }
141
154
  }
155
+ /** A read-only data command: lines from the ReadOnlyRouter, never a confirm or a ControlPort call. */
156
+ async onRead(c) {
157
+ if (!this.reads) {
158
+ await this.reply(c.chatId, ['Read-only data is not available here: use dexterm on your computer.'], 'no_reads');
159
+ return;
160
+ }
161
+ let lines;
162
+ try {
163
+ const r = await this.reads.handle(c);
164
+ lines = r.lines;
165
+ if (r.failed)
166
+ this.o.audit.add(this.now(), 'read_failed', { command: c.command, error: r.failed.slice(0, 160) });
167
+ if (r.limited)
168
+ this.o.audit.add(this.now(), 'rate_limited', { senderId: c.senderId, chatId: c.chatId, command: c.command, scope: 'reads', told: lines.length > 0 });
169
+ }
170
+ catch (e) {
171
+ this.o.audit.add(this.now(), 'hub_error', { command: c.command, error: e instanceof Error ? e.message.slice(0, 160) : String(e).slice(0, 160) });
172
+ lines = ['Could not read that right now (the data sources did not answer). Try again in a minute.'];
173
+ }
174
+ this.o.audit.add(this.now(), 'command_result', { updateId: c.updateId, command: c.command, lines: lines.length, confirms: 0 });
175
+ await this.reply(c.chatId, lines, `cmd_${c.command}`);
176
+ }
142
177
  async onAction(a) {
143
178
  if (this.dup(a.updateId)) {
144
179
  this.o.audit.add(this.now(), 'in_duplicate', { updateId: a.updateId });
@@ -1,7 +1,7 @@
1
1
  // dexterm autotrade chat (task-1527, AUTOTRADE-SPEC §8): notify, control and approve-to-trade through the user's own
2
2
  // bot. connector (the interface) · fake (tests) · telegram (long poll) · redact · format · config (connectors.json) ·
3
- // secrets (keychain / libsecret / 0600 file) · pairing · audit (hash chain) · notify · control · approvals · hub ·
4
- // service (what a runner host starts).
3
+ // secrets (keychain / libsecret / 0600 file) · pairing · audit (hash chain) · notify · control · readonly (task-1426:
4
+ // /check /top /signals) · approvals · hub · service (what a runner host starts).
5
5
  export * from "./connector.js";
6
6
  export * from "./fake.js";
7
7
  export * from "./telegram.js";
@@ -13,6 +13,7 @@ export * from "./pairing.js";
13
13
  export * from "./audit.js";
14
14
  export * from "./notify.js";
15
15
  export * from "./control.js";
16
+ export * from "./readonly.js";
16
17
  export * from "./approvals.js";
17
18
  export * from "./hub.js";
18
19
  export * from "./service.js";