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: "
|
|
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