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 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.0.0",
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
  },