diff --git a/README.md b/README.md index 6d4f30d..91abf4b 100644 --- a/README.md +++ b/README.md @@ -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 `/:` — 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 @@ -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 diff --git a/plugins/crypto/README.md b/plugins/crypto/README.md index 8598906..a0bd540 100644 --- a/plugins/crypto/README.md +++ b/plugins/crypto/README.md @@ -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 `/:` — 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 @@ -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. diff --git a/plugins/crypto/commands/coin.md b/plugins/crypto/commands/coin.md index 7e17f33..4b8f84f 100644 --- a/plugins/crypto/commands/coin.md +++ b/plugins/crypto/commands/coin.md @@ -28,4 +28,4 @@ Fallback: `curl -sS "https://advis0r.com/api/crypto/lookup?q=&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 ` for the match you land on. +- Offer `/crypto:crypto ` for the match you land on. diff --git a/plugins/crypto/commands/crypto.md b/plugins/crypto/commands/crypto.md index aa3b678..a8f8055 100644 --- a/plugins/crypto/commands/crypto.md +++ b/plugins/crypto/commands/crypto.md @@ -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/`. diff --git a/plugins/crypto/commands/pairs.md b/plugins/crypto/commands/pairs.md index b9b2d1c..cca4375 100644 --- a/plugins/crypto/commands/pairs.md +++ b/plugins/crypto/commands/pairs.md @@ -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 ` instead of +- If the user was looking for a specific coin, use `/crypto:coin ` 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*. diff --git a/plugins/crypto/commands/spark.md b/plugins/crypto/commands/spark.md index 0a1f1a4..5ca5497 100644 --- a/plugins/crypto/commands/spark.md +++ b/plugins/crypto/commands/spark.md @@ -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 ` has those. + "momentum" without the technicals to back it — `/crypto:crypto ` has those. - End with the response's own `disclaimer`. diff --git a/plugins/stocks/README.md b/plugins/stocks/README.md index a79dc5d..c81d6bd 100644 --- a/plugins/stocks/README.md +++ b/plugins/stocks/README.md @@ -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 `/:` — 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 diff --git a/plugins/stocks/commands/lookup.md b/plugins/stocks/commands/lookup.md index 93dec82..8b24985 100644 --- a/plugins/stocks/commands/lookup.md +++ b/plugins/stocks/commands/lookup.md @@ -22,6 +22,6 @@ Fallback: `curl -sS "https://advis0r.com/api/lookup?q=&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 ` for those. +- Mark which ones have a report, and offer `/stocks:stocks ` 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. diff --git a/plugins/stocks/commands/reports.md b/plugins/stocks/commands/reports.md index 862bca0..28632b3 100644 --- a/plugins/stocks/commands/reports.md +++ b/plugins/stocks/commands/reports.md @@ -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 ` for anything worth a closer look. +- Offer `/stocks:stocks ` for anything worth a closer look. - This is a coverage list, not a recommendation list. Rank order is score order. diff --git a/plugins/stocks/commands/research.md b/plugins/stocks/commands/research.md index 3abd48e..a333338 100644 --- a/plugins/stocks/commands/research.md +++ b/plugins/stocks/commands/research.md @@ -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. diff --git a/src/integrations.mjs b/src/integrations.mjs index c26a44e..c23a8ad 100644 --- a/src/integrations.mjs +++ b/src/integrations.mjs @@ -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; } diff --git a/src/plugins.mjs b/src/plugins.mjs index 76523e1..3c51f48 100644 --- a/src/plugins.mjs +++ b/src/plugins.mjs @@ -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 + * `/:` — 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; /** diff --git a/test/plugins.test.mjs b/test/plugins.test.mjs index ab27768..9a808f0 100644 --- a/test/plugins.test.mjs +++ b/test/plugins.test.mjs @@ -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)); @@ -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`, + ); } });