@hedge-layer/cli 4.0.0 → 5.0.0
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/README.md +101 -72
- package/dist/index.mjs +151 -1568
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -9
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/
|
|
4
|
-
import { Command
|
|
3
|
+
// src/program.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/auth.ts
|
|
7
7
|
import readline from "readline/promises";
|
|
8
|
+
import { Writable } from "stream";
|
|
8
9
|
|
|
9
10
|
// src/config.ts
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync, unlinkSync } from "fs";
|
|
11
12
|
import { join } from "path";
|
|
12
13
|
import { homedir } from "os";
|
|
13
14
|
var CONFIG_DIR = join(homedir(), ".hedgelayer");
|
|
@@ -22,21 +23,23 @@ function loadConfig() {
|
|
|
22
23
|
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
23
24
|
const parsed = JSON.parse(raw);
|
|
24
25
|
return {
|
|
25
|
-
api_url: parsed.api_url
|
|
26
|
-
token: parsed.token
|
|
26
|
+
api_url: typeof parsed.api_url === "string" ? parsed.api_url : DEFAULT_API_URL,
|
|
27
|
+
token: typeof parsed.token === "string" ? parsed.token : null
|
|
27
28
|
};
|
|
28
29
|
} catch {
|
|
29
30
|
return defaultConfig();
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
function saveConfig(config) {
|
|
33
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
34
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
35
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
36
|
+
chmodSync(CONFIG_FILE, 384);
|
|
35
37
|
}
|
|
36
38
|
function clearConfig() {
|
|
37
39
|
try {
|
|
38
40
|
if (existsSync(CONFIG_FILE)) unlinkSync(CONFIG_FILE);
|
|
39
|
-
} catch {
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code !== "ENOENT") throw error;
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
function configPath() {
|
|
@@ -44,1621 +47,201 @@ function configPath() {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
// src/client.ts
|
|
50
|
+
function validateApiUrl(value) {
|
|
51
|
+
const url = new URL(value);
|
|
52
|
+
const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(url.hostname);
|
|
53
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
54
|
+
throw new Error("API URL must use HTTPS (HTTP is allowed for loopback development servers).");
|
|
55
|
+
}
|
|
56
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
57
|
+
throw new Error("API URL must be an origin, for example https://hedgelayer.ai.");
|
|
58
|
+
}
|
|
59
|
+
return url.origin;
|
|
60
|
+
}
|
|
47
61
|
var ApiClient = class {
|
|
48
|
-
|
|
62
|
+
apiUrl;
|
|
49
63
|
token;
|
|
50
64
|
verbose;
|
|
51
65
|
constructor(opts = {}) {
|
|
52
66
|
const config = loadConfig();
|
|
53
|
-
this.
|
|
54
|
-
this.token = opts.token ??
|
|
67
|
+
this.apiUrl = validateApiUrl(opts.apiUrl ?? process.env.HL_API_URL ?? config.api_url ?? DEFAULT_API_URL);
|
|
68
|
+
this.token = opts.token ?? process.env.HL_TOKEN ?? config.token;
|
|
55
69
|
this.verbose = opts.verbose ?? false;
|
|
56
70
|
}
|
|
57
71
|
get isAuthenticated() {
|
|
58
|
-
return this.token
|
|
72
|
+
return Boolean(this.token);
|
|
59
73
|
}
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
async listTools() {
|
|
75
|
+
const result = await this.request("GET", "/api/v1/tools");
|
|
76
|
+
if (!Array.isArray(result?.tools)) throw new Error("API returned an invalid tool catalog.");
|
|
77
|
+
return result;
|
|
62
78
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
...extra
|
|
67
|
-
};
|
|
68
|
-
if (this.token) {
|
|
69
|
-
h["Authorization"] = `Bearer ${this.token}`;
|
|
79
|
+
async callTool(name, args) {
|
|
80
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
81
|
+
throw new Error("Tool names may contain only letters, numbers, underscores, and hyphens.");
|
|
70
82
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!this.verbose) return;
|
|
75
|
-
const statusStr = status ? ` \u2192 ${status}` : "";
|
|
76
|
-
process.stderr.write(`[verbose] ${method} ${this.baseUrl}${path}${statusStr}
|
|
77
|
-
`);
|
|
78
|
-
}
|
|
79
|
-
async get(path, params) {
|
|
80
|
-
const url = new URL(path, this.baseUrl);
|
|
81
|
-
if (params) {
|
|
82
|
-
for (const [k, v] of Object.entries(params)) {
|
|
83
|
-
if (v !== void 0 && v !== "") url.searchParams.set(k, v);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
this.log("GET", `${url.pathname}${url.search}`);
|
|
87
|
-
const res = await fetch(url.toString(), { headers: this.headers() });
|
|
88
|
-
this.log("GET", `${url.pathname}${url.search}`, res.status);
|
|
89
|
-
if (!res.ok) {
|
|
90
|
-
const body = await res.text();
|
|
91
|
-
throw new ApiError(res.status, body);
|
|
92
|
-
}
|
|
93
|
-
return res.json();
|
|
94
|
-
}
|
|
95
|
-
async post(path, body) {
|
|
96
|
-
this.log("POST", path);
|
|
97
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
98
|
-
method: "POST",
|
|
99
|
-
headers: this.headers(),
|
|
100
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
101
|
-
});
|
|
102
|
-
this.log("POST", path, res.status);
|
|
103
|
-
if (!res.ok) {
|
|
104
|
-
const text = await res.text();
|
|
105
|
-
throw new ApiError(res.status, text);
|
|
83
|
+
const result = await this.request("POST", `/api/v1/tools/${name}`, { arguments: args });
|
|
84
|
+
if (!Array.isArray(result?.content) || result.isError !== void 0 && typeof result.isError !== "boolean") {
|
|
85
|
+
throw new Error("API returned an invalid tool result.");
|
|
106
86
|
}
|
|
107
|
-
return
|
|
87
|
+
return result;
|
|
108
88
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
method
|
|
118
|
-
headers
|
|
119
|
-
body: JSON.stringify(
|
|
89
|
+
async request(method, path, body) {
|
|
90
|
+
const headers = { Accept: "application/json" };
|
|
91
|
+
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
92
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
93
|
+
const url = `${this.apiUrl}${path}`;
|
|
94
|
+
if (this.verbose) process.stderr.write(`${method} ${url}
|
|
95
|
+
`);
|
|
96
|
+
const response = await fetch(url, {
|
|
97
|
+
method,
|
|
98
|
+
headers,
|
|
99
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
100
|
+
redirect: "error",
|
|
101
|
+
signal: AbortSignal.timeout(65e3)
|
|
120
102
|
});
|
|
121
|
-
this.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
let brief;
|
|
103
|
+
if (this.verbose) process.stderr.write(`HTTP ${response.status}
|
|
104
|
+
`);
|
|
105
|
+
const text = await response.text();
|
|
106
|
+
if (!response.ok) throw new ApiError(response.status, text);
|
|
127
107
|
try {
|
|
128
|
-
|
|
108
|
+
return JSON.parse(text);
|
|
129
109
|
} catch {
|
|
130
|
-
throw new
|
|
110
|
+
throw new Error(`API returned invalid JSON (HTTP ${response.status}).`);
|
|
131
111
|
}
|
|
132
|
-
const dur = res.headers.get("X-Duration-Ms");
|
|
133
|
-
const steps = res.headers.get("X-Steps-Completed");
|
|
134
|
-
const tools = res.headers.get("X-Tools-Used");
|
|
135
|
-
return {
|
|
136
|
-
brief,
|
|
137
|
-
durationMs: dur != null ? Number(dur) : null,
|
|
138
|
-
stepsCompleted: steps != null ? Number(steps) : null,
|
|
139
|
-
toolsUsed: tools ? tools.split(",").filter(Boolean) : []
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
async patch(path, body) {
|
|
143
|
-
this.log("PATCH", path);
|
|
144
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
145
|
-
method: "PATCH",
|
|
146
|
-
headers: this.headers(),
|
|
147
|
-
body: JSON.stringify(body)
|
|
148
|
-
});
|
|
149
|
-
this.log("PATCH", path, res.status);
|
|
150
|
-
if (!res.ok) {
|
|
151
|
-
const text = await res.text();
|
|
152
|
-
throw new ApiError(res.status, text);
|
|
153
|
-
}
|
|
154
|
-
return res.json();
|
|
155
|
-
}
|
|
156
|
-
async delete(path) {
|
|
157
|
-
this.log("DELETE", path);
|
|
158
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
159
|
-
method: "DELETE",
|
|
160
|
-
headers: this.headers()
|
|
161
|
-
});
|
|
162
|
-
this.log("DELETE", path, res.status);
|
|
163
|
-
if (!res.ok) {
|
|
164
|
-
const text = await res.text();
|
|
165
|
-
throw new ApiError(res.status, text);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
async stream(path, body) {
|
|
169
|
-
this.log("POST (stream)", path);
|
|
170
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
171
|
-
method: "POST",
|
|
172
|
-
headers: this.headers({ Accept: "text/event-stream" }),
|
|
173
|
-
body: JSON.stringify(body)
|
|
174
|
-
});
|
|
175
|
-
this.log("POST (stream)", path, res.status);
|
|
176
|
-
if (!res.ok) {
|
|
177
|
-
const text = await res.text();
|
|
178
|
-
throw new ApiError(res.status, text);
|
|
179
|
-
}
|
|
180
|
-
if (!res.body) {
|
|
181
|
-
throw new ApiError(0, "No response body for stream");
|
|
182
|
-
}
|
|
183
|
-
return res.body;
|
|
184
|
-
}
|
|
185
|
-
async streamNdjson(path, body) {
|
|
186
|
-
this.log("POST (ndjson)", path);
|
|
187
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
188
|
-
method: "POST",
|
|
189
|
-
headers: this.headers({ Accept: "application/x-ndjson" }),
|
|
190
|
-
body: JSON.stringify(body)
|
|
191
|
-
});
|
|
192
|
-
this.log("POST (ndjson)", path, res.status);
|
|
193
|
-
if (!res.ok) {
|
|
194
|
-
const text = await res.text();
|
|
195
|
-
throw new ApiError(res.status, text);
|
|
196
|
-
}
|
|
197
|
-
if (!res.body) {
|
|
198
|
-
throw new ApiError(0, "No response body for stream");
|
|
199
|
-
}
|
|
200
|
-
return res.body;
|
|
201
112
|
}
|
|
202
113
|
};
|
|
203
114
|
var ApiError = class extends Error {
|
|
204
115
|
constructor(status, body) {
|
|
205
|
-
let
|
|
116
|
+
let message = body || "Request failed";
|
|
206
117
|
try {
|
|
207
118
|
const parsed = JSON.parse(body);
|
|
208
|
-
|
|
119
|
+
if (typeof parsed?.error === "string") message = parsed.error;
|
|
120
|
+
else if (typeof parsed?.error?.message === "string") message = parsed.error.message;
|
|
121
|
+
else if (typeof parsed?.message === "string") message = parsed.message;
|
|
209
122
|
} catch {
|
|
210
|
-
msg = body;
|
|
211
123
|
}
|
|
212
|
-
super(`API error ${status}: ${
|
|
124
|
+
super(`API error ${status}: ${message}`);
|
|
213
125
|
this.status = status;
|
|
214
|
-
this.body = body;
|
|
215
126
|
this.name = "ApiError";
|
|
216
127
|
}
|
|
217
128
|
};
|
|
218
129
|
|
|
219
130
|
// src/output.ts
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
223
|
-
}
|
|
224
|
-
function table(rows, headers) {
|
|
225
|
-
const allRows = headers ? [headers, ...rows] : rows;
|
|
226
|
-
const colWidths = [];
|
|
227
|
-
for (const row of allRows) {
|
|
228
|
-
for (let i = 0; i < row.length; i++) {
|
|
229
|
-
colWidths[i] = Math.max(colWidths[i] ?? 0, stripAnsi(row[i]).length);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
for (let r = 0; r < allRows.length; r++) {
|
|
233
|
-
const row = allRows[r];
|
|
234
|
-
const padded = row.map((cell, i) => {
|
|
235
|
-
const bare = stripAnsi(cell);
|
|
236
|
-
const pad = colWidths[i] - bare.length;
|
|
237
|
-
return cell + " ".repeat(Math.max(0, pad));
|
|
238
|
-
});
|
|
239
|
-
process.stdout.write(" " + padded.join(" ") + "\n");
|
|
240
|
-
if (r === 0 && headers) {
|
|
241
|
-
const sep = colWidths.map((w) => "\u2500".repeat(w)).join("\u2500\u2500");
|
|
242
|
-
process.stdout.write(" " + sep + "\n");
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
function stripAnsi(str) {
|
|
247
|
-
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
248
|
-
}
|
|
249
|
-
function heading(text) {
|
|
250
|
-
process.stdout.write("\n" + chalk.bold(text) + "\n\n");
|
|
251
|
-
}
|
|
252
|
-
function success(text) {
|
|
253
|
-
process.stdout.write(chalk.green("\u2713") + " " + text + "\n");
|
|
254
|
-
}
|
|
255
|
-
function warn(text) {
|
|
256
|
-
process.stdout.write(chalk.yellow("\u26A0") + " " + text + "\n");
|
|
257
|
-
}
|
|
258
|
-
function error(text) {
|
|
259
|
-
process.stderr.write(chalk.red("\u2717") + " " + text + "\n");
|
|
260
|
-
}
|
|
261
|
-
function dim(text) {
|
|
262
|
-
return chalk.dim(text);
|
|
263
|
-
}
|
|
264
|
-
function bold(text) {
|
|
265
|
-
return chalk.bold(text);
|
|
266
|
-
}
|
|
267
|
-
function currency(n) {
|
|
268
|
-
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
269
|
-
}
|
|
270
|
-
function compactCurrency(n) {
|
|
271
|
-
if (!n || isNaN(n)) return "$0";
|
|
272
|
-
if (n >= 1e6) return `$${(n / 1e6).toFixed(1)}M`;
|
|
273
|
-
if (n >= 1e3) return `$${(n / 1e3).toFixed(1)}K`;
|
|
274
|
-
return `$${n.toFixed(0)}`;
|
|
275
|
-
}
|
|
276
|
-
function percent(n) {
|
|
277
|
-
return (n * 100).toFixed(1) + "%";
|
|
278
|
-
}
|
|
279
|
-
function truncate(str, max) {
|
|
280
|
-
if (str.length <= max) return str;
|
|
281
|
-
return str.slice(0, max - 1) + "\u2026";
|
|
282
|
-
}
|
|
283
|
-
function relativeTime(date) {
|
|
284
|
-
const diff = Date.now() - new Date(date).getTime();
|
|
285
|
-
const mins = Math.floor(diff / 6e4);
|
|
286
|
-
if (mins < 1) return "just now";
|
|
287
|
-
if (mins < 60) return `${mins}m ago`;
|
|
288
|
-
const hours = Math.floor(mins / 60);
|
|
289
|
-
if (hours < 24) return `${hours}h ago`;
|
|
290
|
-
const days = Math.floor(hours / 24);
|
|
291
|
-
return `${days}d ago`;
|
|
131
|
+
function json(value) {
|
|
132
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
292
133
|
}
|
|
293
134
|
|
|
294
135
|
// src/commands/auth.ts
|
|
295
|
-
function
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
process.stderr.write(
|
|
303
|
-
`Create an API token at ${bold("https://hedgelayer.ai/account/settings")} \u2192 API Tokens
|
|
304
|
-
|
|
305
|
-
`
|
|
306
|
-
);
|
|
307
|
-
const token = (await rl.question("Paste your API token: ")).trim();
|
|
308
|
-
if (!token.startsWith("hl_") || token.length !== 43) {
|
|
309
|
-
error('Invalid token format. Tokens start with "hl_" and are 43 characters.');
|
|
310
|
-
process.exit(1);
|
|
311
|
-
}
|
|
312
|
-
const apiUrl = globalOpts.apiUrl ?? cmdOpts.apiUrl ?? DEFAULT_API_URL;
|
|
313
|
-
const client = new ApiClient({ token, apiUrl });
|
|
314
|
-
process.stderr.write("\nValidating token...");
|
|
315
|
-
let profile;
|
|
316
|
-
try {
|
|
317
|
-
profile = await client.get("/api/profile");
|
|
318
|
-
} catch {
|
|
319
|
-
process.stderr.write("\n");
|
|
320
|
-
error("Token validation failed. Check your token and try again.");
|
|
321
|
-
process.exit(1);
|
|
322
|
-
}
|
|
323
|
-
process.stderr.write(" done\n\n");
|
|
324
|
-
saveConfig({ api_url: apiUrl, token });
|
|
325
|
-
success(`Logged in as ${bold(profile.handle || profile.user_id)}`);
|
|
326
|
-
process.stderr.write(` Config saved to ${dim(configPath())}
|
|
327
|
-
`);
|
|
328
|
-
} finally {
|
|
329
|
-
rl.close();
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
auth.command("status").description("Show current authentication status").action(async () => {
|
|
333
|
-
const globalOpts = program2.opts();
|
|
334
|
-
const config = loadConfig();
|
|
335
|
-
const token = globalOpts.token ?? config.token;
|
|
336
|
-
if (!token) {
|
|
337
|
-
warn("Not logged in. Run " + bold("hl auth login") + " to authenticate.");
|
|
338
|
-
process.exit(1);
|
|
339
|
-
}
|
|
340
|
-
const client = new ApiClient(globalOpts);
|
|
341
|
-
try {
|
|
342
|
-
const profile = await client.get("/api/profile");
|
|
343
|
-
if (globalOpts.json) {
|
|
344
|
-
json({
|
|
345
|
-
authenticated: true,
|
|
346
|
-
handle: profile.handle,
|
|
347
|
-
user_id: profile.user_id,
|
|
348
|
-
api_url: client.apiUrl
|
|
349
|
-
});
|
|
350
|
-
} else {
|
|
351
|
-
heading("Auth Status");
|
|
352
|
-
table(
|
|
353
|
-
[
|
|
354
|
-
["Handle", bold(profile.handle || "(none)")],
|
|
355
|
-
["User ID", profile.user_id],
|
|
356
|
-
["API URL", client.apiUrl],
|
|
357
|
-
["Config", configPath()]
|
|
358
|
-
]
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
} catch {
|
|
362
|
-
error("Token is invalid or expired. Run " + bold("hl auth login") + " to re-authenticate.");
|
|
363
|
-
process.exit(1);
|
|
136
|
+
async function promptHidden(prompt, options = {}) {
|
|
137
|
+
const input = options.input ?? process.stdin;
|
|
138
|
+
const output = options.output ?? process.stderr;
|
|
139
|
+
const createInterface = options.createInterface ?? readline.createInterface;
|
|
140
|
+
const mutedOutput = new Writable({
|
|
141
|
+
write(_chunk, _encoding, callback) {
|
|
142
|
+
callback();
|
|
364
143
|
}
|
|
365
144
|
});
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
145
|
+
const rl = createInterface({
|
|
146
|
+
input,
|
|
147
|
+
output: mutedOutput,
|
|
148
|
+
terminal: Boolean(input.isTTY)
|
|
369
149
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const { done, value } = await reader.read();
|
|
387
|
-
if (!done) {
|
|
388
|
-
buffer += decoder.decode(value, { stream: true });
|
|
389
|
-
}
|
|
390
|
-
const lines = buffer.split("\n");
|
|
391
|
-
buffer = done ? "" : lines.pop() ?? "";
|
|
392
|
-
for (const line of lines) {
|
|
393
|
-
const trimmed = line.trim();
|
|
394
|
-
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
395
|
-
if (!trimmed.startsWith("data:")) continue;
|
|
396
|
-
const payload = trimmed.slice(5).trim();
|
|
397
|
-
if (payload === "[DONE]") continue;
|
|
398
|
-
let msg;
|
|
399
|
-
try {
|
|
400
|
-
msg = JSON.parse(payload);
|
|
401
|
-
} catch {
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
const type = msg.type;
|
|
405
|
-
switch (type) {
|
|
406
|
-
case "text-delta": {
|
|
407
|
-
const delta = msg.delta;
|
|
408
|
-
if (delta) {
|
|
409
|
-
assistantText += delta;
|
|
410
|
-
callbacks.onText?.(delta);
|
|
411
|
-
}
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
case "tool-input-start": {
|
|
415
|
-
const id = msg.toolCallId;
|
|
416
|
-
const name = msg.toolName;
|
|
417
|
-
if (id && name) {
|
|
418
|
-
activeToolCalls.set(id, { name, argStr: "" });
|
|
419
|
-
}
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
case "tool-input-delta": {
|
|
423
|
-
const id = msg.toolCallId;
|
|
424
|
-
const active = activeToolCalls.get(id);
|
|
425
|
-
if (active) {
|
|
426
|
-
active.argStr += msg.inputTextDelta;
|
|
427
|
-
}
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
case "tool-input-available": {
|
|
431
|
-
const id = msg.toolCallId;
|
|
432
|
-
const active = activeToolCalls.get(id);
|
|
433
|
-
if (active) {
|
|
434
|
-
const args = msg.input ?? active.argStr;
|
|
435
|
-
toolCalls.push({ name: active.name, args });
|
|
436
|
-
callbacks.onToolCall?.(active.name, args);
|
|
437
|
-
}
|
|
438
|
-
break;
|
|
439
|
-
}
|
|
440
|
-
case "tool-output-available": {
|
|
441
|
-
const id = msg.toolCallId;
|
|
442
|
-
const active = activeToolCalls.get(id);
|
|
443
|
-
if (active) {
|
|
444
|
-
callbacks.onToolResult?.(active.name, msg.output);
|
|
445
|
-
if (active.name === "buildMarketBrief" && isMarketBrief(msg.output)) {
|
|
446
|
-
marketBrief = msg.output;
|
|
447
|
-
}
|
|
448
|
-
if (active.name === "getFeed" && isFeedResult(msg.output)) {
|
|
449
|
-
feedResult = msg.output;
|
|
450
|
-
}
|
|
451
|
-
activeToolCalls.delete(id);
|
|
452
|
-
}
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
case "error": {
|
|
456
|
-
throw new Error(msg.errorText ?? "Unknown stream error");
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (done) break;
|
|
461
|
-
}
|
|
462
|
-
return { assistantText, toolCalls, marketBrief, feedResult };
|
|
463
|
-
}
|
|
464
|
-
function isMarketBrief(val) {
|
|
465
|
-
return val !== null && typeof val === "object" && "title" in val && "markets" in val;
|
|
466
|
-
}
|
|
467
|
-
function isFeedResult(val) {
|
|
468
|
-
return val !== null && typeof val === "object" && "markets" in val && "sortedBy" in val;
|
|
469
|
-
}
|
|
470
|
-
async function parseNdjsonStream(body, callbacks = {}) {
|
|
471
|
-
const decoder = new TextDecoder();
|
|
472
|
-
const reader = body.getReader();
|
|
473
|
-
let buffer = "";
|
|
474
|
-
let brief = null;
|
|
475
|
-
let error2 = null;
|
|
476
|
-
while (true) {
|
|
477
|
-
const { done, value } = await reader.read();
|
|
478
|
-
if (!done) {
|
|
479
|
-
buffer += decoder.decode(value, { stream: true });
|
|
480
|
-
}
|
|
481
|
-
const lines = buffer.split("\n");
|
|
482
|
-
buffer = done ? "" : lines.pop() ?? "";
|
|
483
|
-
for (const line of lines) {
|
|
484
|
-
const trimmed = line.trim();
|
|
485
|
-
if (!trimmed) continue;
|
|
486
|
-
let msg;
|
|
487
|
-
try {
|
|
488
|
-
msg = JSON.parse(trimmed);
|
|
489
|
-
} catch {
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
const type = msg.type;
|
|
493
|
-
switch (type) {
|
|
494
|
-
case "progress": {
|
|
495
|
-
callbacks.onProgress?.(msg.step, msg.message);
|
|
496
|
-
break;
|
|
497
|
-
}
|
|
498
|
-
case "brief": {
|
|
499
|
-
brief = msg.data;
|
|
500
|
-
break;
|
|
501
|
-
}
|
|
502
|
-
case "error": {
|
|
503
|
-
error2 = {
|
|
504
|
-
code: msg.code,
|
|
505
|
-
message: msg.message
|
|
506
|
-
};
|
|
507
|
-
break;
|
|
508
|
-
}
|
|
150
|
+
output.write(prompt);
|
|
151
|
+
try {
|
|
152
|
+
return (await rl.question("")).trim();
|
|
153
|
+
} finally {
|
|
154
|
+
output.write("\n");
|
|
155
|
+
rl.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function registerAuthCommands(program) {
|
|
159
|
+
const auth = program.command("auth").description("Manage API authentication");
|
|
160
|
+
auth.command("login").description("Validate and save an API token (hidden prompt, --token, or HL_TOKEN)").action(async () => {
|
|
161
|
+
const options = program.opts();
|
|
162
|
+
let token = options.token ?? process.env.HL_TOKEN;
|
|
163
|
+
if (token === void 0) {
|
|
164
|
+
if (!process.stdin.isTTY) {
|
|
165
|
+
throw new Error("Interactive login requires a terminal. Set HL_TOKEN for scripts.");
|
|
509
166
|
}
|
|
167
|
+
process.stderr.write("Create an API token in Hedge Layer account settings.\n");
|
|
168
|
+
token = await promptHidden("Paste your API token: ");
|
|
510
169
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
function registerBriefCommands(program2) {
|
|
518
|
-
program2.command("brief <query>").description("Generate a Market Brief for a topic (non-interactive)").option("-l, --location <location>", "Geographic context (e.g. 'Middle East', 'US')").option("-t, --time-horizon <horizon>", "Time frame (e.g. '3 months', '2026')").option("--tags <tags>", "Comma-separated focus area tags (e.g. 'geopolitics,energy')").option("--min-volume <n>", "Minimum market volume in USD", parseFloat).option("--max-yes-price <n>", "Maximum YES price (0-1)", parseFloat).action(async (query, cmdOpts) => {
|
|
519
|
-
const globalOpts = program2.opts();
|
|
520
|
-
const client = new ApiClient(globalOpts);
|
|
521
|
-
requireAuth(client);
|
|
522
|
-
const filters = buildFilters(cmdOpts);
|
|
523
|
-
const body = {
|
|
524
|
-
query,
|
|
525
|
-
...cmdOpts.location && { location: cmdOpts.location },
|
|
526
|
-
...cmdOpts.timeHorizon && { timeHorizon: cmdOpts.timeHorizon },
|
|
527
|
-
...filters && { filters },
|
|
528
|
-
stream: true
|
|
529
|
-
};
|
|
530
|
-
await runStreaming(client, body, globalOpts);
|
|
170
|
+
token = token.trim();
|
|
171
|
+
if (!token) throw new Error("API token cannot be empty.");
|
|
172
|
+
const client = new ApiClient({ ...options, token });
|
|
173
|
+
await client.listTools();
|
|
174
|
+
saveConfig({ api_url: client.apiUrl, token });
|
|
175
|
+
json({ authenticated: true, api_url: client.apiUrl, config: configPath() });
|
|
531
176
|
});
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (!hasFilters) return void 0;
|
|
537
|
-
return {
|
|
538
|
-
...opts.minVolume != null && { minVolume: opts.minVolume },
|
|
539
|
-
...opts.maxYesPrice != null && { maxYesPrice: opts.maxYesPrice },
|
|
540
|
-
...tags && tags.length > 0 && { tags }
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
async function runStreaming(client, body, globalOpts) {
|
|
544
|
-
const startTime = Date.now();
|
|
545
|
-
process.stderr.write(chalk2.dim(` Generating brief for "${truncate(body.query, 50)}"...
|
|
546
|
-
`));
|
|
547
|
-
const stream = await client.streamNdjson("/api/brief", body);
|
|
548
|
-
const result = await parseNdjsonStream(stream, {
|
|
549
|
-
onProgress: (step, message) => {
|
|
550
|
-
process.stderr.write(chalk2.dim(` [${step}] `) + message + "\n");
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
554
|
-
if (result.error) {
|
|
555
|
-
error(`Brief failed (${result.error.code}): ${result.error.message}`);
|
|
556
|
-
process.exit(1);
|
|
557
|
-
}
|
|
558
|
-
if (!result.brief) {
|
|
559
|
-
error("No Market Brief was produced.");
|
|
560
|
-
process.exit(1);
|
|
561
|
-
}
|
|
562
|
-
process.stderr.write(chalk2.dim(` Done (${elapsed}s)
|
|
563
|
-
|
|
564
|
-
`));
|
|
565
|
-
displayBrief(result.brief, globalOpts);
|
|
566
|
-
}
|
|
567
|
-
function displayBrief(brief, globalOpts) {
|
|
568
|
-
if (globalOpts.json) {
|
|
569
|
-
json(brief);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
heading("Market Brief");
|
|
573
|
-
process.stdout.write(" " + chalk2.bold(brief.title) + "\n\n");
|
|
574
|
-
process.stdout.write(" " + chalk2.italic(brief.thesis) + "\n");
|
|
575
|
-
if (brief.markets.length > 0) {
|
|
576
|
-
process.stdout.write("\n" + chalk2.bold(" Markets") + "\n\n");
|
|
577
|
-
const rows = brief.markets.map((m) => {
|
|
578
|
-
const prob = percent(m.yesPrice);
|
|
579
|
-
const signals = m.signals.length > 0 ? m.signals.join(", ") : "\u2014";
|
|
580
|
-
const liq = m.liquidity ? currency(m.liquidity) : "\u2014";
|
|
581
|
-
return [truncate(m.question, 40), prob, signals, liq];
|
|
582
|
-
});
|
|
583
|
-
table(rows, ["Market", "Prob", "Signals", "Liq"]);
|
|
584
|
-
process.stdout.write("\n");
|
|
585
|
-
for (const m of brief.markets) {
|
|
586
|
-
process.stdout.write(" " + chalk2.dim("\u25B8 ") + truncate(m.question, 50) + "\n");
|
|
587
|
-
process.stdout.write(" " + chalk2.dim("Causal link: ") + m.causalLink + "\n");
|
|
588
|
-
process.stdout.write(" " + chalk2.dim("Polymarket: ") + m.polymarketUrl + "\n");
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (brief.gaps.length > 0) {
|
|
592
|
-
process.stdout.write("\n" + chalk2.bold(" Coverage Gaps") + "\n\n");
|
|
593
|
-
for (const gap of brief.gaps) {
|
|
594
|
-
process.stdout.write(" " + chalk2.yellow("\u25B8") + " " + gap + "\n");
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
const marketCount = brief.marketCount ?? brief.markets.length;
|
|
598
|
-
process.stdout.write("\n " + chalk2.dim(`${marketCount} markets \xB7 ${brief.gaps.length} coverage gaps`) + "\n");
|
|
599
|
-
}
|
|
600
|
-
function requireAuth(client) {
|
|
601
|
-
if (!client.isAuthenticated) {
|
|
602
|
-
error("Not logged in. Run " + bold("hl auth login") + " first.");
|
|
603
|
-
process.exit(1);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// src/commands/profile.ts
|
|
608
|
-
function registerProfileCommand(program2) {
|
|
609
|
-
program2.command("profile").description("Show your user profile").action(async () => {
|
|
610
|
-
const globalOpts = program2.opts();
|
|
611
|
-
const client = new ApiClient(globalOpts);
|
|
612
|
-
requireAuth2(client);
|
|
613
|
-
const profile = await client.get("/api/profile");
|
|
614
|
-
if (globalOpts.json) {
|
|
615
|
-
json(profile);
|
|
616
|
-
return;
|
|
177
|
+
auth.command("status").description("Validate the current API token").action(async () => {
|
|
178
|
+
const client = new ApiClient(program.opts());
|
|
179
|
+
if (!client.isAuthenticated) {
|
|
180
|
+
throw new Error("No API token configured. Run hl auth login or set HL_TOKEN.");
|
|
617
181
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
["Handle", bold(profile.handle || "(none)")],
|
|
621
|
-
["User ID", profile.user_id],
|
|
622
|
-
["Created", new Date(profile.created_at).toLocaleDateString()]
|
|
623
|
-
]);
|
|
182
|
+
await client.listTools();
|
|
183
|
+
json({ authenticated: true, api_url: client.apiUrl });
|
|
624
184
|
});
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
error("Not logged in. Run " + bold("hl auth login") + " first.");
|
|
629
|
-
process.exit(1);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// src/commands/research.ts
|
|
634
|
-
import readline2 from "readline/promises";
|
|
635
|
-
import chalk4 from "chalk";
|
|
636
|
-
|
|
637
|
-
// src/feed-display.ts
|
|
638
|
-
import chalk3 from "chalk";
|
|
639
|
-
var SORT_LABELS = {
|
|
640
|
-
score: "attention score",
|
|
641
|
-
volume: "24h volume",
|
|
642
|
-
liquidity: "liquidity",
|
|
643
|
-
movement: "price movement",
|
|
644
|
-
spread: "spread tightness",
|
|
645
|
-
recency: "recency",
|
|
646
|
-
extremity: "uncertainty",
|
|
647
|
-
rewards: "rewards",
|
|
648
|
-
rewardYield: "reward yield (per $ liq)",
|
|
649
|
-
lpExpectedReturn: "LP expected return",
|
|
650
|
-
horizon: "time to resolution"
|
|
651
|
-
};
|
|
652
|
-
function scoreBar(score) {
|
|
653
|
-
const width = 15;
|
|
654
|
-
const filled = Math.round(score / 100 * width);
|
|
655
|
-
const empty = width - filled;
|
|
656
|
-
const color = score >= 70 ? chalk3.green : score >= 45 ? chalk3.yellow : chalk3.dim;
|
|
657
|
-
return color("\u2588".repeat(filled)) + chalk3.dim("\u2591".repeat(empty)) + ` ${Math.round(score)}`;
|
|
658
|
-
}
|
|
659
|
-
function displayFeedResult(feed, globalOpts) {
|
|
660
|
-
if (globalOpts.json) {
|
|
661
|
-
json(feed);
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
const sortLabel = SORT_LABELS[feed.sortedBy] ?? feed.sortedBy;
|
|
665
|
-
heading(`Market Feed \u2014 ${feed.marketsReturned} markets by ${sortLabel}`);
|
|
666
|
-
process.stdout.write(
|
|
667
|
-
chalk3.dim(
|
|
668
|
-
` ${feed.totalScanned.toLocaleString()} scanned \xB7 ${feed.totalAfterFilter.toLocaleString()} after filters \xB7 preset ${feed.preset}
|
|
669
|
-
|
|
670
|
-
`
|
|
671
|
-
)
|
|
672
|
-
);
|
|
673
|
-
if (feed.markets.length === 0) {
|
|
674
|
-
warn("No markets matched the criteria.");
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
const rows = feed.markets.map((m) => {
|
|
678
|
-
const prob = percent(m.yesPrice);
|
|
679
|
-
const change = m.oneDayPriceChange;
|
|
680
|
-
const changeStr = change > 0 ? chalk3.green(`+${(change * 100).toFixed(1)}%`) : change < 0 ? chalk3.red(`${(change * 100).toFixed(1)}%`) : chalk3.dim("0.0%");
|
|
681
|
-
return [
|
|
682
|
-
String(m.rank),
|
|
683
|
-
truncate(m.question, 42),
|
|
684
|
-
prob,
|
|
685
|
-
String(Math.round(m.score)),
|
|
686
|
-
compactCurrency(m.volume24h),
|
|
687
|
-
compactCurrency(m.liquidity),
|
|
688
|
-
changeStr
|
|
689
|
-
];
|
|
185
|
+
auth.command("logout").description("Remove the saved API token (HL_TOKEN and --token still apply)").action(() => {
|
|
186
|
+
clearConfig();
|
|
187
|
+
json({ removed: true, config: configPath() });
|
|
690
188
|
});
|
|
691
|
-
table(rows, ["#", "Market", "Prob", "Score", "24h Vol", "Liq", "Chg"]);
|
|
692
|
-
process.stdout.write("\n");
|
|
693
|
-
for (const m of feed.markets.slice(0, 5)) {
|
|
694
|
-
const url = m.polymarketUrl;
|
|
695
|
-
process.stdout.write(" " + chalk3.dim("\u25B8 ") + truncate(m.question, 55) + "\n");
|
|
696
|
-
process.stdout.write(" " + chalk3.dim("Score: ") + scoreBar(m.score) + " ");
|
|
697
|
-
process.stdout.write(chalk3.dim("Spread: ") + m.spread.toFixed(3) + " ");
|
|
698
|
-
if (m.rewardsDailyRate > 0) {
|
|
699
|
-
process.stdout.write(chalk3.dim("Rewards: ") + `$${m.rewardsDailyRate.toFixed(2)}/day`);
|
|
700
|
-
}
|
|
701
|
-
process.stdout.write("\n");
|
|
702
|
-
process.stdout.write(" " + chalk3.dim("Polymarket: ") + url + "\n");
|
|
703
|
-
}
|
|
704
|
-
if (feed.markets.length > 5) {
|
|
705
|
-
process.stdout.write(chalk3.dim(`
|
|
706
|
-
\u2026 and ${feed.markets.length - 5} more
|
|
707
|
-
`));
|
|
708
|
-
}
|
|
709
189
|
}
|
|
710
190
|
|
|
711
|
-
// src/commands/
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
);
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
role,
|
|
727
|
-
content,
|
|
728
|
-
parts: [{ type: "text", text: content }]
|
|
729
|
-
});
|
|
730
|
-
const messages = [];
|
|
731
|
-
let firstMessageTs = null;
|
|
732
|
-
const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
|
|
733
|
-
try {
|
|
734
|
-
while (true) {
|
|
735
|
-
const userInput = await rl.question(chalk4.cyan("You: "));
|
|
736
|
-
if (!userInput.trim()) continue;
|
|
737
|
-
if (userInput.trim() === "/quit") break;
|
|
738
|
-
if (firstMessageTs === null) firstMessageTs = Date.now();
|
|
739
|
-
messages.push(uiMsg("user", userInput));
|
|
740
|
-
process.stderr.write(chalk4.dim("\nAssistant: "));
|
|
741
|
-
try {
|
|
742
|
-
const body = await client.stream("/api/chat", {
|
|
743
|
-
messages,
|
|
744
|
-
assessmentId: id
|
|
745
|
-
});
|
|
746
|
-
const result = await parseStream(body, {
|
|
747
|
-
onText: (text) => process.stderr.write(text),
|
|
748
|
-
onToolCall: (name) => {
|
|
749
|
-
process.stderr.write(chalk4.dim(`
|
|
750
|
-
[tool: ${name}]
|
|
751
|
-
`));
|
|
752
|
-
},
|
|
753
|
-
onToolResult: (name, toolResult) => {
|
|
754
|
-
if (globalOpts.verbose) {
|
|
755
|
-
process.stderr.write(chalk4.dim(` [result: ${name}] ${JSON.stringify(toolResult).slice(0, 200)}
|
|
756
|
-
`));
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
});
|
|
760
|
-
process.stderr.write("\n\n");
|
|
761
|
-
if (result.assistantText) {
|
|
762
|
-
messages.push(uiMsg("assistant", result.assistantText));
|
|
763
|
-
}
|
|
764
|
-
try {
|
|
765
|
-
await persistAssessment(client, id, messages, result.marketBrief, firstMessageTs);
|
|
766
|
-
} catch (persistErr) {
|
|
767
|
-
warn(
|
|
768
|
-
`Could not save session: ${persistErr instanceof Error ? persistErr.message : String(persistErr)}`
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
if (result.feedResult) {
|
|
772
|
-
displayFeedResult(result.feedResult, globalOpts);
|
|
773
|
-
}
|
|
774
|
-
if (result.marketBrief) {
|
|
775
|
-
displayMarketBrief(result.marketBrief, globalOpts);
|
|
776
|
-
break;
|
|
777
|
-
}
|
|
778
|
-
} catch (e) {
|
|
779
|
-
process.stderr.write("\n");
|
|
780
|
-
error(`Chat error: ${e instanceof Error ? e.message : String(e)}`);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
} finally {
|
|
784
|
-
rl.close();
|
|
785
|
-
}
|
|
786
|
-
});
|
|
787
|
-
research.command("run <query>").description("Run research on a topic and return the final Market Brief as JSON (via /api/brief)").action(async (query) => {
|
|
788
|
-
const globalOpts = program2.opts();
|
|
789
|
-
const client = new ApiClient(globalOpts);
|
|
790
|
-
requireAuth3(client);
|
|
791
|
-
process.stderr.write(chalk4.dim(` Researching "${truncate(query, 60)}"...
|
|
792
|
-
`));
|
|
793
|
-
const startTime = Date.now();
|
|
794
|
-
const { brief, durationMs, stepsCompleted, toolsUsed } = await client.postBriefSync(query);
|
|
795
|
-
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
796
|
-
process.stderr.write(chalk4.dim(` Done (${elapsed}s)
|
|
797
|
-
|
|
798
|
-
`));
|
|
799
|
-
const looksLikeBrief = brief && typeof brief === "object" && Array.isArray(brief.markets) && typeof brief.title === "string";
|
|
800
|
-
if (looksLikeBrief) {
|
|
801
|
-
json(brief);
|
|
802
|
-
} else {
|
|
803
|
-
process.stderr.write(chalk4.yellow("No market brief was produced.\n"));
|
|
804
|
-
json({
|
|
805
|
-
brief: null,
|
|
806
|
-
text: null,
|
|
807
|
-
metadata: {
|
|
808
|
-
model: "",
|
|
809
|
-
stepsUsed: stepsCompleted ?? 0,
|
|
810
|
-
toolsUsed,
|
|
811
|
-
durationMs: durationMs ?? Date.now() - startTime
|
|
812
|
-
},
|
|
813
|
-
raw: brief
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
research.command("list").description("List past research sessions").option("-s, --status <status>", "Filter by status").action(async (cmdOpts) => {
|
|
818
|
-
const globalOpts = program2.opts();
|
|
819
|
-
const client = new ApiClient(globalOpts);
|
|
820
|
-
requireAuth3(client);
|
|
821
|
-
const params = { list: "true" };
|
|
822
|
-
if (cmdOpts.status) params.status = cmdOpts.status;
|
|
823
|
-
const data = await client.get("/api/assessments", params);
|
|
824
|
-
if (globalOpts.json) {
|
|
825
|
-
json(data.assessments);
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
if (data.assessments.length === 0) {
|
|
829
|
-
warn("No research sessions found.");
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
heading(`Research Sessions (${data.assessments.length})`);
|
|
833
|
-
const rows = data.assessments.map((a) => {
|
|
834
|
-
const status = formatStatus(a.status);
|
|
835
|
-
const brief = a.market_brief?.title ?? "\u2014";
|
|
836
|
-
const markets = a.market_brief ? String(a.market_brief.marketCount) : "\u2014";
|
|
837
|
-
return [a.id.slice(0, 8), status, truncate(brief, 30), markets, relativeTime(a.created_at)];
|
|
838
|
-
});
|
|
839
|
-
table(rows, ["ID", "Status", "Brief", "Markets", "Created"]);
|
|
840
|
-
});
|
|
841
|
-
research.command("show <id>").description("Show research session details (accepts the short ID shown by `hl research list`)").action(async (id) => {
|
|
842
|
-
const globalOpts = program2.opts();
|
|
843
|
-
const client = new ApiClient(globalOpts);
|
|
844
|
-
requireAuth3(client);
|
|
845
|
-
const fullId = await resolveOrExit(client, id);
|
|
846
|
-
const assessment = await client.get(`/api/assessments/${fullId}`);
|
|
847
|
-
if (globalOpts.json) {
|
|
848
|
-
json(assessment);
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
heading("Research Session " + dim(assessment.id.slice(0, 8)));
|
|
852
|
-
table([
|
|
853
|
-
["Status", formatStatus(assessment.status)],
|
|
854
|
-
["Created", new Date(assessment.created_at).toLocaleString()],
|
|
855
|
-
["Updated", new Date(assessment.updated_at).toLocaleString()]
|
|
856
|
-
]);
|
|
857
|
-
if (assessment.market_brief) {
|
|
858
|
-
displayMarketBrief(assessment.market_brief, globalOpts);
|
|
859
|
-
}
|
|
860
|
-
});
|
|
861
|
-
research.command("delete <id>").description("Delete a research session (accepts the short ID shown by `hl research list`)").action(async (id) => {
|
|
862
|
-
const globalOpts = program2.opts();
|
|
863
|
-
const client = new ApiClient(globalOpts);
|
|
864
|
-
requireAuth3(client);
|
|
865
|
-
const fullId = await resolveOrExit(client, id);
|
|
866
|
-
await client.delete(`/api/assessments/${fullId}`);
|
|
867
|
-
success("Research session deleted.");
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
var FULL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
871
|
-
function matchAssessmentId(idOrPrefix, ids) {
|
|
872
|
-
const needle = idOrPrefix.trim().toLowerCase();
|
|
873
|
-
if (!needle) {
|
|
874
|
-
throw new Error("No research session ID provided.");
|
|
875
|
-
}
|
|
876
|
-
const exact = ids.find((id) => id.toLowerCase() === needle);
|
|
877
|
-
if (exact) return exact;
|
|
878
|
-
const matches = ids.filter((id) => id.toLowerCase().startsWith(needle));
|
|
879
|
-
if (matches.length === 1) return matches[0];
|
|
880
|
-
if (matches.length === 0) {
|
|
881
|
-
throw new Error(
|
|
882
|
-
`No research session found matching "${idOrPrefix}". Run \`hl research list\` to see available sessions.`
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
const shortIds = matches.map((id) => id.slice(0, 8)).join(", ");
|
|
886
|
-
throw new Error(
|
|
887
|
-
`"${idOrPrefix}" matches ${matches.length} research sessions (${shortIds}). Use a longer ID prefix to disambiguate.`
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
async function resolveAssessmentId(client, idOrPrefix) {
|
|
891
|
-
const trimmed = idOrPrefix.trim();
|
|
892
|
-
if (FULL_UUID.test(trimmed)) return trimmed;
|
|
893
|
-
const data = await client.get("/api/assessments", { list: "true" });
|
|
894
|
-
return matchAssessmentId(
|
|
895
|
-
trimmed,
|
|
896
|
-
data.assessments.map((a) => a.id)
|
|
897
|
-
);
|
|
898
|
-
}
|
|
899
|
-
async function resolveOrExit(client, idOrPrefix) {
|
|
191
|
+
// src/commands/tools.ts
|
|
192
|
+
import { readFile } from "fs/promises";
|
|
193
|
+
async function readArguments(options) {
|
|
194
|
+
const sources = [options.args !== void 0, options.file !== void 0, Boolean(options.stdin)];
|
|
195
|
+
if (sources.filter(Boolean).length > 1) {
|
|
196
|
+
throw new Error("Use only one of --args, --file, or --stdin.");
|
|
197
|
+
}
|
|
198
|
+
let source = options.args ?? "{}";
|
|
199
|
+
if (options.file !== void 0) source = await readFile(options.file, "utf8");
|
|
200
|
+
if (options.stdin) {
|
|
201
|
+
source = "";
|
|
202
|
+
process.stdin.setEncoding("utf8");
|
|
203
|
+
for await (const chunk of process.stdin) source += chunk.toString();
|
|
204
|
+
}
|
|
205
|
+
let args;
|
|
900
206
|
try {
|
|
901
|
-
|
|
902
|
-
} catch
|
|
903
|
-
|
|
904
|
-
error(e instanceof Error ? e.message : String(e));
|
|
905
|
-
process.exit(1);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
function requireAuth3(client) {
|
|
909
|
-
if (!client.isAuthenticated) {
|
|
910
|
-
error("Not logged in. Run " + bold("hl auth login") + " first.");
|
|
911
|
-
process.exit(1);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
function buildAssessmentPatch(messages, marketBrief, firstMessageTs) {
|
|
915
|
-
const patch = { messages };
|
|
916
|
-
const meta = {
|
|
917
|
-
timeToBriefMs: null,
|
|
918
|
-
searchQueries: [],
|
|
919
|
-
searchResultCounts: [],
|
|
920
|
-
coverageHit: false,
|
|
921
|
-
completedAt: null
|
|
922
|
-
};
|
|
923
|
-
if (marketBrief && typeof marketBrief === "object" && Array.isArray(marketBrief.markets)) {
|
|
924
|
-
const markets = marketBrief.markets;
|
|
925
|
-
patch.market_brief = marketBrief;
|
|
926
|
-
patch.status = "completed";
|
|
927
|
-
meta.timeToBriefMs = firstMessageTs != null ? Date.now() - firstMessageTs : null;
|
|
928
|
-
meta.coverageHit = markets.length > 0;
|
|
929
|
-
meta.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
930
|
-
}
|
|
931
|
-
patch.metadata = meta;
|
|
932
|
-
return patch;
|
|
933
|
-
}
|
|
934
|
-
async function persistAssessment(client, assessmentId, messages, marketBrief, firstMessageTs) {
|
|
935
|
-
await client.patch(`/api/assessments/${assessmentId}`, buildAssessmentPatch(messages, marketBrief, firstMessageTs));
|
|
936
|
-
}
|
|
937
|
-
function formatStatus(status) {
|
|
938
|
-
switch (status) {
|
|
939
|
-
case "completed":
|
|
940
|
-
return chalk4.green(status);
|
|
941
|
-
case "in_progress":
|
|
942
|
-
return chalk4.yellow(status);
|
|
943
|
-
case "abandoned":
|
|
944
|
-
return chalk4.red(status);
|
|
945
|
-
default:
|
|
946
|
-
return status;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
function displayMarketBrief(brief, globalOpts) {
|
|
950
|
-
if (globalOpts.json) {
|
|
951
|
-
json(brief);
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
heading("Market Brief");
|
|
955
|
-
process.stdout.write(" " + chalk4.bold(brief.title) + "\n\n");
|
|
956
|
-
process.stdout.write(" " + chalk4.italic(brief.thesis) + "\n");
|
|
957
|
-
if (brief.markets.length > 0) {
|
|
958
|
-
process.stdout.write("\n" + chalk4.bold(" Markets") + "\n\n");
|
|
959
|
-
const rows = brief.markets.map((m) => {
|
|
960
|
-
const prob = percent(m.yesPrice);
|
|
961
|
-
const signals = m.signals.length > 0 ? m.signals.join(", ") : "\u2014";
|
|
962
|
-
const liq = m.liquidity ? currency(m.liquidity) : "\u2014";
|
|
963
|
-
return [truncate(m.question, 40), prob, signals, liq];
|
|
964
|
-
});
|
|
965
|
-
table(rows, ["Market", "Prob", "Signals", "Liq"]);
|
|
966
|
-
process.stdout.write("\n");
|
|
967
|
-
for (const m of brief.markets) {
|
|
968
|
-
process.stdout.write(" " + chalk4.dim("\u25B8 ") + truncate(m.question, 50) + "\n");
|
|
969
|
-
process.stdout.write(" " + chalk4.dim("Causal link: ") + m.causalLink + "\n");
|
|
970
|
-
process.stdout.write(" " + chalk4.dim("Polymarket: ") + m.polymarketUrl + "\n");
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
if (brief.gaps.length > 0) {
|
|
974
|
-
process.stdout.write("\n" + chalk4.bold(" Coverage Gaps") + "\n\n");
|
|
975
|
-
for (const gap of brief.gaps) {
|
|
976
|
-
process.stdout.write(" " + chalk4.yellow("\u25B8") + " " + gap + "\n");
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
process.stdout.write("\n " + chalk4.dim(`${brief.marketCount} markets \xB7 ${brief.gaps.length} coverage gaps`) + "\n");
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// src/commands/feed.ts
|
|
983
|
-
import { writeFile } from "fs/promises";
|
|
984
|
-
function requireAuth4(client) {
|
|
985
|
-
if (!client.isAuthenticated) {
|
|
986
|
-
error("Not authenticated. Run `hl auth login` first.");
|
|
987
|
-
process.exit(1);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
var PROFILE_CHOICES = ["lp-opportunity", "liquidity-provider", "liquid-new-or-long"];
|
|
991
|
-
function isProfile(s) {
|
|
992
|
-
return s !== void 0 && PROFILE_CHOICES.includes(s);
|
|
993
|
-
}
|
|
994
|
-
function resolveFeedProfile(screening, profile, warn2 = () => void 0) {
|
|
995
|
-
if (profile && !isProfile(profile)) {
|
|
996
|
-
throw new Error(`Unknown feed profile "${profile}". Use: ${PROFILE_CHOICES.join(" or ")}`);
|
|
997
|
-
}
|
|
998
|
-
if (!screening) return profile;
|
|
999
|
-
if (!isProfile(screening)) {
|
|
1000
|
-
throw new Error(`Unknown screening "${screening}". Use: ${PROFILE_CHOICES.join(" or ")}`);
|
|
1001
|
-
}
|
|
1002
|
-
if (profile && profile !== screening) {
|
|
1003
|
-
warn2(`Both positional and --profile set; using --profile (${profile}).`);
|
|
1004
|
-
return profile;
|
|
1005
|
-
}
|
|
1006
|
-
return profile ?? screening;
|
|
1007
|
-
}
|
|
1008
|
-
function feedQueryParams(opts) {
|
|
1009
|
-
const entries = [];
|
|
1010
|
-
const add = (k, v) => {
|
|
1011
|
-
if (v !== void 0 && v !== "") entries.push([k, v]);
|
|
1012
|
-
};
|
|
1013
|
-
add("profile", opts.profile);
|
|
1014
|
-
add("sortBy", opts.sortBy);
|
|
1015
|
-
add("preset", opts.preset);
|
|
1016
|
-
add("tag", opts.tag);
|
|
1017
|
-
add("minVolume", opts.minVolume);
|
|
1018
|
-
add("minLiquidity", opts.minLiquidity);
|
|
1019
|
-
add("maxLiquidity", opts.maxLiquidity);
|
|
1020
|
-
add("minRewardsDailyRate", opts.minRewardsDailyRate);
|
|
1021
|
-
add("minDaysToEnd", opts.minDaysToEnd);
|
|
1022
|
-
add("maxDaysToEnd", opts.maxDaysToEnd);
|
|
1023
|
-
add("maxMarketAgeHours", opts.maxMarketAgeHours);
|
|
1024
|
-
add("liquidProfile", opts.liquidProfile);
|
|
1025
|
-
add("limit", opts.limit);
|
|
1026
|
-
return Object.fromEntries(entries);
|
|
1027
|
-
}
|
|
1028
|
-
var ENSEMBLE_SOURCES = [
|
|
1029
|
-
{ name: "liquid-core", params: { sortBy: "liquidity", preset: "liquidity-focused" } },
|
|
1030
|
-
{ name: "active-volume", params: { sortBy: "volume", preset: "volume-hunter" } },
|
|
1031
|
-
{ name: "movers", params: { sortBy: "movement", preset: "price-movers" } },
|
|
1032
|
-
{ name: "new-markets", params: { sortBy: "recency", preset: "new-markets" } },
|
|
1033
|
-
{ name: "uncertainty", params: { sortBy: "extremity" } },
|
|
1034
|
-
{ name: "lp-quality", params: { profile: "liquidity-provider", sortBy: "lpExpectedReturn" } }
|
|
1035
|
-
];
|
|
1036
|
-
var EXTREME_PROBABILITY_LOW = 0.07;
|
|
1037
|
-
var EXTREME_PROBABILITY_HIGH = 0.93;
|
|
1038
|
-
var EXTREME_PROBABILITY_MAX_PENALTY = 15;
|
|
1039
|
-
var HORIZON_PEAK_DAYS = 365;
|
|
1040
|
-
var HORIZON_LONG_TERM_DECAY_PER_YEAR = 4;
|
|
1041
|
-
var HORIZON_LONG_TERM_FLOOR = 2;
|
|
1042
|
-
var ENSEMBLE_SOURCE_SCORE_PER_SOURCE = 2;
|
|
1043
|
-
var ENSEMBLE_SOURCE_SCORE_MAX = 8;
|
|
1044
|
-
var ENSEMBLE_MAX_CANDIDATES_PER_EVENT = 2;
|
|
1045
|
-
var ENSEMBLE_MAX_SINGLE_SOURCE_CANDIDATES = 5;
|
|
1046
|
-
function num(value) {
|
|
1047
|
-
return Number.isFinite(value) ? Number(value) : 0;
|
|
1048
|
-
}
|
|
1049
|
-
function extremeProbabilityPenalty(candidate) {
|
|
1050
|
-
const probability = Number.isFinite(candidate.probability) ? Number(candidate.probability) : candidate.yesPrice;
|
|
1051
|
-
const boundedProbability = Math.max(0, Math.min(1, probability));
|
|
1052
|
-
if (boundedProbability < EXTREME_PROBABILITY_LOW) {
|
|
1053
|
-
return (EXTREME_PROBABILITY_LOW - boundedProbability) / EXTREME_PROBABILITY_LOW * EXTREME_PROBABILITY_MAX_PENALTY;
|
|
1054
|
-
}
|
|
1055
|
-
if (boundedProbability > EXTREME_PROBABILITY_HIGH) {
|
|
1056
|
-
return (boundedProbability - EXTREME_PROBABILITY_HIGH) / (1 - EXTREME_PROBABILITY_HIGH) * EXTREME_PROBABILITY_MAX_PENALTY;
|
|
1057
|
-
}
|
|
1058
|
-
return 0;
|
|
1059
|
-
}
|
|
1060
|
-
function horizonScore(days) {
|
|
1061
|
-
if (days === null) return 2;
|
|
1062
|
-
if (days < 3) return 0;
|
|
1063
|
-
if (days <= HORIZON_PEAK_DAYS) {
|
|
1064
|
-
return Math.log1p(days) / Math.log1p(HORIZON_PEAK_DAYS) * 10;
|
|
1065
|
-
}
|
|
1066
|
-
const yearsPastPeak = (days - HORIZON_PEAK_DAYS) / HORIZON_PEAK_DAYS;
|
|
1067
|
-
return Math.max(HORIZON_LONG_TERM_FLOOR, 10 - yearsPastPeak * HORIZON_LONG_TERM_DECAY_PER_YEAR);
|
|
1068
|
-
}
|
|
1069
|
-
function scoreCandidate(candidate, sourceCount) {
|
|
1070
|
-
const liquidityScore = Math.min(25, Math.log1p(Math.max(0, candidate.liquidity)) / Math.log1p(1e6) * 25);
|
|
1071
|
-
const volumeScore = Math.min(25, Math.log1p(Math.max(0, candidate.volume24h)) / Math.log1p(1e6) * 25);
|
|
1072
|
-
const spreadScore = Math.max(0, Math.min(15, (0.12 - Math.max(0, candidate.spread)) / 0.12 * 15));
|
|
1073
|
-
const movementPenalty = Math.min(20, Math.abs(candidate.oneDayPriceChange) * 100);
|
|
1074
|
-
const probabilityPenalty = extremeProbabilityPenalty(candidate);
|
|
1075
|
-
const days = candidate.daysToEnd ?? null;
|
|
1076
|
-
const horizon = horizonScore(days);
|
|
1077
|
-
const rewardScore = Math.max(
|
|
1078
|
-
0,
|
|
1079
|
-
Math.min(20, num(candidate.components?.rewardYield) * 0.1 + Math.max(0, num(candidate.lpExpectedReturnDailyPct)) * 50)
|
|
1080
|
-
);
|
|
1081
|
-
const sourceScore = Math.min(ENSEMBLE_SOURCE_SCORE_MAX, sourceCount * ENSEMBLE_SOURCE_SCORE_PER_SOURCE);
|
|
1082
|
-
return Math.round(
|
|
1083
|
-
(liquidityScore + volumeScore + spreadScore + horizon + rewardScore + sourceScore - movementPenalty - probabilityPenalty) * 10
|
|
1084
|
-
) / 10;
|
|
1085
|
-
}
|
|
1086
|
-
function diversifyCandidates(candidates, limit) {
|
|
1087
|
-
const eventCounts = /* @__PURE__ */ new Map();
|
|
1088
|
-
const singleSourceCounts = /* @__PURE__ */ new Map();
|
|
1089
|
-
const diversified = [];
|
|
1090
|
-
for (const candidate of candidates) {
|
|
1091
|
-
const eventKey = candidate.eventSlug || candidate.slug;
|
|
1092
|
-
const eventCount = eventCounts.get(eventKey) ?? 0;
|
|
1093
|
-
if (eventCount >= ENSEMBLE_MAX_CANDIDATES_PER_EVENT) continue;
|
|
1094
|
-
const singleSource = candidate.sourceProfiles.length === 1 ? candidate.sourceProfiles[0] : null;
|
|
1095
|
-
if (singleSource !== null) {
|
|
1096
|
-
const sourceCount = singleSourceCounts.get(singleSource) ?? 0;
|
|
1097
|
-
if (sourceCount >= ENSEMBLE_MAX_SINGLE_SOURCE_CANDIDATES) continue;
|
|
1098
|
-
singleSourceCounts.set(singleSource, sourceCount + 1);
|
|
1099
|
-
}
|
|
1100
|
-
eventCounts.set(eventKey, eventCount + 1);
|
|
1101
|
-
diversified.push(candidate);
|
|
1102
|
-
if (diversified.length >= limit) break;
|
|
207
|
+
args = JSON.parse(source);
|
|
208
|
+
} catch {
|
|
209
|
+
throw new Error("Tool arguments must be valid JSON.");
|
|
1103
210
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
function buildFeedEnsemble(sourceResults, limit, outputPath = "candidates.json", generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1107
|
-
const bySlug = /* @__PURE__ */ new Map();
|
|
1108
|
-
let totalRawMarkets = 0;
|
|
1109
|
-
for (const { source, result } of sourceResults) {
|
|
1110
|
-
for (const market of result.markets ?? []) {
|
|
1111
|
-
totalRawMarkets++;
|
|
1112
|
-
const existing = bySlug.get(market.slug);
|
|
1113
|
-
if (!existing) {
|
|
1114
|
-
bySlug.set(market.slug, {
|
|
1115
|
-
...market,
|
|
1116
|
-
ensembleScore: 0,
|
|
1117
|
-
sourceProfiles: [source],
|
|
1118
|
-
sourceRanks: { [source]: market.rank }
|
|
1119
|
-
});
|
|
1120
|
-
continue;
|
|
1121
|
-
}
|
|
1122
|
-
existing.sourceProfiles.push(source);
|
|
1123
|
-
existing.sourceRanks[source] = market.rank;
|
|
1124
|
-
if (market.score > existing.score) existing.score = market.score;
|
|
1125
|
-
existing.volume24h = Math.max(existing.volume24h, market.volume24h);
|
|
1126
|
-
existing.liquidity = Math.max(existing.liquidity, market.liquidity);
|
|
1127
|
-
existing.rewardsDailyRate = Math.max(existing.rewardsDailyRate, market.rewardsDailyRate);
|
|
1128
|
-
existing.lpExpectedReturnDailyPct = Math.max(
|
|
1129
|
-
num(existing.lpExpectedReturnDailyPct),
|
|
1130
|
-
num(market.lpExpectedReturnDailyPct)
|
|
1131
|
-
);
|
|
1132
|
-
existing.lpRiskFlags = [.../* @__PURE__ */ new Set([...existing.lpRiskFlags ?? [], ...market.lpRiskFlags ?? []])];
|
|
1133
|
-
}
|
|
211
|
+
if (args === null || typeof args !== "object" || Array.isArray(args)) {
|
|
212
|
+
throw new Error("Tool arguments must be a JSON object.");
|
|
1134
213
|
}
|
|
1135
|
-
|
|
1136
|
-
...candidate,
|
|
1137
|
-
sourceProfiles: [...new Set(candidate.sourceProfiles)],
|
|
1138
|
-
ensembleScore: scoreCandidate(candidate, new Set(candidate.sourceProfiles).size)
|
|
1139
|
-
})).sort((a, b) => b.ensembleScore - a.ensembleScore || b.score - a.score);
|
|
1140
|
-
const diversifiedCandidates = diversifyCandidates(candidates, limit);
|
|
1141
|
-
return {
|
|
1142
|
-
generatedAt,
|
|
1143
|
-
outputPath,
|
|
1144
|
-
totalSources: sourceResults.length,
|
|
1145
|
-
totalRawMarkets,
|
|
1146
|
-
totalCandidates: bySlug.size,
|
|
1147
|
-
marketsReturned: diversifiedCandidates.length,
|
|
1148
|
-
candidates: diversifiedCandidates
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
function parseLimit(value, fallback) {
|
|
1152
|
-
const parsed = Number.parseInt(value ?? "", 10);
|
|
1153
|
-
if (!Number.isFinite(parsed)) return fallback;
|
|
1154
|
-
return Math.max(1, Math.min(100, parsed));
|
|
214
|
+
return args;
|
|
1155
215
|
}
|
|
1156
|
-
function
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
`
|
|
1162
|
-
|
|
1163
|
-
"--profile <name>",
|
|
1164
|
-
`Screening defaults: ${PROFILE_CHOICES.join(", ")} \u2014 explicit flags override`
|
|
1165
|
-
).option("--sort-by <key>", "score | volume | liquidity | movement | spread | recency | extremity | rewards | rewardYield | lpExpectedReturn | horizon").option("--preset <name>", "Attention weight preset (default, volume-hunter, lp-opportunity, \u2026)").option("--tag <slug>", "Polymarket category tag, e.g. crypto, politics").option("--min-volume <usd>", "Minimum 24h volume (USD)").option("--min-liquidity <usd>", "Minimum displayed liquidity (USD)").option("--max-liquidity <usd>", "Maximum displayed liquidity (USD)").option("--min-rewards-daily-rate <usd>", "Minimum LP rewards USD/day").option("--min-days-to-end <n>", "Min calendar days until resolution").option("--max-days-to-end <n>", "Max calendar days until resolution").option("--max-market-age-hours <n>", 'With liquid-new-or-long: max age (hours) for the "new" branch').option("--liquid-profile <mode>", "new-or-long (used by liquid-new-or-long screen)").option("--limit <n>", "Max markets to return (1\u2013100, default 15)", "15");
|
|
1166
|
-
feed.command("ensemble").description("Run multiple feed screens, merge by slug, and write ranked candidates JSON").option("--limit <n>", "Max candidates to return/write (1-100, default 25)", "25").option("--output <file>", "Output JSON path", "candidates.json").action(async (o) => {
|
|
1167
|
-
const globalOpts = program2.opts();
|
|
1168
|
-
const client = new ApiClient(globalOpts);
|
|
1169
|
-
requireAuth4(client);
|
|
1170
|
-
const perSourceLimit = "100";
|
|
1171
|
-
const sourceResults = [];
|
|
1172
|
-
try {
|
|
1173
|
-
for (const source of ENSEMBLE_SOURCES) {
|
|
1174
|
-
const result = await client.get("/api/feed", {
|
|
1175
|
-
...source.params,
|
|
1176
|
-
limit: perSourceLimit
|
|
1177
|
-
});
|
|
1178
|
-
if (result.error) {
|
|
1179
|
-
throw new Error(result.error);
|
|
1180
|
-
}
|
|
1181
|
-
sourceResults.push({ source: source.name, result });
|
|
1182
|
-
}
|
|
1183
|
-
const outputPath = o.output ?? "candidates.json";
|
|
1184
|
-
const ensemble = buildFeedEnsemble(sourceResults, parseLimit(o.limit, 25), outputPath);
|
|
1185
|
-
await writeFile(outputPath, JSON.stringify(ensemble, null, 2) + "\n", "utf8");
|
|
1186
|
-
if (globalOpts.json) {
|
|
1187
|
-
json(ensemble);
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
heading(`Feed Ensemble \u2014 ${ensemble.marketsReturned} candidates`);
|
|
1191
|
-
table(
|
|
1192
|
-
ensemble.candidates.slice(0, 15).map((m) => [
|
|
1193
|
-
String(Math.round(m.ensembleScore)),
|
|
1194
|
-
truncate(m.question, 48),
|
|
1195
|
-
`${Math.round(m.yesPrice * 100)}%`,
|
|
1196
|
-
compactCurrency(m.volume24h),
|
|
1197
|
-
compactCurrency(m.liquidity),
|
|
1198
|
-
m.sourceProfiles.join(",")
|
|
1199
|
-
]),
|
|
1200
|
-
["Score", "Market", "YES", "24h Vol", "Liq", "Sources"]
|
|
1201
|
-
);
|
|
1202
|
-
success(`Wrote ${outputPath}`);
|
|
1203
|
-
} catch (e) {
|
|
1204
|
-
error(e instanceof Error ? e.message : String(e));
|
|
1205
|
-
process.exit(1);
|
|
1206
|
-
}
|
|
216
|
+
function registerToolCommands(program) {
|
|
217
|
+
program.command("tools [name]").description("List available tools and JSON Schemas, or show one tool").action(async (name) => {
|
|
218
|
+
const catalog = await new ApiClient(program.opts()).listTools();
|
|
219
|
+
if (!name) return json(catalog);
|
|
220
|
+
const tool = catalog.tools.find((tool2) => tool2.name === name);
|
|
221
|
+
if (!tool) throw new Error(`Unknown tool: ${name}. Run hl tools to list available tools.`);
|
|
222
|
+
json(tool);
|
|
1207
223
|
});
|
|
1208
|
-
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
} catch (e) {
|
|
1214
|
-
error(e instanceof Error ? e.message : String(e));
|
|
1215
|
-
process.exit(1);
|
|
1216
|
-
}
|
|
1217
|
-
const client = new ApiClient(globalOpts);
|
|
1218
|
-
requireAuth4(client);
|
|
1219
|
-
const params = feedQueryParams({
|
|
1220
|
-
profile,
|
|
1221
|
-
sortBy: o.sortBy,
|
|
1222
|
-
preset: o.preset,
|
|
1223
|
-
tag: o.tag,
|
|
1224
|
-
minVolume: o.minVolume,
|
|
1225
|
-
minLiquidity: o.minLiquidity,
|
|
1226
|
-
maxLiquidity: o.maxLiquidity,
|
|
1227
|
-
minRewardsDailyRate: o.minRewardsDailyRate,
|
|
1228
|
-
minDaysToEnd: o.minDaysToEnd,
|
|
1229
|
-
maxDaysToEnd: o.maxDaysToEnd,
|
|
1230
|
-
maxMarketAgeHours: o.maxMarketAgeHours,
|
|
1231
|
-
liquidProfile: o.liquidProfile,
|
|
1232
|
-
limit: o.limit
|
|
1233
|
-
});
|
|
1234
|
-
try {
|
|
1235
|
-
const result = await client.get("/api/feed", params);
|
|
1236
|
-
if (result.error) {
|
|
1237
|
-
error(result.error);
|
|
1238
|
-
process.exit(1);
|
|
1239
|
-
}
|
|
1240
|
-
displayFeedResult(result, globalOpts);
|
|
1241
|
-
} catch (e) {
|
|
1242
|
-
error(e instanceof Error ? e.message : String(e));
|
|
1243
|
-
process.exit(1);
|
|
1244
|
-
}
|
|
224
|
+
program.command("call <name>").description("Call a tool through the HTTP API; arguments default to {}").option("--args <json>", "Tool arguments as a JSON object").option("--file <path>", "Read tool arguments from a JSON file").option("--stdin", "Read tool arguments from standard input").action(async (name, options) => {
|
|
225
|
+
const args = await readArguments(options);
|
|
226
|
+
const result = await new ApiClient(program.opts()).callTool(name, args);
|
|
227
|
+
json(result);
|
|
228
|
+
if (result.isError) process.exitCode = 1;
|
|
1245
229
|
});
|
|
1246
230
|
}
|
|
1247
231
|
|
|
1248
|
-
// src/
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
function pctFromSignalValue(value) {
|
|
1255
|
-
if (value === null || value === void 0 || Number.isNaN(value)) return null;
|
|
1256
|
-
return Math.abs(value) <= 1 ? value * 100 : value;
|
|
1257
|
-
}
|
|
1258
|
-
function formatProbability(value) {
|
|
1259
|
-
const pct = pctFromSignalValue(value);
|
|
1260
|
-
return pct === null ? "n/a" : `${pct.toFixed(1)}%`;
|
|
1261
|
-
}
|
|
1262
|
-
function formatGap(value) {
|
|
1263
|
-
if (value === null || value === void 0 || Number.isNaN(value)) return "n/a";
|
|
1264
|
-
const points = Math.abs(value) <= 1 ? value * 100 : value;
|
|
1265
|
-
const formatted = `${points >= 0 ? "+" : ""}${points.toFixed(1)}pp`;
|
|
1266
|
-
if (points > 0) return chalk5.green(formatted);
|
|
1267
|
-
if (points < 0) return chalk5.red(formatted);
|
|
1268
|
-
return chalk5.dim(formatted);
|
|
1269
|
-
}
|
|
1270
|
-
function formatStrength(value) {
|
|
1271
|
-
if (value === "strong") return chalk5.green("strong");
|
|
1272
|
-
if (value === "weak") return chalk5.yellow("weak");
|
|
1273
|
-
return value ? chalk5.dim(value) : "n/a";
|
|
1274
|
-
}
|
|
1275
|
-
function analysisItems(result) {
|
|
1276
|
-
if (!result) return [];
|
|
1277
|
-
if (result.analysis) return [result];
|
|
1278
|
-
return result.analyses ?? [];
|
|
1279
|
-
}
|
|
1280
|
-
function titleFor(analysis) {
|
|
1281
|
-
return analysis.market_name || analysis.market_slug || "market";
|
|
1282
|
-
}
|
|
1283
|
-
function displaySignalAnalysis(response, globalOpts) {
|
|
1284
|
-
if (globalOpts.json) {
|
|
1285
|
-
json(response);
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
if (response.error) {
|
|
1289
|
-
error(response.error);
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
const result = response.result;
|
|
1293
|
-
if (result?.error) {
|
|
1294
|
-
error(result.error);
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
const items = analysisItems(result);
|
|
1298
|
-
if (items.length === 0) {
|
|
1299
|
-
warn("No signal analysis returned.");
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
heading(
|
|
1303
|
-
items.length === 1 ? "Signal Analysis" : `Signal Analysis \u2014 ${items.length} markets`
|
|
1304
|
-
);
|
|
1305
|
-
const rows = items.map((item) => {
|
|
1306
|
-
const analysis = item.analysis ?? {};
|
|
1307
|
-
return [
|
|
1308
|
-
truncate(titleFor(analysis), 44),
|
|
1309
|
-
formatProbability(analysis.current_yes_prob),
|
|
1310
|
-
formatProbability(analysis.predicted_prob),
|
|
1311
|
-
formatGap(analysis.probability_gap),
|
|
1312
|
-
formatStrength(analysis.signal_strength),
|
|
1313
|
-
analysis.confidence ?? "n/a"
|
|
1314
|
-
];
|
|
1315
|
-
});
|
|
1316
|
-
table(rows, ["Market", "Market YES", "Agent YES", "Gap", "Signal", "Conf"]);
|
|
1317
|
-
for (const item of items.slice(0, 3)) {
|
|
1318
|
-
const analysis = item.analysis;
|
|
1319
|
-
if (!analysis) continue;
|
|
1320
|
-
process.stdout.write("\n " + chalk5.bold(truncate(titleFor(analysis), 76)) + "\n");
|
|
1321
|
-
if (analysis.market_link) {
|
|
1322
|
-
process.stdout.write(" " + chalk5.dim("Polymarket: ") + analysis.market_link + "\n");
|
|
1323
|
-
}
|
|
1324
|
-
if (analysis.key_factors && analysis.key_factors.length > 0) {
|
|
1325
|
-
process.stdout.write(" " + chalk5.dim("Key factors: ") + analysis.key_factors.slice(0, 4).join("; ") + "\n");
|
|
1326
|
-
}
|
|
1327
|
-
if (analysis.research_findings) {
|
|
1328
|
-
process.stdout.write(
|
|
1329
|
-
" " + chalk5.dim("Research: ") + truncate(analysis.research_findings, 180) + "\n"
|
|
1330
|
-
);
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
if (items.length > 3) {
|
|
1334
|
-
process.stdout.write(chalk5.dim(`
|
|
1335
|
-
... and ${items.length - 3} more
|
|
1336
|
-
`));
|
|
1337
|
-
}
|
|
1338
|
-
if (result?.strong_signal_count !== void 0) {
|
|
1339
|
-
process.stdout.write(
|
|
1340
|
-
chalk5.dim(`
|
|
1341
|
-
Strong signals: ${result.strong_signal_count}
|
|
1342
|
-
`)
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
// src/commands/signal.ts
|
|
1348
|
-
function requireAuth5(client) {
|
|
1349
|
-
if (!client.isAuthenticated) {
|
|
1350
|
-
error("Not authenticated. Run `hl auth login` first.");
|
|
1351
|
-
process.exit(1);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
function collect(value, previous = []) {
|
|
1355
|
-
return [...previous, value];
|
|
1356
|
-
}
|
|
1357
|
-
function parseProbability(value) {
|
|
1358
|
-
const n = Number(value);
|
|
1359
|
-
if (!Number.isFinite(n) || n < 0 || n > 100) {
|
|
1360
|
-
throw new InvalidArgumentError("Expected a probability between 0 and 100");
|
|
1361
|
-
}
|
|
1362
|
-
return n;
|
|
1363
|
-
}
|
|
1364
|
-
async function readStdin() {
|
|
1365
|
-
const chunks = [];
|
|
1366
|
-
for await (const chunk of process.stdin) {
|
|
1367
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1368
|
-
}
|
|
1369
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
1370
|
-
}
|
|
1371
|
-
async function readMarketPayload(path) {
|
|
1372
|
-
if (!path) return {};
|
|
1373
|
-
const raw = path === "-" ? await readStdin() : await readFile(path, "utf8");
|
|
1374
|
-
const parsed = JSON.parse(raw);
|
|
1375
|
-
if (Array.isArray(parsed)) {
|
|
1376
|
-
return { markets: parsed };
|
|
1377
|
-
}
|
|
1378
|
-
if (parsed && typeof parsed === "object" && Array.isArray(parsed.markets)) {
|
|
1379
|
-
return { markets: parsed.markets };
|
|
1380
|
-
}
|
|
1381
|
-
if (parsed && typeof parsed === "object" && parsed.market) {
|
|
1382
|
-
return { market: parsed.market };
|
|
1383
|
-
}
|
|
1384
|
-
if (parsed && typeof parsed === "object") {
|
|
1385
|
-
return { market: parsed };
|
|
1386
|
-
}
|
|
1387
|
-
throw new Error("Market JSON must be an object, an array, or { market | markets }");
|
|
1388
|
-
}
|
|
1389
|
-
function inlineMarketFromOptions(opts) {
|
|
1390
|
-
const market = {};
|
|
1391
|
-
if (opts.question) market.question = opts.question;
|
|
1392
|
-
if (opts.description) market.description = opts.description;
|
|
1393
|
-
if (opts.yesProb !== void 0) market.yesPrice = opts.yesProb;
|
|
1394
|
-
if (opts.noProb !== void 0) market.noPrice = opts.noProb;
|
|
1395
|
-
if (opts.slug) market.slug = opts.slug;
|
|
1396
|
-
if (opts.link) market.link = opts.link;
|
|
1397
|
-
return Object.keys(market).length > 0 ? market : void 0;
|
|
1398
|
-
}
|
|
1399
|
-
async function buildSignalPayload(positionalUrl, opts) {
|
|
1400
|
-
const urls = [positionalUrl, ...opts.url ?? []].filter(
|
|
1401
|
-
(value) => Boolean(value)
|
|
1402
|
-
);
|
|
1403
|
-
const filePayload = await readMarketPayload(opts.market);
|
|
1404
|
-
const inlineMarket = inlineMarketFromOptions(opts);
|
|
1405
|
-
const hasMarketInput = Boolean(filePayload.market || filePayload.markets || inlineMarket);
|
|
1406
|
-
if (urls.length > 0 && hasMarketInput) {
|
|
1407
|
-
throw new Error("Use either URL input or market JSON/options, not both.");
|
|
1408
|
-
}
|
|
1409
|
-
if (filePayload.market && inlineMarket) {
|
|
1410
|
-
throw new Error("Use either --market or inline market options, not both.");
|
|
1411
|
-
}
|
|
1412
|
-
if (filePayload.markets && inlineMarket) {
|
|
1413
|
-
throw new Error("Use either --market or inline market options, not both.");
|
|
1414
|
-
}
|
|
1415
|
-
if (urls.length === 0 && !hasMarketInput) {
|
|
1416
|
-
throw new Error("Provide a Polymarket URL or a market payload.");
|
|
1417
|
-
}
|
|
1418
|
-
const payload = urls.length === 1 ? { url: urls[0] } : urls.length > 1 ? { urls } : filePayload.market ? { market: filePayload.market } : filePayload.markets ? { markets: filePayload.markets } : { market: inlineMarket };
|
|
1419
|
-
if (opts.context) {
|
|
1420
|
-
payload.previous_analysis_context = opts.context;
|
|
1421
|
-
}
|
|
1422
|
-
return payload;
|
|
1423
|
-
}
|
|
1424
|
-
async function runSignalAnalysis(client, payload) {
|
|
1425
|
-
return client.post("/api/signal/analyze", payload);
|
|
1426
|
-
}
|
|
1427
|
-
function registerSignalCommands(program2) {
|
|
1428
|
-
const signal = program2.command("signal").description("Analyze Polymarket probability gaps with the signal agent");
|
|
1429
|
-
signal.command("analyze").description("Estimate true YES probability and compare it with market pricing").argument("[url]", "Polymarket market/event URL to analyze").option("--url <url>", "Additional Polymarket URL; repeat for multiple markets", collect, []).option("--market <file>", "Inline market JSON object/array; use '-' to read stdin").option("--context <text>", "Prior search notes or analysis context for the agent").option("--question <text>", "Inline market question when not using a URL").option("--description <text>", "Inline market description or resolution criteria").option("--yes-prob <prob>", "Current YES probability or price, e.g. 0.52 or 52", parseProbability).option("--no-prob <prob>", "Current NO probability or price, e.g. 0.48 or 48", parseProbability).option("--slug <slug>", "Inline market slug").option("--link <url>", "Inline market link").action(async (url, o) => {
|
|
1430
|
-
const globalOpts = program2.opts();
|
|
1431
|
-
const client = new ApiClient(globalOpts);
|
|
1432
|
-
requireAuth5(client);
|
|
1433
|
-
let payload;
|
|
1434
|
-
try {
|
|
1435
|
-
payload = await buildSignalPayload(url, o);
|
|
1436
|
-
} catch (e) {
|
|
1437
|
-
error(e instanceof Error ? e.message : String(e));
|
|
1438
|
-
process.exit(1);
|
|
1439
|
-
}
|
|
1440
|
-
if (!globalOpts.json) {
|
|
1441
|
-
process.stderr.write(dim(" Running signal analysis...\n"));
|
|
1442
|
-
}
|
|
1443
|
-
const result = await runSignalAnalysis(client, payload);
|
|
1444
|
-
displaySignalAnalysis(result, globalOpts);
|
|
1445
|
-
});
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
// src/commands/quote.ts
|
|
1449
|
-
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
1450
|
-
function requireAuth6(client) {
|
|
1451
|
-
if (!client.isAuthenticated) {
|
|
1452
|
-
error("Not authenticated. Run `hl auth login` first.");
|
|
1453
|
-
process.exit(1);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
function parsePositiveNumber(value) {
|
|
1457
|
-
const parsed = Number(value);
|
|
1458
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1459
|
-
throw new InvalidArgumentError2("Expected a positive number");
|
|
1460
|
-
}
|
|
1461
|
-
return parsed;
|
|
1462
|
-
}
|
|
1463
|
-
function parseQuoteAction(value) {
|
|
1464
|
-
const normalized = value.trim().toUpperCase();
|
|
1465
|
-
if (normalized !== "BUY" && normalized !== "SELL") {
|
|
1466
|
-
throw new InvalidArgumentError2("Expected buy or sell");
|
|
1467
|
-
}
|
|
1468
|
-
return normalized;
|
|
1469
|
-
}
|
|
1470
|
-
function parseQuoteOutcome(value) {
|
|
1471
|
-
const normalized = value.trim().toUpperCase();
|
|
1472
|
-
if (normalized !== "YES" && normalized !== "NO") {
|
|
1473
|
-
throw new InvalidArgumentError2("Expected yes or no");
|
|
1474
|
-
}
|
|
1475
|
-
return normalized;
|
|
1476
|
-
}
|
|
1477
|
-
function parseQuoteRoute(value) {
|
|
1478
|
-
const normalized = value.trim().toLowerCase();
|
|
1479
|
-
if (normalized !== "auto" && normalized !== "aggressive" && normalized !== "passive") {
|
|
1480
|
-
throw new InvalidArgumentError2("Expected auto, aggressive, or passive");
|
|
1481
|
-
}
|
|
1482
|
-
return normalized;
|
|
1483
|
-
}
|
|
1484
|
-
function buildQuotePayload(instrument, opts) {
|
|
1485
|
-
const normalizedInstrument = instrument.trim();
|
|
1486
|
-
if (!normalizedInstrument) {
|
|
1487
|
-
throw new Error("Provide a Polymarket slug or URL.");
|
|
1488
|
-
}
|
|
1489
|
-
const hasCash = opts.cash !== void 0;
|
|
1490
|
-
const hasShares = opts.shares !== void 0;
|
|
1491
|
-
if (hasCash === hasShares) {
|
|
1492
|
-
throw new Error("Provide exactly one of --cash or --shares.");
|
|
1493
|
-
}
|
|
1494
|
-
if (opts.action === "SELL" && hasCash) {
|
|
1495
|
-
throw new Error("SELL quotes require --shares; --cash is BUY-only.");
|
|
1496
|
-
}
|
|
1497
|
-
return {
|
|
1498
|
-
instrument: normalizedInstrument,
|
|
1499
|
-
action: opts.action,
|
|
1500
|
-
outcome: opts.outcome,
|
|
1501
|
-
size: hasCash ? { type: "cash", amount_usd: opts.cash } : { type: "shares", shares: opts.shares },
|
|
1502
|
-
...opts.signalId && { signal_forecast_id: opts.signalId },
|
|
1503
|
-
...opts.capital !== void 0 && { portfolio_capital_usd: opts.capital },
|
|
1504
|
-
route: opts.route,
|
|
1505
|
-
persist: Boolean(opts.save)
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
function formatNumber(value, digits = 2) {
|
|
1509
|
-
return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US", { maximumFractionDigits: digits }) : "n/a";
|
|
1510
|
-
}
|
|
1511
|
-
function formatUsd(value) {
|
|
1512
|
-
return typeof value === "number" && Number.isFinite(value) ? currency(value) : "n/a";
|
|
1513
|
-
}
|
|
1514
|
-
function formatPrice(value) {
|
|
1515
|
-
return typeof value === "number" && Number.isFinite(value) ? `$${value.toFixed(4)}` : "n/a";
|
|
1516
|
-
}
|
|
1517
|
-
function formatPercent(value) {
|
|
1518
|
-
return typeof value === "number" && Number.isFinite(value) ? percent(value) : "n/a";
|
|
1519
|
-
}
|
|
1520
|
-
function displayQuotePreview(preview, globalOpts) {
|
|
1521
|
-
if (globalOpts.json) {
|
|
1522
|
-
json(preview);
|
|
1523
|
-
return;
|
|
1524
|
-
}
|
|
1525
|
-
if (preview.error) {
|
|
1526
|
-
error(preview.error);
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
const request = preview.request;
|
|
1530
|
-
const instrument = preview.instrument;
|
|
1531
|
-
const market = preview.market ?? {};
|
|
1532
|
-
const fill = preview.fill ?? {};
|
|
1533
|
-
const economics = preview.economics ?? {};
|
|
1534
|
-
const signal = preview.signal ?? {};
|
|
1535
|
-
const sizing = preview.sizing_suggestion ?? {};
|
|
1536
|
-
heading(`Quote Preview \u2014 ${preview.status ?? "UNAVAILABLE"}`);
|
|
1537
|
-
table([
|
|
1538
|
-
["Market", instrument.question ?? instrument.slug ?? "n/a"],
|
|
1539
|
-
["Venue", preview.venue ?? "polymarket"],
|
|
1540
|
-
["Action", `${request.action ?? "n/a"} ${request.outcome ?? "n/a"}`],
|
|
1541
|
-
["Route", `${request.route_selected ?? "n/a"} (requested ${request.route_requested ?? "auto"})`],
|
|
1542
|
-
["Observed", preview.observed_at ? new Date(preview.observed_at).toLocaleString() : "n/a"],
|
|
1543
|
-
["Expires", preview.expires_at ? new Date(preview.expires_at).toLocaleString() : "n/a"]
|
|
1544
|
-
]);
|
|
1545
|
-
process.stdout.write("\n");
|
|
1546
|
-
table([
|
|
1547
|
-
["Best bid", formatPrice(market.best_bid)],
|
|
1548
|
-
["Best ask", formatPrice(market.best_ask)],
|
|
1549
|
-
["Spread", formatPrice(market.spread)],
|
|
1550
|
-
["Bid depth", `${formatNumber(market.bid_depth_shares, 4)} shares`],
|
|
1551
|
-
["Ask depth", `${formatNumber(market.ask_depth_shares, 4)} shares`],
|
|
1552
|
-
["Requested cash", formatUsd(fill.requested_cash_usd)],
|
|
1553
|
-
["Requested shares", formatNumber(fill.requested_shares, 4)],
|
|
1554
|
-
["Fillable shares", formatNumber(fill.fillable_shares, 4)],
|
|
1555
|
-
["Safety-capped shares", formatNumber(fill.safety_capped_shares, 4)],
|
|
1556
|
-
["Fill ratio", formatPercent(fill.fill_ratio)],
|
|
1557
|
-
["Average price", formatPrice(fill.average_price)],
|
|
1558
|
-
["Worst price", formatPrice(fill.worst_price)],
|
|
1559
|
-
["Passive limit", formatPrice(fill.passive_limit_price)],
|
|
1560
|
-
["Slippage", typeof fill.slippage_bps === "number" ? `${formatNumber(fill.slippage_bps)} bps` : "n/a"]
|
|
1561
|
-
], ["Quote", "Value"]);
|
|
1562
|
-
process.stdout.write("\n");
|
|
1563
|
-
table([
|
|
1564
|
-
["Gross notional", formatUsd(economics.gross_notional_usd)],
|
|
1565
|
-
["Venue fee", formatUsd(economics.venue_fee_usd)],
|
|
1566
|
-
["Fee source", economics.fee_source ?? "unavailable"],
|
|
1567
|
-
[request.action === "SELL" ? "Net proceeds" : "All-in cost", formatUsd(
|
|
1568
|
-
request.action === "SELL" ? economics.net_proceeds_usd : economics.all_in_cost_usd
|
|
1569
|
-
)],
|
|
1570
|
-
["Max loss", formatUsd(economics.max_loss_usd)],
|
|
1571
|
-
["Max payout", formatUsd(economics.max_payout_usd)],
|
|
1572
|
-
["Profit at payout", formatUsd(economics.max_profit_usd)],
|
|
1573
|
-
["Foregone payout", request.action === "SELL" ? formatUsd(economics.foregone_payout_usd) : "n/a"],
|
|
1574
|
-
["Break-even probability", formatPercent(economics.break_even_probability)]
|
|
1575
|
-
], ["Economics", "Value"]);
|
|
1576
|
-
if (preview.signal) {
|
|
1577
|
-
process.stdout.write("\n");
|
|
1578
|
-
table([
|
|
1579
|
-
["Forecast YES", formatPercent(signal.forecast_yes)],
|
|
1580
|
-
["Forecast interval", `${formatPercent(signal.lower_bound)} \u2013 ${formatPercent(signal.upper_bound)}`],
|
|
1581
|
-
["Midpoint edge", formatPercent(signal.midpoint_edge)],
|
|
1582
|
-
["Conservative edge", formatPercent(signal.conservative_edge)]
|
|
1583
|
-
], ["Signal", "Value"]);
|
|
1584
|
-
}
|
|
1585
|
-
if (preview.sizing_suggestion) {
|
|
1586
|
-
process.stdout.write("\n");
|
|
1587
|
-
table([
|
|
1588
|
-
["Suggested cash", formatUsd(sizing.suggested_max_spend_usd)],
|
|
1589
|
-
["Suggested shares", formatNumber(sizing.suggested_shares, 4)],
|
|
1590
|
-
["Capital fraction", formatPercent(sizing.allocation_fraction)]
|
|
1591
|
-
], ["Non-binding sizing", "Value"]);
|
|
1592
|
-
}
|
|
1593
|
-
if (preview.risks?.length) {
|
|
1594
|
-
process.stdout.write("\n");
|
|
1595
|
-
for (const risk of preview.risks) warn(risk);
|
|
1596
|
-
}
|
|
1597
|
-
if (preview.id) {
|
|
1598
|
-
process.stdout.write("\n" + dim(` Saved preview: ${preview.id}
|
|
1599
|
-
`));
|
|
1600
|
-
}
|
|
1601
|
-
process.stdout.write("\n");
|
|
1602
|
-
warn("Preview only \u2014 no order was signed or submitted.");
|
|
1603
|
-
const marketUrl = instrument.market_url;
|
|
1604
|
-
if (marketUrl) process.stdout.write(dim(` Market: ${marketUrl}
|
|
1605
|
-
`));
|
|
1606
|
-
}
|
|
1607
|
-
function registerQuoteCommand(program2) {
|
|
1608
|
-
program2.command("quote").description("Preview the cost, liquidity, and risk of a Polymarket trade").argument("<slug-or-url>", "Polymarket market slug or URL").requiredOption("--action <action>", "buy | sell", parseQuoteAction).requiredOption("--outcome <outcome>", "yes | no", parseQuoteOutcome).option("--cash <usd>", "Maximum cash to spend (BUY only)", parsePositiveNumber).option("--shares <shares>", "Number of outcome shares", parsePositiveNumber).option("--signal-id <uuid>", "Saved Signal forecast to include in edge calculations").option("--capital <usd>", "Manual portfolio capital for non-binding BUY sizing", parsePositiveNumber).option("--route <route>", "auto | aggressive | passive", parseQuoteRoute, "auto").option("--save", "Save a freshly generated preview to quote history").action(async (instrument, opts) => {
|
|
1609
|
-
const globalOpts = program2.opts();
|
|
1610
|
-
const client = new ApiClient(globalOpts);
|
|
1611
|
-
requireAuth6(client);
|
|
1612
|
-
let payload;
|
|
1613
|
-
try {
|
|
1614
|
-
payload = buildQuotePayload(instrument, opts);
|
|
1615
|
-
} catch (error2) {
|
|
1616
|
-
error(error2 instanceof Error ? error2.message : String(error2));
|
|
1617
|
-
process.exit(1);
|
|
1618
|
-
}
|
|
1619
|
-
if (!globalOpts.json) {
|
|
1620
|
-
process.stderr.write(dim(" Refreshing public market data and order book...\n"));
|
|
1621
|
-
}
|
|
1622
|
-
const preview = await client.post("/api/quote", payload);
|
|
1623
|
-
displayQuotePreview(preview, globalOpts);
|
|
1624
|
-
});
|
|
232
|
+
// src/program.ts
|
|
233
|
+
function createProgram(version) {
|
|
234
|
+
const program = new Command().name("hl").description("Hedge Layer \u2014 unified financial data and execution over HTTP. Results are JSON.").version(version).option("--api-url <url>", "API origin (or HL_API_URL)").option("--token <token>", "API token (or HL_TOKEN)").option("--verbose", "Log HTTP method, URL, and status to stderr");
|
|
235
|
+
registerAuthCommands(program);
|
|
236
|
+
registerToolCommands(program);
|
|
237
|
+
return program;
|
|
1625
238
|
}
|
|
1626
239
|
|
|
1627
240
|
// src/index.ts
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
registerResearchCommands(program);
|
|
1634
|
-
registerFeedCommand(program);
|
|
1635
|
-
registerSignalCommands(program);
|
|
1636
|
-
registerQuoteCommand(program);
|
|
1637
|
-
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
1638
|
-
const opts = program.opts();
|
|
1639
|
-
if (opts.color === false) {
|
|
1640
|
-
process.env.FORCE_COLOR = "0";
|
|
1641
|
-
}
|
|
1642
|
-
if (opts.verbose) {
|
|
1643
|
-
const cmdName = actionCommand.name();
|
|
1644
|
-
process.stderr.write(dim(`[verbose] Running: hl ${cmdName}
|
|
1645
|
-
`));
|
|
1646
|
-
}
|
|
1647
|
-
});
|
|
1648
|
-
async function main() {
|
|
1649
|
-
try {
|
|
1650
|
-
await program.parseAsync(process.argv);
|
|
1651
|
-
} catch (e) {
|
|
1652
|
-
if (e instanceof Error && e.message.includes("API error")) {
|
|
1653
|
-
error(e.message);
|
|
1654
|
-
} else {
|
|
1655
|
-
error(`Unexpected error: ${e instanceof Error ? e.message : String(e)}`);
|
|
1656
|
-
if (program.opts().verbose) {
|
|
1657
|
-
console.error(e);
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
process.exit(1);
|
|
1661
|
-
}
|
|
241
|
+
try {
|
|
242
|
+
await createProgram("5.0.0").parseAsync(process.argv);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
process.stderr.write(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) + "\n");
|
|
245
|
+
process.exitCode = 1;
|
|
1662
246
|
}
|
|
1663
|
-
main();
|
|
1664
247
|
//# sourceMappingURL=index.mjs.map
|