mt-signals 1.1.0 → 1.2.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 +7 -0
- package/bin/mt-signals.js +32 -3
- package/lib/client.test.js +58 -0
- package/lib/format.js +7 -1
- package/lib/format.test.js +18 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,6 +21,9 @@ mt-signals top 5
|
|
|
21
21
|
# Breakout trends + hot signals (score 6+)
|
|
22
22
|
mt-signals trending
|
|
23
23
|
|
|
24
|
+
# Action queue by build-now / monitor / ignore
|
|
25
|
+
mt-signals triage
|
|
26
|
+
|
|
24
27
|
# Deep dive on a topic
|
|
25
28
|
mt-signals lookup "MCP server"
|
|
26
29
|
|
|
@@ -36,6 +39,7 @@ mt-signals signals --category=ai
|
|
|
36
39
|
| `signals` | List all scored signals (filterable by category, sort, min score) |
|
|
37
40
|
| `top [N]` | Top N signals ranked by score (default: 10) |
|
|
38
41
|
| `trending` | Breakout signals (1.8x+ rolling avg) and hot signals (score 6+) |
|
|
42
|
+
| `triage [N]` | Group signals into `build_now`, `monitor`, and `ignore` with product mapping and recommended action |
|
|
39
43
|
|
|
40
44
|
### Analysis
|
|
41
45
|
| Command | Description |
|
|
@@ -62,6 +66,9 @@ mt-signals top 10 --json | jq '.[0].topic'
|
|
|
62
66
|
# Pipe trending breakouts to another tool
|
|
63
67
|
mt-signals trending --json | jq '.breakouts'
|
|
64
68
|
|
|
69
|
+
# Pull just the build-now queue
|
|
70
|
+
mt-signals triage --json | jq '.build_now'
|
|
71
|
+
|
|
65
72
|
# Filter high-score AI signals
|
|
66
73
|
mt-signals signals --category=ai --min=7 --json
|
|
67
74
|
```
|
package/bin/mt-signals.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { request, loadConfig, saveConfig, getBaseUrl, getApiKey } = require("../lib/client");
|
|
5
|
-
const { fmt, catColor, scoreColor, bar, sparkline } = require("../lib/format");
|
|
5
|
+
const { fmt, catColor, scoreColor, triageColor, bar, sparkline } = require("../lib/format");
|
|
6
6
|
|
|
7
7
|
const args = process.argv.slice(2);
|
|
8
8
|
const flags = {};
|
|
@@ -66,7 +66,7 @@ async function main() {
|
|
|
66
66
|
const cat = catColor(s.category).padEnd(22);
|
|
67
67
|
const topic = fmt.bold((s.topic || "").padEnd(35).slice(0, 35));
|
|
68
68
|
const src = fmt.dim((s.source || "").padEnd(8).slice(0, 8));
|
|
69
|
-
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)}`);
|
|
69
|
+
console.log(` ${sc} ${cat} ${topic} ${src} ${triageColor(s.triage || "ignore")} U:${bar(s.urgency, 10, 5)} B:${bar(s.buyer_intent, 10, 5)} D:${bar(s.durability, 10, 5)}`);
|
|
70
70
|
}
|
|
71
71
|
console.log(fmt.dim(`\n Last recalculated: ${data.last_recalculated || "never"}\n`));
|
|
72
72
|
break;
|
|
@@ -84,8 +84,9 @@ async function main() {
|
|
|
84
84
|
const rank = fmt.dim(`#${String(i + 1).padStart(2)}`);
|
|
85
85
|
const sc = scoreColor(s.score)(String((s.score || 0).toFixed(1)));
|
|
86
86
|
const cat = catColor(s.category);
|
|
87
|
-
console.log(` ${rank} ${sc} ${fmt.bold(s.topic || "")} ${cat}`);
|
|
87
|
+
console.log(` ${rank} ${sc} ${fmt.bold(s.topic || "")} ${cat} ${triageColor(s.triage || "ignore")}`);
|
|
88
88
|
if (s.summary) console.log(` ${fmt.dim(s.summary.slice(0, 90))}`);
|
|
89
|
+
if (s.recommended_action) console.log(` ${fmt.dim(s.recommended_action)}`);
|
|
89
90
|
}
|
|
90
91
|
console.log();
|
|
91
92
|
break;
|
|
@@ -144,6 +145,13 @@ async function main() {
|
|
|
144
145
|
console.log(` ${fmt.cyan("Durability:")} ${bar(s.durability, 10, 15)} ${(s.durability || 0).toFixed(1)}`);
|
|
145
146
|
|
|
146
147
|
if (s.summary) console.log(`\n ${fmt.dim(s.summary)}`);
|
|
148
|
+
console.log(`\n ${fmt.cyan("Triage:")} ${triageColor(s.triage || "ignore")}`);
|
|
149
|
+
if (s.product_mapping && s.product_mapping.length > 0) {
|
|
150
|
+
console.log(` ${fmt.cyan("Products:")} ${s.product_mapping.join(", ")}`);
|
|
151
|
+
}
|
|
152
|
+
if (s.recommended_action) {
|
|
153
|
+
console.log(` ${fmt.cyan("Action:")} ${s.recommended_action}`);
|
|
154
|
+
}
|
|
147
155
|
|
|
148
156
|
if (s.actionable_for && s.actionable_for.length > 0) {
|
|
149
157
|
console.log(`\n ${fmt.cyan("Actionable for:")} ${s.actionable_for.join(", ")}`);
|
|
@@ -183,6 +191,25 @@ async function main() {
|
|
|
183
191
|
break;
|
|
184
192
|
}
|
|
185
193
|
|
|
194
|
+
case "triage": {
|
|
195
|
+
const limit = positional[1] || flags.limit || "15";
|
|
196
|
+
const data = await request("GET", `/signals/triage?limit=${limit}`);
|
|
197
|
+
if (json) return out(data);
|
|
198
|
+
|
|
199
|
+
console.log(fmt.bold(`\n Research Signal Triage\n`));
|
|
200
|
+
for (const bucket of ["build_now", "monitor", "ignore"]) {
|
|
201
|
+
const items = data[bucket] || [];
|
|
202
|
+
if (!items.length) continue;
|
|
203
|
+
console.log(` ${triageColor(bucket)} (${items.length})\n`);
|
|
204
|
+
for (const item of items) {
|
|
205
|
+
console.log(` ${scoreColor(item.score)(String((item.score || 0).toFixed(1)).padStart(4))} ${fmt.bold(item.topic || "")} ${catColor(item.category)} ${fmt.dim((item.product_mapping || []).join(", "))}`);
|
|
206
|
+
console.log(` ${fmt.dim(item.recommended_action || "")}`);
|
|
207
|
+
}
|
|
208
|
+
console.log();
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
186
213
|
case "stats": {
|
|
187
214
|
const data = await request("GET", "/stats");
|
|
188
215
|
if (json) return out(data);
|
|
@@ -267,6 +294,7 @@ ${fmt.bold("COMMANDS")}
|
|
|
267
294
|
${fmt.cyan("signals")} [--category=X] [--sort=X] List all scored signals
|
|
268
295
|
${fmt.cyan("top")} [N] Top N signals (default: 10)
|
|
269
296
|
${fmt.cyan("trending")} Breakout signals + hot (score 6+)
|
|
297
|
+
${fmt.cyan("triage")} [N] Build-now / monitor / ignore queue
|
|
270
298
|
${fmt.cyan("lookup")} <topic> Deep dive on a specific signal
|
|
271
299
|
${fmt.cyan("category")} <name> Signals by category
|
|
272
300
|
${fmt.cyan("stats")} Category breakdown + top 10
|
|
@@ -288,6 +316,7 @@ ${fmt.bold("EXAMPLES")}
|
|
|
288
316
|
mt-signals signals # All signals ranked by score
|
|
289
317
|
mt-signals top 5 # Top 5 signals
|
|
290
318
|
mt-signals trending # Breakouts + hot signals
|
|
319
|
+
mt-signals triage 12 # Action queue by build/monitor/ignore
|
|
291
320
|
mt-signals signals --category=ai # AI signals only
|
|
292
321
|
mt-signals signals --sort=buyer_intent # Sort by buyer intent
|
|
293
322
|
mt-signals lookup "MCP server" # Deep dive
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
function mockRequest(statusCode, body) {
|
|
8
|
+
return (opts, cb) => {
|
|
9
|
+
const res = new EventEmitter();
|
|
10
|
+
res.statusCode = statusCode;
|
|
11
|
+
const req = new EventEmitter();
|
|
12
|
+
req.write = vi.fn();
|
|
13
|
+
req.end = vi.fn(() => {
|
|
14
|
+
cb(res);
|
|
15
|
+
res.emit('data', typeof body === 'string' ? body : JSON.stringify(body));
|
|
16
|
+
res.emit('end');
|
|
17
|
+
});
|
|
18
|
+
req.destroy = vi.fn();
|
|
19
|
+
return req;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('research-signal cli client', () => {
|
|
24
|
+
const HOME = '/tmp/vitest-home-mt-signals';
|
|
25
|
+
const CONFIG_DIR = path.join(HOME, '.mt-signals');
|
|
26
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.resetModules();
|
|
30
|
+
process.env.HOME = HOME;
|
|
31
|
+
delete process.env.MT_SIGNALS_URL;
|
|
32
|
+
delete process.env.MT_SIGNALS_API_KEY;
|
|
33
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ url: 'https://example.com', apiKey: 'k' }));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('getBaseUrl and getApiKey read config', () => {
|
|
38
|
+
const require = createRequire(import.meta.url);
|
|
39
|
+
delete require.cache[require.resolve('./client.js')];
|
|
40
|
+
const mod = require('./client.js');
|
|
41
|
+
expect(mod.getBaseUrl()).toBe('https://example.com');
|
|
42
|
+
expect(mod.getApiKey()).toBe('k');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('request appends key when provided', async () => {
|
|
46
|
+
const require = createRequire(import.meta.url);
|
|
47
|
+
const https = require('https');
|
|
48
|
+
const originalRequest = https.request;
|
|
49
|
+
const mockReq = vi.fn(mockRequest(200, { ok: 1 }));
|
|
50
|
+
https.request = mockReq;
|
|
51
|
+
delete require.cache[require.resolve('./client.js')];
|
|
52
|
+
const mod = require('./client.js');
|
|
53
|
+
await mod.request('GET', '/v1/health');
|
|
54
|
+
https.request = originalRequest;
|
|
55
|
+
const opts = mockReq.mock.calls[0][0];
|
|
56
|
+
expect(opts.path).toContain('key=k');
|
|
57
|
+
});
|
|
58
|
+
});
|
package/lib/format.js
CHANGED
|
@@ -34,6 +34,12 @@ function scoreColor(score) {
|
|
|
34
34
|
return fmt.dim;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function triageColor(triage) {
|
|
38
|
+
if (triage === "build_now") return fmt.green(triage);
|
|
39
|
+
if (triage === "monitor") return fmt.yellow(triage);
|
|
40
|
+
return fmt.dim(triage || "ignore");
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
function bar(value, max, width = 10) {
|
|
38
44
|
const filled = Math.round((value / (max || 10)) * width);
|
|
39
45
|
const empty = width - filled;
|
|
@@ -47,4 +53,4 @@ function sparkline(values) {
|
|
|
47
53
|
return values.map((v) => fmt.cyan(chars[Math.min(Math.floor((v / max) * 7), 7)])).join("");
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
module.exports = { fmt, catColor, scoreColor, bar, sparkline };
|
|
56
|
+
module.exports = { fmt, catColor, scoreColor, triageColor, bar, sparkline };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const fmt = require('./format.js');
|
|
6
|
+
|
|
7
|
+
describe('research-signal cli format', () => {
|
|
8
|
+
it('catColor and scoreColor return strings', () => {
|
|
9
|
+
expect(fmt.catColor('ai')).toContain('ai');
|
|
10
|
+
expect(fmt.scoreColor(8)('8')).toContain('8');
|
|
11
|
+
expect(fmt.triageColor('build_now')).toContain('build_now');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('bar and sparkline render', () => {
|
|
15
|
+
expect(fmt.bar(5, 10, 5)).toBeTypeOf('string');
|
|
16
|
+
expect(fmt.sparkline([1, 2, 3])).toBeTypeOf('string');
|
|
17
|
+
});
|
|
18
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mt-signals",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI for MetalTorque Research Signals — scored trend intelligence from HN, Reddit, ArXiv, and GitHub.",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "CLI for MetalTorque Research Signals — scored trend intelligence with build/monitor/ignore triage from HN, Reddit, ArXiv, and GitHub.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mt-signals": "bin/mt-signals.js"
|
|
7
7
|
},
|