channels.tools 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # channels.tools
2
2
 
3
3
  Channels for the [pi](https://pi.dev) coding agent. Connect **channel
4
- servers** — small processes that push events from the outside world into a
5
- live session and the session becomes reachable: a push **wakes it when
4
+ servers** — small local processes that push events into a live session and
5
+ the session becomes reachable by your other tools: a push **wakes it when
6
6
  idle** and **queues, coalesced, when it's mid-turn**. Each server's tools are
7
7
  registered so the agent can act on what arrived.
8
8
 
9
- Mail buses, code-review watchers, CI, file watchers, webhooks — anything that
10
- can speak the (simple) protocol below can drive an agent session.
9
+ Mail buses, code-review servers, CI and file watchers — anything on your
10
+ machine that can speak the (simple) protocol below can drive an agent
11
+ session. Servers are subprocesses the session itself spawns from your config;
12
+ nothing listens on a network.
11
13
 
12
14
  Site: [channels.tools](https://channels.tools)
13
15
 
@@ -51,8 +53,8 @@ speak this protocol.
51
53
  ## The protocol (`claude/channel` convention)
52
54
 
53
55
  A channel server is a subprocess speaking **newline-delimited JSON-RPC 2.0
54
- over stdio** (no `Content-Length` framing). Originating as an experimental
55
- Claude Code convention, hence the capability name.
56
+ over stdio** (no `Content-Length` framing). `claude/channel` is the
57
+ convention's wire name.
56
58
 
57
59
  1. **Handshake** — client sends `initialize`; the server's result must
58
60
  declare `capabilities.experimental["claude/channel"]` and may include an
@@ -69,11 +71,13 @@ Claude Code convention, hence the capability name.
69
71
  4. **Shutdown** — SIGTERM on session end (SIGKILL after a grace period).
70
72
 
71
73
  The reference server in [`examples/file-channel.js`](examples/file-channel.js)
72
- implements the whole convention in ~60 dependency-free lines: append a line
73
- to a watched file and an idle pi session wakes with it.
74
+ implements the whole convention dependency-free (the server itself is ~60
75
+ lines; the rest of the file is the demo): append a line to a watched file and
76
+ an idle pi session wakes with it.
74
77
 
75
78
  ```bash
76
- node examples/file-channel.js --demo # prints the wire exchange, no pi needed
79
+ node examples/file-channel.js --demo # narrated walkthrough, no pi needed
80
+ node examples/file-channel.js --demo --wire # the same exchange as raw wire frames
77
81
  ```
78
82
 
79
83
  ## Test
@@ -5,12 +5,14 @@
5
5
  // session as a channel event. Declares one tool (file_channel_status) so the
6
6
  // tool-proxy path is exercised too.
7
7
  //
8
- // node file-channel.js <path> run as a channel server (pi spawns this)
9
- // node file-channel.js --demo print the wire exchange, no pi needed
8
+ // node file-channel.js <path> run as a channel server (pi spawns this)
9
+ // node file-channel.js --demo narrated walkthrough, no pi needed
10
+ // node file-channel.js --demo --wire the same exchange as raw wire frames
10
11
  //
11
12
  // Wire format: newline-delimited JSON-RPC 2.0 over stdio.
12
13
 
13
14
  import { openSync, readSync, fstatSync, watchFile, closeSync } from "node:fs";
15
+ import { setTimeout as sleep } from "node:timers/promises";
14
16
 
15
17
  const NOTIFICATION = "notifications/claude/channel";
16
18
  const CAPABILITY = "claude/channel";
@@ -22,13 +24,69 @@ if (!arg || arg === "--help") {
22
24
  }
23
25
 
24
26
  if (arg === "--demo") {
25
- const show = (dir, msg) => console.log(`${dir} ${JSON.stringify(msg)}`);
26
- show("<-", { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "channels.tools", version: "0.1.0" } } });
27
- show("->", { jsonrpc: "2.0", id: 1, result: { capabilities: { experimental: { [CAPABILITY]: {} } }, instructions: "Lines appended to the watched file arrive as channel events." } });
28
- show("<-", { jsonrpc: "2.0", method: "notifications/initialized" });
29
- show("<-", { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
30
- show("->", { jsonrpc: "2.0", id: 2, result: { tools: [{ name: "file_channel_status", description: "Report the watched path and lines delivered.", inputSchema: { type: "object", properties: {} } }] } });
31
- show("->", { jsonrpc: "2.0", method: NOTIFICATION, params: { content: "new line appended: hello", meta: { count: 1, source_path: "inbox.txt" } } });
27
+ // One scripted exchange, printable two ways: --wire shows the literal
28
+ // ndjson frames; the default narrates the same frames as a story.
29
+ const frames = {
30
+ init: { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "channels.tools", version: "0.1.0" } } },
31
+ initResult: { jsonrpc: "2.0", id: 1, result: { capabilities: { experimental: { [CAPABILITY]: {} } }, instructions: "Lines appended to the watched file arrive as channel events." } },
32
+ initialized: { jsonrpc: "2.0", method: "notifications/initialized" },
33
+ toolsList: { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} },
34
+ toolsResult: { jsonrpc: "2.0", id: 2, result: { tools: [{ name: "file_channel_status", description: "Report the watched path and lines delivered.", inputSchema: { type: "object", properties: {} } }] } },
35
+ event1: { jsonrpc: "2.0", method: NOTIFICATION, params: { content: "deploy finished: api v2.4.1 is live", meta: { count: 1, source_path: "inbox.txt" } } },
36
+ event3: { jsonrpc: "2.0", method: NOTIFICATION, params: { content: "tests passed: 214/214\nreview approved: PR #87\nnew issue filed: login flake", meta: { count: 3, source_path: "inbox.txt" } } },
37
+ };
38
+
39
+ if (process.argv.includes("--wire")) {
40
+ for (const [name, frame] of Object.entries(frames)) {
41
+ const dir = frame.method === NOTIFICATION || frame.result ? "->" : "<-";
42
+ console.log(`${dir} ${JSON.stringify(frame)}`);
43
+ void name;
44
+ }
45
+ process.exit(0);
46
+ }
47
+
48
+ const tty = process.stdout.isTTY;
49
+ const paint = (code, s) => (tty ? `\x1b[${code}m${s}\x1b[0m` : s);
50
+ const dim = (s) => paint(2, s);
51
+ const cyan = (s) => paint(36, s);
52
+ const bold = (s) => paint(1, s);
53
+ const green = (s) => paint(32, s);
54
+ const rule = (title) => console.log(`\n${dim("──")} ${bold(title)} ${dim("─".repeat(Math.max(2, 56 - title.length)))}`);
55
+ const say = (s) => console.log(s);
56
+ const pause = () => (tty ? sleep(650) : Promise.resolve());
57
+
58
+ say(`${bold("file-channel")} ${dim("·")} a channel server: one file in, channel events out`);
59
+ say(dim("the same exchange as raw wire frames: --demo --wire"));
60
+
61
+ rule("1 · handshake — the session spawns this server");
62
+ await pause();
63
+ say(` ${cyan("→")} initialize`);
64
+ say(` ${cyan("←")} capabilities.experimental[${green('"claude/channel"')}] ${green("✓")}`);
65
+ say(` ${dim("instructions:")} "Lines appended to the watched file arrive as channel events."`);
66
+ await pause();
67
+ say(` ${cyan("→")} tools/list`);
68
+ say(` ${cyan("←")} 1 tool: ${bold("file_channel_status")} ${dim("— registered with the agent, callable by name")}`);
69
+
70
+ rule("2 · something happens on your machine");
71
+ await pause();
72
+ say(` ${dim("$")} echo ${green('"deploy finished: api v2.4.1 is live"')} >> inbox.txt`);
73
+ await pause();
74
+ say(` ${cyan("←")} notifications/claude/channel ${dim("(count 1, source inbox.txt)")}`);
75
+ say(` ${bold("deploy finished: api v2.4.1 is live")}`);
76
+ say(` ${dim("the session is idle — this push wakes it and starts a turn")}`);
77
+
78
+ rule("3 · three more land while the agent is busy");
79
+ await pause();
80
+ say(` ${dim("$")} echo ... >> inbox.txt ${dim("×3, mid-turn")}`);
81
+ await pause();
82
+ say(` ${cyan("←")} notifications/claude/channel ${dim("(count 3 — one push, coalesced)")}`);
83
+ for (const line of frames.event3.params.content.split("\n")) say(` ${bold(line)}`);
84
+ say(` ${dim("buffered while the turn ran; delivered together when it settled")}`);
85
+
86
+ rule("try it live");
87
+ say(` add to ${cyan("~/.pi/agent/channels.json")}:`);
88
+ say(` { "channelServers": { "file": { "command": "node", "args": ["${dim("/path/to/")}file-channel.js", "inbox.txt"] } } }`);
89
+ say(` then, with the session idle: ${dim("$")} echo hi >> inbox.txt ${dim("— and watch it wake")}\n`);
32
90
  process.exit(0);
33
91
  }
34
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channels.tools",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Channels for the pi coding agent: connect channel servers that push events into a live session — wake it when idle, queue when busy — and proxy their tools. A client for the claude/channel convention.",
5
5
  "type": "module",
6
6
  "license": "MIT",