mt-signals 1.0.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 +95 -0
- package/bin/mt-signals.js +38 -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
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# mt-signals
|
|
2
|
+
|
|
3
|
+
CLI for [MetalTorque Research Signals](https://signals.metaltorque.dev) — scored trend intelligence from HN, Reddit, ArXiv, and GitHub.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx mt-signals signals # Run without installing
|
|
9
|
+
npm i -g mt-signals # Or install globally
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# List all scored signals
|
|
16
|
+
mt-signals signals
|
|
17
|
+
|
|
18
|
+
# Top 5 hottest signals
|
|
19
|
+
mt-signals top 5
|
|
20
|
+
|
|
21
|
+
# Breakout trends + hot signals (score 6+)
|
|
22
|
+
mt-signals trending
|
|
23
|
+
|
|
24
|
+
# Action queue by build-now / monitor / ignore
|
|
25
|
+
mt-signals triage
|
|
26
|
+
|
|
27
|
+
# Deep dive on a topic
|
|
28
|
+
mt-signals lookup "MCP server"
|
|
29
|
+
|
|
30
|
+
# Filter by category
|
|
31
|
+
mt-signals signals --category=ai
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
### Discovery
|
|
37
|
+
| Command | Description |
|
|
38
|
+
|---------|-------------|
|
|
39
|
+
| `signals` | List all scored signals (filterable by category, sort, min score) |
|
|
40
|
+
| `top [N]` | Top N signals ranked by score (default: 10) |
|
|
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 |
|
|
43
|
+
|
|
44
|
+
### Analysis
|
|
45
|
+
| Command | Description |
|
|
46
|
+
|---------|-------------|
|
|
47
|
+
| `lookup <topic>` | Deep dive — score breakdown, evidence, trend history, actionable-for audience |
|
|
48
|
+
| `category <name>` | All signals in a category (ai, infrastructure, security, devtools, data, business, career, market) |
|
|
49
|
+
| `stats` | Aggregate stats — total signals, category breakdown, top 10 |
|
|
50
|
+
|
|
51
|
+
### Management
|
|
52
|
+
| Command | Description |
|
|
53
|
+
|---------|-------------|
|
|
54
|
+
| `recalculate` | Force score recalculation (applies time decay, detects new breakouts) |
|
|
55
|
+
| `config` | View current configuration |
|
|
56
|
+
| `config set <key> <value>` | Set config value (keys: `url`, `apiKey`) |
|
|
57
|
+
|
|
58
|
+
## Agent / CI Usage
|
|
59
|
+
|
|
60
|
+
Every command supports `--json`:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# JSON output for scripting
|
|
64
|
+
mt-signals top 10 --json | jq '.[0].topic'
|
|
65
|
+
|
|
66
|
+
# Pipe trending breakouts to another tool
|
|
67
|
+
mt-signals trending --json | jq '.breakouts'
|
|
68
|
+
|
|
69
|
+
# Pull just the build-now queue
|
|
70
|
+
mt-signals triage --json | jq '.build_now'
|
|
71
|
+
|
|
72
|
+
# Filter high-score AI signals
|
|
73
|
+
mt-signals signals --category=ai --min=7 --json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Options
|
|
77
|
+
|
|
78
|
+
| Flag | Description |
|
|
79
|
+
|------|-------------|
|
|
80
|
+
| `--json` | Output raw JSON (for piping/agents) |
|
|
81
|
+
| `--category=X` | Filter: ai, infrastructure, security, devtools, data, business, career, market |
|
|
82
|
+
| `--sort=X` | Sort: score, urgency, buyer_intent, durability |
|
|
83
|
+
| `--min=N` | Minimum score filter |
|
|
84
|
+
| `--limit=N` | Max results |
|
|
85
|
+
| `--version` | Print version |
|
|
86
|
+
|
|
87
|
+
## Environment Variables
|
|
88
|
+
|
|
89
|
+
| Variable | Description |
|
|
90
|
+
|----------|-------------|
|
|
91
|
+
| `MT_SIGNALS_API_KEY` | Access key for the API (overrides saved config) |
|
|
92
|
+
| `MT_SIGNALS_URL` | Server URL (default: `https://signals.metaltorque.dev`) |
|
|
93
|
+
| `NO_COLOR` | Disable colored output |
|
|
94
|
+
|
|
95
|
+
Configuration is stored in `~/.mt-signals/config.json`.
|
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 = {};
|
|
@@ -18,6 +18,12 @@ for (const a of args) {
|
|
|
18
18
|
const cmd = positional[0];
|
|
19
19
|
const json = flags.json;
|
|
20
20
|
|
|
21
|
+
if (flags.version || flags.v) {
|
|
22
|
+
const { version } = require("../package.json");
|
|
23
|
+
console.log(version);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
function out(data) {
|
|
22
28
|
if (json) return console.log(JSON.stringify(data, null, 2));
|
|
23
29
|
return data;
|
|
@@ -60,7 +66,7 @@ async function main() {
|
|
|
60
66
|
const cat = catColor(s.category).padEnd(22);
|
|
61
67
|
const topic = fmt.bold((s.topic || "").padEnd(35).slice(0, 35));
|
|
62
68
|
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)}`);
|
|
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)}`);
|
|
64
70
|
}
|
|
65
71
|
console.log(fmt.dim(`\n Last recalculated: ${data.last_recalculated || "never"}\n`));
|
|
66
72
|
break;
|
|
@@ -78,8 +84,9 @@ async function main() {
|
|
|
78
84
|
const rank = fmt.dim(`#${String(i + 1).padStart(2)}`);
|
|
79
85
|
const sc = scoreColor(s.score)(String((s.score || 0).toFixed(1)));
|
|
80
86
|
const cat = catColor(s.category);
|
|
81
|
-
console.log(` ${rank} ${sc} ${fmt.bold(s.topic || "")} ${cat}`);
|
|
87
|
+
console.log(` ${rank} ${sc} ${fmt.bold(s.topic || "")} ${cat} ${triageColor(s.triage || "ignore")}`);
|
|
82
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)}`);
|
|
83
90
|
}
|
|
84
91
|
console.log();
|
|
85
92
|
break;
|
|
@@ -138,6 +145,13 @@ async function main() {
|
|
|
138
145
|
console.log(` ${fmt.cyan("Durability:")} ${bar(s.durability, 10, 15)} ${(s.durability || 0).toFixed(1)}`);
|
|
139
146
|
|
|
140
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
|
+
}
|
|
141
155
|
|
|
142
156
|
if (s.actionable_for && s.actionable_for.length > 0) {
|
|
143
157
|
console.log(`\n ${fmt.cyan("Actionable for:")} ${s.actionable_for.join(", ")}`);
|
|
@@ -177,6 +191,25 @@ async function main() {
|
|
|
177
191
|
break;
|
|
178
192
|
}
|
|
179
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
|
+
|
|
180
213
|
case "stats": {
|
|
181
214
|
const data = await request("GET", "/stats");
|
|
182
215
|
if (json) return out(data);
|
|
@@ -261,6 +294,7 @@ ${fmt.bold("COMMANDS")}
|
|
|
261
294
|
${fmt.cyan("signals")} [--category=X] [--sort=X] List all scored signals
|
|
262
295
|
${fmt.cyan("top")} [N] Top N signals (default: 10)
|
|
263
296
|
${fmt.cyan("trending")} Breakout signals + hot (score 6+)
|
|
297
|
+
${fmt.cyan("triage")} [N] Build-now / monitor / ignore queue
|
|
264
298
|
${fmt.cyan("lookup")} <topic> Deep dive on a specific signal
|
|
265
299
|
${fmt.cyan("category")} <name> Signals by category
|
|
266
300
|
${fmt.cyan("stats")} Category breakdown + top 10
|
|
@@ -282,6 +316,7 @@ ${fmt.bold("EXAMPLES")}
|
|
|
282
316
|
mt-signals signals # All signals ranked by score
|
|
283
317
|
mt-signals top 5 # Top 5 signals
|
|
284
318
|
mt-signals trending # Breakouts + hot signals
|
|
319
|
+
mt-signals triage 12 # Action queue by build/monitor/ignore
|
|
285
320
|
mt-signals signals --category=ai # AI signals only
|
|
286
321
|
mt-signals signals --sort=buyer_intent # Sort by buyer intent
|
|
287
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
|
},
|