copilot-tap-extension 2.0.8 → 2.0.9
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 +2 -1
- package/SOUL.md +51 -0
- package/bin/install.mjs +2 -1
- package/dist/copilot-instructions.md +5 -0
- package/dist/extension.mjs +361 -20
- package/dist/version.json +1 -1
- package/docs/adr/0001-persistent-config-default-ownership.md +33 -0
- package/docs/adr/0002-local-provider-gateway-runtime-security.md +36 -0
- package/docs/adr/0003-emitter-delivery-lifecycle.md +68 -0
- package/docs/adr/0004-persistent-config-canonical-streams.md +86 -0
- package/docs/adr/0005-provider-sdk-push-and-dynamic-tools.md +48 -0
- package/docs/adr/0006-command-emitter-cwd-workspace-boundary.md +46 -0
- package/docs/adr/0007-runtime-session-workspace-context.md +62 -0
- package/docs/evals.md +41 -0
- package/docs/evolution-of-tap-icon.html +989 -0
- package/docs/providers.md +242 -0
- package/docs/recipes/adaptive-agent.md +303 -0
- package/docs/recipes/agent-brainstorm/100-extension-ideas.md +288 -0
- package/docs/recipes/agent-brainstorm/deep-ideas.md +216 -0
- package/docs/recipes/ambient-guardian.md +314 -0
- package/docs/recipes/browser-bridge.md +162 -0
- package/docs/recipes/codex-goals-for-tap-goal.md +136 -0
- package/docs/recipes/copilot-sdk-canvas.md +147 -0
- package/docs/recipes/deferred-cognition.md +310 -0
- package/docs/recipes/provider-integration-patterns.md +93 -0
- package/docs/recipes/provider-interface-advanced.md +1364 -0
- package/docs/recipes/provider-interface-core-profile.md +568 -0
- package/docs/recipes/tap-control-plane-roadmap.md +60 -0
- package/docs/recipes/universal-tool-gateway.md +202 -0
- package/docs/reference.md +229 -0
- package/docs/use-cases.md +348 -0
- package/package.json +4 -1
- package/providers/detour/README.md +84 -0
- package/providers/detour/bridge.js +219 -0
- package/providers/detour/index.mjs +322 -0
- package/providers/detour/package-lock.json +577 -0
- package/providers/detour/package.json +19 -0
- package/providers/detour/scripts/build.mjs +31 -0
- package/providers/detour/src/bridge.js +256 -0
- package/providers/detour/src/contracts.js +40 -0
- package/providers/detour/src/inspector.js +260 -0
- package/providers/detour/src/inspector.test.mjs +53 -0
- package/providers/detour/src/panel.js +465 -0
- package/providers/detour/src/provider-core.js +233 -0
- package/providers/detour/src/provider-core.test.mjs +185 -0
- package/providers/detour/src/react-context-core.js +143 -0
- package/providers/detour/src/react-context.js +44 -0
- package/providers/detour/src/react-context.test.mjs +41 -0
- package/providers/templates/README.md +23 -0
- package/providers/templates/ci-review-provider.mjs +46 -0
- package/providers/templates/detour-workflow-provider.mjs +41 -0
- package/providers/templates/jira-github-provider.mjs +42 -0
- package/providers/templates/provider-utils.mjs +45 -0
- package/providers/templates/sast-triage-provider.mjs +51 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# How to use copilot-channels-extension
|
|
2
|
+
|
|
3
|
+
This extension is most useful anywhere a user would otherwise say:
|
|
4
|
+
|
|
5
|
+
- "Keep an eye on this."
|
|
6
|
+
- "Tell me when this changes."
|
|
7
|
+
- "Only interrupt me for the important parts."
|
|
8
|
+
- "Watch it for now, and if it proves useful, keep it around."
|
|
9
|
+
|
|
10
|
+
The key idea is simple:
|
|
11
|
+
|
|
12
|
+
- **EventEmitter** = the ONLY primary resource users define — a background command (CommandEmitter) or prompt (PromptEmitter)
|
|
13
|
+
- **EventStream** = auto-created named stream of accepted output (same name as the emitter)
|
|
14
|
+
- **EventFilter** = ordered rule list: `[{ match, outcome }]` — first match wins
|
|
15
|
+
- **SessionInjector** = derived automatically; controls whether EventStream updates are proactively injected
|
|
16
|
+
- **Lifespan** = `temporary` for this session, `persistent` for future sessions
|
|
17
|
+
- **Ownership** = `userOwned` for protected emitters, `modelOwned` for live tuning
|
|
18
|
+
|
|
19
|
+
### Event outcomes
|
|
20
|
+
|
|
21
|
+
| Outcome | Behavior |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `drop` | Discard — does not enter the EventStream |
|
|
24
|
+
| `keep` | Store in the EventStream |
|
|
25
|
+
| `surface` | Keep + show in Copilot session timeline via `session.log()` |
|
|
26
|
+
| `inject` | Keep + surface + inject into Copilot via `session.send()` |
|
|
27
|
+
|
|
28
|
+
PromptEmitter events always inject (no filter applied). CommandEmitter events go through the EventFilter.
|
|
29
|
+
|
|
30
|
+
## Execution shapes
|
|
31
|
+
|
|
32
|
+
| Shape | Config | When to use it |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| Continuous CommandEmitter | `command` | Tail a log, run a watch task, or consume a streaming source |
|
|
35
|
+
| Timed CommandEmitter | `command` + `runInterval` | Poll an API, re-run validation, or check a recurring state |
|
|
36
|
+
| OneTime PromptEmitter | `prompt` | Ask the agent to perform one background inspection or maintenance pass |
|
|
37
|
+
| Timed PromptEmitter | `prompt` + `runInterval` | Re-run a prompt in a session-scoped `/tap-loop` style workflow |
|
|
38
|
+
|
|
39
|
+
## The golden workflow
|
|
40
|
+
|
|
41
|
+
1. Start with a **temporary** EventEmitter (`lifespan="temporary"`).
|
|
42
|
+
2. Enable the SessionInjector unless the stream is naturally sparse.
|
|
43
|
+
3. Let the emitter produce a few real events (keep-all bootstrap — no EventFilter rules yet).
|
|
44
|
+
4. Read EventStream history.
|
|
45
|
+
5. Add EventFilter rules progressively:
|
|
46
|
+
- add `{ "match": "<noise>", "outcome": "drop" }` first to remove obvious noise
|
|
47
|
+
- add `{ "match": "<signal>", "outcome": "inject" }` for important events
|
|
48
|
+
- end with `{ "match": ".*", "outcome": "keep" }` as a catch-all
|
|
49
|
+
6. If the workflow is recurring, add `runInterval` and make it timed.
|
|
50
|
+
7. If the workflow is recurring across sessions, promote it to **persistent** and make it **userOwned**.
|
|
51
|
+
|
|
52
|
+
The EventFilter is hot-swappable while the emitter runs. Start broad, observe, then tighten.
|
|
53
|
+
|
|
54
|
+
## Command vs prompt
|
|
55
|
+
|
|
56
|
+
Use a **CommandEmitter** when the signal already exists outside the agent:
|
|
57
|
+
|
|
58
|
+
- CI logs
|
|
59
|
+
- GitHub CLI queries
|
|
60
|
+
- ticket APIs
|
|
61
|
+
- release feeds
|
|
62
|
+
- file tails
|
|
63
|
+
|
|
64
|
+
Use a **PromptEmitter** when the work is mainly reasoning, summarization, or maintenance:
|
|
65
|
+
|
|
66
|
+
- "check whether there are new review comments and summarize only actionable changes"
|
|
67
|
+
- "re-check the deploy and tell me whether it is safe to continue"
|
|
68
|
+
- "look for new urgent issues or failing runs and summarize what changed"
|
|
69
|
+
- "run a maintenance pass on the current branch"
|
|
70
|
+
|
|
71
|
+
Use `runInterval` when either of those should repeat on a session-scoped interval. Timed PromptEmitters fire immediately on creation, then repeat on the interval.
|
|
72
|
+
|
|
73
|
+
## Pattern library
|
|
74
|
+
|
|
75
|
+
## 1. PR babysitting and code review
|
|
76
|
+
|
|
77
|
+
| Scenario | Emitter | EventStream | Good defaults |
|
|
78
|
+
| --- | --- | --- | --- |
|
|
79
|
+
| Hot PR babysitter | Poll `gh pr view <n> --json reviews,comments,statusCheckRollup` | `pr-activity` | Start temporary; userOwned emitter; modelOwned EventFilter; inject on `changes requested`, `failed`, `review submitted` |
|
|
80
|
+
| Reviewer response lag | Poll requested reviewers and timestamps | `pr-reviewers` | Persistent for team workflow; userOwned; inject when a PR stays unreviewed too long |
|
|
81
|
+
| Merge conflict detector | Compare PR branch with base branch on an interval | `pr-conflicts` | Temporary; inject important signals; drop non-critical file paths after first run |
|
|
82
|
+
| Label and approval gate | Poll labels, approval count, blocking checks | `pr-gate` | Persistent; userOwned thresholds; inject on `approved`, `blocked`, `missing-review` |
|
|
83
|
+
| Auto-rerun watcher | Poll reruns or status changes in CI for a PR | `pr-ci` | Temporary; keep-all at first; drop bot chatter and duplicate check states |
|
|
84
|
+
|
|
85
|
+
## 2. CI, build, and test monitoring
|
|
86
|
+
|
|
87
|
+
| Scenario | Emitter | EventStream | Good defaults |
|
|
88
|
+
| --- | --- | --- | --- |
|
|
89
|
+
| Failing test stream | Watch `npm test -- --watch`, `pytest -f`, or similar | `test-results` | Temporary; inject `FAIL`, `ERROR`, `TIMEOUT` |
|
|
90
|
+
| Typecheck watcher | Run `tsc --watch` or equivalent | `types` | Temporary or persistent; drop dependency noise; inject compiler errors only |
|
|
91
|
+
| Coverage regression tracker | Poll coverage output or parse report files | `coverage` | Persistent for mature repos; userOwned thresholds; inject on drops below target |
|
|
92
|
+
| Build artifact size drift | Run bundle analyzer or publish-size script | `build-artifacts` | Persistent; inject on threshold breaches, not on every successful build |
|
|
93
|
+
| Flaky test quarantine | Poll repeated test runs and state changes | `flaky-tests` | Persistent; history matters more than injection; inject only on new or worsening flakes |
|
|
94
|
+
|
|
95
|
+
## 3. Issues, bugs, and backlog health
|
|
96
|
+
|
|
97
|
+
| Scenario | Emitter | EventStream | Good defaults |
|
|
98
|
+
| --- | --- | --- | --- |
|
|
99
|
+
| Critical bug queue | Poll `gh issue list` for severity labels | `critical-bugs` | Persistent; userOwned emitter; inject on high-severity new issues |
|
|
100
|
+
| Untriaged issue queue | Poll issues with no assignee or no triage label | `triage-queue` | Persistent; keep-all if low volume, add EventFilter if noisy |
|
|
101
|
+
| Stale backlog debt | Poll old issues or items untouched for 30+ days | `backlog-debt` | Persistent; inject only when stale items cross a threshold |
|
|
102
|
+
| Release blocker tracker | Poll blockers and post-mortem issues | `release-status` | Temporary per release, then archive; inject on open/closed transitions |
|
|
103
|
+
| Regression issue detector | Combine failing CI signals with issue creation | `regressions` | Temporary during active fire-fighting; model tunes the EventFilter aggressively |
|
|
104
|
+
|
|
105
|
+
## 4. Email, inboxes, and alert feeds
|
|
106
|
+
|
|
107
|
+
| Scenario | Emitter | EventStream | Good defaults |
|
|
108
|
+
| --- | --- | --- | --- |
|
|
109
|
+
| Executive or escalation inbox | Poll IMAP or an email API through a script | `urgent-emails` | Persistent; userOwned; inject on senders, subjects, or mailbox labels that matter |
|
|
110
|
+
| Personal inbox triage | Poll unread messages and normalize to one line per email | `inbox-digest` | Temporary first; keep-all, then drop newsletters and auto-replies |
|
|
111
|
+
| On-call alert bridge | Poll PagerDuty, Opsgenie, or similar | `oncall-alerts` | Persistent; inject on severity transitions; drop maintenance-window noise |
|
|
112
|
+
| Mention aggregator | Poll Slack, Teams, GitHub, and email mentions into one stream | `mentions` | Temporary during focused work; inject only on direct, actionable mentions |
|
|
113
|
+
| Suspicious mail or phishing queue | Poll a mail security feed | `suspicious-mail` | Persistent and userOwned; inject only on high-confidence signals |
|
|
114
|
+
|
|
115
|
+
## 5. Deployments, logs, and operations
|
|
116
|
+
|
|
117
|
+
| Scenario | Emitter | EventStream | Good defaults |
|
|
118
|
+
| --- | --- | --- | --- |
|
|
119
|
+
| Kubernetes pod health | Run `kubectl get pods -w` or a poller | `k8s-health` | Persistent; inject on readiness failures and crash loops |
|
|
120
|
+
| Deployment rollout watcher | Monitor deploy script output or pipeline states | `deploy-ci` | Temporary during rollout; drop info chatter quickly |
|
|
121
|
+
| Error-log tail | `tail -f` an error log or app log pipeline | `app-errors` | Temporary during incidents; start with keep-all and tighten after first burst |
|
|
122
|
+
| DB lag or replica health | Poll replication lag or replica status | `db-replication` | Persistent; sparse stream; keep-all with inject on threshold breaches |
|
|
123
|
+
| Canary or rollback gate | Poll health endpoints or smoke checks | `health-gate` | Temporary; userOwned success criteria; inject on repeated failures only |
|
|
124
|
+
|
|
125
|
+
## 6. Local developer loops
|
|
126
|
+
|
|
127
|
+
| Scenario | Emitter | EventStream | Good defaults |
|
|
128
|
+
| --- | --- | --- | --- |
|
|
129
|
+
| Local test watch | `jest --watch`, `vitest --watch`, etc. | `test-output` | Temporary; modelOwned EventFilter okay; drop timing and framework noise |
|
|
130
|
+
| Lint watch | `eslint --watch`, `ruff check --watch`, etc. | `lint` | Temporary; inject on errors; keep warnings in history if useful |
|
|
131
|
+
| Build watch | `npm run build -- --watch`, `cargo watch`, etc. | `build` | Temporary; drop routine rebuild lines after first run |
|
|
132
|
+
| Integration harness | Run verbose integration suite or local environment harness | `integration` | Temporary; inject on failures and timeouts only |
|
|
133
|
+
| Multi-stream coding loop | Run tests, types, and lint in separate emitters | `types`, `lint`, `test-output` | Temporary; enable SessionInjector only for the most blocking stream |
|
|
134
|
+
|
|
135
|
+
## 7. Security and compliance
|
|
136
|
+
|
|
137
|
+
| Scenario | Emitter | EventStream | Good defaults |
|
|
138
|
+
| --- | --- | --- | --- |
|
|
139
|
+
| Dependency vulnerability watch | Run `npm audit`, `pip-audit`, `cargo audit`, etc. | `deps-security` | Persistent; userOwned baseline; inject on high/critical or new CVEs |
|
|
140
|
+
| Secret scanning | Run `detect-secrets`, `trufflehog`, or a custom regex scanner | `secrets-scan` | Persistent; drop known templates; inject on high-confidence leaks |
|
|
141
|
+
| License compliance drift | Run a license scanner | `license-compliance` | Persistent and userOwned; inject only on banned or unknown licenses |
|
|
142
|
+
| Supply-chain verification | Poll signature/checksum verification output | `supply-chain-verify` | Persistent; inject on unsigned or mismatched artifacts |
|
|
143
|
+
| Policy audit stream | Run `checkov`, `tfsec`, `kube-bench`, SAST/DAST tools | `compliance-audit` | Persistent; keep full history; inject on critical failures only |
|
|
144
|
+
|
|
145
|
+
## 8. Support, customer feedback, and community
|
|
146
|
+
|
|
147
|
+
| Scenario | Emitter | EventStream | Good defaults |
|
|
148
|
+
| --- | --- | --- | --- |
|
|
149
|
+
| Support backlog watcher | Poll a ticket API for open/SLA-breach tickets | `support-backlog` | Persistent; inject on SLA breaches and escalations only |
|
|
150
|
+
| Community signal emitter | Poll Discord, Slack, forums, or Reddit for keywords | `community-signals` | Start temporary; drop jokes, bot chatter, and duplicate reposts |
|
|
151
|
+
| Feature request stream | Poll Discussions, forms, or webhook logs | `feature-requests` | Persistent; keep-all, then drop duplicates once the themes are known |
|
|
152
|
+
| Moderation queue | Poll flagged posts or moderation APIs | `moderation-queue` | Persistent and userOwned; inject on severe content only |
|
|
153
|
+
| Incident communication queue | Poll support or community channels for outage chatter | `incident-comms` | Temporary during incidents; model tightens the EventFilter fast |
|
|
154
|
+
|
|
155
|
+
## 9. Research, docs, and knowledge monitoring
|
|
156
|
+
|
|
157
|
+
| Scenario | Emitter | EventStream | Good defaults |
|
|
158
|
+
| --- | --- | --- | --- |
|
|
159
|
+
| Paper feed watcher | Poll arXiv or a research API | `research-feeds` | Persistent; keep-all, inject on topics or authors that matter |
|
|
160
|
+
| Release-note tracker | Poll GitHub releases or changelog feeds | `releases` | Persistent; inject on breaking changes, deprecations, and security notes |
|
|
161
|
+
| Competitor news emitter | Poll blogs, RSS, or product feeds | `competitive-intel` | Persistent; drop rumor/analysis posts after first week |
|
|
162
|
+
| Docs staleness detector | Scan docs by age or Git history | `doc-staleness` | Temporary during doc audits; inject on core docs only |
|
|
163
|
+
| Deadline and event calendar | Poll calendars or JSON feeds | `event-deadlines` | Persistent; inject only when deadlines are approaching |
|
|
164
|
+
|
|
165
|
+
## 10. Releases, scheduled jobs, and business processes
|
|
166
|
+
|
|
167
|
+
| Scenario | Emitter | EventStream | Good defaults |
|
|
168
|
+
| --- | --- | --- | --- |
|
|
169
|
+
| Package publish watcher | Monitor `npm publish`, release scripts, or publishing logs | `publish-log` | Temporary on release day, then persist if recurring |
|
|
170
|
+
| Scheduled job health | Poll cron, DAG, or batch-job status | `jobs-status` | Persistent; inject on state transitions, not polling chatter |
|
|
171
|
+
| Data pipeline validation | Monitor ETL validator output | `data-pipeline` | Temporary for new pipelines, persistent for production checks |
|
|
172
|
+
| Artifact registry emitter | Poll for RCs or package versions in a registry | `release-artifacts` | Temporary during releases; inject on exact version matches |
|
|
173
|
+
| Reconciliation and finance checks | Poll reconciliation scripts or audit output | `reconciliation` | Persistent and userOwned; inject on material mismatches only |
|
|
174
|
+
|
|
175
|
+
## How to decide temporary vs persistent
|
|
176
|
+
|
|
177
|
+
Choose **temporary** (`lifespan="temporary"`) when:
|
|
178
|
+
|
|
179
|
+
- this is tied to one incident, one PR, or one debugging session
|
|
180
|
+
- you do not yet know the right EventFilter rules
|
|
181
|
+
- the stream shape is unknown and likely noisy
|
|
182
|
+
- the model should be free to tune things live
|
|
183
|
+
- the timed schedule should stop when the current session ends
|
|
184
|
+
|
|
185
|
+
Choose **persistent** (`lifespan="persistent"`) when:
|
|
186
|
+
|
|
187
|
+
- the same emitter is useful across sessions
|
|
188
|
+
- the command and thresholds are stable
|
|
189
|
+
- the rules encode team policy or operational practice
|
|
190
|
+
- the user wants the workflow to come back automatically
|
|
191
|
+
|
|
192
|
+
## How to split userOwned vs modelOwned
|
|
193
|
+
|
|
194
|
+
Keep it **userOwned** when:
|
|
195
|
+
|
|
196
|
+
- the emitter touches security, compliance, email, finance, or release gates
|
|
197
|
+
- the command embeds important org-specific assumptions
|
|
198
|
+
- the EventStream is now part of team workflow
|
|
199
|
+
- a mistaken change would create real risk
|
|
200
|
+
|
|
201
|
+
Let it be **modelOwned** when:
|
|
202
|
+
|
|
203
|
+
- this is a temporary investigative emitter
|
|
204
|
+
- the main problem is noise reduction, not policy
|
|
205
|
+
- the user wants the agent to learn what matters from the live stream
|
|
206
|
+
- EventFilter tuning is expected to change several times during one task
|
|
207
|
+
|
|
208
|
+
## Design tips
|
|
209
|
+
|
|
210
|
+
1. Prefer one concern per EventStream (one emitter per concern).
|
|
211
|
+
2. Normalize your emitter output so each line is meaningful.
|
|
212
|
+
3. Use the EventFilter outcome hierarchy: drop noise → inject signal → keep the rest.
|
|
213
|
+
4. Drop noise before narrowing what gets injected.
|
|
214
|
+
5. If you are polling something repeatedly, prefer `runInterval` over re-running it manually.
|
|
215
|
+
6. If you create the same emitter more than a few times, promote it to persistent config.
|
|
216
|
+
7. If the user cares about ownership, switch the persistent version to `ownership="userOwned"` after the workflow stabilizes.
|
|
217
|
+
|
|
218
|
+
## General SDK patterns worth borrowing
|
|
219
|
+
|
|
220
|
+
The official `@github/copilot-sdk` examples are useful even when a pattern is not specifically about EventStreams or EventEmitters. These are good extension ideas to combine with this repo's emitter model.
|
|
221
|
+
|
|
222
|
+
### 1. Log important extension state to the timeline
|
|
223
|
+
|
|
224
|
+
Use `session.log()` instead of `console.log()` to explain what the extension is doing:
|
|
225
|
+
|
|
226
|
+
- emitter started or stopped
|
|
227
|
+
- EventFilter updated
|
|
228
|
+
- config loaded
|
|
229
|
+
- retries or recoverable failures
|
|
230
|
+
|
|
231
|
+
Use `ephemeral: true` for noisy operational messages that should not stick around forever.
|
|
232
|
+
|
|
233
|
+
### 2. Use hooks to shape behavior around tool use
|
|
234
|
+
|
|
235
|
+
The SDK examples show several high-value hook patterns:
|
|
236
|
+
|
|
237
|
+
- `onUserPromptSubmitted` to add hidden context or trigger follow-up behavior
|
|
238
|
+
- `onPreToolUse` to deny risky commands or rewrite arguments
|
|
239
|
+
- `onPostToolUse` to add context after a tool finishes
|
|
240
|
+
- `onErrorOccurred` to retry, skip, or abort cleanly
|
|
241
|
+
|
|
242
|
+
For this repo, the natural extension is to combine hooks with EventStreams:
|
|
243
|
+
|
|
244
|
+
- if a risky shell command appears, log it and post a note into an ops or audit EventStream
|
|
245
|
+
- after a code-edit tool runs, trigger a temporary build or lint emitter
|
|
246
|
+
- when a recurring failure happens, inject a short background follow-up with `session.send()`
|
|
247
|
+
|
|
248
|
+
### 3. Add custom helper tools next to the emitter tools
|
|
249
|
+
|
|
250
|
+
The official examples include simple tools that:
|
|
251
|
+
|
|
252
|
+
- run a shell command
|
|
253
|
+
- fetch data from an API
|
|
254
|
+
- copy text to the clipboard
|
|
255
|
+
|
|
256
|
+
That maps well to this repo. Good companion tools would be:
|
|
257
|
+
|
|
258
|
+
- `fetch_release_notes`
|
|
259
|
+
- `poll_ticket_queue_once`
|
|
260
|
+
- `summarize_stream`
|
|
261
|
+
- `snapshot_emitter_state`
|
|
262
|
+
|
|
263
|
+
Use emitters for ongoing signals and helper tools for one-shot actions.
|
|
264
|
+
|
|
265
|
+
### 4. React to session events, not just your own process output
|
|
266
|
+
|
|
267
|
+
The SDK examples show how to listen to session events such as:
|
|
268
|
+
|
|
269
|
+
- `tool.execution_start`
|
|
270
|
+
- `tool.execution_complete`
|
|
271
|
+
- `assistant.message`
|
|
272
|
+
- `session.idle`
|
|
273
|
+
- `session.error`
|
|
274
|
+
|
|
275
|
+
That is useful here even though emitters already push updates directly. Examples:
|
|
276
|
+
|
|
277
|
+
- start a temporary validation emitter after a build tool starts
|
|
278
|
+
- clear a transient EventFilter once the session goes idle
|
|
279
|
+
- attach extra context when a tool fails repeatedly
|
|
280
|
+
- mirror important lifecycle events into an EventStream for auditability
|
|
281
|
+
|
|
282
|
+
### 5. Watch files and workspace artifacts
|
|
283
|
+
|
|
284
|
+
The examples show `fs.watch` and `watchFile` patterns for:
|
|
285
|
+
|
|
286
|
+
- `plan.md`
|
|
287
|
+
- repo files edited manually by the user
|
|
288
|
+
|
|
289
|
+
That pairs well with this repo when a workflow mixes code changes and emitters:
|
|
290
|
+
|
|
291
|
+
- watch `plan.md` and post "plan changed" into a planning EventStream
|
|
292
|
+
- watch files under `logs/` and create an emitter automatically
|
|
293
|
+
- detect user edits to a config file and refresh the corresponding emitter
|
|
294
|
+
|
|
295
|
+
### 6. Use `session.send()` and `session.sendAndWait()` intentionally
|
|
296
|
+
|
|
297
|
+
Use:
|
|
298
|
+
|
|
299
|
+
- `session.send()` for fire-and-forget background nudges
|
|
300
|
+
- `session.sendAndWait()` only when the extension genuinely needs the agent's answer before continuing
|
|
301
|
+
|
|
302
|
+
For EventStream injection, `session.send()` is usually the right fit. For a helper flow like "fetch data, then ask the agent to summarize it before updating config", `sendAndWait()` can make sense.
|
|
303
|
+
|
|
304
|
+
### 7. Build permission and user-input workflows into the extension
|
|
305
|
+
|
|
306
|
+
The SDK examples also show:
|
|
307
|
+
|
|
308
|
+
- custom permission logic via `onPermissionRequest`
|
|
309
|
+
- user questions via `onUserInputRequest`
|
|
310
|
+
|
|
311
|
+
These are powerful in this repo for guarded workflows:
|
|
312
|
+
|
|
313
|
+
- ask before persisting a new emitter
|
|
314
|
+
- deny destructive shell commands from helper tools
|
|
315
|
+
- request confirmation before overriding a userOwned EventFilter
|
|
316
|
+
- collect thresholds or keywords interactively instead of hardcoding them
|
|
317
|
+
|
|
318
|
+
### 8. Add canvas surfaces for visual workflows
|
|
319
|
+
|
|
320
|
+
The local Copilot CLI SDK exposes experimental canvas support through `createCanvas` and `joinSession({ canvases: [...] })`. A canvas is an extension-owned UI surface that the agent or host can open, focus, close, and invoke actions against.
|
|
321
|
+
|
|
322
|
+
This is useful when text-only EventStreams are not enough:
|
|
323
|
+
|
|
324
|
+
- stream dashboards that show emitter health and recent events
|
|
325
|
+
- dependency graphs or PR-review boards
|
|
326
|
+
- browser-debug panels backed by a local loopback renderer
|
|
327
|
+
- incident timelines with action buttons for refresh, filter, or acknowledge
|
|
328
|
+
- tap's built-in diagnostics canvas, opened through `tap_open_diagnostics_canvas`, which combines streams, emitters, providers, logs, queues, and session events
|
|
329
|
+
|
|
330
|
+
Important constraints for this repo:
|
|
331
|
+
|
|
332
|
+
- canvas actions are declared with JSON Schema and invoked through `invoke_canvas_action`
|
|
333
|
+
- `open()` returns host chrome metadata such as `title`, `status`, and a renderer `url`
|
|
334
|
+
- per-instance resources should be keyed by `instanceId` and cleaned up in `onClose`
|
|
335
|
+
- external tap providers cannot declare Copilot SDK canvases over the current WebSocket protocol; canvas work belongs in the extension layer unless the provider protocol is explicitly extended
|
|
336
|
+
- diagnostics canvases should use bounded/redacted snapshots rather than unbounded raw transcript or token payloads
|
|
337
|
+
|
|
338
|
+
See [Copilot SDK canvas surfaces](./recipes/copilot-sdk-canvas.md) for the detailed local SDK findings and a working skeleton.
|
|
339
|
+
|
|
340
|
+
### 9. Keep it cross-platform
|
|
341
|
+
|
|
342
|
+
The examples call out Windows-specific concerns:
|
|
343
|
+
|
|
344
|
+
- detect Windows with `process.platform === "win32"`
|
|
345
|
+
- prefer the right shell and stderr redirection syntax
|
|
346
|
+
- use Windows-safe process launching
|
|
347
|
+
|
|
348
|
+
That is especially relevant here because emitters are shell-driven and this repo is intended to be copied into real projects on different operating systems.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-tap-extension",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "Copilot CLI extension for background event emitters, event streams, and session injection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"bin/",
|
|
16
16
|
"dist/",
|
|
17
|
+
"docs/",
|
|
18
|
+
"providers/",
|
|
17
19
|
"README.md",
|
|
20
|
+
"SOUL.md",
|
|
18
21
|
"LICENSE"
|
|
19
22
|
],
|
|
20
23
|
"scripts": {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ⚡ Detour — Browser ↔ Agent Bridge
|
|
2
|
+
|
|
3
|
+
Detour lets the Copilot agent inject JavaScript into any browser page and receive console logs back in real-time.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────┐ ws://127.0.0.1:9401?token=... ┌─────────────┐ ws://localhost:9400 ┌─────────┐
|
|
9
|
+
│ Browser │◄────────────────────►│ Detour │◄────────────────────►│ ※ tap │
|
|
10
|
+
│ (page) │ eval + console logs │ Provider │ tool calls │ Gateway │
|
|
11
|
+
└─────────┘ └─────────────┘ └─────────┘
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
1. **Detour provider** runs locally — connects to tap gateway and serves a token-protected browser WebSocket
|
|
15
|
+
2. **Browser bridge** is injected by Detour — connects to Detour, hooks `console.*`, listens for eval commands
|
|
16
|
+
3. **Agent** calls `inject_js` / `get_console_logs` / `list_browser_clients` through tap
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. Install dependencies
|
|
22
|
+
cd providers/detour
|
|
23
|
+
npm install
|
|
24
|
+
|
|
25
|
+
# 2. Run the provider (grab token from your Copilot session)
|
|
26
|
+
# PowerShell:
|
|
27
|
+
$env:TAP_PROVIDER_TOKEN = "<token>"; node index.mjs
|
|
28
|
+
# Bash:
|
|
29
|
+
TAP_PROVIDER_TOKEN=<token> node index.mjs
|
|
30
|
+
|
|
31
|
+
# 3. Copy the printed Bridge URL into Detour's Inject on load rule.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The provider prints a URL like `http://127.0.0.1:9401/bridge.js?token=...`.
|
|
35
|
+
Use that exact URL so the injected bridge can authenticate to the local WebSocket.
|
|
36
|
+
|
|
37
|
+
You'll see **⚡ Detour connected** in the console. The agent now has 3 new tools:
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | Description |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `inject_js` | Execute JS in the browser page context. Returns the result. Supports async (Promises). |
|
|
44
|
+
| `get_console_logs` | Retrieve captured console.log/warn/error/info/debug output. Filter by level or client. |
|
|
45
|
+
| `list_browser_clients` | List all connected browser tabs with URL, title, and client ID. |
|
|
46
|
+
|
|
47
|
+
## Environment variables
|
|
48
|
+
|
|
49
|
+
| Variable | Default | Description |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `TAP_PROVIDER_TOKEN` | (required) | Auth token from Copilot session |
|
|
52
|
+
| `TAP_GATEWAY_URL` | `ws://localhost:9400` | Gateway WebSocket URL |
|
|
53
|
+
| `DETOUR_PORT` | `9401` | Port for browser connections |
|
|
54
|
+
| `DETOUR_BRIDGE_TOKEN` | random per run | Optional fixed token for the browser bridge HTTP and WebSocket endpoints |
|
|
55
|
+
|
|
56
|
+
## Example agent usage
|
|
57
|
+
|
|
58
|
+
Once connected, the agent can:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Agent: "Let me check what's on the page"
|
|
62
|
+
→ calls inject_js({ code: "document.title" })
|
|
63
|
+
← "My Cool App"
|
|
64
|
+
|
|
65
|
+
Agent: "Let me look at the DOM structure"
|
|
66
|
+
→ calls inject_js({ code: "document.querySelector('main').innerHTML.slice(0, 1000)" })
|
|
67
|
+
← "<div class='hero'>..."
|
|
68
|
+
|
|
69
|
+
Agent: "Any errors on the page?"
|
|
70
|
+
→ calls get_console_logs({ level: "error" })
|
|
71
|
+
← [{ level: "error", args: ["Failed to fetch /api/users"], timestamp: "..." }]
|
|
72
|
+
|
|
73
|
+
Agent: "Let me fix that button"
|
|
74
|
+
→ calls inject_js({ code: "document.querySelector('#submit-btn').style.display = 'block'" })
|
|
75
|
+
← "block"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Multiple pages
|
|
79
|
+
|
|
80
|
+
You can paste the snippet into multiple browser tabs. Each gets a unique client ID. Use `list_browser_clients` to see them, and pass `client_id` to target a specific tab.
|
|
81
|
+
|
|
82
|
+
## Security note
|
|
83
|
+
|
|
84
|
+
Detour binds its browser bridge to loopback and requires the printed bridge token for HTTP and WebSocket access. It still allows arbitrary JS execution on authenticated connected pages — use responsibly and only on pages you control.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detour ↔ Agent Bridge
|
|
3
|
+
*
|
|
4
|
+
* Injected by the Detour Chrome extension via "Inject on load" rules.
|
|
5
|
+
* Connects to the Detour provider over WebSocket and enables:
|
|
6
|
+
* - Remote JS execution from the Copilot agent
|
|
7
|
+
* - Console log capture (log, warn, error, info, debug)
|
|
8
|
+
* - Uncaught error + unhandled rejection capture
|
|
9
|
+
* - Floating status badge showing connection state
|
|
10
|
+
*/
|
|
11
|
+
(function () {
|
|
12
|
+
"use strict";
|
|
13
|
+
if (window.__detourBridge) return;
|
|
14
|
+
|
|
15
|
+
var WS_URL = "__DET0UR_WS_URL__";
|
|
16
|
+
var RECONNECT_MS = 3000;
|
|
17
|
+
var ws = null;
|
|
18
|
+
|
|
19
|
+
window.__detourBridge = { connected: false };
|
|
20
|
+
|
|
21
|
+
// ── Status badge ──────────────────────────────────────────────────────
|
|
22
|
+
var badge = document.createElement("div");
|
|
23
|
+
badge.id = "__detour-badge";
|
|
24
|
+
badge.setAttribute("style", [
|
|
25
|
+
"position:fixed", "bottom:12px", "right:12px", "z-index:2147483647",
|
|
26
|
+
"padding:6px 12px", "border-radius:20px",
|
|
27
|
+
"font:600 12px/1 -apple-system,system-ui,sans-serif",
|
|
28
|
+
"color:#fff", "background:#d44", "opacity:0.92",
|
|
29
|
+
"pointer-events:none", "transition:background .3s,opacity .3s",
|
|
30
|
+
"box-shadow:0 2px 8px rgba(0,0,0,.3)",
|
|
31
|
+
].join(";"));
|
|
32
|
+
badge.textContent = "⚡ Detour: connecting…";
|
|
33
|
+
|
|
34
|
+
function showBadge() {
|
|
35
|
+
if (!badge.parentNode) {
|
|
36
|
+
(document.body || document.documentElement).appendChild(badge);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setBadgeState(state) {
|
|
41
|
+
if (state === "connected") {
|
|
42
|
+
badge.textContent = "⚡ Detour: connected";
|
|
43
|
+
badge.style.background = "#1a8c3a";
|
|
44
|
+
// Fade out after 3s
|
|
45
|
+
clearTimeout(badge._hideTimer);
|
|
46
|
+
badge.style.opacity = "0.92";
|
|
47
|
+
badge._hideTimer = setTimeout(function () { badge.style.opacity = "0"; }, 3000);
|
|
48
|
+
} else if (state === "disconnected") {
|
|
49
|
+
badge.textContent = "⚡ Detour: disconnected";
|
|
50
|
+
badge.style.background = "#d44";
|
|
51
|
+
badge.style.opacity = "0.92";
|
|
52
|
+
clearTimeout(badge._hideTimer);
|
|
53
|
+
} else {
|
|
54
|
+
badge.textContent = "⚡ Detour: connecting…";
|
|
55
|
+
badge.style.background = "#c90";
|
|
56
|
+
badge.style.opacity = "0.92";
|
|
57
|
+
clearTimeout(badge._hideTimer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Show badge once DOM is ready
|
|
62
|
+
if (document.body) showBadge();
|
|
63
|
+
else document.addEventListener("DOMContentLoaded", showBadge);
|
|
64
|
+
|
|
65
|
+
// ── Console intercept ─────────────────────────────────────────────────
|
|
66
|
+
var orig = {};
|
|
67
|
+
["log", "warn", "error", "info", "debug"].forEach(function (level) {
|
|
68
|
+
orig[level] = console[level].bind(console);
|
|
69
|
+
console[level] = function () {
|
|
70
|
+
var args = Array.prototype.slice.call(arguments);
|
|
71
|
+
orig[level].apply(console, args);
|
|
72
|
+
sendConsole(level, args);
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
window.addEventListener("error", function (e) {
|
|
77
|
+
sendConsole("error", ["Uncaught: " + e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
window.addEventListener("unhandledrejection", function (e) {
|
|
81
|
+
sendConsole("error", ["Unhandled rejection: " + e.reason]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
function sendConsole(level, args) {
|
|
85
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
86
|
+
var serialized = args.map(function (a) {
|
|
87
|
+
try {
|
|
88
|
+
if (typeof a === "string") return a;
|
|
89
|
+
if (a instanceof Error) return a.name + ": " + a.message;
|
|
90
|
+
if (a instanceof HTMLElement) return a.outerHTML.slice(0, 200);
|
|
91
|
+
return JSON.stringify(a);
|
|
92
|
+
} catch (e) { return String(a); }
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
ws.send(JSON.stringify({
|
|
96
|
+
type: "console",
|
|
97
|
+
level: level,
|
|
98
|
+
args: serialized,
|
|
99
|
+
timestamp: new Date().toISOString(),
|
|
100
|
+
}));
|
|
101
|
+
} catch (e) { /* ignore */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Serializer ────────────────────────────────────────────────────────
|
|
105
|
+
function serialize(value) {
|
|
106
|
+
if (value === undefined) return "undefined";
|
|
107
|
+
if (value === null) return "null";
|
|
108
|
+
if (value instanceof HTMLElement) return value.outerHTML.slice(0, 5000);
|
|
109
|
+
if (typeof value === "object") {
|
|
110
|
+
try { return JSON.stringify(value, null, 2); }
|
|
111
|
+
catch (e) { return String(value); }
|
|
112
|
+
}
|
|
113
|
+
return String(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Eval handler (supports async/Promise results) ─────────────────────
|
|
117
|
+
function handleEval(msg) {
|
|
118
|
+
var result = { type: "eval.result", id: msg.id };
|
|
119
|
+
try {
|
|
120
|
+
var value = (0, eval)(msg.code);
|
|
121
|
+
if (value && typeof value.then === "function") {
|
|
122
|
+
value.then(
|
|
123
|
+
function (resolved) {
|
|
124
|
+
result.value = serialize(resolved);
|
|
125
|
+
ws.send(JSON.stringify(result));
|
|
126
|
+
},
|
|
127
|
+
function (rejected) {
|
|
128
|
+
result.error = String(rejected);
|
|
129
|
+
ws.send(JSON.stringify(result));
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
result.value = serialize(value);
|
|
134
|
+
ws.send(JSON.stringify(result));
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
result.error = err.name + ": " + err.message;
|
|
138
|
+
ws.send(JSON.stringify(result));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Pending asks (browser→agent with response) ────────────────────────
|
|
143
|
+
var pendingAsks = {};
|
|
144
|
+
var askIdCounter = 0;
|
|
145
|
+
|
|
146
|
+
// Fire-and-forget message to agent
|
|
147
|
+
window.__detourBridge.send = function (message) {
|
|
148
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
149
|
+
orig.warn("[Detour] Not connected — message not sent");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
ws.send(JSON.stringify({ type: "page.message", message: message }));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Ask agent a question, returns a Promise that resolves with the response
|
|
156
|
+
window.__detourBridge.ask = function (message, timeoutMs) {
|
|
157
|
+
return new Promise(function (resolve, reject) {
|
|
158
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
159
|
+
return reject(new Error("Detour bridge not connected"));
|
|
160
|
+
}
|
|
161
|
+
var id = "ask-" + (++askIdCounter);
|
|
162
|
+
var timer = setTimeout(function () {
|
|
163
|
+
delete pendingAsks[id];
|
|
164
|
+
reject(new Error("Ask timed out after " + (timeoutMs || 30000) + "ms"));
|
|
165
|
+
}, timeoutMs || 30000);
|
|
166
|
+
pendingAsks[id] = { resolve: resolve, reject: reject, timer: timer };
|
|
167
|
+
ws.send(JSON.stringify({ type: "page.ask", id: id, message: message }));
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// ── WebSocket connection ──────────────────────────────────────────────
|
|
172
|
+
function connect() {
|
|
173
|
+
setBadgeState("connecting");
|
|
174
|
+
try { ws = new WebSocket(WS_URL); } catch (e) {
|
|
175
|
+
setBadgeState("disconnected");
|
|
176
|
+
setTimeout(connect, RECONNECT_MS);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ws.onopen = function () {
|
|
181
|
+
window.__detourBridge.connected = true;
|
|
182
|
+
setBadgeState("connected");
|
|
183
|
+
orig.log("%c⚡ Detour bridge connected", "color:#0f0;font-weight:bold;font-size:13px");
|
|
184
|
+
orig.log("%c window.__detourBridge.send('msg') — send to agent", "color:#aaa");
|
|
185
|
+
orig.log("%c window.__detourBridge.ask('msg') — ask agent (returns Promise)", "color:#aaa");
|
|
186
|
+
ws.send(JSON.stringify({
|
|
187
|
+
type: "identify",
|
|
188
|
+
url: location.href,
|
|
189
|
+
title: document.title || location.hostname,
|
|
190
|
+
}));
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
ws.onmessage = function (event) {
|
|
194
|
+
var msg;
|
|
195
|
+
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
196
|
+
if (msg.type === "eval") handleEval(msg);
|
|
197
|
+
if (msg.type === "ask.reply") {
|
|
198
|
+
var pending = pendingAsks[msg.id];
|
|
199
|
+
if (pending) {
|
|
200
|
+
clearTimeout(pending.timer);
|
|
201
|
+
delete pendingAsks[msg.id];
|
|
202
|
+
if (msg.error) pending.reject(new Error(msg.error));
|
|
203
|
+
else pending.resolve(msg.reply);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
ws.onclose = function () {
|
|
209
|
+
window.__detourBridge.connected = false;
|
|
210
|
+
setBadgeState("disconnected");
|
|
211
|
+
orig.log("%c⚡ Detour bridge disconnected, reconnecting...", "color:#f80");
|
|
212
|
+
setTimeout(connect, RECONNECT_MS);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
ws.onerror = function () { /* onclose will fire */ };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
connect();
|
|
219
|
+
})();
|