Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,18 +376,25 @@ moshcode plugin install crypto # add the marketplace + install `crypto`
moshcode plugin remove stocks # take it back off
```

`stocks@moshcode` adds `/stocks`, `/signals`, `/research`, `/lookup`,
`/reports`, and `/discover` — the same advis0r research surface described above,
`stocks@moshcode` adds `/stocks:stocks`, `/stocks:signals`, `/stocks:research`,
`/stocks:lookup`, `/stocks:reports`, and `/stocks:discover` — the same advis0r research surface described above,
driven from inside a coding session.

`crypto@moshcode` adds `/crypto`, `/quote`, `/book`, `/bars`, `/spark`,
`/pairs`, and `/coin`. It ships separately because it is a different surface,
`crypto@moshcode` adds `/crypto:crypto`, `/crypto:quote`, `/crypto:book`,
`/crypto:bars`, `/crypto:spark`, `/crypto:pairs`, and `/crypto:coin`. It ships separately because it is a different surface,
not a mode of the first: live venue reads instead of stored snapshots, and a
score that must not be ranked against an equity's.

Restart the engine after installing either; a newly installed plugin is not live
in a session that is already running.

Plugin commands are namespaced `/<plugin>:<command>` — always, not only when two
plugins collide — so it is `/stocks:signals AAPL`, and a bare `/signals` answers
`Unknown command`. Typing `/` and picking from the menu inserts the right form.
This is the one place the two surfaces differ: inside the moshcode pit the same
research is plain `/stocks …` and `/crypto …`, because those are moshcode's own
commands rather than a plugin's.

The equivalent by hand:

```sh
Expand All @@ -405,7 +412,7 @@ any marketplace". `moshcode plugin install` runs both steps for you.
### Upgrading from `ticker@moshcode`

`stocks` was called `ticker` before v0.29.0. Installing the new id does **not**
replace the old one — engines install plugins side by side, so `/stocks` would
replace the old one — engines install plugins side by side, so `/stocks:stocks` would
come from two plugins at once. Remove the old id first:

```sh
Expand Down
23 changes: 14 additions & 9 deletions plugins/crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ across Alpaca's US crypto venue.

| command | what it does |
| --- | --- |
| `/crypto BTC` | price, technicals, score, supply, order book |
| `/quote ETH-USD` | latest trade and quote, with the spread in bps |
| `/book BTC-USD` | top of the order book, both sides |
| `/bars ETH-USD` | historical OHLCV at any supported timeframe |
| `/spark BTC ETH SOL` | recent moves across pairs, ranked |
| `/pairs` | every supported pair, grouped by quote asset |
| `/coin bitcoin` | asset name → `BTC/USD` |
| `/crypto:crypto BTC` | price, technicals, score, supply, order book |
| `/crypto:quote ETH-USD` | latest trade and quote, with the spread in bps |
| `/crypto:book BTC-USD` | top of the order book, both sides |
| `/crypto:bars ETH-USD` | historical OHLCV at any supported timeframe |
| `/crypto:spark BTC ETH SOL` | recent moves across pairs, ranked |
| `/crypto:pairs` | every supported pair, grouped by quote asset |
| `/crypto:coin bitcoin` | asset name → `BTC/USD` |

Pairs are accepted as `BTC`, `BTC-USD`, `BTC/USD` or `BTCUSD`. A bare asset
resolves to that asset's USD pair.

The `crypto:` prefix is not optional. Claude Code namespaces every plugin
command as `/<plugin>:<command>` — always, not only when two plugins collide —
so a bare `/crypto` answers `Unknown command`. Typing `/` and picking from the
menu inserts the right form for you.

## Install

```bash
Expand Down Expand Up @@ -45,9 +50,9 @@ Point the commands at another instance with `MOSHCODE_ADVISOR_URL`.
## Why this is separate from `stocks`

They answer different questions from different data, and share only a hostname.
A `/stocks` report is a **stored snapshot** built from transcripts, SEC
A `/stocks:stocks` report is a **stored snapshot** built from transcripts, SEC
fundamentals and extracted signals — its risk is a stale price read as a live
one. A `/crypto` report is a **live venue read** with no transcripts, no
one. A `/crypto:crypto` report is a **live venue read** with no transcripts, no
filings and no signals — its risk is the opposite: a price that is accurate to
the second and stale by the time you act on it.

