@nexstone/rift-cli 0.1.1

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 (137) hide show
  1. package/LICENSE +201 -0
  2. package/bin/run.js +22 -0
  3. package/dist/commands/algo.d.ts +32 -0
  4. package/dist/commands/algo.js +719 -0
  5. package/dist/commands/audit.d.ts +13 -0
  6. package/dist/commands/audit.js +37 -0
  7. package/dist/commands/auth-status.d.ts +14 -0
  8. package/dist/commands/auth-status.js +118 -0
  9. package/dist/commands/auth.d.ts +14 -0
  10. package/dist/commands/auth.js +275 -0
  11. package/dist/commands/backtest.d.ts +26 -0
  12. package/dist/commands/backtest.js +283 -0
  13. package/dist/commands/collect/start.d.ts +11 -0
  14. package/dist/commands/collect/start.js +78 -0
  15. package/dist/commands/collect/status.d.ts +6 -0
  16. package/dist/commands/collect/status.js +60 -0
  17. package/dist/commands/compare.d.ts +16 -0
  18. package/dist/commands/compare.js +130 -0
  19. package/dist/commands/config.d.ts +16 -0
  20. package/dist/commands/config.js +143 -0
  21. package/dist/commands/cost.d.ts +20 -0
  22. package/dist/commands/cost.js +104 -0
  23. package/dist/commands/cross-asset.d.ts +14 -0
  24. package/dist/commands/cross-asset.js +39 -0
  25. package/dist/commands/data/fetch.d.ts +15 -0
  26. package/dist/commands/data/fetch.js +82 -0
  27. package/dist/commands/data/list.d.ts +6 -0
  28. package/dist/commands/data/list.js +28 -0
  29. package/dist/commands/data-inventory.d.ts +9 -0
  30. package/dist/commands/data-inventory.js +24 -0
  31. package/dist/commands/deposit.d.ts +10 -0
  32. package/dist/commands/deposit.js +222 -0
  33. package/dist/commands/doctor.d.ts +6 -0
  34. package/dist/commands/doctor.js +87 -0
  35. package/dist/commands/funding-browser.d.ts +12 -0
  36. package/dist/commands/funding-browser.js +33 -0
  37. package/dist/commands/guide.d.ts +6 -0
  38. package/dist/commands/guide.js +15 -0
  39. package/dist/commands/home.d.ts +23 -0
  40. package/dist/commands/home.js +210 -0
  41. package/dist/commands/init.d.ts +7 -0
  42. package/dist/commands/init.js +122 -0
  43. package/dist/commands/install.d.ts +9 -0
  44. package/dist/commands/install.js +89 -0
  45. package/dist/commands/interactive.d.ts +17 -0
  46. package/dist/commands/interactive.js +179 -0
  47. package/dist/commands/lessons.d.ts +12 -0
  48. package/dist/commands/lessons.js +33 -0
  49. package/dist/commands/montecarlo.d.ts +19 -0
  50. package/dist/commands/montecarlo.js +168 -0
  51. package/dist/commands/more.d.ts +11 -0
  52. package/dist/commands/more.js +227 -0
  53. package/dist/commands/new.d.ts +14 -0
  54. package/dist/commands/new.js +306 -0
  55. package/dist/commands/pairs.d.ts +22 -0
  56. package/dist/commands/pairs.js +147 -0
  57. package/dist/commands/perp/close.d.ts +12 -0
  58. package/dist/commands/perp/close.js +57 -0
  59. package/dist/commands/perp/long.d.ts +14 -0
  60. package/dist/commands/perp/long.js +38 -0
  61. package/dist/commands/perp/short.d.ts +14 -0
  62. package/dist/commands/perp/short.js +27 -0
  63. package/dist/commands/perp/status.d.ts +9 -0
  64. package/dist/commands/perp/status.js +26 -0
  65. package/dist/commands/portfolio/alerts.d.ts +6 -0
  66. package/dist/commands/portfolio/alerts.js +47 -0
  67. package/dist/commands/portfolio/backtest.d.ts +12 -0
  68. package/dist/commands/portfolio/backtest.js +178 -0
  69. package/dist/commands/portfolio/create.d.ts +7 -0
  70. package/dist/commands/portfolio/create.js +195 -0
  71. package/dist/commands/portfolio/start.d.ts +9 -0
  72. package/dist/commands/portfolio/start.js +64 -0
  73. package/dist/commands/portfolio/status.d.ts +6 -0
  74. package/dist/commands/portfolio/status.js +128 -0
  75. package/dist/commands/portfolio/stop.d.ts +6 -0
  76. package/dist/commands/portfolio/stop.js +81 -0
  77. package/dist/commands/portfolio-backtest.d.ts +13 -0
  78. package/dist/commands/portfolio-backtest.js +37 -0
  79. package/dist/commands/portfolio-matrix.d.ts +12 -0
  80. package/dist/commands/portfolio-matrix.js +30 -0
  81. package/dist/commands/quick-test.d.ts +17 -0
  82. package/dist/commands/quick-test.js +45 -0
  83. package/dist/commands/research.d.ts +57 -0
  84. package/dist/commands/research.js +1976 -0
  85. package/dist/commands/scout.d.ts +14 -0
  86. package/dist/commands/scout.js +184 -0
  87. package/dist/commands/serve.d.ts +9 -0
  88. package/dist/commands/serve.js +1176 -0
  89. package/dist/commands/setup/proxy.d.ts +10 -0
  90. package/dist/commands/setup/proxy.js +267 -0
  91. package/dist/commands/spot/buy.d.ts +14 -0
  92. package/dist/commands/spot/buy.js +38 -0
  93. package/dist/commands/spot/sell.d.ts +14 -0
  94. package/dist/commands/spot/sell.js +39 -0
  95. package/dist/commands/strategies/list.d.ts +6 -0
  96. package/dist/commands/strategies/list.js +34 -0
  97. package/dist/commands/sweep.d.ts +19 -0
  98. package/dist/commands/sweep.js +137 -0
  99. package/dist/commands/sync.d.ts +17 -0
  100. package/dist/commands/sync.js +54 -0
  101. package/dist/commands/test-trade.d.ts +6 -0
  102. package/dist/commands/test-trade.js +97 -0
  103. package/dist/commands/trade.d.ts +26 -0
  104. package/dist/commands/trade.js +274 -0
  105. package/dist/commands/transfer.d.ts +13 -0
  106. package/dist/commands/transfer.js +65 -0
  107. package/dist/commands/verify.d.ts +16 -0
  108. package/dist/commands/verify.js +38 -0
  109. package/dist/commands/walkforward.d.ts +20 -0
  110. package/dist/commands/walkforward.js +191 -0
  111. package/dist/commands/withdraw.d.ts +12 -0
  112. package/dist/commands/withdraw.js +55 -0
  113. package/dist/commands/workbench-create.d.ts +13 -0
  114. package/dist/commands/workbench-create.js +39 -0
  115. package/dist/lib/account-mode.d.ts +44 -0
  116. package/dist/lib/account-mode.js +96 -0
  117. package/dist/lib/analyzer.d.ts +4 -0
  118. package/dist/lib/analyzer.js +62 -0
  119. package/dist/lib/base-command.d.ts +35 -0
  120. package/dist/lib/base-command.js +49 -0
  121. package/dist/lib/credentials.d.ts +46 -0
  122. package/dist/lib/credentials.js +137 -0
  123. package/dist/lib/engine-passthrough.d.ts +28 -0
  124. package/dist/lib/engine-passthrough.js +60 -0
  125. package/dist/lib/fees.d.ts +52 -0
  126. package/dist/lib/fees.js +97 -0
  127. package/dist/lib/python-bridge.d.ts +24 -0
  128. package/dist/lib/python-bridge.js +182 -0
  129. package/dist/lib/setup-status.d.ts +32 -0
  130. package/dist/lib/setup-status.js +121 -0
  131. package/dist/lib/status-footer.d.ts +35 -0
  132. package/dist/lib/status-footer.js +101 -0
  133. package/dist/lib/tui.d.ts +130 -0
  134. package/dist/lib/tui.js +300 -0
  135. package/dist/lib/walletconnect.d.ts +70 -0
  136. package/dist/lib/walletconnect.js +407 -0
  137. package/package.json +49 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Wallet credential management — canonical snake_case schema, matching
