mt-signals 1.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/bin/mt-signals.js +292 -0
- package/lib/client.js +69 -0
- package/lib/format.js +50 -0
- package/package.json +23 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { request, loadConfig, saveConfig, getBaseUrl, getApiKey } = require("../lib/client");
|
|
5
|
+
const { fmt, catColor, scoreColor, bar, sparkline } = require("../lib/format");
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const flags = {};
|
|
9
|
+
const positional = [];
|
|
10
|
+
for (const a of args) {
|
|
11
|
+
if (a.startsWith("--")) {
|
|
12
|
+
const [k, v] = a.slice(2).split("=");
|
|
13
|
+
flags[k] = v === undefined ? true : v;
|
|
14
|
+
} else {
|
|
15
|
+
positional.push(a);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const cmd = positional[0];
|
|
19
|
+
const json = flags.json;
|
|
20
|
+
|
|
21
|
+
function out(data) {
|
|
22
|
+
if (json) return console.log(JSON.stringify(data, null, 2));
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function die(msg) {
|
|
27
|
+
console.error(fmt.red(`Error: ${msg}`));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
try {
|
|
33
|
+
switch (cmd) {
|
|
34
|
+
case "list":
|
|
35
|
+
case "signals":
|
|
36
|
+
case undefined: {
|
|
37
|
+
if (cmd === undefined && positional.length === 0 && !flags.category && !flags.sort && !flags.min && !flags.limit) {
|
|
38
|
+
// No args at all — show help
|
|
39
|
+
showHelp();
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
const qs = [];
|
|
43
|
+
if (flags.category) qs.push(`category=${encodeURIComponent(flags.category)}`);
|
|
44
|
+
if (flags.min) qs.push(`min_score=${flags.min}`);
|
|
45
|
+
if (flags.limit) qs.push(`limit=${flags.limit}`);
|
|
46
|
+
if (flags.sort) qs.push(`sort=${flags.sort}`);
|
|
47
|
+
const query = qs.length > 0 ? `?${qs.join("&")}` : "";
|
|
48
|
+
const data = await request("GET", `/signals${query}`);
|
|
49
|
+
if (json) return out(data);
|
|
50
|
+
|
|
51
|
+
const sigs = data.signals || [];
|
|
52
|
+
if (sigs.length === 0) {
|
|
53
|
+
console.log(fmt.dim("\n No signals found.\n"));
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(fmt.bold(`\n Research Signals (${data.total} total)\n`));
|
|
58
|
+
for (const s of sigs) {
|
|
59
|
+
const sc = scoreColor(s.score)(String((s.score || 0).toFixed(1)).padStart(4));
|
|
60
|
+
const cat = catColor(s.category).padEnd(22);
|
|
61
|
+
const topic = fmt.bold((s.topic || "").padEnd(35).slice(0, 35));
|
|
62
|
+
const src = fmt.dim((s.source || "").padEnd(8).slice(0, 8));
|
|
63
|
+
console.log(` ${sc} ${cat} ${topic} ${src} U:${bar(s.urgency, 10, 5)} B:${bar(s.buyer_intent, 10, 5)} D:${bar(s.durability, 10, 5)}`);
|
|
64
|
+
}
|
|
65
|
+
console.log(fmt.dim(`\n Last recalculated: ${data.last_recalculated || "never"}\n`));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case "top": {
|
|
70
|
+
const limit = positional[1] || flags.limit || "10";
|
|
71
|
+
const data = await request("GET", `/signals?limit=${limit}`);
|
|
72
|
+
if (json) return out(data);
|
|
73
|
+
|
|
74
|
+
const sigs = data.signals || [];
|
|
75
|
+
console.log(fmt.bold(`\n Top ${sigs.length} Signals\n`));
|
|
76
|
+
for (let i = 0; i < sigs.length; i++) {
|
|
77
|
+
const s = sigs[i];
|
|
78
|
+
const rank = fmt.dim(`#${String(i + 1).padStart(2)}`);
|
|
79
|
+
const sc = scoreColor(s.score)(String((s.score || 0).toFixed(1)));
|
|
80
|
+
const cat = catColor(s.category);
|
|
81
|
+
console.log(` ${rank} ${sc} ${fmt.bold(s.topic || "")} ${cat}`);
|
|
82
|
+
if (s.summary) console.log(` ${fmt.dim(s.summary.slice(0, 90))}`);
|
|
83
|
+
}
|
|
84
|
+
console.log();
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "trending":
|
|
89
|
+
case "hot": {
|
|
90
|
+
const data = await request("GET", "/signals/trending");
|
|
91
|
+
if (json) return out(data);
|
|
92
|
+
|
|
93
|
+
console.log(fmt.bold(`\n Trending Signals\n`));
|
|
94
|
+
|
|
95
|
+
const breakouts = data.breakouts || [];
|
|
96
|
+
if (breakouts.length > 0) {
|
|
97
|
+
console.log(fmt.yellow(` Breakouts (${breakouts.length})\n`));
|
|
98
|
+
for (const b of breakouts) {
|
|
99
|
+
console.log(` ${fmt.yellow("\u26a1")} ${fmt.bold(b.topic)} ${fmt.yellow(b.multiplier + "x")} ${fmt.dim(b.previous_avg + " \u2192 " + b.current)} ${catColor(b.category)}`);
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
} else {
|
|
103
|
+
console.log(fmt.dim(" No breakout signals.\n"));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const hot = data.hot_signals || [];
|
|
107
|
+
if (hot.length > 0) {
|
|
108
|
+
console.log(fmt.green(` Hot Signals (score 6+)\n`));
|
|
109
|
+
for (const s of hot) {
|
|
110
|
+
const sc = scoreColor(s.score)(String((s.score || 0).toFixed(1)));
|
|
111
|
+
console.log(` ${sc} ${fmt.bold(s.topic || "")} ${catColor(s.category)}`);
|
|
112
|
+
if (s.summary) console.log(` ${fmt.dim(s.summary.slice(0, 90))}`);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
console.log(fmt.dim(" No hot signals.\n"));
|
|
116
|
+
}
|
|
117
|
+
console.log();
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "lookup":
|
|
122
|
+
case "topic": {
|
|
123
|
+
const topic = positional.slice(1).join(" ") || flags.topic;
|
|
124
|
+
if (!topic) die("Usage: mt-signals lookup <topic>");
|
|
125
|
+
const encoded = encodeURIComponent(topic.replace(/\s+/g, "-"));
|
|
126
|
+
const data = await request("GET", `/signals/${encoded}`);
|
|
127
|
+
if (json) return out(data);
|
|
128
|
+
|
|
129
|
+
const s = data.signal;
|
|
130
|
+
if (!s) { console.log(fmt.dim("\n Signal not found.\n")); break; }
|
|
131
|
+
|
|
132
|
+
console.log(fmt.bold(`\n ${s.topic}\n`));
|
|
133
|
+
console.log(` ${fmt.cyan("Score:")} ${scoreColor(s.score)(String((s.score || 0).toFixed(1)))}`);
|
|
134
|
+
console.log(` ${fmt.cyan("Category:")} ${catColor(s.category)}`);
|
|
135
|
+
console.log(` ${fmt.cyan("Source:")} ${s.source || "—"} (${s.source_count || 1} sources)`);
|
|
136
|
+
console.log(` ${fmt.cyan("Urgency:")} ${bar(s.urgency, 10, 15)} ${(s.urgency || 0).toFixed(1)}`);
|
|
137
|
+
console.log(` ${fmt.cyan("Buyer Intent:")} ${bar(s.buyer_intent, 10, 15)} ${(s.buyer_intent || 0).toFixed(1)}`);
|
|
138
|
+
console.log(` ${fmt.cyan("Durability:")} ${bar(s.durability, 10, 15)} ${(s.durability || 0).toFixed(1)}`);
|
|
139
|
+
|
|
140
|
+
if (s.summary) console.log(`\n ${fmt.dim(s.summary)}`);
|
|
141
|
+
|
|
142
|
+
if (s.actionable_for && s.actionable_for.length > 0) {
|
|
143
|
+
console.log(`\n ${fmt.cyan("Actionable for:")} ${s.actionable_for.join(", ")}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (s.evidence && s.evidence.length > 0) {
|
|
147
|
+
console.log(fmt.bold("\n Evidence\n"));
|
|
148
|
+
for (const e of s.evidence.slice(0, 10)) {
|
|
149
|
+
console.log(` ${fmt.dim("\u2022")} ${e}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const hist = data.history || [];
|
|
154
|
+
if (hist.length > 2) {
|
|
155
|
+
const scores = hist.map(h => h.score);
|
|
156
|
+
console.log(`\n ${fmt.cyan("Trend:")} ${sparkline(scores)} ${fmt.dim("(" + hist.length + " data points)")}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(fmt.dim(`\n Updated: ${s.updated || s.created || "—"}\n`));
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case "category":
|
|
164
|
+
case "cat": {
|
|
165
|
+
const cat = positional[1] || flags.category;
|
|
166
|
+
if (!cat) die("Usage: mt-signals category <name>\n Categories: ai, infrastructure, security, devtools, data, business, career, market");
|
|
167
|
+
const data = await request("GET", `/signals/category/${encodeURIComponent(cat)}`);
|
|
168
|
+
if (json) return out(data);
|
|
169
|
+
|
|
170
|
+
const sigs = data.signals || [];
|
|
171
|
+
console.log(fmt.bold(`\n ${catColor(data.category)} (${data.total} signals)\n`));
|
|
172
|
+
for (const s of sigs) {
|
|
173
|
+
const sc = scoreColor(s.score)(String((s.score || 0).toFixed(1)).padStart(4));
|
|
174
|
+
console.log(` ${sc} ${fmt.bold((s.topic || "").padEnd(35).slice(0, 35))} U:${(s.urgency || 0).toFixed(1)} B:${(s.buyer_intent || 0).toFixed(1)} D:${(s.durability || 0).toFixed(1)}`);
|
|
175
|
+
}
|
|
176
|
+
console.log();
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case "stats": {
|
|
181
|
+
const data = await request("GET", "/stats");
|
|
182
|
+
if (json) return out(data);
|
|
183
|
+
|
|
184
|
+
console.log(fmt.bold(`\n Signal Stats\n`));
|
|
185
|
+
console.log(` ${fmt.cyan("Total Signals:")} ${fmt.bold(String(data.total_signals))}`);
|
|
186
|
+
console.log(` ${fmt.cyan("History Entries:")} ${fmt.bold(String(data.history_entries))}`);
|
|
187
|
+
|
|
188
|
+
const cats = Object.entries(data.categories || {});
|
|
189
|
+
if (cats.length > 0) {
|
|
190
|
+
console.log(fmt.bold("\n Categories\n"));
|
|
191
|
+
for (const [name, info] of cats.sort((a, b) => b[1].count - a[1].count)) {
|
|
192
|
+
const avg = scoreColor(info.avg_score)(String(info.avg_score));
|
|
193
|
+
console.log(` ${catColor(name).padEnd(22)} ${String(info.count).padStart(3)} signals avg ${avg} ${fmt.dim("top: " + (info.top_topic || "—").slice(0, 30))}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const top = data.top_10 || [];
|
|
198
|
+
if (top.length > 0) {
|
|
199
|
+
console.log(fmt.bold("\n Top 10\n"));
|
|
200
|
+
for (let i = 0; i < top.length; i++) {
|
|
201
|
+
const s = top[i];
|
|
202
|
+
console.log(` ${fmt.dim("#" + (i + 1))} ${scoreColor(s.score)(String((s.score || 0).toFixed(1)))} ${fmt.bold(s.topic || "")} ${catColor(s.category)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
console.log();
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case "recalculate":
|
|
210
|
+
case "recalc": {
|
|
211
|
+
const data = await request("POST", "/trigger", { action: "recalculate" });
|
|
212
|
+
if (json) return out(data);
|
|
213
|
+
console.log(fmt.green(`\n Scores recalculated. ${data.total || 0} signals updated.\n`));
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case "config": {
|
|
218
|
+
const sub = positional[1];
|
|
219
|
+
if (sub === "set") {
|
|
220
|
+
const key = positional[2];
|
|
221
|
+
const val = positional[3];
|
|
222
|
+
if (!key || !val) die("Usage: mt-signals config set <key> <value>\n Keys: url, apiKey");
|
|
223
|
+
const cfg = loadConfig();
|
|
224
|
+
cfg[key] = val;
|
|
225
|
+
saveConfig(cfg);
|
|
226
|
+
console.log(fmt.green(`\n Set ${key} = ${val}\n`));
|
|
227
|
+
} else {
|
|
228
|
+
console.log(fmt.bold("\n Configuration\n"));
|
|
229
|
+
console.log(` ${fmt.cyan("URL:")} ${getBaseUrl()}`);
|
|
230
|
+
console.log(` ${fmt.cyan("API Key:")} ${getApiKey() ? fmt.dim(getApiKey().slice(0, 8) + "...") : fmt.dim("(not set)")}`);
|
|
231
|
+
console.log(` ${fmt.cyan("Config:")} ${require("path").join(require("os").homedir(), ".mt-signals", "config.json")}`);
|
|
232
|
+
console.log();
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case "help":
|
|
238
|
+
default: {
|
|
239
|
+
if (cmd && cmd !== "help") die(`Unknown command: ${cmd}\nRun 'mt-signals help' for usage.`);
|
|
240
|
+
showHelp();
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if (json) {
|
|
246
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
die(err.message);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function showHelp() {
|
|
254
|
+
console.log(`
|
|
255
|
+
${fmt.bold("mt-signals")} — Research Signal Intelligence
|
|
256
|
+
|
|
257
|
+
${fmt.bold("USAGE")}
|
|
258
|
+
mt-signals <command> [options]
|
|
259
|
+
|
|
260
|
+
${fmt.bold("COMMANDS")}
|
|
261
|
+
${fmt.cyan("signals")} [--category=X] [--sort=X] List all scored signals
|
|
262
|
+
${fmt.cyan("top")} [N] Top N signals (default: 10)
|
|
263
|
+
${fmt.cyan("trending")} Breakout signals + hot (score 6+)
|
|
264
|
+
${fmt.cyan("lookup")} <topic> Deep dive on a specific signal
|
|
265
|
+
${fmt.cyan("category")} <name> Signals by category
|
|
266
|
+
${fmt.cyan("stats")} Category breakdown + top 10
|
|
267
|
+
${fmt.cyan("recalculate")} Force score recalculation
|
|
268
|
+
${fmt.cyan("config")} [set <key> <value>] View/set configuration
|
|
269
|
+
|
|
270
|
+
${fmt.bold("OPTIONS")}
|
|
271
|
+
--json Output raw JSON
|
|
272
|
+
--category=X Filter: ai, infrastructure, security, devtools, data, business, career, market
|
|
273
|
+
--sort=X Sort: score, urgency, buyer_intent, durability
|
|
274
|
+
--min=N Minimum score filter
|
|
275
|
+
--limit=N Max results
|
|
276
|
+
|
|
277
|
+
${fmt.bold("ENVIRONMENT")}
|
|
278
|
+
MT_SIGNALS_URL API base URL (default: https://signals.metaltorque.dev)
|
|
279
|
+
MT_SIGNALS_API_KEY Access key for the API
|
|
280
|
+
|
|
281
|
+
${fmt.bold("EXAMPLES")}
|
|
282
|
+
mt-signals signals # All signals ranked by score
|
|
283
|
+
mt-signals top 5 # Top 5 signals
|
|
284
|
+
mt-signals trending # Breakouts + hot signals
|
|
285
|
+
mt-signals signals --category=ai # AI signals only
|
|
286
|
+
mt-signals signals --sort=buyer_intent # Sort by buyer intent
|
|
287
|
+
mt-signals lookup "MCP server" # Deep dive
|
|
288
|
+
mt-signals stats --json # Pipe stats to another tool
|
|
289
|
+
`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
main();
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const https = require("https");
|
|
2
|
+
const http = require("http");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), ".mt-signals");
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
9
|
+
|
|
10
|
+
function loadConfig() {
|
|
11
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")); } catch { return {}; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function saveConfig(cfg) {
|
|
15
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
16
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getBaseUrl() {
|
|
20
|
+
return (process.env.MT_SIGNALS_URL || loadConfig().url || "https://signals.metaltorque.dev").replace(/\/$/, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getApiKey() {
|
|
24
|
+
return process.env.MT_SIGNALS_API_KEY || loadConfig().apiKey || "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function request(method, urlPath, body, timeout = 30000) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const base = getBaseUrl();
|
|
30
|
+
const apiKey = getApiKey();
|
|
31
|
+
const sep = urlPath.includes("?") ? "&" : "?";
|
|
32
|
+
const fullUrl = apiKey ? `${base}${urlPath}${sep}key=${apiKey}` : `${base}${urlPath}`;
|
|
33
|
+
const mod = fullUrl.startsWith("https") ? https : http;
|
|
34
|
+
const parsed = new URL(fullUrl);
|
|
35
|
+
|
|
36
|
+
const headers = { "Content-Type": "application/json", "User-Agent": "mt-signals-cli/1.0.0" };
|
|
37
|
+
|
|
38
|
+
const opts = {
|
|
39
|
+
hostname: parsed.hostname,
|
|
40
|
+
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
41
|
+
path: parsed.pathname + parsed.search,
|
|
42
|
+
method,
|
|
43
|
+
headers,
|
|
44
|
+
timeout,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const req = mod.request(opts, (res) => {
|
|
48
|
+
let data = "";
|
|
49
|
+
res.on("data", (c) => (data += c));
|
|
50
|
+
res.on("end", () => {
|
|
51
|
+
try {
|
|
52
|
+
const json = JSON.parse(data);
|
|
53
|
+
if (res.statusCode >= 400) return reject(new Error(json.error || `HTTP ${res.statusCode}`));
|
|
54
|
+
resolve(json);
|
|
55
|
+
} catch {
|
|
56
|
+
if (res.statusCode >= 400) return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 300)}`));
|
|
57
|
+
resolve(data);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
req.on("error", reject);
|
|
63
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("Request timed out")); });
|
|
64
|
+
if (body) req.write(JSON.stringify(body));
|
|
65
|
+
req.end();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { request, loadConfig, saveConfig, getBaseUrl, getApiKey, CONFIG_DIR, CONFIG_FILE };
|
package/lib/format.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const NO_COLOR = process.env.NO_COLOR || process.env.TERM === "dumb";
|
|
2
|
+
const c = (code, s) => NO_COLOR ? s : `\x1b[${code}m${s}\x1b[0m`;
|
|
3
|
+
|
|
4
|
+
const fmt = {
|
|
5
|
+
bold: (s) => c("1", s),
|
|
6
|
+
dim: (s) => c("2", s),
|
|
7
|
+
green: (s) => c("32", s),
|
|
8
|
+
red: (s) => c("31", s),
|
|
9
|
+
yellow: (s) => c("33", s),
|
|
10
|
+
cyan: (s) => c("36", s),
|
|
11
|
+
magenta: (s) => c("35", s),
|
|
12
|
+
blue: (s) => c("34", s),
|
|
13
|
+
white: (s) => c("37", s),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const CAT_COLORS = {
|
|
17
|
+
ai: fmt.magenta,
|
|
18
|
+
infrastructure: fmt.green,
|
|
19
|
+
security: fmt.red,
|
|
20
|
+
devtools: fmt.yellow,
|
|
21
|
+
data: fmt.blue,
|
|
22
|
+
business: fmt.magenta,
|
|
23
|
+
career: fmt.cyan,
|
|
24
|
+
market: fmt.yellow,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function catColor(cat) {
|
|
28
|
+
return (CAT_COLORS[cat] || fmt.dim)(cat || "unknown");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function scoreColor(score) {
|
|
32
|
+
if (score >= 7) return fmt.green;
|
|
33
|
+
if (score >= 4) return fmt.yellow;
|
|
34
|
+
return fmt.dim;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function bar(value, max, width = 10) {
|
|
38
|
+
const filled = Math.round((value / (max || 10)) * width);
|
|
39
|
+
const empty = width - filled;
|
|
40
|
+
return fmt.cyan("\u2588".repeat(filled)) + fmt.dim("\u2591".repeat(empty));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sparkline(values) {
|
|
44
|
+
const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
45
|
+
if (!values.length) return "";
|
|
46
|
+
const max = Math.max(...values, 1);
|
|
47
|
+
return values.map((v) => fmt.cyan(chars[Math.min(Math.floor((v / max) * 7), 7)])).join("");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { fmt, catColor, scoreColor, bar, sparkline };
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mt-signals",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for MetalTorque Research Signals — scored trend intelligence from HN, Reddit, ArXiv, and GitHub.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"mt-signals": "bin/mt-signals.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": ["research", "signals", "trends", "market-intelligence", "ai", "swarm", "cli"],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "MetalTorque",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/joepangallo/research-signal-api"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=16"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"lib/",
|
|
21
|
+
"README.md"
|
|
22
|
+
]
|
|
23
|
+
}
|