Expand Down
2 changes: 1 addition & 1 deletion plugins/crypto/commands/coin.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ Fallback: `curl -sS "https://advis0r.com/api/crypto/lookup?q=<url-encoded>&limit
match is the one they meant.
- No match: say this venue lists no such pair, and do not invent a symbol. The
coin may exist and simply not be listed here — those are different answers.
- Offer `/crypto <PAIR>` for the match you land on.
- Offer `/crypto:crypto <PAIR>` for the match you land on.
4 changes: 2 additions & 2 deletions plugins/crypto/commands/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ resolves to that asset's USD pair.
- Crypto trades 24/7 with no circuit breakers and no market close. Never
describe a crypto price as "at the close" or "premarket".
- State `generatedAt` / `fetchedAt`. This is a live read, so it goes stale in
seconds, not days — the opposite failure mode from a stored `/stocks` report.
seconds, not days — the opposite failure mode from a stored `/stocks:stocks` report.
- The score's liquidity component counts venue-local volume only, so it is
**not comparable** to an equity's score from `/stocks`. Do not rank the two
**not comparable** to an equity's score from `/stocks:stocks`. Do not rank the two
against each other.
- End with the response's own `disclaimer`. This is research, not advice.
- Link the shareable page: `https://advis0r.com/crypto/<PAIR>`.
2 changes: 1 addition & 1 deletion plugins/crypto/commands/pairs.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ URL-safe spelling (`BTC-USD`) used in paths; `symbol` is canonical (`BTC/USD`).
different markets with three different liquidity profiles.
- **`idle` means listed but not currently printing trades** — it is not the same
as unsupported. Show idle pairs, marked, rather than filtering them out.
- If the user was looking for a specific coin, use `/coin <name>` instead of
- If the user was looking for a specific coin, use `/crypto:coin <name>` instead of
scanning this list for them.
- This is the coverage of one venue, not of crypto. A coin missing here is
missing *from Alpaca's US venue*.
2 changes: 1 addition & 1 deletion plugins/crypto/commands/spark.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ Fallback: `curl -sS "https://advis0r.com/api/crypto/sparklines?symbols=BTC-USD,E
- Name the window (`start` → `end`) and the period. "Up 5%" over 24h and over 7d
are different claims.
- Do not extrapolate a trend from 24 points, and do not call a direction
"momentum" without the technicals to back it — `/crypto <PAIR>` has those.
"momentum" without the technicals to back it — `/crypto:crypto <PAIR>` has those.
- End with the response's own `disclaimer`.
24 changes: 16 additions & 8 deletions plugins/stocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,29 @@ and ranked watchlists.

| command | what it does |
| --- | --- |
| `/stocks NVDA` | score, technicals, fundamentals, thesis, signals, sources |
| `/signals AAPL` | what was actually said, quoted and sourced |
| `/research data center` | full-text search across every indexed transcript |
| `/lookup rivian` | company name → `RIVN` |
| `/reports` | every stored report, best score first |
| `/discover fusion` | a ranked watchlist for a topic (slow) |
| `/stocks:stocks NVDA` | score, technicals, fundamentals, thesis, signals, sources |
| `/stocks:signals AAPL` | what was actually said, quoted and sourced |
| `/stocks:research data center` | full-text search across every indexed transcript |
| `/stocks:lookup rivian` | company name → `RIVN` |
| `/stocks:reports` | every stored report, best score first |
| `/stocks:discover fusion` | a ranked watchlist for a topic (slow) |

The `stocks:` prefix is not optional. Claude Code namespaces every plugin
command as `/<plugin>:<command>` — always, not only when two plugins collide —
so a bare `/stocks` answers `Unknown command`. Typing `/` and picking from the
menu inserts the right form for you.

(Inside the moshcode pit itself, `/stocks …` *is* bare — that is moshcode's own
command, not this plugin's.)

## Upgrading from `ticker@moshcode`

This plugin used to be called `ticker`. Both it and its headline command were
renamed so the name says which market it covers, now that `/crypto` sits beside
renamed so the name says which market it covers, now that `/crypto:crypto` sits beside
it.

Installing the new one does **not** replace the old one — engines install
plugins side by side, so `/stocks` would come from two plugins at once. Remove
plugins side by side, so `/stocks:stocks` would come from two plugins at once. Remove
the old id first:

```bash
Expand Down
2 changes: 1 addition & 1 deletion plugins/stocks/commands/lookup.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ Fallback: `curl -sS "https://advis0r.com/api/lookup?q=<url-encoded>&limit=10"`
## Rules

