dexterm-cli 0.1.1 → 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.
- package/dist/packages/agents/src/bus.js +57 -6
- package/dist/packages/agents/src/executor.js +25 -0
- package/dist/packages/agents/src/follow.js +105 -0
- package/dist/packages/agents/src/gate.js +3 -0
- package/dist/packages/agents/src/identity.js +41 -2
- package/dist/packages/agents/src/index.js +5 -0
- package/dist/packages/agents/src/spectator-server.js +153 -0
- package/dist/packages/agents/src/spectator.js +212 -0
- package/dist/packages/agents/src/tiers.js +5 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +6 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +11 -3
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +13 -3
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +3 -3
- package/dist/packages/cli/src/agent-cmd.js +107 -10
- package/dist/packages/cli/src/agent-spectator.js +37 -0
- package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
- package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
- package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
- package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
- package/dist/packages/cli/src/alerts/creds.js +83 -0
- package/dist/packages/cli/src/alerts/format.js +66 -0
- package/dist/packages/cli/src/alerts/index.js +10 -0
- package/dist/packages/cli/src/alerts/notifier.js +53 -0
- package/dist/packages/cli/src/alerts/route.js +19 -0
- package/dist/packages/cli/src/alerts/types.js +1 -0
- package/dist/packages/cli/src/alerts-cmd.js +212 -0
- package/dist/packages/cli/src/app-help-gen.js +365 -0
- package/dist/packages/cli/src/backtest-cmd.js +62 -3
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/doctor.js +21 -6
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/follow-cmd.js +107 -1
- package/dist/packages/cli/src/help-cmd.js +48 -0
- package/dist/packages/cli/src/live/index.js +66 -3
- package/dist/packages/cli/src/live/screens/token-live.js +24 -0
- package/dist/packages/cli/src/live/signals.js +109 -0
- package/dist/packages/cli/src/live/trade.js +24 -1
- package/dist/packages/cli/src/main.js +596 -49
- package/dist/packages/cli/src/mcp-cmd.js +321 -0
- package/dist/packages/cli/src/pnl-app.js +221 -0
- package/dist/packages/cli/src/pnl-card.js +615 -0
- package/dist/packages/cli/src/record-cmd.js +455 -0
- package/dist/packages/cli/src/rules.js +69 -2
- package/dist/packages/cli/src/screens/follows.js +91 -2
- package/dist/packages/cli/src/screens/overlay.js +3 -0
- package/dist/packages/cli/src/screens/positions.js +1 -1
- package/dist/packages/cli/src/screens/rules.js +1 -1
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- package/dist/packages/cli/src/spectate-cmd.js +104 -0
- package/dist/packages/cli/src/state.js +1 -1
- package/dist/packages/cli/src/update.js +294 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/voice-cmd.js +194 -0
- package/dist/packages/cli/src/voice-hud.js +1 -1
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/custom/src/rules.js +3 -2
- package/dist/packages/custom/src/schema.js +24 -1
- package/dist/packages/data/src/analyst-actions.js +130 -3
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/fast-lane.js +16 -2
- package/dist/packages/data/src/index.js +4 -0
- package/dist/packages/data/src/intel/helius-history.js +12 -1
- package/dist/packages/data/src/intel/live-signals.js +185 -0
- package/dist/packages/data/src/larp/link-baseline.js +196 -0
- package/dist/packages/data/src/sources/dexscreener.js +14 -0
- package/dist/packages/data/src/sources/pumpportal.js +6 -1
- package/dist/packages/data/src/trade/copy/index.js +1 -1
- package/dist/packages/data/src/trade/copy/stats.js +36 -2
- package/dist/packages/data/src/trade/copy/tracker.js +3 -2
- package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
- package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
- package/dist/packages/mcp/src/app-help.js +135 -0
- package/dist/packages/mcp/src/index.js +3 -0
- package/dist/packages/mcp/src/server.js +179 -5
- package/dist/packages/mcp/src/spectator-sink.js +72 -0
- package/dist/packages/packs/src/index.js +3 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/packs/src/skill-care.js +206 -0
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +25 -0
- package/dist/packages/popup/src/pages/spectator.js +74 -0
- package/dist/packages/popup/src/selftest.js +19 -0
- package/dist/packages/popup/src/server.js +7 -1
- package/dist/packages/tui/src/index.js +3 -0
- package/dist/packages/tui/src/keymap.js +12 -0
- package/dist/packages/tui/src/raster-font.js +195 -0
- package/dist/packages/tui/src/raster.js +213 -0
- package/dist/packages/voice/src/controller.js +57 -4
- package/dist/packages/voice/src/doctor.js +96 -11
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +55 -8
- package/dist/packages/voice/src/index.js +9 -3
- package/dist/packages/voice/src/speakable.js +111 -0
- package/dist/packages/voice/src/stt/whisper.js +14 -4
- package/dist/packages/voice/src/tts/espeak.js +131 -0
- package/dist/packages/voice/src/tts/piper-setup.js +229 -0
- package/dist/packages/voice/src/tts/piper.js +449 -0
- package/dist/packages/voice/src/tts/say.js +72 -35
- package/dist/packages/voice/src/tts/voices.js +209 -0
- package/package.json +5 -9
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
// dexterm — `dexterm record` (task-1414): a HEADLESS collector that keeps the history recorder fed when
|
|
2
|
+
// nobody has the TUI open. The recorder (task-1329) only grows while the interactive app runs; the backtest
|
|
3
|
+
// (task-1401) is only worth re-running on fresh data, so this command runs the exact same recorder jobs
|
|
4
|
+
// (observe snapshots + feed membership, the 1 h/2 h/6 h/24 h/72 h outcome sweeps, labels, retention) with no
|
|
5
|
+
// TUI, to be driven by a user-level launchd LaunchAgent on the Mac mini.
|
|
6
|
+
//
|
|
7
|
+
// dexterm record [run] [--interval S] [--ticks N | --once] [--maintain S] [--max-db-mb N] [--no-newpairs] [--db PATH] [--verbose]
|
|
8
|
+
// dexterm record install [--interval S] [--max-db-mb N] [--load] [--dry-run]
|
|
9
|
+
// dexterm record uninstall [--unload]
|
|
10
|
+
// dexterm record status [--db PATH]
|
|
11
|
+
//
|
|
12
|
+
// SAFETY / BUDGETS: this builds the keyless live feed and the recorder exactly as the live app does, so every
|
|
13
|
+
// request goes through the data layer's rate limiter + health gate and the sweep's own per-sweep / per-day caps
|
|
14
|
+
// (DEFAULT_SWEEP). It never builds a signer, never trades, never prints the Helius key (the URL is built inside
|
|
15
|
+
// the data layer; only the provider name is ever logged). Modest CPU: one feed refresh per `--interval` (60 s
|
|
16
|
+
// default) and recorder maintenance at most every `--maintain` seconds. A soft disk cap pauses OBSERVATION
|
|
17
|
+
// (the growth source) above `--max-db-mb` while labels + retention keep running, so the DB cannot run away.
|
|
18
|
+
import { mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { dirname, join, resolve } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { defaultDbPath, resolveRpcUrl, extraRpcUrls, redactSecrets } from '../../data/src/index.js';
|
|
23
|
+
import { PumpPortalClient } from '../../data/src/index.js';
|
|
24
|
+
import { Recorder, recorderEnabled } from '../../recorder/src/index.js';
|
|
25
|
+
import { createLiveFeed } from "./feed.js";
|
|
26
|
+
import { NewPairsLive } from "./live/index.js";
|
|
27
|
+
/** The LaunchAgent label (reverse-DNS). One collector per user. */
|
|
28
|
+
export const COLLECTOR_LABEL = 'xyz.dexterm.collector';
|
|
29
|
+
/** Default spacing between feed refreshes (one observation pass). Background work: keep it unhurried. */
|
|
30
|
+
export const DEFAULT_INTERVAL_MS = 60_000;
|
|
31
|
+
/** Default spacing between recorder maintenance passes (sweeps/labels/retention). */
|
|
32
|
+
export const DEFAULT_MAINTAIN_MS = 60_000;
|
|
33
|
+
/** Default soft cap on the whole DB file. Above it, observation pauses; sweeps/labels/retention continue. */
|
|
34
|
+
export const DEFAULT_MAX_DB_MB = 1024;
|
|
35
|
+
/** Build the real collector engine: the keyless live feed, the recorder attached to its store, and (unless
|
|
36
|
+
* disabled) the PumpPortal new-pairs stream folded into the feed's candidate pool so fresh launches get
|
|
37
|
+
* recorded before the REST feeds index them. */
|
|
38
|
+
export function buildLiveEngine(env, opts) {
|
|
39
|
+
const rpc = resolveRpcUrl({ env });
|
|
40
|
+
const feed = createLiveFeed({
|
|
41
|
+
dbPath: opts.dbPath, rpcUrl: rpc.url, rpcFallbackUrl: rpc.fallbackUrl, rpcPublicPool: true, rpcExtraUrls: extraRpcUrls(env),
|
|
42
|
+
onRequest: opts.verbose ? (req, meta, o) => opts.onLog?.(` ${meta.source.padEnd(14)} ${String(o.status ?? 'ERR').padStart(3)} ${String(o.latencyMs).padStart(5)}ms ${redactSecrets(req.url).slice(0, 110)}\n`) : undefined,
|
|
43
|
+
});
|
|
44
|
+
const recorder = new Recorder({ path: opts.dbPath, onLog: opts.verbose ? opts.onLog : undefined });
|
|
45
|
+
recorder.attach(feed.dl.store);
|
|
46
|
+
let newpairs = null;
|
|
47
|
+
if (!opts.noNewpairs) {
|
|
48
|
+
newpairs = new NewPairsLive(feed.dl, {});
|
|
49
|
+
const client = new PumpPortalClient({ health: feed.dl.health, clock: feed.dl.clock, onEvent: (ev) => newpairs.handleEvent(ev), onStatus: () => { } });
|
|
50
|
+
newpairs.attach(client, 'live');
|
|
51
|
+
newpairs.start();
|
|
52
|
+
const np = newpairs;
|
|
53
|
+
feed.extraMints = () => [...np.rows.keys()];
|
|
54
|
+
}
|
|
55
|
+
const dl = feed.dl;
|
|
56
|
+
return {
|
|
57
|
+
async observe() {
|
|
58
|
+
const before = recorder.counters.written;
|
|
59
|
+
await feed.refresh();
|
|
60
|
+
return recorder.counters.written - before;
|
|
61
|
+
},
|
|
62
|
+
async maintain() { await recorder.maintain(dl); },
|
|
63
|
+
prune() { try {
|
|
64
|
+
recorder.prune();
|
|
65
|
+
}
|
|
66
|
+
catch { /* logged inside; never fatal */ } },
|
|
67
|
+
dbBytes() {
|
|
68
|
+
try {
|
|
69
|
+
const db = recorder.database;
|
|
70
|
+
const pc = db.prepare('PRAGMA page_count').get().page_count;
|
|
71
|
+
const ps = db.prepare('PRAGMA page_size').get().page_size;
|
|
72
|
+
return Number(pc) * Number(ps);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
newPairs() { return newpairs ? newpairs.rows.size : 0; },
|
|
79
|
+
close() {
|
|
80
|
+
try {
|
|
81
|
+
newpairs?.stop();
|
|
82
|
+
}
|
|
83
|
+
catch { /* ignore */ }
|
|
84
|
+
try {
|
|
85
|
+
feed.close();
|
|
86
|
+
}
|
|
87
|
+
catch { /* ignore */ }
|
|
88
|
+
try {
|
|
89
|
+
recorder.close();
|
|
90
|
+
}
|
|
91
|
+
catch { /* ignore */ }
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Drive the engine. Each tick (after the first waits `intervalMs`): if a disk cap is set and the DB is at/over
|
|
97
|
+
* it, force a retention pass; if it is still over, SKIP observation this tick (so the growth source pauses) but
|
|
98
|
+
* keep running maintenance. Otherwise observe one pass. Maintenance runs at most every `maintainEveryMs`.
|
|
99
|
+
* Never throws out of the loop: an engine error is logged and the loop continues to the next tick.
|
|
100
|
+
*/
|
|
101
|
+
export async function runCollectorLoop(engine, opts) {
|
|
102
|
+
const res = { ticks: 0, observePasses: 0, rowsWritten: 0, maintainPasses: 0, cappedTicks: 0 };
|
|
103
|
+
let lastMaintain = -Infinity;
|
|
104
|
+
for (let i = 0; i < opts.ticks && !opts.aborted(); i++) {
|
|
105
|
+
if (i > 0) {
|
|
106
|
+
await opts.sleep(opts.intervalMs);
|
|
107
|
+
if (opts.aborted())
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
res.ticks++;
|
|
111
|
+
let over = false;
|
|
112
|
+
if (opts.maxDbBytes !== null) {
|
|
113
|
+
const b = engine.dbBytes();
|
|
114
|
+
if (b !== null && b >= opts.maxDbBytes) {
|
|
115
|
+
engine.prune();
|
|
116
|
+
const b2 = engine.dbBytes();
|
|
117
|
+
over = b2 !== null && b2 >= opts.maxDbBytes;
|
|
118
|
+
if (over) {
|
|
119
|
+
res.cappedTicks++;
|
|
120
|
+
opts.onLog?.(`record: DB at ${(b2 / 1e6).toFixed(0)} MB ≥ cap ${(opts.maxDbBytes / 1e6).toFixed(0)} MB — observation paused this tick (labels + retention still run)\n`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!over) {
|
|
125
|
+
try {
|
|
126
|
+
res.rowsWritten += await engine.observe();
|
|
127
|
+
res.observePasses++;
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
opts.onLog?.(`record: observe failed: ${redactSecrets(e.message)}\n`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const now = opts.now();
|
|
134
|
+
if (now - lastMaintain >= opts.maintainEveryMs) {
|
|
135
|
+
lastMaintain = now;
|
|
136
|
+
try {
|
|
137
|
+
await engine.maintain();
|
|
138
|
+
res.maintainPasses++;
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
opts.onLog?.(`record: maintain failed: ${redactSecrets(e.message)}\n`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return res;
|
|
146
|
+
}
|
|
147
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
148
|
+
/**
|
|
149
|
+
* A user-level LaunchAgent (no sudo). KeepAlive so a crash or a reboot-after-login brings it back, RunAtLoad,
|
|
150
|
+
* ProcessType Background + Nice 10 + Low throttling so it never competes with foreground work, and a
|
|
151
|
+
* ThrottleInterval so a crash-loop cannot spin. It writes to `logPath`. NOTE (documented, and in `install`
|
|
152
|
+
* output): a LaunchAgent does NOT start before the user logs in — this Mac must be logged in for the
|
|
153
|
+
* collector to run; a true pre-login daemon would be a LaunchDaemon under sudo, which this task does not use.
|
|
154
|
+
*/
|
|
155
|
+
export function renderCollectorPlist(o) {
|
|
156
|
+
const args = [o.nodePath, o.binPath, 'record', 'run', '--interval', String(Math.round(o.intervalMs / 1000)), '--max-db-mb', String(o.maxDbMb)];
|
|
157
|
+
const argXml = args.map((a) => ` <string>${esc(a)}</string>`).join('\n');
|
|
158
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
159
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
160
|
+
<plist version="1.0">
|
|
161
|
+
<dict>
|
|
162
|
+
<key>Label</key>
|
|
163
|
+
<string>${esc(o.label)}</string>
|
|
164
|
+
<key>ProgramArguments</key>
|
|
165
|
+
<array>
|
|
166
|
+
${argXml}
|
|
167
|
+
</array>
|
|
168
|
+
<key>WorkingDirectory</key>
|
|
169
|
+
<string>${esc(o.workingDir)}</string>
|
|
170
|
+
<key>RunAtLoad</key>
|
|
171
|
+
<true/>
|
|
172
|
+
<key>KeepAlive</key>
|
|
173
|
+
<true/>
|
|
174
|
+
<key>ThrottleInterval</key>
|
|
175
|
+
<integer>30</integer>
|
|
176
|
+
<key>ProcessType</key>
|
|
177
|
+
<string>Background</string>
|
|
178
|
+
<key>Nice</key>
|
|
179
|
+
<integer>10</integer>
|
|
180
|
+
<key>LowPriorityIO</key>
|
|
181
|
+
<true/>
|
|
182
|
+
<key>StandardOutPath</key>
|
|
183
|
+
<string>${esc(o.logPath)}</string>
|
|
184
|
+
<key>StandardErrorPath</key>
|
|
185
|
+
<string>${esc(o.logPath)}</string>
|
|
186
|
+
</dict>
|
|
187
|
+
</plist>
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
/** `~/Library/LaunchAgents/<label>.plist` for the invoking user (HOME from env). */
|
|
191
|
+
export function plistPath(env, label = COLLECTOR_LABEL) {
|
|
192
|
+
const home = env.HOME || env.USERPROFILE || '';
|
|
193
|
+
return join(home, 'Library', 'LaunchAgents', `${label}.plist`);
|
|
194
|
+
}
|
|
195
|
+
/** The data directory that holds the DB (and the collector log), from the resolved DB path. */
|
|
196
|
+
function dataDir(env, db) {
|
|
197
|
+
return dirname(db && db !== ':memory:' ? db : defaultDbPath(env));
|
|
198
|
+
}
|
|
199
|
+
/** The source bin shim this file ships next to (…/packages/cli/bin/dexterm.mjs). */
|
|
200
|
+
function sourceBin() {
|
|
201
|
+
return fileURLToPath(new URL('../bin/dexterm.mjs', import.meta.url));
|
|
202
|
+
}
|
|
203
|
+
export function parseRun(argv) {
|
|
204
|
+
const a = { intervalMs: DEFAULT_INTERVAL_MS, ticks: Infinity, maintainMs: DEFAULT_MAINTAIN_MS, maxDbBytes: DEFAULT_MAX_DB_MB * 1_000_000, noNewpairs: false, db: null, verbose: false };
|
|
205
|
+
for (let i = 0; i < argv.length; i++) {
|
|
206
|
+
const t = argv[i];
|
|
207
|
+
if (t === '--verbose' || t === '-v')
|
|
208
|
+
a.verbose = true;
|
|
209
|
+
else if (t === '--no-newpairs')
|
|
210
|
+
a.noNewpairs = true;
|
|
211
|
+
else if (t === '--once')
|
|
212
|
+
a.ticks = 1;
|
|
213
|
+
else if (t === '--ticks') {
|
|
214
|
+
const n = Number(argv[++i]);
|
|
215
|
+
if (!Number.isInteger(n) || n < 1)
|
|
216
|
+
return { error: '--ticks needs an integer ≥ 1' };
|
|
217
|
+
a.ticks = n;
|
|
218
|
+
}
|
|
219
|
+
else if (t === '--interval') {
|
|
220
|
+
const n = Number(argv[++i]);
|
|
221
|
+
if (!Number.isFinite(n) || n < 1)
|
|
222
|
+
return { error: '--interval needs seconds ≥ 1' };
|
|
223
|
+
a.intervalMs = Math.round(n * 1000);
|
|
224
|
+
}
|
|
225
|
+
else if (t === '--maintain') {
|
|
226
|
+
const n = Number(argv[++i]);
|
|
227
|
+
if (!Number.isFinite(n) || n < 1)
|
|
228
|
+
return { error: '--maintain needs seconds ≥ 1' };
|
|
229
|
+
a.maintainMs = Math.round(n * 1000);
|
|
230
|
+
}
|
|
231
|
+
else if (t === '--max-db-mb') {
|
|
232
|
+
const n = Number(argv[++i]);
|
|
233
|
+
if (!Number.isFinite(n) || n < 0)
|
|
234
|
+
return { error: '--max-db-mb needs a number ≥ 0 (0 = no cap)' };
|
|
235
|
+
a.maxDbBytes = n === 0 ? null : Math.round(n * 1_000_000);
|
|
236
|
+
}
|
|
237
|
+
else if (t === '--db') {
|
|
238
|
+
a.db = argv[++i] ?? null;
|
|
239
|
+
if (!a.db)
|
|
240
|
+
return { error: '--db needs a PATH' };
|
|
241
|
+
}
|
|
242
|
+
else
|
|
243
|
+
return { error: `unknown option "${t}"` };
|
|
244
|
+
}
|
|
245
|
+
return a;
|
|
246
|
+
}
|
|
247
|
+
export const RECORD_USAGE = `dexterm record: keep the history recorder fed with no TUI open (task-1414).
|
|
248
|
+
|
|
249
|
+
dexterm record [run] [--interval S] [--ticks N | --once] [--maintain S] [--max-db-mb N] [--no-newpairs] [--db PATH] [--verbose]
|
|
250
|
+
dexterm record install [--interval S] [--max-db-mb N] [--load] [--dry-run]
|
|
251
|
+
dexterm record uninstall [--unload]
|
|
252
|
+
dexterm record status [--db PATH]
|
|
253
|
+
|
|
254
|
+
run the collector loop: one keyless feed refresh per --interval (default 60 s) feeds the recorder
|
|
255
|
+
(snapshots + feed membership + new pairs), recorder maintenance (outcome sweeps, labels, retention)
|
|
256
|
+
every --maintain seconds. --max-db-mb is a soft cap (default 1024): above it observation pauses while
|
|
257
|
+
labels + retention keep running. --no-newpairs drops the PumpPortal stream. Ctrl-C stops cleanly
|
|
258
|
+
(labels whatever is due). Requests honour the data layer's rate limits, health gate and sweep budgets.
|
|
259
|
+
install write a user-level launchd LaunchAgent (no sudo) that runs \`record run\`; prints the launchctl
|
|
260
|
+
command. --load also runs launchctl (off by default). --dry-run prints the plist, writes nothing.
|
|
261
|
+
uninstall remove the LaunchAgent plist; --unload also runs launchctl bootout.
|
|
262
|
+
status recorder stats + whether the LaunchAgent is installed + DB size vs the cap.
|
|
263
|
+
|
|
264
|
+
NOTE: a LaunchAgent does not start before the user logs in on this Mac (it is not a pre-login daemon). The
|
|
265
|
+
collector runs only while this user is logged in. Free-tier APIs only; the Helius key is never printed.`;
|
|
266
|
+
export async function recordCommand(argv, io, deps = {}) {
|
|
267
|
+
const sub = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'run';
|
|
268
|
+
const rest = argv[0] && !argv[0].startsWith('--') ? argv.slice(1) : argv;
|
|
269
|
+
if (sub === 'help' || argv.includes('--help') || argv.includes('-h')) {
|
|
270
|
+
io.stdout.write(RECORD_USAGE + '\n');
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
if (sub === 'install')
|
|
274
|
+
return install(rest, io);
|
|
275
|
+
if (sub === 'uninstall')
|
|
276
|
+
return uninstall(rest, io);
|
|
277
|
+
if (sub === 'status')
|
|
278
|
+
return status(rest, io);
|
|
279
|
+
if (sub === 'run')
|
|
280
|
+
return run(rest, io, deps);
|
|
281
|
+
io.stderr.write(`dexterm record: unknown subcommand "${sub}"\n\n${RECORD_USAGE}\n`);
|
|
282
|
+
return 2;
|
|
283
|
+
}
|
|
284
|
+
async function run(argv, io, deps) {
|
|
285
|
+
const parsed = parseRun(argv);
|
|
286
|
+
if ('error' in parsed) {
|
|
287
|
+
io.stderr.write(`dexterm record: ${parsed.error}\n\n${RECORD_USAGE}\n`);
|
|
288
|
+
return 2;
|
|
289
|
+
}
|
|
290
|
+
const now = deps.now ?? (() => Date.now());
|
|
291
|
+
const dbPath = parsed.db ?? defaultDbPath(io.env);
|
|
292
|
+
if (!deps.engine && dbPath === ':memory:') {
|
|
293
|
+
io.stderr.write('dexterm record: an in-memory DB keeps no history; give a --db PATH\n');
|
|
294
|
+
return 2;
|
|
295
|
+
}
|
|
296
|
+
if (!deps.engine && !recorderEnabled(io.env, false)) {
|
|
297
|
+
io.stderr.write('dexterm record: the recorder is disabled (DEXTERM_RECORDER=0); nothing to collect\n');
|
|
298
|
+
return 2;
|
|
299
|
+
}
|
|
300
|
+
// SIGINT/SIGTERM-aware cancellable sleeper (launchd sends SIGTERM to stop the agent).
|
|
301
|
+
const stop = (() => {
|
|
302
|
+
let aborted = false;
|
|
303
|
+
let wake = () => { };
|
|
304
|
+
return {
|
|
305
|
+
get aborted() { return aborted; },
|
|
306
|
+
abort() { aborted = true; wake(); },
|
|
307
|
+
sleep(ms) { return new Promise((r) => { const t = setTimeout(r, ms); t.unref?.(); wake = () => { clearTimeout(t); r(); }; }); },
|
|
308
|
+
};
|
|
309
|
+
})();
|
|
310
|
+
const log = (s) => io.stderr.write(s);
|
|
311
|
+
const engine = deps.engine ?? buildLiveEngine(io.env, { dbPath, noNewpairs: parsed.noNewpairs, verbose: parsed.verbose, onLog: log });
|
|
312
|
+
const own = !deps.engine;
|
|
313
|
+
const onSig = () => { io.stderr.write('\ndexterm record: stopping…\n'); stop.abort(); };
|
|
314
|
+
if (own) {
|
|
315
|
+
process.on('SIGINT', onSig);
|
|
316
|
+
process.on('SIGTERM', onSig);
|
|
317
|
+
}
|
|
318
|
+
if (parsed.verbose || parsed.ticks !== Infinity)
|
|
319
|
+
io.stderr.write(`dexterm record: db ${dbPath} · interval ${Math.round(parsed.intervalMs / 1000)}s · maintain ${Math.round(parsed.maintainMs / 1000)}s · cap ${parsed.maxDbBytes === null ? 'off' : `${Math.round(parsed.maxDbBytes / 1e6)} MB`} · newpairs ${parsed.noNewpairs ? 'off' : 'on'}\n`);
|
|
320
|
+
let result;
|
|
321
|
+
try {
|
|
322
|
+
result = await runCollectorLoop(engine, {
|
|
323
|
+
intervalMs: parsed.intervalMs, ticks: parsed.ticks, maintainEveryMs: parsed.maintainMs, maxDbBytes: parsed.maxDbBytes,
|
|
324
|
+
now, sleep: (ms) => stop.sleep(ms), aborted: () => stop.aborted, onLog: log,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
if (own) {
|
|
329
|
+
process.removeListener('SIGINT', onSig);
|
|
330
|
+
process.removeListener('SIGTERM', onSig);
|
|
331
|
+
}
|
|
332
|
+
engine.close();
|
|
333
|
+
}
|
|
334
|
+
if (parsed.verbose || parsed.ticks !== Infinity)
|
|
335
|
+
io.stdout.write(`record: ${result.observePasses} observe pass(es), ${result.rowsWritten} snapshot row(s), ${result.maintainPasses} maintenance pass(es)${result.cappedTicks ? `, ${result.cappedTicks} tick(s) paused at the disk cap` : ''}\n`);
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
function install(argv, io) {
|
|
339
|
+
let intervalMs = DEFAULT_INTERVAL_MS, maxDbMb = DEFAULT_MAX_DB_MB, load = false, dry = false;
|
|
340
|
+
for (let i = 0; i < argv.length; i++) {
|
|
341
|
+
const t = argv[i];
|
|
342
|
+
if (t === '--load')
|
|
343
|
+
load = true;
|
|
344
|
+
else if (t === '--dry-run')
|
|
345
|
+
dry = true;
|
|
346
|
+
else if (t === '--interval') {
|
|
347
|
+
const n = Number(argv[++i]);
|
|
348
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
349
|
+
io.stderr.write('dexterm record: --interval needs seconds ≥ 1\n');
|
|
350
|
+
return 2;
|
|
351
|
+
}
|
|
352
|
+
intervalMs = Math.round(n * 1000);
|
|
353
|
+
}
|
|
354
|
+
else if (t === '--max-db-mb') {
|
|
355
|
+
const n = Number(argv[++i]);
|
|
356
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
357
|
+
io.stderr.write('dexterm record: --max-db-mb needs a number ≥ 1\n');
|
|
358
|
+
return 2;
|
|
359
|
+
}
|
|
360
|
+
maxDbMb = n;
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
io.stderr.write(`dexterm record install: unknown option "${t}"\n`);
|
|
364
|
+
return 2;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const path = plistPath(io.env);
|
|
368
|
+
const logPath = join(dataDir(io.env), 'collector.log');
|
|
369
|
+
const bin = sourceBin();
|
|
370
|
+
const plist = renderCollectorPlist({ label: COLLECTOR_LABEL, nodePath: process.execPath, binPath: bin, intervalMs, maxDbMb, logPath, workingDir: resolve(dirname(bin), '..', '..', '..') });
|
|
371
|
+
if (dry) {
|
|
372
|
+
io.stdout.write(plist);
|
|
373
|
+
io.stderr.write(`\ndexterm record: dry run — would write ${path}\n`);
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
377
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
378
|
+
writeFileSync(path, plist);
|
|
379
|
+
io.stdout.write(`wrote ${path}\nlog ${logPath}\n\nEnable it (user-level, no sudo):\n launchctl bootstrap gui/$(id -u) ${path}\n launchctl kickstart -k gui/$(id -u)/${COLLECTOR_LABEL}\nStop/remove it: dexterm record uninstall --unload\n\nNOTE: a LaunchAgent does NOT start before you log in on this Mac; the collector runs only while logged in.\n`);
|
|
380
|
+
if (load)
|
|
381
|
+
return runLaunchctl(['bootstrap', `gui/${process.getuid?.() ?? 0}`, path], io);
|
|
382
|
+
io.stderr.write('dexterm record: plist written but NOT loaded (pass --load, or run the launchctl command above) once you have checked it.\n');
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
function uninstall(argv, io) {
|
|
386
|
+
const unload = argv.includes('--unload');
|
|
387
|
+
const path = plistPath(io.env);
|
|
388
|
+
if (unload)
|
|
389
|
+
runLaunchctl(['bootout', `gui/${process.getuid?.() ?? 0}/${COLLECTOR_LABEL}`], io);
|
|
390
|
+
if (existsSync(path)) {
|
|
391
|
+
rmSync(path, { force: true });
|
|
392
|
+
io.stdout.write(`removed ${path}\n`);
|
|
393
|
+
}
|
|
394
|
+
else
|
|
395
|
+
io.stdout.write(`no LaunchAgent at ${path}\n`);
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
function status(argv, io) {
|
|
399
|
+
let db = null;
|
|
400
|
+
for (let i = 0; i < argv.length; i++) {
|
|
401
|
+
if (argv[i] === '--db') {
|
|
402
|
+
db = argv[++i] ?? null;
|
|
403
|
+
if (!db) {
|
|
404
|
+
io.stderr.write('dexterm record: --db needs a PATH\n');
|
|
405
|
+
return 2;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const path = plistPath(io.env);
|
|
410
|
+
const installed = existsSync(path);
|
|
411
|
+
const dbPath = db ?? defaultDbPath(io.env);
|
|
412
|
+
io.stdout.write(`LaunchAgent ${installed ? `installed (${path})` : 'not installed (dexterm record install)'}\n`);
|
|
413
|
+
io.stdout.write(`service note LaunchAgents do not start before login; the collector runs only while logged in\n`);
|
|
414
|
+
if (dbPath === ':memory:') {
|
|
415
|
+
io.stdout.write('recorder in-memory DB (no history)\n');
|
|
416
|
+
return 0;
|
|
417
|
+
}
|
|
418
|
+
let rec = null;
|
|
419
|
+
try {
|
|
420
|
+
rec = new Recorder({ path: dbPath, enabled: false });
|
|
421
|
+
const s = rec.stats();
|
|
422
|
+
const when = (t) => (t === null ? '—' : new Date(t).toISOString());
|
|
423
|
+
io.stdout.write([
|
|
424
|
+
`db ${dbPath} (${s.dbBytes === null ? '—' : `${(s.dbBytes / 1e6).toFixed(1)} MB`})`,
|
|
425
|
+
`snapshots ${s.snapshots} (live ${s.live}, sweep ${s.sweep}, t0 anchors ${s.t0})`,
|
|
426
|
+
`outcomes ${s.outcomes}${s.outcomes ? ` (${Object.entries(s.outcomesByLabel).map(([k, v]) => `${k} ${v}`).join(', ')})` : ''}`,
|
|
427
|
+
`sweeps ${s.sweeps} runs · ${s.sweepCallsToday} DexScreener calls today`,
|
|
428
|
+
`span ${when(s.oldestT)} → ${when(s.newestT)}`,
|
|
429
|
+
].join('\n') + '\n');
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
io.stderr.write(`dexterm record status: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
rec?.close();
|
|
437
|
+
}
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
/** Run launchctl best-effort (only on --load/--unload). Never throws: a failure is reported, the plist stays. */
|
|
441
|
+
function runLaunchctl(args, io) {
|
|
442
|
+
try {
|
|
443
|
+
const r = spawnSync('launchctl', args, { stdio: 'inherit' });
|
|
444
|
+
if (r.status !== 0) {
|
|
445
|
+
io.stderr.write(`dexterm record: launchctl ${args[0]} exited ${r.status ?? 'null'}\n`);
|
|
446
|
+
return 1;
|
|
447
|
+
}
|
|
448
|
+
io.stdout.write(`launchctl ${args[0]} ok\n`);
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
catch (e) {
|
|
452
|
+
io.stderr.write(`dexterm record: launchctl failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
453
|
+
return 1;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RulesEngine, ruleToJson, ruleToText, slug, applyAndJournal, describeActions, } from '../../custom/src/index.js';
|
|
2
|
+
import { alertToDelivery, proposalToDelivery } from "./alerts/format.js";
|
|
2
3
|
export const RULE_INPUT_MAX = 400;
|
|
3
4
|
/** The engine observes/evaluates at most this often (the feed itself moves every 10 s; fixtures drift every tick). */
|
|
4
5
|
export const RULES_TICK_MS = 2_000;
|
|
@@ -8,8 +9,11 @@ export function newRulesUi(engine = new RulesEngine()) {
|
|
|
8
9
|
return {
|
|
9
10
|
engine, errors: [], pane: 'rules', selected: 0, selectedProposal: 0, input: null, editingId: null, dry: null, notice: null,
|
|
10
11
|
log: [], proposals: [], pending: { popups: [], bells: 0, ask: null, ticket: null }, lastTickAt: 0, nextId: 1, fires: 0,
|
|
12
|
+
pendingDeliveries: [],
|
|
11
13
|
};
|
|
12
14
|
}
|
|
15
|
+
/** Cap on the push-delivery backlog (a long run with no notifier should never grow unbounded). */
|
|
16
|
+
const MAX_DELIVERIES = 100;
|
|
13
17
|
/**
|
|
14
18
|
* Build the engine from `config.rules` plus the rules of installed packs (task-1358 Tier 4: a pack rule whose id the
|
|
15
19
|
* user already has in config.rules is shadowed by the user's copy). Bad rules are reported, good ones run. Never throws.
|
|
@@ -33,7 +37,24 @@ export function rulesUiOf(state) {
|
|
|
33
37
|
state.rulesUi = initRules(state.custom, state.now.getTime());
|
|
34
38
|
return state.rulesUi;
|
|
35
39
|
}
|
|
36
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* task-1398: LARP-detector grade as a number so a grade change is a rule (`larp.delta`). Higher = more real.
|
|
42
|
+
* The string verdict is only known here (cli); the engine sees the number. `null` for no verdict (clause false).
|
|
43
|
+
*/
|
|
44
|
+
export const LARP_GRADES = { REAL_PRODUCT: 3, EARLY_BUT_REAL: 2, UNVERIFIABLE: 1, LARP: 0 };
|
|
45
|
+
export function larpGrade(verdict) {
|
|
46
|
+
if (!verdict)
|
|
47
|
+
return null;
|
|
48
|
+
const g = LARP_GRADES[verdict];
|
|
49
|
+
return g === undefined ? null : g;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The rows a rule can see: the screener feed (no skeletons) with the wallet-intel top-cluster share and the LARP
|
|
53
|
+
* grade attached. task-1404: the OPEN token also carries its LIVE holder/rug signal fields (top10 / dev / bundled /
|
|
54
|
+
* snipers / fresh / cluster / lpLocked / bonding / mintable / freezable), so a rule like `dev.delta < 0` or
|
|
55
|
+
* `mintable > 0` fires on the real streaming values, not the on-open snapshot. A token opened by `--mint` that is
|
|
56
|
+
* not in the screener feed is appended as its own row so rules still see it.
|
|
57
|
+
*/
|
|
37
58
|
export function ruleRowsOf(state) {
|
|
38
59
|
const out = [];
|
|
39
60
|
const seen = new Set();
|
|
@@ -44,14 +65,56 @@ export function ruleRowsOf(state) {
|
|
|
44
65
|
const c = res.analysis.clusters[0];
|
|
45
66
|
return c ? c.pct : null;
|
|
46
67
|
};
|
|
68
|
+
const openMint = state.live?.token?.view.mint ?? null;
|
|
47
69
|
for (const r of state.screener.rows) {
|
|
48
70
|
if (!r.mint || r.skeleton || seen.has(r.mint))
|
|
49
71
|
continue;
|
|
50
72
|
seen.add(r.mint);
|
|
51
|
-
|
|
73
|
+
const row = Object.assign(Object.create(null), r, { cluster: clusterOf(r.mint), larp: larpGrade(state.larpByMint?.get(r.mint)?.verdict) });
|
|
74
|
+
if (r.mint === openMint)
|
|
75
|
+
overlaySignalFields(row, state, r.mint);
|
|
76
|
+
out.push(row);
|
|
77
|
+
}
|
|
78
|
+
// A token opened by mint (not in the screener feed): synthesize a row from its live view so rules see it too.
|
|
79
|
+
if (openMint && !seen.has(openMint) && state.live?.token) {
|
|
80
|
+
const v = state.live.token.view;
|
|
81
|
+
const row = Object.assign(Object.create(null), {
|
|
82
|
+
mint: v.mint, symbol: v.symbol, price: v.price, mcap: v.mcap, fdv: v.fdv, liq: v.liq, vol24: v.vol24,
|
|
83
|
+
holders: v.holders, org: v.org, top10: v.top10, dev: v.dev, lpLocked: v.lpLocked,
|
|
84
|
+
cluster: clusterOf(v.mint), larp: larpGrade(state.larpByMint?.get(v.mint)?.verdict),
|
|
85
|
+
ageMin: v.ageMin, chg: v.chg, launchpad: v.launchpad, flags: v.flags, verified: v.verified, sus: v.sus,
|
|
86
|
+
});
|
|
87
|
+
overlaySignalFields(row, state, v.mint);
|
|
88
|
+
out.push(row);
|
|
52
89
|
}
|
|
53
90
|
return out;
|
|
54
91
|
}
|
|
92
|
+
/** Overlay the live-signals engine's latest values (non-null wins) onto a rule row (task-1404). */
|
|
93
|
+
function overlaySignalFields(row, state, mint) {
|
|
94
|
+
const f = state.live?.signals?.fields(mint);
|
|
95
|
+
if (!f)
|
|
96
|
+
return;
|
|
97
|
+
if (f.top10 !== null)
|
|
98
|
+
row.top10 = f.top10;
|
|
99
|
+
if (f.dev !== null)
|
|
100
|
+
row.dev = f.dev;
|
|
101
|
+
if (f.bundled !== null)
|
|
102
|
+
row.bundled = f.bundled;
|
|
103
|
+
if (f.snipers !== null)
|
|
104
|
+
row.snipers = f.snipers;
|
|
105
|
+
if (f.cluster !== null)
|
|
106
|
+
row.cluster = f.cluster;
|
|
107
|
+
if (f.lpLocked !== null)
|
|
108
|
+
row.lpLocked = f.lpLocked;
|
|
109
|
+
if (f.bonding !== null)
|
|
110
|
+
row.bonding = f.bonding;
|
|
111
|
+
if (f.fresh !== null)
|
|
112
|
+
row.fresh = f.fresh;
|
|
113
|
+
if (f.mintable !== null)
|
|
114
|
+
row.mintable = f.mintable;
|
|
115
|
+
if (f.freezable !== null)
|
|
116
|
+
row.freezable = f.freezable;
|
|
117
|
+
}
|
|
55
118
|
/**
|
|
56
119
|
* One engine tick: observe the rows, evaluate, and turn the outputs into app state. Self-throttled to RULES_TICK_MS.
|
|
57
120
|
* Alerts become the first pending toast + log lines; ticket/ask become PROPOSAL cards; watch/tag touch the user's
|
|
@@ -91,6 +154,8 @@ function applyOutputs(state, ui, res, rows, now) {
|
|
|
91
154
|
if (a.kind === 'alert') {
|
|
92
155
|
push({ ...base, kind: 'alert', level: a.level, text: a.text });
|
|
93
156
|
summary.alerts.push(`${f.symbol}: ${a.text}`);
|
|
157
|
+
if (ui.pendingDeliveries.length < MAX_DELIVERIES)
|
|
158
|
+
ui.pendingDeliveries.push(alertToDelivery(a));
|
|
94
159
|
}
|
|
95
160
|
else if (a.kind === 'proposal') {
|
|
96
161
|
const card = a.type === 'ticket'
|
|
@@ -99,6 +164,8 @@ function applyOutputs(state, ui, res, rows, now) {
|
|
|
99
164
|
ui.proposals.unshift(card);
|
|
100
165
|
if (ui.proposals.length > MAX_PROPOSALS)
|
|
101
166
|
ui.proposals.length = MAX_PROPOSALS;
|
|
167
|
+
if (ui.pendingDeliveries.length < MAX_DELIVERIES)
|
|
168
|
+
ui.pendingDeliveries.push(proposalToDelivery(a));
|
|
102
169
|
summary.proposals += 1;
|
|
103
170
|
push({ ...base, kind: 'proposal', level: 'info', text: `PROPOSAL ${describeCard(card)} — ctrl+r, tab, ⏎ to act (you confirm there)` });
|
|
104
171
|
}
|