opencode-specstream 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SpecStream SAS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,85 @@
1
- # Temporary Holding Version
1
+ # opencode-specstream
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ SpecStream plugin for [OpenCode](https://opencode.ai). SpecStream is the live web data engine for teams and agents: describe a list in plain words ("roofing companies in Columbus, Ohio with their website and phone") and get every matching row, each with the page it came from. You see the price first, you approve a ceiling, and You pay for each result delivered. A search that finds nothing costs nothing.
4
+
5
+ The plugin does two things when OpenCode starts:
6
+
7
+ - adds the hosted SpecStream MCP server, `https://api.specstream.ai/mcp`, to your config as `specstream`
8
+ - adds four agent skills: `compile` (read the request back), `review-work` (get the price), `approve-work` (approve it) and `continue-work` (read the rows, send them on, keep them current)
9
+
10
+ ## Install
11
+
12
+ OpenCode 1.18.29 or newer, and OpenCode 2:
13
+
14
+ ```json
15
+ {
16
+ "$schema": "https://opencode.ai/config.json",
17
+ "plugin": ["opencode-specstream"]
18
+ }
19
+ ```
20
+
21
+ In OpenCode 2 the key is `plugins`:
22
+
23
+ ```json
24
+ {
25
+ "$schema": "https://opencode.ai/config.json",
26
+ "plugins": ["opencode-specstream"]
27
+ }
28
+ ```
29
+
30
+ OpenCode installs the package on the next launch.
31
+
32
+ ## Sign in
33
+
34
+ The server uses OAuth, so you do not paste a key. Run:
35
+
36
+ ```sh
37
+ opencode mcp auth specstream
38
+ ```
39
+
40
+ Your browser opens, you sign in to SpecStream, and OpenCode keeps the token. New accounts start with $10 of work free for 30 days, no card.
41
+
42
+ To use an API key instead, for example on a machine with no browser, create one under **Agents & API** in the [workspace](https://specstream.ai/workspace) and set it where OpenCode starts:
43
+
44
+ ```sh
45
+ export SPECSTREAM_API_KEY="ss_live_..."
46
+ ```
47
+
48
+ With the variable set, the plugin sends it as a bearer header and turns the browser sign-in off. Give the key only what the task needs: its scope, projects, an expiry and a spending limit.
49
+
50
+ ## Use it
51
+
52
+ Ask for a list:
53
+
54
+ > Find food banks in Texas, with their annual revenue.
55
+
56
+ The agent reads the request back, shows the quoted ceiling, and starts only after you say yes. Rows come back with their source pages, ready to export as CSV, Excel or JSON or to send to Google Sheets, Notion, Airtable, HubSpot or Salesforce.
57
+
58
+ ## Options
59
+
60
+ Both parts are on by default. Turn either off with plugin options:
61
+
62
+ ```json
63
+ {
64
+ "plugin": [["opencode-specstream", { "skills": false }]]
65
+ }
66
+ ```
67
+
68
+ | Option | Default | Effect |
69
+ | --- | --- | --- |
70
+ | `mcp` | `true` | `false` leaves the MCP server out |
71
+ | `skills` | `true` | `false` leaves the four skills out |
72
+
73
+ A `specstream` entry you already wrote under `mcp` is never replaced.
74
+
75
+ ## Develop
76
+
77
+ ```sh
78
+ npm ci
79
+ npm run check # tsc --noEmit
80
+ npm test
81
+ ```
82
+
83
+ The skills in `skills/` are copies of the repository's [`skills/`](../../skills); a test fails when they drift. The package ships its TypeScript source, as OpenCode runs plugins directly.
84
+
85
+ Docs: https://specstream.ai/docs/mcp. Licensed MIT.
package/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { server } from "./v1.ts";
2
+ import { v2 } from "./v2.ts";
3
+
4
+ // One entrypoint for both hosts: OpenCode 1 calls `server()`, OpenCode 2 reads `id` and `setup()`.
5
+ export default { ...v2, server };
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "opencode-specstream",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "SpecStream plugin for OpenCode: adds the SpecStream MCP server and four agent skills, so your agent can ask for a list in plain words and get every matching row with its source.",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "files": [
8
+ "index.ts",
9
+ "v1.ts",
10
+ "v2.ts",
11
+ "shared.ts",
12
+ "skills",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "keywords": [
17
+ "opencode",
18
+ "opencode-plugin",
19
+ "specstream",
20
+ "mcp",
21
+ "lead-lists",
22
+ "web-data",
23
+ "web-search",
24
+ "agent-skills"
25
+ ],
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/specstream/specstream-tools.git",
30
+ "directory": "integrations/opencode"
31
+ },
32
+ "homepage": "https://specstream.ai/docs/mcp",
33
+ "author": {
34
+ "name": "SpecStream",
35
+ "url": "https://specstream.ai"
36
+ },
37
+ "engines": {
38
+ "opencode": "^1.18.29 || ^2.0.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "check": "tsc --noEmit",
45
+ "test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --test test/plugin.test.ts"
46
+ },
47
+ "devDependencies": {
48
+ "@opencode-ai/plugin": "^1.18.34",
49
+ "@opencode/plugin": "^2.0.23",
50
+ "@types/node": "^22.0.0",
51
+ "typescript": "^5.6.0"
52
+ }
53
+ }
package/shared.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export const MCP_NAME = "specstream";
6
+ export const MCP_URL = "https://api.specstream.ai/mcp";
7
+
8
+ export const skillsDir = join(dirname(fileURLToPath(import.meta.url)), "skills");
9
+
10
+ export type Options = { mcp?: boolean; skills?: boolean };
11
+
12
+ export type RemoteServer = {
13
+ type: "remote";
14
+ url: string;
15
+ headers?: Record<string, string>;
16
+ oauth?: false;
17
+ };
18
+
19
+ /** Reads a plugin option that defaults to on. Only an explicit `false` turns it off. */
20
+ export function enabled(options: unknown, key: keyof Options): boolean {
21
+ if (typeof options !== "object" || options === null) return true;
22
+ return (options as Options)[key] !== false;
23
+ }
24
+
25
+ /**
26
+ * The hosted server signs people in through the browser (OAuth), so no key is needed.
27
+ * A SPECSTREAM_API_KEY in the environment switches to a bearer header, for machines with no browser.
28
+ */
29
+ export function remoteServer(env: Record<string, string | undefined> = process.env): RemoteServer {
30
+ const key = env.SPECSTREAM_API_KEY?.trim();
31
+ if (!key) return { type: "remote", url: MCP_URL };
32
+ return { type: "remote", url: MCP_URL, headers: { Authorization: `Bearer ${key}` }, oauth: false };
33
+ }
34
+
35
+ export type Skill = { id: string; name: string; description: string; path: string; content: string };
36
+
37
+ /** Reads one skill file: single-line `name` and `description` frontmatter, then the body. */
38
+ export function parseSkill(file: string): Skill {
39
+ const raw = readFileSync(file, "utf8");
40
+ const fence = raw.match(/^---\n([\s\S]*?)\n---\n?/);
41
+ if (!fence) throw new Error(`${file} has no frontmatter`);
42
+ const field = (key: string) =>
43
+ fence[1].match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^(["'])(.*)\1$/, "$2") ?? "";
44
+ const name = field("name");
45
+ if (!name) throw new Error(`${file} has no name`);
46
+ return { id: name, name, description: field("description"), path: file, content: raw.slice(fence[0].length).trim() };
47
+ }
48
+
49
+ export function bundledSkills(root: string = skillsDir): Skill[] {
50
+ return readdirSync(root, { withFileTypes: true })
51
+ .filter((entry) => entry.isDirectory())
52
+ .map((entry) => parseSkill(join(root, entry.name, "SKILL.md")))
53
+ .sort((a, b) => a.name.localeCompare(b.name));
54
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: approve-work
3
+ description: Approve an exact price, up to the maximum the person set. This starts paid work.
4
+ ---
5
+
6
+ # Approve a price
7
+
8
+ SpecStream is the live web data engine for teams and agents. Use the configured MCP server at `https://api.specstream.ai/mcp`.
9
+
10
+ `authorize_capacity_work` starts paid work, so treat it as a write. It needs the reviewed `quote_id` and `quote_hash`, the approved maximum as an integer in `confirm_max_cents`, and a stable `idempotency_key`. A standing project or agent authority must cover this exact scope and limit. A funded account is not enough.
11
+
12
+ Save the same key before you send the call. If the response is unclear, recover the same work with `get_capacity_request` or `get_capacity_quote`. Never create a new key, price or run just to recover a lost response. A price that expired or changed needs a new approval.
13
+
14
+ Do not top up, subscribe, add a seat or open a payment page. If the balance or the authority is missing, say exactly what is missing and keep the request.
15
+
16
+ Use `get_find` for progress. Rows appear while the run works, and a run can be stopped and keeps what it found. Only delivered matches are charged. Unsure results stay unsure.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: compile
3
+ description: Read a plain-English request back to the person before anything runs. No price, no work.
4
+ ---
5
+
6
+ # Read the request
7
+
8
+ SpecStream is the live web data engine for teams and agents. Use the configured MCP server at `https://api.specstream.ai/mcp`.
9
+
10
+ Use `compile` to read the person's request. Say back what it found: the kind of thing, the conditions, the details to bring back and the quantity. Separate the conditions that must hold from the details that are nice to have. Use `edit_spec` for a deliberate change. Never drop a hard condition quietly, and never treat missing evidence as a no. Reading a request does not start any work.
11
+
12
+ Suggest requests that work well: local businesses with ratings, companies from registries, grants, tenders, clinical trials, papers, datasets, regulations, vulnerabilities, apps, repositories, packages, podcasts and books. Companies that use a tool, funding and new-executive signals, US nonprofits and UK financial firms by FCA status also work. If someone asks for people by job title, say that results may be few. `list_request_templates` returns requests that already work for each kind.
13
+
14
+ The same saved request can be revised, branched, compared and shared, in the workspace or through this agent. Never ask for an API key in chat. Use the connector's own sign-in.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: continue-work
3
+ description: Read the finished rows with the page behind each fact, send them where the person works, and start new work only on purpose.
4
+ ---
5
+
6
+ # Read, send and continue
7
+
8
+ SpecStream is the live web data engine for teams and agents. Use the configured MCP server at `https://api.specstream.ai/mcp`.
9
+
10
+ Read `list_find_rows` and follow each `next_cursor` through the `cursor` argument until it is null. The first page is not the whole result. Open `get_find_decisions` to show each condition check with the value observed, its source and the time it was read. Never invent links or fill in unknown facts. Keep the original read times and do not call reused evidence fresh.
11
+
12
+ When the person asks, `send_to_destination` sends selected results to Google Sheets, Excel, Notion, Airtable, HubSpot, Salesforce or an Outlook draft. An Outlook draft lands in the person's own Drafts folder and is never sent. `export_find` makes a download link as CSV, Excel, JSON or HTML. Reading, exporting and sharing cost nothing.
13
+
14
+ Finding more, adding facts or rechecking is new work with its own reviewed price. A recheck costs half the rate for a row that changed and 1 cent for a row that did not.
15
+
16
+ `create_capacity_schedule` sets up a monitor and is a write. It needs a scope, a cadence, a validity period and limits per run and per period. A monitor reruns on its schedule and labels each change: still matches, no longer matches, unresolved or unavailable. Creating one does not fund it. A blocked or missed run is not an unchanged result and is not bought later. Pause or revoke a monitor when the owner says so.
17
+
18
+ The workspace is the place to explore the full history. Do not present a small result widget as the whole workspace.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: review-work
3
+ description: Get the real price for the exact request. Nothing runs.
4
+ ---
5
+
6
+ # See the price
7
+
8
+ SpecStream is the live web data engine for teams and agents. Use the configured MCP server at `https://api.specstream.ai/mcp`.
9
+
10
+ Call `quote_capacity_work` with the `ast_hash` from `compile`, the operation, the number of results wanted, the useful minimum, and the original `parent_find_id`, `project_id` or `revision_id` when one applies. Asking for more uses `extend`, adding facts uses `add_facts` and rechecking uses `refresh`.
11
+
12
+ Say the scope, the conditions, the maximum price and the expiry in text. Results cost 5 cents, 15 cents or $1 each, plus 5 cents for a person's email and 15 cents for a phone, and nothing when nothing matches. A price lasts 30 minutes. The price does not spend any balance and does not start a job.
13
+
14
+ Use `get_capacity_balance` to read the balance. A balance is not permission to spend. Do not copy the maximum into an approval unless the person said to, or an existing bounded task already allows it. Plans and top-ups live in the person's account, not in chat.
package/v1.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import { MCP_NAME, enabled, remoteServer, skillsDir } from "./shared.ts";
3
+
4
+ /** OpenCode 1: the config hook adds the MCP server and the skills folder. Anything the user already set wins. */
5
+ export const server: Plugin = async (_input, options) => {
6
+ return {
7
+ async config(input) {
8
+ if (enabled(options, "mcp")) {
9
+ input.mcp ??= {};
10
+ input.mcp[MCP_NAME] ??= remoteServer();
11
+ }
12
+ if (enabled(options, "skills")) {
13
+ // The V1 SDK's Config type does not declare skills.paths yet; OpenCode reads it.
14
+ const config = input as typeof input & { skills?: { paths?: string[] } };
15
+ config.skills ??= {};
16
+ config.skills.paths ??= [];
17
+ if (!config.skills.paths.includes(skillsDir)) config.skills.paths.push(skillsDir);
18
+ }
19
+ },
20
+ };
21
+ };
package/v2.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { Plugin } from "@opencode/plugin";
2
+ import { MCP_NAME, bundledSkills, enabled, remoteServer } from "./shared.ts";
3
+
4
+ /**
5
+ * OpenCode 2: the same two additions, through the MCP and skill transforms.
6
+ * Type-only import: OpenCode 1 loads this file too and has no `@opencode/plugin` to resolve.
7
+ */
8
+ export const v2: Plugin.Plugin = {
9
+ id: "specstream",
10
+ async setup(ctx) {
11
+ if (enabled(ctx.options, "mcp")) {
12
+ await ctx.mcp.transform((editor) => {
13
+ if (!editor.get(MCP_NAME)) {
14
+ // The config schema classes are structural; a plain object is accepted at runtime.
15
+ editor.set(MCP_NAME, remoteServer() as never);
16
+ }
17
+ });
18
+ }
19
+ if (enabled(ctx.options, "skills")) {
20
+ const skills = bundledSkills();
21
+ await ctx.skill.transform((editor) => {
22
+ for (const skill of skills) {
23
+ if (!editor.get(skill.id)) editor.add(skill as never);
24
+ }
25
+ });
26
+ }
27
+ },
28
+ };