3
+ * what the Python engine writes via `rift agent-pair` (rift_trade.api_wallet
4
+ * → rift_core.keys.APIWalletKey, serialized via Pydantic).
5
+ *
6
+ * File location: ~/.rift/credentials (no extension, matches Python)
7
+ * Format: single-account JSON (snake_case fields).
8
+ *
9
+ * The loader is tolerant of two legacy formats:
10
+ * (a) ~/.rift/credentials.json with camelCase fields (old TS auth.ts writer)
11
+ * (b) Multi-account wrapped: {"default": {…}}
12
+ * Both are migrated to the canonical form on first read.
13
+ *
14
+ * The Python engine is the authoritative writer. The TS `rift auth setup`
15
+ * flow also writes to the same path + schema.
16
+ */
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+ const CRED_DIR = path.join(process.env.HOME || '~', '.rift');
20
+ const CANONICAL_PATH = path.join(CRED_DIR, 'credentials');
21
+ const LEGACY_PATH = path.join(CRED_DIR, 'credentials.json');
22
+ function ensureDir() {
23
+ if (!fs.existsSync(CRED_DIR)) {
24
+ fs.mkdirSync(CRED_DIR, { recursive: true, mode: 0o700 });
25
+ }
26
+ }
27
+ /** Map any of the known legacy formats onto the canonical snake_case shape. */
28
+ function migrate(data) {
29
+ // Multi-account wrapper from old TS writer.
30
+ if (data && typeof data === 'object' && 'default' in data
31
+ && data.default && typeof data.default === 'object'
32
+ && !('private_key' in data) && !('privateKey' in data)) {
33
+ return migrate(data.default);
34
+ }
35
+ const private_key = data.private_key ?? data.privateKey;
36
+ const address = data.address ?? data.apiWalletAddress;
37
+ const network = data.network;
38
+ if (!private_key || !address)
39
+ return null;
40
+ // Mainnet-only post-testnet rip. Any legacy testnet-paired wallet is
41
+ // refused so the user gets a clear "re-pair on mainnet" message instead
42
+ // of trades silently misrouting.
43
+ if (network && network !== 'mainnet')
44
+ return null;
45
+ return {
46
+ address: String(address).toLowerCase(),
47
+ private_key: String(private_key).replace(/^0x/, '').toLowerCase(),
48
+ network: 'mainnet',
49
+ name: data.name,
50
+ registered_at: data.registered_at ?? data.createdAt,
51
+ registered_tx: data.registered_tx ?? null,
52
+ account_address: (data.account_address ?? data.accountAddress)?.toLowerCase(),
53
+ type: data.type,
54
+ agent_approved: data.agent_approved ?? data.agentApproved,
55
+ builder_fee_approved: data.builder_fee_approved ?? data.builderFeeApproved,
56
+ };
57
+ }
58
+ function readFile(filePath) {
59
+ if (!fs.existsSync(filePath))
60
+ return null;
61
+ try {
62
+ const raw = fs.readFileSync(filePath, 'utf-8');
63
+ return migrate(JSON.parse(raw));
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ export function loadCredentials() {
70
+ // Canonical path first — matches Python's writer.
71
+ const canonical = readFile(CANONICAL_PATH);
72
+ if (canonical)
73
+ return canonical;
74
+ // Legacy TS path. If found, migrate it to the canonical location so
75
+ // future reads don't keep migrating.
76
+ const legacy = readFile(LEGACY_PATH);
77
+ if (legacy) {
78
+ try {
79
+ saveCredentials(legacy);
80
+ }
81
+ catch {
82
+ // Best effort — return what we read even if rewrite fails.
83
+ }
84
+ return legacy;
85
+ }
86
+ return null;
87
+ }
88
+ export function saveCredentials(creds) {
89
+ ensureDir();
90
+ // Atomic write: tmp + rename, with 0600 perms.
91
+ const payload = JSON.stringify(creds, null, 2);
92
+ const tmp = CANONICAL_PATH + '.tmp';
93
+ fs.writeFileSync(tmp, payload, { mode: 0o600 });
94
+ fs.renameSync(tmp, CANONICAL_PATH);
95
+ try {
96
+ fs.chmodSync(CANONICAL_PATH, 0o600);
97
+ }
98
+ catch {
99
+ // Permission tightening is best-effort.
100
+ }
101
+ }
102
+ export function hasCredentials() {
103
+ return loadCredentials() !== null;
104
+ }
105
+ /**
106
+ * Returns true if the wallet is ready for live trading (mainnet-only).
107
+ *
108
+ * - File must exist with `address` + `private_key` (pairing complete).
109
+ * - `agent_approved` defaults to true when the file exists — the file is
110
+ * only written after a successful approveAgent transaction.
111
+ * - `builder_fee_approved` must be explicit `true`. Absent → user is
112
+ * prompted to run `rift approve-builder-fee` before their first trade.
113
+ */
114
+ export function hasFullSetup() {
115
+ const creds = loadCredentials();
116
+ if (!creds)
117
+ return false;
118
+ if (!creds.private_key || !creds.address)
119
+ return false;
120
+ const agentOk = creds.agent_approved !== false; // absent = trust file
121
+ return agentOk && creds.builder_fee_approved === true;
122
+ }
123
+ /** Main wallet address — falls back to API wallet address when not stored
124
+ * (older Python-paired wallets don't include account_address). */
125
+ export function getAccountAddress(creds) {
126
+ return creds.account_address ?? creds.address;
127
+ }
128
+ export function maskKey(key) {
129
+ if (key.length <= 10)
130
+ return '****';
131
+ return key.slice(0, 6) + '...' + key.slice(-4);
132
+ }
133
+ export function maskAddress(addr) {
134
+ if (addr.length <= 10)
135
+ return addr;
136
+ return addr.slice(0, 6) + '...' + addr.slice(-4);
137
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Thin passthrough helper for TS wrappers around Python engine commands.
3
+ *
4
+ * Each engine command emits NDJSON over stdout. This helper:
5
+ * - mirrors `progress` / `status` messages as human-readable lines,
6
+ * - surfaces `error` messages via the caller's `error()` (which exits),
7
+ * - pretty-prints any `result` payload as JSON.
8
+ *
9
+ * Wrappers that need to render rich tables/colors should NOT use this —
10
+ * see cost.ts for the per-command rendering pattern.
11
+ */
12
+ export interface PassthroughOptions {
13
+ command: string;
14
+ args: string[];
15
+ /** Bound to the command's this.log */
16
+ log: (msg: string) => void;
17
+ /**
18
+ * Bound to the command's this.error — must exit with a non-zero code.
19
+ * Only invoked for genuine engine crashes (non-zero exit with no
20
+ * structured error message already surfaced).
21
+ */
22
+ error: (msg: string) => never;
23
+ /** Bound to the command's this.exit — silent non-zero exit. */
24
+ exit: (code: number) => never;
25
+ /** When true, only emit the final result JSON (no progress/status output) */
26
+ jsonOnly?: boolean;
27
+ }
28
+ export declare function passthroughToEngine(opts: PassthroughOptions): Promise<void>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Thin passthrough helper for TS wrappers around Python engine commands.
3
+ *
4
+ * Each engine command emits NDJSON over stdout. This helper:
5
+ * - mirrors `progress` / `status` messages as human-readable lines,
6
+ * - surfaces `error` messages via the caller's `error()` (which exits),
7
+ * - pretty-prints any `result` payload as JSON.
8
+ *
9
+ * Wrappers that need to render rich tables/colors should NOT use this —
10
+ * see cost.ts for the per-command rendering pattern.
11
+ */
12
+ import { runEngine } from './python-bridge.js';
13
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
14
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
15
+ export async function passthroughToEngine(opts) {
16
+ // Track whether the engine surfaced its own structured error message.
17
+ // If it did, we suppress the generic "Engine exited with code N" footer
18
+ // on the non-zero exit — the user already saw the helpful message.
19
+ let surfacedError = false;
20
+ try {
21
+ await runEngine(opts.command, opts.args, (msg) => {
22
+ const type = msg.type;
23
+ if (type === 'error' && msg.msg) {
24
+ // Log + flag instead of opts.error() — throwing CLIError from an
25
+ // async readline callback isn't caught by oclif's lifecycle and
26
+ // the message gets swallowed silently. Let the engine's non-zero
27
+ // exit propagate via runEngine's rejection path; the catch below
28
+ // exits silently when an error has already been surfaced.
29
+ if (!opts.jsonOnly)
30
+ opts.log(` ${red('Error:')} ${msg.msg}`);
31
+ surfacedError = true;
32
+ return;
33
+ }
34
+ if (opts.jsonOnly && type !== 'result')
35
+ return;
36
+ if (type === 'progress' && msg.msg) {
37
+ opts.log(dim(` ${msg.msg}`));
38
+ }
39
+ else if (type === 'status' && msg.msg) {
40
+ opts.log(` ${msg.msg}`);
41
+ }
42
+ else if (type === 'result') {
43
+ // Drop the type field; emit the rest as pretty JSON.
44
+ const { type: _t, ...rest } = msg;
45
+ opts.log(JSON.stringify(rest, null, 2));
46
+ }
47
+ else if (msg.msg) {
48
+ opts.log(` ${msg.msg}`);
49
+ }
50
+ });
51
+ }
52
+ catch (err) {
53
+ if (surfacedError) {
54
+ opts.exit(1);
55
+ }
56
+ // Engine crashed without emitting a structured error message — re-raise
57
+ // via opts.error so the user sees the stderr-extracted message.
58
+ opts.error(err instanceof Error ? err.message : String(err));
59
+ }
60
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Builder fee management for Hyperliquid.
3
+ *
4
+ * Uses Hyperliquid's "builder code" system — a protocol-level fee for apps
5
+ * that place trades on behalf of users. NOT referral codes (that's separate).
6
+ *
7
+ * Two-layer consent:
8
+ * 1. Local consent — user agrees to fee in CLI (gates all commands)
9
+ * 2. On-chain approval — user's MAIN wallet signs ApproveBuilderFee (gates live trading)
10
+ * API/agent wallets CANNOT sign this — only the main wallet.
11
+ *
12
+ * Fee mechanics:
13
+ * - Perps: 0.03% (f=30) on BOTH sides of perp trades
14
+ * - Spot: 1% (f=1000) on SELL side only
15
+ * - Hyperliquid charges automatically at execution, credits to builder wallet
16
+ * - Builder fees accumulate in Hyperliquid's referral rewards infrastructure
17
+ * - Claiming is MANUAL via app.hyperliquid.xyz UI (no API action for claiming)
18
+ * - Claimed rewards go to builder wallet's SPOT balance
19
+ *
20
+ * Backtesting, research, simulation, and workbench are free — no approval needed.
21
+ */
22
+ export declare const BUILDER_ADDRESS: string;
23
+ export declare const BUILDER_FEE_F = 30;
24
+ export declare const BUILDER_FEE_F_PERP = 30;
25
+ export declare const BUILDER_FEE_F_SPOT = 1000;
26
+ export declare const BUILDER_FEE_RATE = "1%";
27
+ export declare const BUILDER_FEE_DISPLAY = "0.03% perps / 1% spot";
28
+ interface FeeConsent {
29
+ approved: boolean;
30
+ approvedAt: string;
31
+ feeRate: string;
32
+ onChainApproved?: boolean;
33
+ onChainApprovedAt?: string;
34
+ }
35
+ /** Check if user has given local CLI consent (required for all commands) */
36
+ export declare function hasApprovedFees(): boolean;
37
+ /** Record local CLI consent */
38
+ export declare function approveFees(): void;
39
+ /** Check if user has done on-chain approval (required for live trading) */
40
+ export declare function hasOnChainApproval(): boolean;
41
+ /** Record that on-chain approval was submitted */
42
+ export declare function recordOnChainApproval(): void;
43
+ export declare function getFeeStatus(): FeeConsent | null;
44
+ /**
45
+ * Get the builder parameter to attach to every live order.
46
+ * This is what makes the fee collection work.
47
+ */
48
+ export declare function getOrderBuilderParam(): {
49
+ b: string;
50
+ f: number;
51
+ };
52
+ export {};
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Builder fee management for Hyperliquid.
3
+ *
4
+ * Uses Hyperliquid's "builder code" system — a protocol-level fee for apps
5
+ * that place trades on behalf of users. NOT referral codes (that's separate).
6
+ *
7
+ * Two-layer consent:
8
+ * 1. Local consent — user agrees to fee in CLI (gates all commands)
9
+ * 2. On-chain approval — user's MAIN wallet signs ApproveBuilderFee (gates live trading)
10
+ * API/agent wallets CANNOT sign this — only the main wallet.
11
+ *
12
+ * Fee mechanics:
13
+ * - Perps: 0.03% (f=30) on BOTH sides of perp trades
14
+ * - Spot: 1% (f=1000) on SELL side only
15
+ * - Hyperliquid charges automatically at execution, credits to builder wallet
16
+ * - Builder fees accumulate in Hyperliquid's referral rewards infrastructure
17
+ * - Claiming is MANUAL via app.hyperliquid.xyz UI (no API action for claiming)
18
+ * - Claimed rewards go to builder wallet's SPOT balance
19
+ *
20
+ * Backtesting, research, simulation, and workbench are free — no approval needed.
21
+ */
22
+ import * as fs from 'node:fs';
23
+ import * as path from 'node:path';
24
+ // RIFT builder wallet — resolved from segments (matches Python engine)
25
+ const _B1 = '0x0916EAb573';
26
+ const _B2 = '817F02b96665386c';
27
+ const _B3 = '944e297A765d7C';
28
+ export const BUILDER_ADDRESS = _B1 + _B2 + _B3;
29
+ // Perp fee: 0.03% = 3 basis points = 30 tenths of basis points
30
+ export const BUILDER_FEE_F = 30;
31
+ export const BUILDER_FEE_F_PERP = 30;
32
+ export const BUILDER_FEE_F_SPOT = 1000; // 1% on spot (sell side only)
33
+ // Approval rate: 1% (covers both spot max and perps)
34
+ export const BUILDER_FEE_RATE = '1%';
35
+ export const BUILDER_FEE_DISPLAY = '0.03% perps / 1% spot';
36
+ function getConfigPath() {
37
+ return path.join(process.env.HOME || '~', '.rift', 'config.json');
38
+ }
39
+ function loadConfig() {
40
+ const p = getConfigPath();
41
+ if (!fs.existsSync(p))
42
+ return {};
43
+ try {
44
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
45
+ }
46
+ catch {
47
+ return {};
48
+ }
49
+ }
50
+ function saveConfig(config) {
51
+ const dir = path.dirname(getConfigPath());
52
+ if (!fs.existsSync(dir))
53
+ fs.mkdirSync(dir, { recursive: true });
54
+ fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2), { mode: 0o600 });
55
+ }
56
+ /** Check if user has given local CLI consent (required for all commands) */
57
+ export function hasApprovedFees() {
58
+ const config = loadConfig();
59
+ return config.fees?.approved === true;
60
+ }
61
+ /** Record local CLI consent */
62
+ export function approveFees() {
63
+ const config = loadConfig();
64
+ config.fees = {
65
+ ...config.fees,
66
+ approved: true,
67
+ approvedAt: new Date().toISOString(),
68
+ feeRate: BUILDER_FEE_RATE,
69
+ };
70
+ saveConfig(config);
71
+ }
72
+ /** Check if user has done on-chain approval (required for live trading) */
73
+ export function hasOnChainApproval() {
74
+ const config = loadConfig();
75
+ return config.fees?.onChainApproved === true;
76
+ }
77
+ /** Record that on-chain approval was submitted */
78
+ export function recordOnChainApproval() {
79
+ const config = loadConfig();
80
+ config.fees = {
81
+ ...config.fees,
82
+ onChainApproved: true,
83
+ onChainApprovedAt: new Date().toISOString(),
84
+ };
85
+ saveConfig(config);
86
+ }
87
+ export function getFeeStatus() {
88
+ const config = loadConfig();
89
+ return config.fees || null;
90
+ }
91
+ /**
92
+ * Get the builder parameter to attach to every live order.
93
+ * This is what makes the fee collection work.
94
+ */
95
+ export function getOrderBuilderParam() {
96
+ return { b: BUILDER_ADDRESS, f: BUILDER_FEE_F };
97
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Bridge between the TypeScript CLI and the Python engine.
3
+ * Spawns Python processes and reads NDJSON output line by line.
4
+ */
5
+ export interface EngineMessage {
6
+ type: 'progress' | 'result' | 'error' | 'status' | 'trade' | 'candle' | 'heartbeat' | 'shutdown' | 'step' | 'step_done' | 'soak';
7
+ [key: string]: unknown;
8
+ }
9
+ export declare function getDataDir(): string;
10
+ export declare function runEngine(command: string, args: string[], onMessage: (msg: EngineMessage) => void, extraEnv?: Record<string, string>): Promise<void>;
11
+ /** Get the child process from a runEngine promise (for sending signals) */
12
+ export declare function getEngineProcess(promise: Promise<void>): import('node:child_process').ChildProcess | null;
13
+ /**
14
+ * Spawn the Python engine as a detached daemon process.
15
+ * Returns immediately after the process is started.
16
+ * The daemon writes its own PID file and log file.
17
+ */
18
+ export declare function spawnDaemon(command: string, args: string[], extraEnv?: Record<string, string>): {
19
+ pid: number;
20
+ };
21
+ /** Session directory paths (must match Python's ALGO_DIR layout) */
22
+ export declare function getAlgoSessionsDir(): string;
23
+ export declare function getAlgoPidsDir(): string;
24
+ export declare function getAlgoLogsDir(): string;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Bridge between the TypeScript CLI and the Python engine.
3
+ * Spawns Python processes and reads NDJSON output line by line.
4
+ */
5
+ import { spawn, execSync } from 'node:child_process';
6
+ import { createInterface } from 'node:readline';
7
+ import * as path from 'node:path';
8
+ import * as fs from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ function getEngineDir() {
13
+ let dir = path.resolve(__dirname);
14
+ for (let i = 0; i < 10; i++) {
15
+ const engineDir = path.join(dir, 'engine');
16
+ // Require both pyproject.toml AND the rift.cli entry point.
17
+ // Prevents matching packages/engine (the new rift_engine library)
18
+ // before reaching the legacy engine/ host that owns -m rift.cli.
19
+ if (fs.existsSync(path.join(engineDir, 'pyproject.toml')) &&
20
+ fs.existsSync(path.join(engineDir, 'src', 'rift', 'cli.py')))
21
+ return engineDir;
22
+ dir = path.dirname(dir);
23
+ }
24
+ throw new Error('Cannot find RIFT engine directory');
25
+ }
26
+ function findPython() {
27
+ // Check engine's uv-managed venv first
28
+ const engineVenv = path.join(getEngineDir(), '.venv', 'bin', 'python3');
29
+ if (fs.existsSync(engineVenv))
30
+ return engineVenv;
31
+ // Check for managed venv in data dir
32
+ const dataVenv = path.join(getDataDir(), 'venv', 'bin', 'python3');
33
+ if (fs.existsSync(dataVenv))
34
+ return dataVenv;
35
+ // Fall back to system Python
36
+ for (const cmd of ['python3.14', 'python3.13', 'python3']) {
37
+ try {
38
+ execSync(`${cmd} --version`, { stdio: 'ignore' });
39
+ return cmd;
40
+ }
41
+ catch {
42
+ continue;
43
+ }
44
+ }
45
+ throw new Error('Python 3.13+ not found. Install Python or run: rift setup');
46
+ }
47
+ function getStrategiesDir() {
48
+ let dir = path.resolve(__dirname);
49
+ for (let i = 0; i < 10; i++) {
50
+ const strategiesDir = path.join(dir, 'strategies');
51
+ if (fs.existsSync(strategiesDir))
52
+ return strategiesDir;
53
+ dir = path.dirname(dir);
54
+ }
55
+ const projectRoot = path.resolve(__dirname, '..', '..', '..', '..');
56
+ return path.join(projectRoot, 'strategies');
57
+ }
58
+ export function getDataDir() {
59
+ return path.join(process.env.HOME || '~', '.rift');
60
+ }
61
+ export async function runEngine(command, args, onMessage, extraEnv) {
62
+ const python = findPython();
63
+ const engineDir = getEngineDir();
64
+ const strategiesDir = getStrategiesDir();
65
+ const fullArgs = [
66
+ '-m', 'rift.cli',
67
+ command,
68
+ ...args,
69
+ ];
70
+ // Only add --strategies-dir for commands that accept it
71
+ if (['backtest', 'strategies', 'compare', 'walk-forward', 'sweep', 'montecarlo', 'portfolio-backtest', 'research', 'quick-test', 'algo'].includes(command)) {
72
+ fullArgs.push('--strategies-dir', strategiesDir);
73
+ }
74
+ const proc = spawn(python, fullArgs, {
75
+ cwd: engineDir,
76
+ env: {
77
+ ...process.env,
78
+ ...extraEnv,
79
+ PYTHONPATH: path.join(engineDir, 'src'),
80
+ PYTHONUNBUFFERED: '1',
81
+ },
82
+ stdio: ['ignore', 'pipe', 'pipe'],
83
+ });
84
+ const promise = new Promise((resolve, reject) => {
85
+ const rl = createInterface({ input: proc.stdout });
86
+ rl.on('line', (line) => {
87
+ try {
88
+ const msg = JSON.parse(line);
89
+ onMessage(msg);
90
+ }
91
+ catch {
92
+ // Non-JSON output — ignore
93
+ }
94
+ });
95
+ let stderr = '';
96
+ proc.stderr.on('data', (chunk) => {
97
+ stderr += chunk.toString();
98
+ });
99
+ proc.on('close', (code) => {
100
+ if (code === 0 || code === null) {
101
+ resolve();
102
+ }
103
+ else {
104
+ const lines = stderr.trim().split('\n').filter(l => l.trim());
105
+ // Typer's click-rich style emits errors inside a unicode-box:
106
+ // ╭─ Error ──...──╮
107
+ // │ <message> │
108
+ // ╰────...──────╯
109
+ // Pull the message line out so the user sees "No such command X"
110
+ // instead of the box-drawing close character that happens to be
111
+ // the last stderr line.
112
+ const typerBoxMsg = (() => {
113
+ for (const l of lines) {
114
+ const m = l.match(/^\s*│\s+(.+?)\s+│?\s*$/);
115
+ if (m && m[1].trim())
116
+ return m[1].trim();
117
+ }
118
+ return null;
119
+ })();
120
+ const lastLine = lines[lines.length - 1] || '';
121
+ let errorLine = typerBoxMsg
122
+ || lines.find(l => /^Error:/.test(l.trim()))
123
+ || lines.find(l => /^(KeyError|ValueError|TypeError|RuntimeError|FileNotFoundError|ImportError|AttributeError|NameError):/.test(l.trim()))
124
+ || lastLine;
125
+ errorLine = errorLine.trim().replace(/^Error:\s*/, '');
126
+ reject(new Error(errorLine || `Engine exited with code ${code}`));
127
+ }
128
+ });
129
+ proc.on('error', (err) => {
130
+ reject(new Error(`Failed to start engine: ${err.message}`));
131
+ });
132
+ });
133
+ promise._proc = proc;
134
+ return promise;
135
+ }
136
+ /** Get the child process from a runEngine promise (for sending signals) */
137
+ export function getEngineProcess(promise) {
138
+ return promise?._proc ?? null;
139
+ }
140
+ /**
141
+ * Spawn the Python engine as a detached daemon process.
142
+ * Returns immediately after the process is started.
143
+ * The daemon writes its own PID file and log file.
144
+ */
145
+ export function spawnDaemon(command, args, extraEnv) {
146
+ const python = findPython();
147
+ const engineDir = getEngineDir();
148
+ const fullArgs = [
149
+ '-m', 'rift.cli',
150
+ command,
151
+ ...args,
152
+ '--daemon',
153
+ ];
154
+ // Open /dev/null for stdio — daemon writes to its own log file
155
+ const devNull = fs.openSync('/dev/null', 'r+');
156
+ const proc = spawn(python, fullArgs, {
157
+ cwd: engineDir,
158
+ env: {
159
+ ...process.env,
160
+ ...extraEnv,
161
+ PYTHONPATH: path.join(engineDir, 'src'),
162
+ PYTHONUNBUFFERED: '1',
163
+ },
164
+ stdio: [devNull, devNull, devNull],
165
+ detached: true,
166
+ });
167
+ // Let the daemon run independently of this process
168
+ proc.unref();
169
+ const pid = proc.pid ?? 0;
170
+ fs.closeSync(devNull);
171
+ return { pid };
172
+ }
173
+ /** Session directory paths (must match Python's ALGO_DIR layout) */
174
+ export function getAlgoSessionsDir() {
175
+ return path.join(getDataDir(), 'algo', 'sessions');
176
+ }
177
+ export function getAlgoPidsDir() {
178
+ return path.join(getDataDir(), 'algo', 'pids');
179
+ }
180
+ export function getAlgoLogsDir() {
181
+ return path.join(getDataDir(), 'algo', 'logs');
182
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Phase 0 setup-status detector.
3
+ *
4
+ * Determines what state the user's RIFT installation is in by checking
5
+ * file system state — cheap (just stat calls), runs on every CLI command
6
+ * to drive the status footer and bare-rift wizard routing.
7
+ *
8
+ * No Python required; pure TS for speed (every CLI invocation reads this).
9
+ */
10
+ export type SetupState = 'fresh' | 'research-only' | 'incomplete' | 'live-ready' | 'kill-active' | 'agent-revoked';
11
+ export interface SetupStatus {
12
+ state: SetupState;
13
+ agentAddress: string | null;
14
+ network: 'mainnet' | null;
15
+ hasMainWalletPaired: boolean;
16
+ hasApiWallet: boolean;
17
+ hasEnvConfig: boolean;
18
+ tokenCount: number;
19
+ killSwitchActive: boolean;
20
+ riftVersion: string;
21
+ }
22
+ /**
23
+ * Inspect the local file system and return the current setup state.
24
+ *
25
+ * Pure function — no network, no Python invocation, no chain calls.
26
+ * Safe to call from every CLI command (every footer render reads this).
27
+ */
28
+ export declare function getSetupStatus(): SetupStatus;
29
+ /** Convenience: true iff the user can run live trading right now. */
30
+ export declare function canTradeNow(s?: SetupStatus): boolean;
31
+ /** Convenience: short address ellipsis for display (0x1234…abcd). */
32
+ export declare function shortAddr(addr: string | null): string;