- Show every match with its exchange — "Delta" is an airline and a faucet company.
- Mark which ones have a report, and offer `/stocks <SYMBOL>` for those.
- Mark which ones have a report, and offer `/stocks:stocks <SYMBOL>` for those.
- One unambiguous match: say the symbol and go straight to offering the report.
- No match: say the *directory* has no match, and do not invent a symbol.
2 changes: 1 addition & 1 deletion plugins/stocks/commands/reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ generatedAt }`, and `total` is how many exist.
- **`generatedAt` per row, always.** These are snapshots taken at different
times; a table that hides that reads as one consistent as-of date.
- A row with no `aiProvider` was scored deterministically, not by a model.
- Offer `/stocks <SYMBOL>` for anything worth a closer look.
- Offer `/stocks:stocks <SYMBOL>` for anything worth a closer look.
- This is a coverage list, not a recommendation list. Rank order is score order.
2 changes: 1 addition & 1 deletion plugins/stocks/commands/research.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ hit is a hit — but relevance is not ranked. Read before summarizing.
- Cluster the hits by ticker and say which companies came up, with dates.
- Quote sparingly and attribute each quote to its speaker and ticker.
- If nothing matches, say the *index* has no match — this searches advis0r's
indexed corpus, not the whole web. Suggest `/lookup` if the query looks like
indexed corpus, not the whole web. Suggest `/stocks:lookup` if the query looks like
a company name.
2 changes: 1 addition & 1 deletion src/integrations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ export async function pluginCommand(tokens, { run, installedSet } = {}) {
// A newly installed plugin is not live in an already-running engine, and the
// first thing anyone does is type the slash command and conclude it failed.
if (!anyFailed(results) && verb === "install" && results.some((r) => r.status === "installed")) {
console.log(info(`restart the engine, then try ${acid(`${plugin.commands[0]} NVDA`)}`));
console.log(info(`restart the engine, then try ${acid(plugin.example ?? plugin.commands[0])}`));
}
return anyFailed(results) ? 1 : 0;
}
26 changes: 23 additions & 3 deletions src/plugins.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,40 @@ export function marketplaceSource(env = process.env) {
return String(env.MOSHCODE_PLUGIN_SOURCE || "moshcoder/moshcode").trim() || "moshcoder/moshcode";
}

/** The plugins this marketplace ships. Mirrors .claude-plugin/marketplace.json. */
/**
* The plugins this marketplace ships. Mirrors .claude-plugin/marketplace.json.
*
* `commands` carry their namespace because that is how they are actually
* invoked. Claude Code namespaces every plugin command as
* `/<plugin>:<command>` — always, not only when two plugins collide — so a bare
* `/crypto` is simply not a command, and advertising one sends people to
* "Unknown command: /crypto" on their first try.
* https://code.claude.com/docs/en/plugins
*
* `example` exists because the invitation printed after an install has to be
* runnable. It used to append a hardcoded "NVDA" to whatever came first in
* `commands`, which told anyone installing the crypto plugin to try a stock.
*/
export const PLUGINS = [
{
name: "stocks",
description: "equity research slash commands backed by advis0r.com",
commands: ["/stocks", "/signals", "/research", "/lookup", "/reports", "/discover"],
commands: ["/stocks:stocks", "/stocks:signals", "/stocks:research", "/stocks:lookup", "/stocks:reports", "/stocks:discover"],
example: "/stocks:stocks NVDA",
},
{
name: "crypto",
description: "crypto market data slash commands backed by advis0r.com",
commands: ["/crypto", "/quote", "/book", "/bars", "/spark", "/pairs", "/coin"],
commands: ["/crypto:crypto", "/crypto:quote", "/crypto:book", "/crypto:bars", "/crypto:spark", "/crypto:pairs", "/crypto:coin"],
example: "/crypto:crypto BTC",
},
];

/** How Claude Code namespaces a plugin's command. */
export function pluginCommandName(plugin, file) {
return `/${plugin}:${String(file).replace(/\.md$/, "")}`;
}

export const DEFAULT_PLUGIN = PLUGINS[0].name;

/**
Expand Down
16 changes: 14 additions & 2 deletions test/plugins.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { fileURLToPath } from "node:url";
import { ENGINES } from "../src/engines.mjs";
import {
MARKETPLACE_NAME, PLUGINS, PLUGIN_ENGINES, RETIRED_PLUGINS, marketplaceSource,
planPluginCommand, pluginId, resolvePlugin, resolveRetiredPlugin, runPluginCommand,
planPluginCommand, pluginCommandName, pluginId, resolvePlugin, resolveRetiredPlugin,
runPluginCommand,
} from "../src/plugins.mjs";

const ROOT = fileURLToPath(new URL("..", import.meta.url));
Expand Down Expand Up @@ -144,11 +145,22 @@ test("every plugin the marketplace lists exists, with a manifest and its command

const catalog = PLUGINS.find((p) => p.name === entry.name);
const files = fs.readdirSync(new URL("commands/", dir)).filter((f) => f.endsWith(".md"));
// Namespaced, because that is how Claude Code invokes them. Comparing bare
// filenames is what let the catalog advertise `/crypto` for two releases —
// a name that matched the file on disk and did not exist as a command.
assert.deepEqual(
files.map((f) => `/${f.replace(/\.md$/, "")}`).sort(),
files.map((f) => pluginCommandName(entry.name, f)).sort(),
[...catalog.commands].sort(),
`${entry.name} advertises commands it does not ship`,
);
assert.ok(
catalog.example?.startsWith(`/${entry.name}:`),
`${entry.name}'s example must invoke one of its own namespaced commands`,
);
assert.ok(
catalog.commands.includes(catalog.example.split(" ")[0]),
`${entry.name}'s example invokes a command it does not ship`,
);
}
});

Expand Down
Loading