moshcode 0.27.0 → 0.29.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/.claude-plugin/marketplace.json +3 -3
- package/README.md +33 -19
- package/bin/moshcode.mjs +3 -3
- package/package.json +1 -1
- package/plugins/crypto/README.md +2 -2
- package/plugins/crypto/commands/crypto.md +2 -2
- package/plugins/{ticker → stocks}/.claude-plugin/plugin.json +2 -2
- package/plugins/{ticker → stocks}/README.md +22 -4
- package/plugins/{ticker → stocks}/commands/discover.md +2 -2
- package/plugins/{ticker → stocks}/commands/lookup.md +3 -3
- package/plugins/{ticker → stocks}/commands/reports.md +3 -3
- package/plugins/{ticker → stocks}/commands/research.md +2 -2
- package/plugins/{ticker → stocks}/commands/signals.md +2 -2
- package/plugins/{ticker/commands/ticker.md → stocks/commands/stocks.md} +2 -2
- package/src/advisor.mjs +33 -33
- package/src/cli-schema.mjs +26 -26
- package/src/crypto.mjs +5 -5
- package/src/help.mjs +14 -0
- package/src/integrations.mjs +16 -4
- package/src/plugins.mjs +28 -2
- package/src/tui.mjs +12 -7
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
},
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
|
-
"name": "
|
|
11
|
+
"name": "stocks",
|
|
12
12
|
"description": "Equity research slash commands backed by advis0r.com: scored reports, extracted signals, transcript search, company-name lookup, and ranked watchlists.",
|
|
13
|
-
"source": "./plugins/
|
|
13
|
+
"source": "./plugins/stocks",
|
|
14
14
|
"category": "productivity",
|
|
15
15
|
"author": {
|
|
16
16
|
"name": "moshcoder",
|
|
17
17
|
"url": "https://moshcode.sh"
|
|
18
18
|
},
|
|
19
|
-
"homepage": "https://github.com/moshcoder/moshcode#
|
|
19
|
+
"homepage": "https://github.com/moshcoder/moshcode#stocks",
|
|
20
20
|
"keywords": ["stocks", "equity", "research", "markets", "advis0r"]
|
|
21
21
|
},
|
|
22
22
|
{
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ or miss one that does. A test fails the build when it drifts.
|
|
|
46
46
|
| `moshcode engines` | engines | list engines and installation status |
|
|
47
47
|
| `moshcode tools` | tools | list workflow tools and installation status |
|
|
48
48
|
| `moshcode trade` | tools | look up markets and trade through Alpaca |
|
|
49
|
-
| `moshcode
|
|
49
|
+
| `moshcode stocks` <br>`advisor` | tools | equity research from advis0r.com |
|
|
50
50
|
| `moshcode crypto` <br>`coins` | tools | crypto market data from advis0r.com |
|
|
51
51
|
| `moshcode plugin` <br>`plugins` | extend | install moshcode's slash commands into Claude Code |
|
|
52
52
|
| `moshcode commands` | script | list built-in moshscript commands |
|
|
@@ -214,34 +214,34 @@ Alpaca's CLI has no confirmation prompts; `--submit` intentionally removes
|
|
|
214
214
|
MoshCode's preview guard. Live trading additionally requires Alpaca's `--live`
|
|
215
215
|
opt-in or corresponding environment setting.
|
|
216
216
|
|
|
217
|
-
### Equity research (`moshcode
|
|
217
|
+
### Equity research (`moshcode stocks`)
|
|
218
218
|
|
|
219
|
-
Where `trade` is Alpaca's order book, `
|
|
219
|
+
Where `trade` is Alpaca's order book, `stocks` is the research desk:
|
|
220
220
|
[advis0r.com](https://advis0r.com/api)'s public read-only API, rendered in the
|
|
221
221
|
pit. No key, no login, no write routes, no binary to install:
|
|
222
222
|
|
|
223
223
|
```sh
|
|
224
|
-
moshcode
|
|
225
|
-
moshcode
|
|
226
|
-
moshcode
|
|
227
|
-
moshcode
|
|
228
|
-
moshcode
|
|
229
|
-
moshcode
|
|
230
|
-
moshcode
|
|
224
|
+
moshcode stocks NVDA # score, technicals, fundamentals, thesis, signals
|
|
225
|
+
moshcode stocks lookup rivian # company name → RIVN
|
|
226
|
+
moshcode stocks signals AAPL # what was said, quoted and sourced
|
|
227
|
+
moshcode stocks search "data center" # across every indexed transcript
|
|
228
|
+
moshcode stocks reports --limit 10 # the stored index, best score first
|
|
229
|
+
moshcode stocks discover fusion # a ranked watchlist (slow — analyzes each candidate)
|
|
230
|
+
moshcode stocks open NVDA # the shareable report page
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
Add `--json` to any of them for the raw response. The same facade is `/
|
|
233
|
+
Add `--json` to any of them for the raw response. The same facade is `/stocks …`
|
|
234
234
|
in the pit, and `MOSHCODE_ADVISOR_URL` points it at another instance.
|
|
235
235
|
|
|
236
236
|
Reports are **stored snapshots**, not live quotes: every response carries
|
|
237
237
|
`reportGeneratedAt` and every renderer prints it, alongside whether the price is
|
|
238
238
|
delayed and which feed produced it. Scores labelled `offline` come from
|
|
239
239
|
deterministic rules rather than a model. It is a research aid, not advice, and
|
|
240
|
-
nothing under `
|
|
240
|
+
nothing under `stocks` can place an order.
|
|
241
241
|
|
|
242
242
|
### Crypto market data (`moshcode crypto`)
|
|
243
243
|
|
|
244
|
-
`crypto` is `
|
|
244
|
+
`crypto` is `stocks`'s sibling on the same host: advis0r's read-only crypto
|
|
245
245
|
routes over Alpaca's US crypto venue, which trades 24/7 and needs no extra
|
|
246
246
|
subscription.
|
|
247
247
|
|
|
@@ -260,7 +260,7 @@ Pairs are accepted as `BTC`, `BTC-USD`, `BTC/USD` or `BTCUSD` — a bare asset
|
|
|
260
260
|
resolves to that asset's USD pair. `--json` gives the raw response, `/crypto …`
|
|
261
261
|
is the same facade in the pit, and `MOSHCODE_ADVISOR_URL` points it elsewhere.
|
|
262
262
|
|
|
263
|
-
Unlike a `
|
|
263
|
+
Unlike a `stocks` report, this is a **live venue read**, not a stored snapshot —
|
|
264
264
|
there are no transcripts, no filings and no signals behind a crypto pair, and
|
|
265
265
|
the failure mode runs the other way: the price is accurate to the second and
|
|
266
266
|
stale by the time you act on it. Every response stamps when it was fetched.
|
|
@@ -268,7 +268,7 @@ stale by the time you act on it. Every response stamps when it was fetched.
|
|
|
268
268
|
The technical score counts venue-local liquidity, so it is **not comparable** to
|
|
269
269
|
an equity's score, and each response ships the `caveats` that say so. Prices are
|
|
270
270
|
Alpaca's US venue alone and can differ materially from other exchanges. Research
|
|
271
|
-
aid, not advice — and like `
|
|
271
|
+
aid, not advice — and like `stocks`, nothing under `crypto` can place an order.
|
|
272
272
|
|
|
273
273
|
### Social posting from the pit
|
|
274
274
|
|
|
@@ -371,12 +371,12 @@ inside your engine too:
|
|
|
371
371
|
|
|
372
372
|
```sh
|
|
373
373
|
moshcode plugin list # what the marketplace ships, and who can take it
|
|
374
|
-
moshcode plugin install # add the marketplace + install `
|
|
374
|
+
moshcode plugin install # add the marketplace + install `stocks`
|
|
375
375
|
moshcode plugin install crypto # add the marketplace + install `crypto`
|
|
376
|
-
moshcode plugin remove
|
|
376
|
+
moshcode plugin remove stocks # take it back off
|
|
377
377
|
```
|
|
378
378
|
|
|
379
|
-
`
|
|
379
|
+
`stocks@moshcode` adds `/stocks`, `/signals`, `/research`, `/lookup`,
|
|
380
380
|
`/reports`, and `/discover` — the same advis0r research surface described above,
|
|
381
381
|
driven from inside a coding session.
|
|
382
382
|
|
|
@@ -392,10 +392,24 @@ The equivalent by hand:
|
|
|
392
392
|
|
|
393
393
|
```sh
|
|
394
394
|
claude plugin marketplace add moshcoder/moshcode
|
|
395
|
-
claude plugin install
|
|
395
|
+
claude plugin install stocks@moshcode
|
|
396
396
|
claude plugin install crypto@moshcode
|
|
397
397
|
```
|
|
398
398
|
|
|
399
|
+
### Upgrading from `ticker@moshcode`
|
|
400
|
+
|
|
401
|
+
`stocks` was called `ticker` before v0.29.0. Installing the new id does **not**
|
|
402
|
+
replace the old one — engines install plugins side by side, so `/stocks` would
|
|
403
|
+
come from two plugins at once. Remove the old id first:
|
|
404
|
+
|
|
405
|
+
```sh
|
|
406
|
+
moshcode plugin remove ticker
|
|
407
|
+
moshcode plugin install stocks
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`remove ticker` keeps working for exactly that reason, even though
|
|
411
|
+
`install ticker` no longer does.
|
|
412
|
+
|
|
399
413
|
Claude Code is currently the only engine with a plugin primitive. The others are
|
|
400
414
|
reported as skipped with a reason, the same way they are for skills, rather than
|
|
401
415
|
being left out of the summary. `MOSHCODE_PLUGIN_SOURCE=.` installs from a local
|
package/bin/moshcode.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { runUpgrade } from "../src/upgrade.mjs";
|
|
|
20
20
|
import { selfUpdateCommand } from "../src/selfupdate.mjs";
|
|
21
21
|
import { describeUninstall, uninstallPlan } from "../src/uninstall.mjs";
|
|
22
22
|
import { mcpCommand, pluginCommand, skillCommand } from "../src/integrations.mjs";
|
|
23
|
-
import {
|
|
23
|
+
import { stocksCommand } from "../src/advisor.mjs";
|
|
24
24
|
import { cryptoCommand } from "../src/crypto.mjs";
|
|
25
25
|
import { canOpenBrowser, openBrowser } from "../src/open-url.mjs";
|
|
26
26
|
import { locate, tilde } from "../src/pwd.mjs";
|
|
@@ -347,8 +347,8 @@ async function main() {
|
|
|
347
347
|
propagateExit(r.code, r.signal);
|
|
348
348
|
return;
|
|
349
349
|
}
|
|
350
|
-
if (cmd === "
|
|
351
|
-
const code = await
|
|
350
|
+
if (cmd === "stocks" || cmd === "advisor") {
|
|
351
|
+
const code = await stocksCommand(rest, {
|
|
352
352
|
openUrl: (url) => canOpenBrowser() && openBrowser(url),
|
|
353
353
|
});
|
|
354
354
|
if (code) process.exitCode = code;
|
package/package.json
CHANGED
package/plugins/crypto/README.md
CHANGED
|
@@ -41,10 +41,10 @@ command falls back to `curl` against the same endpoints.
|
|
|
41
41
|
|
|
42
42
|
Point the commands at another instance with `MOSHCODE_ADVISOR_URL`.
|
|
43
43
|
|
|
44
|
-
## Why this is separate from `
|
|
44
|
+
## Why this is separate from `stocks`
|
|
45
45
|
|
|
46
46
|
They answer different questions from different data, and share only a hostname.
|
|
47
|
-
A `/
|
|
47
|
+
A `/stocks` report is a **stored snapshot** built from transcripts, SEC
|
|
48
48
|
fundamentals and extracted signals — its risk is a stale price read as a live
|
|
49
49
|
one. A `/crypto` report is a **live venue read** with no transcripts, no
|
|
50
50
|
filings and no signals — its risk is the opposite: a price that is accurate to
|
|
@@ -42,9 +42,9 @@ resolves to that asset's USD pair.
|
|
|
42
42
|
- Crypto trades 24/7 with no circuit breakers and no market close. Never
|
|
43
43
|
describe a crypto price as "at the close" or "premarket".
|
|
44
44
|
- State `generatedAt` / `fetchedAt`. This is a live read, so it goes stale in
|
|
45
|
-
seconds, not days — the opposite failure mode from a stored `/
|
|
45
|
+
seconds, not days — the opposite failure mode from a stored `/stocks` report.
|
|
46
46
|
- The score's liquidity component counts venue-local volume only, so it is
|
|
47
|
-
**not comparable** to an equity's score from `/
|
|
47
|
+
**not comparable** to an equity's score from `/stocks`. Do not rank the two
|
|
48
48
|
against each other.
|
|
49
49
|
- End with the response's own `disclaimer`. This is research, not advice.
|
|
50
50
|
- Link the shareable page: `https://advis0r.com/crypto/<PAIR>`.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
|
-
"name": "
|
|
3
|
+
"name": "stocks",
|
|
4
4
|
"description": "Equity research slash commands backed by advis0r.com: scored reports, extracted signals, transcript search, company-name lookup, and ranked watchlists.",
|
|
5
5
|
"version": "0.1.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "moshcoder",
|
|
8
8
|
"url": "https://moshcode.sh"
|
|
9
9
|
},
|
|
10
|
-
"homepage": "https://github.com/moshcoder/moshcode#
|
|
10
|
+
"homepage": "https://github.com/moshcoder/moshcode#stocks",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": ["stocks", "equity", "research", "markets", "advis0r"]
|
|
13
13
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# stocks — equity research in your engine 🤘
|
|
2
2
|
|
|
3
3
|
Slash commands backed by [advis0r.com](https://advis0r.com/api): scored research
|
|
4
4
|
reports, extracted signals with sources, transcript search, company-name lookup,
|
|
@@ -6,13 +6,31 @@ and ranked watchlists.
|
|
|
6
6
|
|
|
7
7
|
| command | what it does |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| `/
|
|
9
|
+
| `/stocks NVDA` | score, technicals, fundamentals, thesis, signals, sources |
|
|
10
10
|
| `/signals AAPL` | what was actually said, quoted and sourced |
|
|
11
11
|
| `/research data center` | full-text search across every indexed transcript |
|
|
12
12
|
| `/lookup rivian` | company name → `RIVN` |
|
|
13
13
|
| `/reports` | every stored report, best score first |
|
|
14
14
|
| `/discover fusion` | a ranked watchlist for a topic (slow) |
|
|
15
15
|
|
|
16
|
+
## Upgrading from `ticker@moshcode`
|
|
17
|
+
|
|
18
|
+
This plugin used to be called `ticker`. Both it and its headline command were
|
|
19
|
+
renamed so the name says which market it covers, now that `/crypto` sits beside
|
|
20
|
+
it.
|
|
21
|
+
|
|
22
|
+
Installing the new one does **not** replace the old one — engines install
|
|
23
|
+
plugins side by side, so `/stocks` would come from two plugins at once. Remove
|
|
24
|
+
the old id first:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
moshcode plugin remove ticker
|
|
28
|
+
moshcode plugin install stocks
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`moshcode plugin remove ticker` keeps working for exactly this reason, even
|
|
32
|
+
though `moshcode plugin install ticker` no longer does.
|
|
33
|
+
|
|
16
34
|
## Install
|
|
17
35
|
|
|
18
36
|
```bash
|
|
@@ -23,7 +41,7 @@ Or straight from Claude Code:
|
|
|
23
41
|
|
|
24
42
|
```bash
|
|
25
43
|
claude plugin marketplace add moshcoder/moshcode
|
|
26
|
-
claude plugin install
|
|
44
|
+
claude plugin install stocks@moshcode
|
|
27
45
|
```
|
|
28
46
|
|
|
29
47
|
Restart the engine afterwards — a newly installed plugin is not live in a
|
|
@@ -31,7 +49,7 @@ session that is already running.
|
|
|
31
49
|
|
|
32
50
|
## How it works
|
|
33
51
|
|
|
34
|
-
Each command shells out to `moshcode
|
|
52
|
+
Each command shells out to `moshcode stocks …`, which calls advis0r's public,
|
|
35
53
|
read-only API. No key, no login, no write routes. With `moshcode` absent, every
|
|
36
54
|
command falls back to `curl` against the same endpoints.
|
|
37
55
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Build a ranked watchlist for a topic (slow — it analyzes each candidate).
|
|
3
3
|
argument-hint: "[topic]"
|
|
4
|
-
allowed-tools: Bash(moshcode
|
|
4
|
+
allowed-tools: Bash(moshcode stocks:*), Bash(curl -sS https://advis0r.com/api/:*)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Task
|
|
@@ -9,7 +9,7 @@ allowed-tools: Bash(moshcode ticker:*), Bash(curl -sS https://advis0r.com/api/:*
|
|
|
9
9
|
Rank candidates for `$ARGUMENTS` (no topic → the default watchlist).
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
moshcode
|
|
12
|
+
moshcode stocks discover $ARGUMENTS --limit 10 --json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Fallback: `curl -sS --max-time 180 "https://advis0r.com/api/discover?topic=<url-encoded>&provider=offline&horizon=2&limit=10"`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Find a ticker symbol by company name (rivian → RIVN).
|
|
3
3
|
argument-hint: <company name>
|
|
4
|
-
allowed-tools: Bash(moshcode
|
|
4
|
+
allowed-tools: Bash(moshcode stocks:*), Bash(curl -sS https://advis0r.com/api/:*)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Task
|
|
@@ -9,7 +9,7 @@ allowed-tools: Bash(moshcode ticker:*), Bash(curl -sS https://advis0r.com/api/:*
|
|
|
9
9
|
Resolve `$ARGUMENTS` to a ticker symbol.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
moshcode
|
|
12
|
+
moshcode stocks lookup $ARGUMENTS --limit 10 --json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Fallback: `curl -sS "https://advis0r.com/api/lookup?q=<url-encoded>&limit=10"`
|
|
@@ -22,6 +22,6 @@ Fallback: `curl -sS "https://advis0r.com/api/lookup?q=<url-encoded>&limit=10"`
|
|
|
22
22
|
## Rules
|
|
23
23
|
|
|
24
24
|
- Show every match with its exchange — "Delta" is an airline and a faucet company.
|
|
25
|
-
- Mark which ones have a report, and offer `/
|
|
25
|
+
- Mark which ones have a report, and offer `/stocks <SYMBOL>` for those.
|
|
26
26
|
- One unambiguous match: say the symbol and go straight to offering the report.
|
|
27
27
|
- No match: say the *directory* has no match, and do not invent a symbol.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Every stored advis0r research report, best score first.
|
|
3
3
|
argument-hint: "[--limit n] [--sort recent|score|ticker]"
|
|
4
|
-
allowed-tools: Bash(moshcode
|
|
4
|
+
allowed-tools: Bash(moshcode stocks:*), Bash(curl -sS https://advis0r.com/api/:*)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Task
|
|
@@ -9,7 +9,7 @@ allowed-tools: Bash(moshcode ticker:*), Bash(curl -sS https://advis0r.com/api/:*
|
|
|
9
9
|
List the stored reports.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
moshcode
|
|
12
|
+
moshcode stocks reports $ARGUMENTS --json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Fallback: `curl -sS "https://advis0r.com/api/reports?sort=score&limit=25"`
|
|
@@ -26,5 +26,5 @@ generatedAt }`, and `total` is how many exist.
|
|
|
26
26
|
- **`generatedAt` per row, always.** These are snapshots taken at different
|
|
27
27
|
times; a table that hides that reads as one consistent as-of date.
|
|
28
28
|
- A row with no `aiProvider` was scored deterministically, not by a model.
|
|
29
|
-
- Offer `/
|
|
29
|
+
- Offer `/stocks <SYMBOL>` for anything worth a closer look.
|
|
30
30
|
- This is a coverage list, not a recommendation list. Rank order is score order.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Full-text search across every indexed earnings transcript and article.
|
|
3
3
|
argument-hint: <words to search for>
|
|
4
|
-
allowed-tools: Bash(moshcode
|
|
4
|
+
allowed-tools: Bash(moshcode stocks:*), Bash(curl -sS https://advis0r.com/api/:*)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Task
|
|
@@ -9,7 +9,7 @@ allowed-tools: Bash(moshcode ticker:*), Bash(curl -sS https://advis0r.com/api/:*
|
|
|
9
9
|
Search the transcript index for `$ARGUMENTS`.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
moshcode
|
|
12
|
+
moshcode stocks search $ARGUMENTS --limit 20 --json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Fallback: `curl -sS "https://advis0r.com/api/search?q=<url-encoded>&limit=20"`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: What was actually said about a ticker — extracted signals with quotes and sources.
|
|
3
3
|
argument-hint: <SYMBOL>
|
|
4
|
-
allowed-tools: Bash(moshcode
|
|
4
|
+
allowed-tools: Bash(moshcode stocks:*), Bash(curl -sS https://advis0r.com/api/:*)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Task
|
|
@@ -9,7 +9,7 @@ allowed-tools: Bash(moshcode ticker:*), Bash(curl -sS https://advis0r.com/api/:*
|
|
|
9
9
|
List the extracted signals for `$ARGUMENTS`.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
moshcode
|
|
12
|
+
moshcode stocks signals $ARGUMENTS --json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Fallback: `curl -sS "https://advis0r.com/api/signals?ticker=$ARGUMENTS"`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Research one ticker — score, technicals, fundamentals, thesis, signals, and sources.
|
|
3
3
|
argument-hint: <SYMBOL>
|
|
4
|
-
allowed-tools: Bash(moshcode
|
|
4
|
+
allowed-tools: Bash(moshcode stocks:*), Bash(curl -sS https://advis0r.com/api/:*)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Task
|
|
@@ -11,7 +11,7 @@ Pull the stored research report for `$ARGUMENTS` and summarize it for the user.
|
|
|
11
11
|
Run:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
moshcode
|
|
14
|
+
moshcode stocks $ARGUMENTS --json
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
If `moshcode` is not installed, fall back to the API directly:
|
package/src/advisor.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `moshcode
|
|
1
|
+
// `moshcode stocks` — equity research from advis0r.com, in the pit.
|
|
2
2
|
//
|
|
3
3
|
// Same split as src/trade.mjs: argument translation is pure and testable, the
|
|
4
4
|
// network call is injectable, and rendering is a function of the decoded JSON.
|
|
@@ -18,7 +18,7 @@ export function advisorBase(env = process.env) {
|
|
|
18
18
|
return (raw || DEFAULT_ADVISOR_URL).replace(/\/+$/, "");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const USAGE = `usage: moshcode
|
|
21
|
+
const USAGE = `usage: moshcode stocks <symbol|verb> [args…]
|
|
22
22
|
|
|
23
23
|
<symbol> the stored research report for one ticker
|
|
24
24
|
report <symbol> same thing, when a symbol looks like a verb
|
|
@@ -40,17 +40,17 @@ const USAGE = `usage: moshcode ticker <symbol|verb> [args…]
|
|
|
40
40
|
Research aid, not advice. Every route is public, read-only, and served from
|
|
41
41
|
stored snapshots — see the generated-at stamp printed with each report.`;
|
|
42
42
|
|
|
43
|
-
export function
|
|
43
|
+
export function stocksUsage() {
|
|
44
44
|
return USAGE;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** Verb names, in help order. cli-schema's
|
|
48
|
-
export const
|
|
47
|
+
/** Verb names, in help order. cli-schema's STOCKS_VERBS must match (drift test). */
|
|
48
|
+
export const STOCKS_VERB_NAMES = [
|
|
49
49
|
"report", "signals", "search", "lookup", "reports", "discover", "tickers", "stats", "open",
|
|
50
50
|
];
|
|
51
51
|
|
|
52
|
-
// Aliases exist because muscle memory differs: `/
|
|
53
|
-
// `/
|
|
52
|
+
// Aliases exist because muscle memory differs: `/stocks news AAPL` and
|
|
53
|
+
// `/stocks quotes AAPL` should not be errors when the intent is obvious.
|
|
54
54
|
const VERB_ALIASES = {
|
|
55
55
|
signal: "signals", news: "signals",
|
|
56
56
|
find: "search", grep: "search", q: "search",
|
|
@@ -66,7 +66,7 @@ const VERB_ALIASES = {
|
|
|
66
66
|
/** Resolve a first argument to a canonical verb, or null when it is a symbol. */
|
|
67
67
|
export function resolveVerb(word) {
|
|
68
68
|
const key = String(word ?? "").toLowerCase();
|
|
69
|
-
if (
|
|
69
|
+
if (STOCKS_VERB_NAMES.includes(key)) return key;
|
|
70
70
|
return VERB_ALIASES[key] ?? null;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -114,13 +114,13 @@ function positiveInt(value, { max }) {
|
|
|
114
114
|
const SORTS = ["recent", "score", "ticker"];
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* Translate `
|
|
117
|
+
* Translate `stocks` arguments into a request the caller can execute.
|
|
118
118
|
*
|
|
119
119
|
* Returns one of `{ usage }`, `{ error }`, or
|
|
120
120
|
* `{ verb, path, query, json, open? }` — never performs IO, so the whole
|
|
121
121
|
* argument surface is testable without a network.
|
|
122
122
|
*/
|
|
123
|
-
export function
|
|
123
|
+
export function stocksArgs(input = []) {
|
|
124
124
|
const args = input.map(String);
|
|
125
125
|
const jsonFlag = takeFlag(args, "--json", { boolean: true });
|
|
126
126
|
let rest = jsonFlag.rest;
|
|
@@ -131,26 +131,26 @@ export function tickerArgs(input = []) {
|
|
|
131
131
|
const horizonFlag = takeFlag(rest, "--horizon"); rest = horizonFlag.rest;
|
|
132
132
|
const providerFlag = takeFlag(rest, "--provider"); rest = providerFlag.rest;
|
|
133
133
|
|
|
134
|
-
if (limitFlag.missing) return { error: "
|
|
135
|
-
if (sortFlag.missing) return { error: `
|
|
136
|
-
if (horizonFlag.missing) return { error: "
|
|
137
|
-
if (providerFlag.missing) return { error: "
|
|
134
|
+
if (limitFlag.missing) return { error: "stocks --limit requires a positive number" };
|
|
135
|
+
if (sortFlag.missing) return { error: `stocks --sort requires one of ${SORTS.join(", ")}` };
|
|
136
|
+
if (horizonFlag.missing) return { error: "stocks --horizon requires 1 or 2" };
|
|
137
|
+
if (providerFlag.missing) return { error: "stocks --provider requires a name" };
|
|
138
138
|
|
|
139
139
|
// A limit above the server's own cap is silently clamped there; clamping here
|
|
140
140
|
// too keeps `--limit 9999` from reading like a promise the API never made.
|
|
141
141
|
const limit = limitFlag.value == null ? null : positiveInt(limitFlag.value, { max: 50 });
|
|
142
142
|
if (limitFlag.value != null && limit == null) {
|
|
143
|
-
return { error: "
|
|
143
|
+
return { error: "stocks --limit requires a positive number" };
|
|
144
144
|
}
|
|
145
145
|
if (sortFlag.value != null && !SORTS.includes(sortFlag.value.toLowerCase())) {
|
|
146
|
-
return { error: `
|
|
146
|
+
return { error: `stocks --sort must be one of ${SORTS.join(", ")}` };
|
|
147
147
|
}
|
|
148
148
|
if (horizonFlag.value != null && !["1", "2"].includes(String(horizonFlag.value))) {
|
|
149
|
-
return { error: "
|
|
149
|
+
return { error: "stocks --horizon must be 1 or 2" };
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
const stray = rest.find((arg) => arg.startsWith("-") && arg !== "-");
|
|
153
|
-
if (stray) return { error: `unknown
|
|
153
|
+
if (stray) return { error: `unknown stocks flag ${JSON.stringify(stray)}` };
|
|
154
154
|
|
|
155
155
|
const [first, ...tail] = rest;
|
|
156
156
|
if (!first) return { usage: true };
|
|
@@ -158,16 +158,16 @@ export function tickerArgs(input = []) {
|
|
|
158
158
|
const verb = resolveVerb(first);
|
|
159
159
|
const words = verb ? tail : rest;
|
|
160
160
|
|
|
161
|
-
// No verb → the first word is the ticker. `/
|
|
161
|
+
// No verb → the first word is the ticker. `/stocks AAPL` is the headline
|
|
162
162
|
// case and must stay the shortest thing anyone types.
|
|
163
163
|
const wantsReport = verb == null || verb === "report" || verb === "open";
|
|
164
164
|
if (wantsReport) {
|
|
165
165
|
const raw = words[0];
|
|
166
|
-
if (!raw) return { error: `
|
|
166
|
+
if (!raw) return { error: `stocks ${verb === "open" ? "open" : "report"} requires a ticker symbol` };
|
|
167
167
|
const symbol = normalizeSymbol(raw);
|
|
168
168
|
if (!symbol) {
|
|
169
169
|
return {
|
|
170
|
-
error: `${JSON.stringify(String(raw))} is not a ticker symbol — try: moshcode
|
|
170
|
+
error: `${JSON.stringify(String(raw))} is not a ticker symbol — try: moshcode stocks lookup ${String(raw)}`,
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
if (verb === "open") return { verb: "open", symbol, open: `/ticker/${encodeURIComponent(symbol)}`, json };
|
|
@@ -176,16 +176,16 @@ export function tickerArgs(input = []) {
|
|
|
176
176
|
|
|
177
177
|
if (verb === "signals") {
|
|
178
178
|
const symbol = normalizeSymbol(words[0]);
|
|
179
|
-
if (!words[0]) return { error: "
|
|
179
|
+
if (!words[0]) return { error: "stocks signals requires a ticker symbol" };
|
|
180
180
|
if (!symbol) {
|
|
181
|
-
return { error: `${JSON.stringify(String(words[0]))} is not a ticker symbol — try: moshcode
|
|
181
|
+
return { error: `${JSON.stringify(String(words[0]))} is not a ticker symbol — try: moshcode stocks lookup ${words[0]}` };
|
|
182
182
|
}
|
|
183
183
|
return { verb, symbol, path: "/api/signals", query: { ticker: symbol }, json };
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
if (verb === "search" || verb === "lookup") {
|
|
187
187
|
const q = words.join(" ").trim();
|
|
188
|
-
if (!q) return { error: `
|
|
188
|
+
if (!q) return { error: `stocks ${verb} requires something to look for` };
|
|
189
189
|
const path = verb === "search" ? "/api/search" : "/api/lookup";
|
|
190
190
|
return { verb, path, query: { q, ...(limit ? { limit: String(limit) } : {}) }, json };
|
|
191
191
|
}
|
|
@@ -221,7 +221,7 @@ export function tickerArgs(input = []) {
|
|
|
221
221
|
if (verb === "tickers") return { verb, path: "/api/tickers", query: {}, json };
|
|
222
222
|
if (verb === "stats") return { verb, path: "/api/stats", query: {}, json };
|
|
223
223
|
|
|
224
|
-
return { error: `unknown
|
|
224
|
+
return { error: `unknown stocks command ${JSON.stringify(String(first))}` };
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/** Build the absolute URL for a translated request. */
|
|
@@ -248,7 +248,7 @@ export async function fetchAdvisor(request, { fetchImpl = globalThis.fetch, base
|
|
|
248
248
|
try {
|
|
249
249
|
const res = await fetchImpl(url, {
|
|
250
250
|
signal: controller.signal,
|
|
251
|
-
headers: { accept: "application/json", "user-agent": "moshcode-
|
|
251
|
+
headers: { accept: "application/json", "user-agent": "moshcode-stocks" },
|
|
252
252
|
});
|
|
253
253
|
const text = await res.text();
|
|
254
254
|
let data;
|
|
@@ -468,7 +468,7 @@ function renderLookup(d) {
|
|
|
468
468
|
const report = m.hasReport ? acid(" ✓ report") : ash(" · no report yet");
|
|
469
469
|
lines.push(` ${acid(String(m.symbol).padEnd(8))}${bone(clip(m.name, 44).padEnd(46))}${ash(String(m.exchange ?? ""))}${report}`);
|
|
470
470
|
}
|
|
471
|
-
lines.push("", ` ${ash("then:")} ${bone(`moshcode
|
|
471
|
+
lines.push("", ` ${ash("then:")} ${bone(`moshcode stocks ${matches[0].symbol}`)}`);
|
|
472
472
|
return lines.join("\n");
|
|
473
473
|
}
|
|
474
474
|
|
|
@@ -484,7 +484,7 @@ function renderReports(d) {
|
|
|
484
484
|
`${bone(money(r.lastPrice).padStart(11))} ${ash(clip(r.companyName, 28).padEnd(29))}${ash(day(r.generatedAt))}`,
|
|
485
485
|
);
|
|
486
486
|
}
|
|
487
|
-
lines.push("", ` ${ash("detail:")} ${bone(`moshcode
|
|
487
|
+
lines.push("", ` ${ash("detail:")} ${bone(`moshcode stocks ${reports[0].ticker}`)}`);
|
|
488
488
|
return lines.join("\n");
|
|
489
489
|
}
|
|
490
490
|
|
|
@@ -550,12 +550,12 @@ export function renderAdvisor(verb, data, { columns } = {}) {
|
|
|
550
550
|
}
|
|
551
551
|
|
|
552
552
|
/**
|
|
553
|
-
* Run a `
|
|
553
|
+
* Run a `stocks` invocation end to end. Returns a process exit code.
|
|
554
554
|
*
|
|
555
555
|
* `deps` exists so tests drive the whole command — parse, fetch, render — with
|
|
556
556
|
* no network and no stdout.
|
|
557
557
|
*/
|
|
558
|
-
export async function
|
|
558
|
+
export async function stocksCommand(argv = [], deps = {}) {
|
|
559
559
|
const {
|
|
560
560
|
out = (s) => console.log(s),
|
|
561
561
|
fail = (s) => console.error(s),
|
|
@@ -565,8 +565,8 @@ export async function tickerCommand(argv = [], deps = {}) {
|
|
|
565
565
|
columns = process.stdout.columns,
|
|
566
566
|
} = deps;
|
|
567
567
|
|
|
568
|
-
const request =
|
|
569
|
-
if (request.usage) { out(
|
|
568
|
+
const request = stocksArgs(argv);
|
|
569
|
+
if (request.usage) { out(stocksUsage()); return 0; }
|
|
570
570
|
if (request.error) { fail(danger(`✗ ${request.error}`)); return 1; }
|
|
571
571
|
|
|
572
572
|
if (request.verb === "open") {
|
|
@@ -587,7 +587,7 @@ export async function tickerCommand(argv = [], deps = {}) {
|
|
|
587
587
|
if (request.json) { out(JSON.stringify(res.data, null, 2)); return 1; }
|
|
588
588
|
fail(danger(`✗ ${message}`));
|
|
589
589
|
if (res.data?.didYouMean?.symbol) {
|
|
590
|
-
fail(` ${ash("try:")} ${bone(`moshcode
|
|
590
|
+
fail(` ${ash("try:")} ${bone(`moshcode stocks ${res.data.didYouMean.symbol}`)}`);
|
|
591
591
|
}
|
|
592
592
|
return 1;
|
|
593
593
|
}
|
package/src/cli-schema.mjs
CHANGED
|
@@ -290,14 +290,14 @@ export const CORE_CLI_COMMANDS = [
|
|
|
290
290
|
note: "buy/sell inject --dry-run unless --submit is present. Alpaca defaults to paper trading; live trading requires its separate --live opt-in.",
|
|
291
291
|
},
|
|
292
292
|
{
|
|
293
|
-
name: "
|
|
293
|
+
name: "stocks",
|
|
294
294
|
group: "tools",
|
|
295
295
|
description: "equity research from advis0r.com",
|
|
296
296
|
synopsis: [
|
|
297
|
-
["moshcode
|
|
298
|
-
["moshcode
|
|
297
|
+
["moshcode stocks <symbol>", "the stored research report for one ticker"],
|
|
298
|
+
["moshcode stocks <verb> [args…]", ""],
|
|
299
299
|
],
|
|
300
|
-
verbs: "
|
|
300
|
+
verbs: "STOCKS_VERBS",
|
|
301
301
|
flags: [
|
|
302
302
|
["--json", "print the raw API response", ""],
|
|
303
303
|
["--limit <n>", "cap results (search/lookup/reports/discover)", "the API's own default"],
|
|
@@ -306,16 +306,16 @@ export const CORE_CLI_COMMANDS = [
|
|
|
306
306
|
["--provider <p>", "discover: analysis provider", "offline"],
|
|
307
307
|
],
|
|
308
308
|
examples: [
|
|
309
|
-
["moshcode
|
|
310
|
-
["moshcode
|
|
311
|
-
["moshcode
|
|
312
|
-
["moshcode
|
|
313
|
-
["moshcode
|
|
309
|
+
["moshcode stocks NVDA", "score, technicals, thesis, signals, sources"],
|
|
310
|
+
["moshcode stocks lookup rivian", "company name → RIVN"],
|
|
311
|
+
["moshcode stocks signals AAPL", "what was actually said, with sources"],
|
|
312
|
+
["moshcode stocks search 'data center'", "across every indexed transcript"],
|
|
313
|
+
["moshcode stocks reports --limit 10", "the stored index, best score first"],
|
|
314
314
|
],
|
|
315
315
|
seeAlso: ["trade", "plugin", "tools"],
|
|
316
316
|
note: "research aid, not advice — reports are stored snapshots and every one prints when it was generated. Set MOSHCODE_ADVISOR_URL to point at another instance.",
|
|
317
317
|
},
|
|
318
|
-
{ name: "advisor", aliasOf: "
|
|
318
|
+
{ name: "advisor", aliasOf: "stocks", description: "alias for stocks" },
|
|
319
319
|
{
|
|
320
320
|
name: "crypto",
|
|
321
321
|
group: "tools",
|
|
@@ -342,7 +342,7 @@ export const CORE_CLI_COMMANDS = [
|
|
|
342
342
|
["moshcode crypto bars ETH --timeframe 1Hour", "historical OHLCV"],
|
|
343
343
|
["moshcode crypto book BTC-USD --depth 5", "top of book, both sides"],
|
|
344
344
|
],
|
|
345
|
-
seeAlso: ["
|
|
345
|
+
seeAlso: ["stocks", "trade", "plugin"],
|
|
346
346
|
note: "research aid, not advice — prices are Alpaca's US crypto venue alone and can differ materially from other exchanges. Crypto trades 24/7 with no circuit breakers. Set MOSHCODE_ADVISOR_URL to point at another instance.",
|
|
347
347
|
},
|
|
348
348
|
{ name: "coins", aliasOf: "crypto", description: "alias for crypto" },
|
|
@@ -357,7 +357,7 @@ export const CORE_CLI_COMMANDS = [
|
|
|
357
357
|
["moshcode plugin install", "add the marketplace and install ticker"],
|
|
358
358
|
["moshcode plugin list", "what this marketplace ships, and what is installed"],
|
|
359
359
|
],
|
|
360
|
-
seeAlso: ["skill", "mcp", "
|
|
360
|
+
seeAlso: ["skill", "mcp", "stocks"],
|
|
361
361
|
note: "Claude Code is the only engine with a plugin primitive; the others are reported as skipped, exactly as they are for skills.",
|
|
362
362
|
},
|
|
363
363
|
{ name: "plugins", aliasOf: "plugin", description: "alias for plugin" },
|
|
@@ -539,42 +539,42 @@ export const DNS_VERBS = [
|
|
|
539
539
|
];
|
|
540
540
|
|
|
541
541
|
/**
|
|
542
|
-
* `
|
|
542
|
+
* `stocks`'s verbs.
|
|
543
543
|
*
|
|
544
544
|
* `report` exists so a symbol that collides with a verb name still has an
|
|
545
545
|
* unambiguous spelling; without it, the bare-symbol shortcut would have no
|
|
546
546
|
* escape hatch. src/advisor.mjs owns the parser and test/advisor.test.mjs
|
|
547
547
|
* fails when the two lists disagree.
|
|
548
548
|
*/
|
|
549
|
-
export const
|
|
550
|
-
{ name: "report", description: "the stored research report for one ticker", synopsis: [["moshcode
|
|
551
|
-
{ name: "signals", description: "every extracted signal for a ticker", synopsis: [["moshcode
|
|
549
|
+
export const STOCKS_VERBS = [
|
|
550
|
+
{ name: "report", description: "the stored research report for one ticker", synopsis: [["moshcode stocks report <symbol>", "same as `moshcode stocks <symbol>`"]] },
|
|
551
|
+
{ name: "signals", description: "every extracted signal for a ticker", synopsis: [["moshcode stocks signals <symbol>", ""]] },
|
|
552
552
|
{
|
|
553
553
|
name: "search", description: "full-text search across indexed transcripts",
|
|
554
|
-
synopsis: [["moshcode
|
|
554
|
+
synopsis: [["moshcode stocks search <words…> [--limit n]", ""]],
|
|
555
555
|
},
|
|
556
556
|
{
|
|
557
557
|
name: "lookup", description: "find a ticker by company name",
|
|
558
|
-
synopsis: [["moshcode
|
|
558
|
+
synopsis: [["moshcode stocks lookup <company…> [--limit n]", "rivian → RIVN"]],
|
|
559
559
|
},
|
|
560
560
|
{
|
|
561
561
|
name: "reports", description: "every stored report",
|
|
562
|
-
synopsis: [["moshcode
|
|
562
|
+
synopsis: [["moshcode stocks reports [--sort recent|score|ticker] [--limit n]", ""]],
|
|
563
563
|
},
|
|
564
564
|
{
|
|
565
565
|
name: "discover", description: "a ranked watchlist for a topic",
|
|
566
|
-
synopsis: [["moshcode
|
|
566
|
+
synopsis: [["moshcode stocks discover [topic…] [--horizon 1|2] [--provider p] [--limit n]", ""]],
|
|
567
567
|
note: "ranks by analyzing each candidate — this one takes minutes, not milliseconds.",
|
|
568
568
|
},
|
|
569
|
-
{ name: "tickers", description: "every ticker present in the index", synopsis: [["moshcode
|
|
570
|
-
{ name: "stats", description: "index coverage counts", synopsis: [["moshcode
|
|
571
|
-
{ name: "open", description: "open the shareable report page in a browser", synopsis: [["moshcode
|
|
569
|
+
{ name: "tickers", description: "every ticker present in the index", synopsis: [["moshcode stocks tickers", ""]] },
|
|
570
|
+
{ name: "stats", description: "index coverage counts", synopsis: [["moshcode stocks stats", ""]] },
|
|
571
|
+
{ name: "open", description: "open the shareable report page in a browser", synopsis: [["moshcode stocks open <symbol>", ""]] },
|
|
572
572
|
];
|
|
573
573
|
|
|
574
574
|
/**
|
|
575
575
|
* `crypto`'s verbs.
|
|
576
576
|
*
|
|
577
|
-
* `report` earns its place for the same reason
|
|
577
|
+
* `report` earns its place for the same reason stocks's does — a bare pair is
|
|
578
578
|
* the shortcut, so a pair that collides with a verb name needs a spelling that
|
|
579
579
|
* cannot be mistaken for one. src/crypto.mjs owns the parser and
|
|
580
580
|
* test/crypto.test.mjs fails when the two lists disagree.
|
|
@@ -626,7 +626,7 @@ export const VERB_TABLES = {
|
|
|
626
626
|
UPGRADE_TARGETS,
|
|
627
627
|
DNS_VERBS,
|
|
628
628
|
TRADE_VERBS,
|
|
629
|
-
|
|
629
|
+
STOCKS_VERBS,
|
|
630
630
|
CRYPTO_VERBS,
|
|
631
631
|
PLUGIN_VERBS,
|
|
632
632
|
};
|
|
@@ -655,7 +655,7 @@ export const PIT_COMMANDS = [
|
|
|
655
655
|
description: "list workflow tools, or run one" },
|
|
656
656
|
{ name: "trade", args: "<verb> [args…]", cli: "trade",
|
|
657
657
|
description: "look up markets and preview/place Alpaca orders" },
|
|
658
|
-
{ name: "
|
|
658
|
+
{ name: "stocks", aliases: ["advisor"], args: "<symbol|verb> [args…]", cli: "stocks",
|
|
659
659
|
description: "equity research from advis0r.com" },
|
|
660
660
|
{ name: "crypto", aliases: ["coins"], args: "<pair|verb> [args…]", cli: "crypto",
|
|
661
661
|
description: "crypto market data from advis0r.com" },
|
package/src/crypto.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// function of the decoded JSON. Every route is public and read-only, so there
|
|
6
6
|
// is no login verb and no write verb — `moshcode trade` is where orders live.
|
|
7
7
|
//
|
|
8
|
-
// This is a *sibling* of `
|
|
9
|
-
// different questions from different data. A
|
|
8
|
+
// This is a *sibling* of `stocks`, not a mode of it, because the two answer
|
|
9
|
+
// different questions from different data. A stocks report is a stored snapshot
|
|
10
10
|
// built from transcripts, SEC fundamentals and extracted signals. A crypto
|
|
11
11
|
// report is a live read of Alpaca's US crypto venue: no transcripts, no
|
|
12
12
|
// filings, no signals, and a `fetchedAt` measured in seconds rather than days.
|
|
@@ -50,7 +50,7 @@ export const CRYPTO_VERB_NAMES = [
|
|
|
50
50
|
"report", "quote", "snapshot", "technicals", "bars", "book", "spark", "assets", "lookup", "open",
|
|
51
51
|
];
|
|
52
52
|
|
|
53
|
-
// The same reasoning as
|
|
53
|
+
// The same reasoning as stocks's alias table: `/crypto price BTC` and
|
|
54
54
|
// `/crypto candles BTC` should not be errors when the intent is obvious.
|
|
55
55
|
// `search` maps to lookup rather than erroring — crypto has no transcript
|
|
56
56
|
// index to search, and a directory lookup is what the word means here.
|
|
@@ -83,7 +83,7 @@ export function resolveVerb(word) {
|
|
|
83
83
|
* the API resolves it to that asset's USD pair, and inventing the `-USD` here
|
|
84
84
|
* would silently break the day a base has no USD pair.
|
|
85
85
|
*
|
|
86
|
-
* Deliberately narrow, like
|
|
86
|
+
* Deliberately narrow, like stocks's: the whole job of the check is to tell
|
|
87
87
|
* `BTC` from `bitcoin` and send the second one to lookup with a useful message
|
|
88
88
|
* instead of a 400. Bases run to five characters (SUSHI, MATIC, TRUMP), so six
|
|
89
89
|
* leaves room for the concatenated `BTCUSD` spelling without swallowing words.
|
|
@@ -293,7 +293,7 @@ export function cryptoUrl(request, { base = advisorBase() } = {}) {
|
|
|
293
293
|
* Execute a translated request. `fetchImpl` is injectable for tests.
|
|
294
294
|
*
|
|
295
295
|
* Every crypto route is a live venue read, so one timeout fits all of them —
|
|
296
|
-
* unlike
|
|
296
|
+
* unlike stocks, which has to budget separately for `discover`'s per-candidate
|
|
297
297
|
* analysis.
|
|
298
298
|
*/
|
|
299
299
|
export async function fetchCrypto(request, { fetchImpl = globalThis.fetch, base = advisorBase(), timeoutMs = 45_000 } = {}) {
|
package/src/help.mjs
CHANGED
|
@@ -103,6 +103,18 @@ function distance(a, b) {
|
|
|
103
103
|
return prev[b.length];
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Commands that were renamed, and what they became.
|
|
108
|
+
*
|
|
109
|
+
* Edit distance cannot help here — `ticker` is nowhere near `stocks` — so a
|
|
110
|
+
* rename that isn't recorded leaves people with a bare "unknown command" for a
|
|
111
|
+
* verb that worked yesterday. This is deliberately *not* an alias table: the
|
|
112
|
+
* old name stays dead, it just says where its replacement went.
|
|
113
|
+
*/
|
|
114
|
+
export const RENAMED_COMMANDS = {
|
|
115
|
+
ticker: "stocks",
|
|
116
|
+
};
|
|
117
|
+
|
|
106
118
|
/**
|
|
107
119
|
* The nearest command to something that is not one, or null.
|
|
108
120
|
*
|
|
@@ -114,6 +126,8 @@ function distance(a, b) {
|
|
|
114
126
|
export function suggest(input, extra = []) {
|
|
115
127
|
const typed = String(input ?? "").toLowerCase();
|
|
116
128
|
if (!typed) return null;
|
|
129
|
+
// A known rename beats any distance match — it is not a guess.
|
|
130
|
+
if (RENAMED_COMMANDS[typed]) return RENAMED_COMMANDS[typed];
|
|
117
131
|
const candidates = [...CORE_CLI_COMMANDS.map((c) => c.name), ...extra]
|
|
118
132
|
.filter((n) => !n.startsWith("-"));
|
|
119
133
|
|
package/src/integrations.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./skills.mjs";
|
|
11
11
|
import {
|
|
12
12
|
MARKETPLACE_NAME, PLUGINS, PLUGIN_ENGINES, marketplaceSource, planPluginCommand,
|
|
13
|
-
pluginId, resolvePlugin, runPluginCommand,
|
|
13
|
+
pluginId, resolvePlugin, resolveRetiredPlugin, runPluginCommand,
|
|
14
14
|
} from "./plugins.mjs";
|
|
15
15
|
import { catalogList, resolveCatalog } from "./mcp-catalog.mjs";
|
|
16
16
|
import { MCP_VERBS, PLUGIN_VERBS, SKILL_VERBS } from "./cli-schema.mjs";
|
|
@@ -315,10 +315,22 @@ export async function pluginCommand(tokens, { run, installedSet } = {}) {
|
|
|
315
315
|
const stray = rest.find((t) => String(t).startsWith("-"));
|
|
316
316
|
if (stray) { console.log(err(`unknown plugin flag "${stray}"`)); return 1; }
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
let plugin = resolvePlugin(rest[0]);
|
|
319
319
|
if (!plugin) {
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
// A retired name still has to be removable: the old plugin is installed in
|
|
321
|
+
// someone's engine right now, and installing its replacement puts a second
|
|
322
|
+
// copy of the same slash commands beside it rather than replacing it.
|
|
323
|
+
const retired = resolveRetiredPlugin(rest[0]);
|
|
324
|
+
if (retired && verb === "remove") {
|
|
325
|
+
plugin = { name: retired.name, description: `retired — renamed to ${retired.renamedTo}`, commands: [] };
|
|
326
|
+
} else if (retired) {
|
|
327
|
+
console.log(err(`"${retired.name}" is now "${retired.renamedTo}" — install ${bone(pluginId(retired.renamedTo))}`));
|
|
328
|
+
console.log(info(`already have the old one? ${bone(`moshcode plugin remove ${retired.name}`)} first`));
|
|
329
|
+
return 1;
|
|
330
|
+
} else {
|
|
331
|
+
console.log(err(`unknown plugin "${rest[0]}" — this marketplace ships ${PLUGINS.map((p) => p.name).join(", ")}`));
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
322
334
|
}
|
|
323
335
|
|
|
324
336
|
const source = marketplaceSource();
|
package/src/plugins.mjs
CHANGED
|
@@ -29,9 +29,9 @@ export function marketplaceSource(env = process.env) {
|
|
|
29
29
|
/** The plugins this marketplace ships. Mirrors .claude-plugin/marketplace.json. */
|
|
30
30
|
export const PLUGINS = [
|
|
31
31
|
{
|
|
32
|
-
name: "
|
|
32
|
+
name: "stocks",
|
|
33
33
|
description: "equity research slash commands backed by advis0r.com",
|
|
34
|
-
commands: ["/
|
|
34
|
+
commands: ["/stocks", "/signals", "/research", "/lookup", "/reports", "/discover"],
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
name: "crypto",
|
|
@@ -42,12 +42,38 @@ export const PLUGINS = [
|
|
|
42
42
|
|
|
43
43
|
export const DEFAULT_PLUGIN = PLUGINS[0].name;
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Plugins this marketplace no longer ships, and what replaced them.
|
|
47
|
+
*
|
|
48
|
+
* A renamed plugin is not the same problem as a renamed command. The old
|
|
49
|
+
* command simply stops existing; an old *plugin* is still sitting installed in
|
|
50
|
+
* someone's engine, still serving its slash commands, and the new one installs
|
|
51
|
+
* alongside it rather than over it — so `/stocks` would resolve to two plugins
|
|
52
|
+
* at once. Removal therefore has to keep reaching a name that install refuses.
|
|
53
|
+
*/
|
|
54
|
+
export const RETIRED_PLUGINS = [
|
|
55
|
+
{ name: "ticker", renamedTo: "stocks" },
|
|
56
|
+
];
|
|
57
|
+
|
|
45
58
|
export function resolvePlugin(name) {
|
|
46
59
|
if (!name) return PLUGINS.find((p) => p.name === DEFAULT_PLUGIN) ?? null;
|
|
47
60
|
const key = String(name).toLowerCase().replace(/@.*$/, "");
|
|
48
61
|
return PLUGINS.find((p) => p.name === key) ?? null;
|
|
49
62
|
}
|
|
50
63
|
|
|
64
|
+
/**
|
|
65
|
+
* A retired plugin by its old name, or null.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately separate from resolvePlugin: `remove ticker` must work so the
|
|
68
|
+
* stale install can be cleaned up, and `install ticker` must not, or the rename
|
|
69
|
+
* never actually happens.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveRetiredPlugin(name) {
|
|
72
|
+
if (!name) return null;
|
|
73
|
+
const key = String(name).toLowerCase().replace(/@.*$/, "");
|
|
74
|
+
return RETIRED_PLUGINS.find((p) => p.name === key) ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
/** Fully-qualified plugin id, the form `claude plugin install` disambiguates with. */
|
|
52
78
|
export function pluginId(name) {
|
|
53
79
|
return `${name}@${MARKETPLACE_NAME}`;
|
package/src/tui.mjs
CHANGED
|
@@ -20,12 +20,12 @@ import { fetchMotdAd } from "./ads.mjs";
|
|
|
20
20
|
import { runScript } from "./runtime.mjs";
|
|
21
21
|
import { moshVocabulary } from "./commands.mjs";
|
|
22
22
|
import { mcpCommand, pluginCommand, skillCommand } from "./integrations.mjs";
|
|
23
|
-
import {
|
|
23
|
+
import { stocksCommand } from "./advisor.mjs";
|
|
24
24
|
import { cryptoCommand } from "./crypto.mjs";
|
|
25
25
|
import { canOpenBrowser, openBrowser } from "./open-url.mjs";
|
|
26
26
|
import { banner, hr, acid, ash, bone, dim, ok, err, warn, info, moshcodeVersion } from "./ui.mjs";
|
|
27
27
|
import { CORE_CLI_COMMAND_NAMES } from "./cli-schema.mjs";
|
|
28
|
-
import { findPitCommand, pitHelpModel, renderPitCommand, suggest, wantsHelp } from "./help.mjs";
|
|
28
|
+
import { RENAMED_COMMANDS, findPitCommand, pitHelpModel, renderPitCommand, suggest, wantsHelp } from "./help.mjs";
|
|
29
29
|
import { openNewTab } from "./tabs.mjs";
|
|
30
30
|
|
|
31
31
|
const PROMPT = () => acid("mosh ") + dim("▸ ");
|
|
@@ -673,13 +673,13 @@ export async function tui() {
|
|
|
673
673
|
rl = mkrl();
|
|
674
674
|
continue;
|
|
675
675
|
}
|
|
676
|
-
// `/
|
|
676
|
+
// `/stocks` renders in the pit rather than handing the terminal to a tool:
|
|
677
677
|
// there is no advis0r binary to launch, only a public read-only API.
|
|
678
|
-
if (cmd === "
|
|
679
|
-
await
|
|
678
|
+
if (cmd === "stocks" || cmd === "advisor") {
|
|
679
|
+
await stocksCommand(rest, { openUrl: (url) => canOpenBrowser() && openBrowser(url) });
|
|
680
680
|
continue;
|
|
681
681
|
}
|
|
682
|
-
// `/crypto` renders in the pit for the same reason `/
|
|
682
|
+
// `/crypto` renders in the pit for the same reason `/stocks` does: there is
|
|
683
683
|
// no crypto binary to hand the terminal to, only a public read-only API.
|
|
684
684
|
if (cmd === "crypto" || cmd === "coins") {
|
|
685
685
|
await cryptoCommand(rest, { openUrl: (url) => canOpenBrowser() && openBrowser(url) });
|
|
@@ -722,7 +722,12 @@ export async function tui() {
|
|
|
722
722
|
rl = mkrl();
|
|
723
723
|
continue;
|
|
724
724
|
}
|
|
725
|
-
|
|
725
|
+
// A renamed verb gets pointed at its replacement; `/ticker` was a pit
|
|
726
|
+
// command for a release, so a bare "unknown command" is a dead end here.
|
|
727
|
+
const renamed = RENAMED_COMMANDS[cmd];
|
|
728
|
+
console.log(err(renamed
|
|
729
|
+
? `unknown command "${line}" — /${cmd} is now /${renamed}.`
|
|
730
|
+
: `unknown command "${line}". /help for the list.`));
|
|
726
731
|
}
|
|
727
732
|
|
|
728
733
|
try { rl.close(); } catch { /* noop */ }
|