copilot-tap-extension 1.1.0 → 1.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/bin/install.mjs CHANGED
@@ -40,6 +40,7 @@ Installs:
40
40
  extensions/tap/extension.mjs The bundled ※ tap extension
41
41
  extensions/tap/version.json Installed version metadata
42
42
  skills/loop/SKILL.md The /loop skill for prompt-based loops
43
+ skills/create-provider/SKILL.md The /create-provider skill for scaffolding providers
43
44
  copilot-instructions.md Agent instructions for using ※ tap
44
45
  `);
45
46
  }
@@ -172,6 +173,11 @@ function install(flags) {
172
173
  dest: path.join(targetRoot, "skills", "loop", "SKILL.md"),
173
174
  label: "skills/loop/SKILL.md"
174
175
  },
176
+ {
177
+ src: path.join(distDir, "skills", "create-provider", "SKILL.md"),
178
+ dest: path.join(targetRoot, "skills", "create-provider", "SKILL.md"),
179
+ label: "skills/create-provider/SKILL.md"
180
+ },
175
181
  {
176
182
  src: path.join(distDir, "copilot-instructions.md"),
177
183
  dest: path.join(targetRoot, "copilot-instructions.md"),
@@ -0,0 +1,341 @@
1
+ ---
2
+ name: create-provider
3
+ description: "Create an external tool provider that connects to ※ tap. Use when the user says 'create a provider', 'build a provider', 'scaffold a provider', 'add an external tool', 'connect a service to tap', or wants to extend Copilot with tools written in any language."
4
+ argument-hint: "<what the provider should do>"
5
+ user-invocable: true
6
+ ---
7
+
8
+ Create a provider process that connects to the ※ tap gateway and registers tools with the Copilot session.
9
+
10
+ ## What is a provider?
11
+
12
+ A provider is any external process that speaks the ※ tap WebSocket protocol. It connects to the gateway on `ws://localhost:9400`, authenticates with a token, and registers tools. Copilot can then invoke those tools, and calls are routed through the gateway to the provider.
13
+
14
+ Providers know nothing about the Copilot SDK. They just handle JSON messages over WebSocket.
15
+
16
+ ## Protocol overview
17
+
18
+ ```
19
+ Provider connects → sends auth → receives sessions → sends hello (with tools) → receives hello.ack → BOUND
20
+ ```
21
+
22
+ ### Connection state machine
23
+
24
+ ```
25
+ AwaitAuth ──auth──► AwaitHello ──hello──► Bound ──goodbye/disconnect──► Disconnected
26
+ ```
27
+
28
+ ### Message types (10)
29
+
30
+ | Direction | Type | Purpose |
31
+ |---|---|---|
32
+ | Provider → Gateway | `auth` | First message — send `TAP_PROVIDER_TOKEN` |
33
+ | Gateway → Provider | `sessions` | Available sessions — pick one |
34
+ | Provider → Gateway | `hello` | Register name, protocol version, session, and tools |
35
+ | Gateway → Provider | `hello.ack` | Confirmation — provider is now bound |
36
+ | Gateway → Provider | `tool.call` | Copilot invokes a tool — provider must respond |
37
+ | Provider → Gateway | `tool.result` | Tool response (exactly one per call) |
38
+ | Gateway → Provider | `tool.cancel` | Cancel a pending call — respond with CANCELLED |
39
+ | Gateway → Provider | `session.lifecycle` | Session state changes (started/idle/shutdown.pending) |
40
+ | Gateway → Provider | `error` | Something went wrong |
41
+ | Provider → Gateway | `goodbye` | Clean disconnect |
42
+
43
+ ### Tool definitions
44
+
45
+ Each tool in the `hello` message:
46
+
47
+ ```json
48
+ {
49
+ "name": "tool_name",
50
+ "description": "What the tool does",
51
+ "parameters": {
52
+ "type": "object",
53
+ "properties": {
54
+ "arg1": { "type": "string", "description": "..." }
55
+ },
56
+ "required": ["arg1"]
57
+ }
58
+ }
59
+ ```
60
+
61
+ - `name` (required) — unique, must not conflict with tap tools or other providers
62
+ - `description` (required) — what the tool does
63
+ - `parameters` (required) — JSON Schema for arguments
64
+ - `timeout` (optional) — max execution time in ms
65
+ - Max 100 tools per provider
66
+
67
+ ### Tool results
68
+
69
+ Success:
70
+ ```json
71
+ { "type": "tool.result", "id": "<call-id>", "data": "result string or JSON" }
72
+ ```
73
+
74
+ Failure:
75
+ ```json
76
+ { "type": "tool.result", "id": "<call-id>", "error": "message", "errorCode": "NOT_FOUND" }
77
+ ```
78
+
79
+ Error codes: `NOT_FOUND`, `TIMEOUT`, `CANCELLED`, `INTERNAL`
80
+
81
+ ### Payload limits
82
+
83
+ - `tool.result`: max 5 MB
84
+ - All other messages: max 2 MB
85
+
86
+ ### Error codes from gateway
87
+
88
+ | Code | Fatal? | Meaning |
89
+ |---|---|---|
90
+ | `AUTH_FAILED` | Yes | Bad token — close connection |
91
+ | `UNSUPPORTED_VERSION` | Yes | Wrong protocolVersion — close |
92
+ | `INVALID_SESSION` | No | Unknown session ID |
93
+ | `TOOL_CONFLICT` | No | Tool name already taken |
94
+ | `PAYLOAD_TOO_LARGE` | No | Message exceeds limit |
95
+
96
+ ### Forward compatibility
97
+
98
+ - Ignore unknown fields in incoming messages
99
+ - Ignore unknown gateway→provider message types (log and discard)
100
+
101
+ ## Required behavior
102
+
103
+ When this skill is invoked:
104
+
105
+ 1. **Ask the user** what the provider should do if the input is vague. Get clarity on:
106
+ - What tools should it expose?
107
+ - What does each tool do?
108
+ - What language? (default to Node.js if not specified)
109
+ - Does it need external APIs or libraries?
110
+
111
+ 2. **Generate a complete, runnable provider** that:
112
+ - Reads `TAP_PROVIDER_TOKEN` from the environment
113
+ - Connects to `ws://localhost:9400`
114
+ - Sends `auth` with the token
115
+ - On `sessions`, picks the first session and sends `hello` with tool definitions
116
+ - On `hello.ack`, logs that it's registered
117
+ - On `tool.call`, dispatches to the right handler and sends `tool.result`
118
+ - On `tool.cancel`, responds with `{ error: "Cancelled", errorCode: "CANCELLED" }`
119
+ - On `session.lifecycle` with `shutdown.pending`, sends `goodbye` and closes
120
+ - On `error`, logs it; if fatal (`AUTH_FAILED`, `UNSUPPORTED_VERSION`), closes
121
+ - Ignores unknown message types (forward compatibility)
122
+ - Reconnects on disconnect (with backoff)
123
+
124
+ 3. **Place the file** in `providers/<name>/` at the project root, or wherever the user specifies.
125
+
126
+ 4. **After generating**, tell the user how to run it:
127
+ ```
128
+ # Grab the token from the Copilot session environment:
129
+ echo $TAP_PROVIDER_TOKEN # macOS/Linux
130
+ echo %TAP_PROVIDER_TOKEN% # Windows
131
+
132
+ # Run the provider:
133
+ TAP_PROVIDER_TOKEN=<token> node providers/<name>/index.mjs
134
+ ```
135
+
136
+ 5. **Explain** that once connected, the tools appear in Copilot automatically — no restart needed.
137
+
138
+ ## Templates
139
+
140
+ ### Node.js skeleton
141
+
142
+ ```js
143
+ import WebSocket from "ws";
144
+
145
+ const TOKEN = process.env.TAP_PROVIDER_TOKEN;
146
+ if (!TOKEN) {
147
+ console.error("TAP_PROVIDER_TOKEN not set");
148
+ process.exit(1);
149
+ }
150
+
151
+ const PROVIDER_NAME = "{{name}}";
152
+ const TOOLS = [
153
+ {{#each tools}}
154
+ {
155
+ name: "{{this.name}}",
156
+ description: "{{this.description}}",
157
+ parameters: {
158
+ type: "object",
159
+ properties: { {{this.properties}} },
160
+ required: [{{this.required}}]
161
+ }
162
+ },
163
+ {{/each}}
164
+ ];
165
+
166
+ function handleToolCall(toolName, args) {
167
+ switch (toolName) {
168
+ {{#each tools}}
169
+ case "{{this.name}}":
170
+ // TODO: implement {{this.name}}
171
+ return `{{this.name}} called with ${JSON.stringify(args)}`;
172
+ {{/each}}
173
+ default:
174
+ throw new Error(`Unknown tool: ${toolName}`);
175
+ }
176
+ }
177
+
178
+ function connect() {
179
+ const ws = new WebSocket("ws://localhost:9400");
180
+
181
+ ws.on("open", () => {
182
+ ws.send(JSON.stringify({ type: "auth", token: TOKEN }));
183
+ });
184
+
185
+ ws.on("message", (raw) => {
186
+ const msg = JSON.parse(raw);
187
+
188
+ switch (msg.type) {
189
+ case "sessions":
190
+ if (!msg.active.length) { ws.close(); return; }
191
+ ws.send(JSON.stringify({
192
+ type: "hello",
193
+ name: PROVIDER_NAME,
194
+ protocolVersion: 2,
195
+ session: msg.active[0].id,
196
+ tools: TOOLS
197
+ }));
198
+ break;
199
+
200
+ case "hello.ack":
201
+ console.log(`✅ ${PROVIDER_NAME} registered as ${msg.providerId}`);
202
+ break;
203
+
204
+ case "tool.call": {
205
+ let result;
206
+ try {
207
+ const data = handleToolCall(msg.tool, msg.args);
208
+ result = { type: "tool.result", id: msg.id, data };
209
+ } catch (err) {
210
+ result = { type: "tool.result", id: msg.id, error: err.message, errorCode: "INTERNAL" };
211
+ }
212
+ ws.send(JSON.stringify(result));
213
+ break;
214
+ }
215
+
216
+ case "tool.cancel":
217
+ ws.send(JSON.stringify({
218
+ type: "tool.result", id: msg.id,
219
+ error: "Cancelled", errorCode: "CANCELLED"
220
+ }));
221
+ break;
222
+
223
+ case "session.lifecycle":
224
+ if (msg.state === "shutdown.pending") {
225
+ ws.send(JSON.stringify({ type: "goodbye", reason: "session ending" }));
226
+ ws.close();
227
+ }
228
+ break;
229
+
230
+ case "error":
231
+ console.error(`❌ [${msg.code}]: ${msg.message}`);
232
+ if (msg.code === "AUTH_FAILED" || msg.code === "UNSUPPORTED_VERSION") ws.close();
233
+ break;
234
+
235
+ default:
236
+ break; // forward compat: ignore unknown types
237
+ }
238
+ });
239
+
240
+ ws.on("close", () => {
241
+ console.log("Disconnected. Reconnecting in 5s...");
242
+ setTimeout(connect, 5000);
243
+ });
244
+
245
+ ws.on("error", (err) => {
246
+ console.error("WebSocket error:", err.message);
247
+ });
248
+ }
249
+
250
+ connect();
251
+ ```
252
+
253
+ ### Python skeleton
254
+
255
+ ```python
256
+ import asyncio, json, os, websockets
257
+
258
+ TOKEN = os.environ.get("TAP_PROVIDER_TOKEN")
259
+ if not TOKEN:
260
+ raise SystemExit("TAP_PROVIDER_TOKEN not set")
261
+
262
+ PROVIDER_NAME = "{{name}}"
263
+ TOOLS = [
264
+ # {{#each tools}}
265
+ {
266
+ "name": "{{this.name}}",
267
+ "description": "{{this.description}}",
268
+ "parameters": {
269
+ "type": "object",
270
+ "properties": { {{this.properties}} },
271
+ "required": [{{this.required}}],
272
+ },
273
+ },
274
+ # {{/each}}
275
+ ]
276
+
277
+ def handle_tool_call(tool_name, args):
278
+ # TODO: implement tool handlers
279
+ return f"{tool_name} called with {json.dumps(args)}"
280
+
281
+ async def connect():
282
+ while True:
283
+ try:
284
+ async with websockets.connect("ws://localhost:9400") as ws:
285
+ await ws.send(json.dumps({"type": "auth", "token": TOKEN}))
286
+
287
+ async for raw in ws:
288
+ msg = json.loads(raw)
289
+
290
+ if msg["type"] == "sessions":
291
+ if not msg["active"]:
292
+ return
293
+ await ws.send(json.dumps({
294
+ "type": "hello",
295
+ "name": PROVIDER_NAME,
296
+ "protocolVersion": 2,
297
+ "session": msg["active"][0]["id"],
298
+ "tools": TOOLS,
299
+ }))
300
+
301
+ elif msg["type"] == "hello.ack":
302
+ print(f"✅ {PROVIDER_NAME} registered as {msg['providerId']}")
303
+
304
+ elif msg["type"] == "tool.call":
305
+ try:
306
+ data = handle_tool_call(msg["tool"], msg["args"])
307
+ result = {"type": "tool.result", "id": msg["id"], "data": data}
308
+ except Exception as e:
309
+ result = {"type": "tool.result", "id": msg["id"], "error": str(e), "errorCode": "INTERNAL"}
310
+ await ws.send(json.dumps(result))
311
+
312
+ elif msg["type"] == "tool.cancel":
313
+ await ws.send(json.dumps({
314
+ "type": "tool.result", "id": msg["id"],
315
+ "error": "Cancelled", "errorCode": "CANCELLED",
316
+ }))
317
+
318
+ elif msg["type"] == "session.lifecycle":
319
+ if msg["state"] == "shutdown.pending":
320
+ await ws.send(json.dumps({"type": "goodbye", "reason": "session ending"}))
321
+ return
322
+
323
+ elif msg["type"] == "error":
324
+ print(f"❌ [{msg['code']}]: {msg['message']}")
325
+ if msg["code"] in ("AUTH_FAILED", "UNSUPPORTED_VERSION"):
326
+ return
327
+
328
+ except (ConnectionError, websockets.ConnectionClosed):
329
+ print("Disconnected. Reconnecting in 5s...")
330
+ await asyncio.sleep(5)
331
+
332
+ asyncio.run(connect())
333
+ ```
334
+
335
+ ## If the user asks about an existing provider
336
+
337
+ If the user mentions a running provider or asks about connected providers, use `tap_list_streams` and check if the gateway has active connections rather than scaffolding a new one.
338
+
339
+ ## Language support
340
+
341
+ The protocol is plain JSON over WebSocket. Generate providers in whatever language the user prefers. The templates above cover Node.js and Python. For other languages (Go, Rust, C#, etc.), follow the same message flow and adapt to that language's WebSocket library.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: loop
3
- description: "Create a prompt-based scheduled loop with tap. Use for requests like '/loop 5m check the deploy' or any ask to run a prompt on a recurring interval."
3
+ description: "Run a prompt on a recurring schedule or when idle. Use when the user says 'loop', 'every 5 minutes', 'check periodically', 'keep watching', 'repeat this', 'run when idle', or wants any prompt to re-run automatically."
4
4
  argument-hint: "<interval|idle> <prompt>"
5
5
  user-invocable: true
6
6
  ---
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.1.0"
2
+ "version": "1.1.2"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-tap-extension",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Copilot CLI extension for background event emitters, event streams, and session injection.",
5
5
  "type": "module",
6
6
  "license": "MIT",