# Channels `@resonance/channels` is everything that *delivers* the published data: a typed API client, agent tools (MCP server and a dsh seam), the interactive report renderer, and the scheduled e-mail. Contract: DESIGN §12 and §15. The examples below read the public instance, `https://wzznne.github.io/AI-Resonance/api/v1`. Point them at your own deployment (`https://.github.io//api/v1`) or at a local folder to use your own radar. | Path | What | |---|---| | `src/client.ts` | `createClient({ baseUrl } \| { dir })` — typed readers for every `/api/v1` file | | `src/tools.ts` | six framework-agnostic tool descriptors | | `src/mcp.ts` | stdio MCP server over those tools | | `src/dsh.ts` | the same tools as raw JSON-Schema definitions for dsh | | `src/report/` | `renderReport`, `renderEmail`, `buildDailyInput`, `buildWeeklyInput` (export `@resonance/channels/report`) | | `src/mail/` | `gate.ts` (dependency-free "is a mail due?"), `cli.ts` (`pnpm mail`), `send.ts`, `state.ts` | ## 1. API client ```ts import { createClient } from '@resonance/channels' const api = createClient({ baseUrl: 'https://wzznne.github.io/AI-Resonance/api/v1' }) // or { dir: 'packages/web/public/api/v1' } const day = await api.daily() // latest closed edition; api.daily('2026-09-18') for one day const live = await api.live() // the open edition, ranked so far (window.settled = false) const week = await api.weekly() // newest ISO week, or api.weekly('2026-W38') const hist = await api.entity('gh:owner/repo') const mail = await api.mailStatus() // which e-mail slots went out, last outcome (never addresses) ``` Every reader rejects with `ApiError` (`status` 404 for a missing file). Files are cached in memory for five minutes (`ttlMs`), and files written by a newer pipeline (`schema` above the client's) are refused with an explicit message. ## 2. Agent tools All tools return one compact JSON value. Boards: `repos` · `papers` · `news` · `social` (X + Reddit) · `labs` (official AI-company updates). | Tool | Use it for | Arguments | |---|---|---| | `resonance_today` | what is hot today / on a date | `board`, `date` (`YYYY-MM-DD` or `"live"`), `category`, `limit`, `lang`, `includeTrend` | | `resonance_search` | has X been trending? | `query`, `board`, `days`, `minScore`, `limit` | | `resonance_entity` | spike or sustained trend? | `key` (or any GitHub / arXiv / HF / HN / Reddit / X / lab URL), `lang`, `includeTrend` | | `resonance_clusters` | what echoes across sources | `date` | | `resonance_weekly` | what mattered this week | `week`, `lang` | | `resonance_digest` | a ready-to-quote Markdown briefing | `lang` | Outputs carry the edition `window` (timezone, from, to, settled), the brief when the pipeline wrote one, and per item rank, key, title, url, category, blurb, key points, score total + points per signal, board-specific metrics (social: platform, community/handle, likes/comments only when the counts are real; labs: company, kind, surface, date, `fresh`), trend and resonance links. A missing day or week answers `{ found: false, … }` instead of failing. ## 3. MCP server ```sh # Claude Code claude mcp add ai-resonance --env RESONANCE_API=https://wzznne.github.io/AI-Resonance/api/v1 -- node /packages/channels/src/mcp.ts ``` ```json { "mcpServers": { "ai-resonance": { "command": "node", "args": ["/packages/channels/src/mcp.ts"], "env": { "RESONANCE_API": "https://wzznne.github.io/AI-Resonance/api/v1" } } } } ``` `` is the path of a local clone (`git clone https://github.com/WZZNNE/AI-Resonance.git`, then `pnpm i`). The web app's **Settings › Channels** shows both snippets with the site's own API URL filled in, next to the feed, digest, `llms.txt` and `manifest.json` links. `RESONANCE_DIR=` reads a local `/api/v1` folder instead (it wins when both are set). Node ≥ 22.18 runs the TypeScript source directly. Diagnostics go to stderr; stdout belongs to the protocol. ## 4. dsh dsh accepts raw JSON-Schema tool definitions through `ctx.tools.register()`; `createDshTools()` returns exactly that shape (`name`, `description`, `parameters`, `execute(args, exec)` honouring `exec.signal`). The whole plugin: ```ts // resonance-dsh/src/index.ts — an out-of-tree dsh plugin import type { Context } from '@deepseek-ai/cordis' import { createDshTools } from '@resonance/channels/dsh' export const name = 'resonance' export const inject = ['tools'] export function apply(ctx: Context) { for (const tool of createDshTools({ baseUrl: 'https://wzznne.github.io/AI-Resonance/api/v1' })) ctx.tools.register(tool) } ``` Mount it through your profile patch (`$DSH_HOME/profiles//cordis.patch.yml`: `- insert: [{ id: resonance, name: resonance-dsh }]`) with the package resolvable from the profile, e.g. `dsh plugin --profile web add `. Tools unregister when the plugin is disposed. `@resonance/channels` has no dsh dependency. ## 5. Interactive reports ```ts import { buildDailyInput, buildWeeklyInput, renderEmail, renderReport } from '@resonance/channels/report' const input = buildDailyInput(daily, manifest) // or buildWeeklyInput(weekly, dailiesOfThatWeek, manifest) const html = renderReport(input, { lang: 'zh', preliminary: !daily.window.settled }) const mail = renderEmail(input, { lang: 'zh', reportUrl }) // { subject, html, text } ``` Everything under `report/` is pure, DOM-free and dependency-free, so the pipeline (hosted `api/v1/report/..html`), the web app's Export view and the mail job produce identical output from the same data. **Builders.** `buildDailyInput(daily, manifest, { now? })` takes the top N of every board in manifest order, the edition window, brief and clusters. `buildWeeklyInput(weekly, dailies, manifest, { now? })` takes the week's top N per board by weekly heat (N = the board's `size`), resolves each entry to its newest full item from the given editions (entries whose editions were not passed are left out), renumbers ranks, and spans the window from the first to the last given edition (settled only when all seven are there and settled). `generatedAt` defaults to the data's own time, so an unchanged edition renders a byte-identical file. The web app adds `userSummaries` (its cached one-click verdicts) and may filter `sections` before rendering. **Options.** `lang` (initial language; both are embedded), `theme` (`auto` · `light` · `dark`), `perBoard`, `preliminary` (banner for an unsettled edition). **The file.** One `` document: inline CSS, an inline vanilla controller (about 5 KB; a test enforces ≤ 15 KB), and the data as JSON in `