copilot-tap-extension 2.0.7 → 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.
Files changed (58) hide show
  1. package/README.md +4 -1
  2. package/SOUL.md +51 -0
  3. package/bin/install.mjs +7 -1
  4. package/dist/copilot-instructions.md +15 -0
  5. package/dist/extension.mjs +823 -29
  6. package/dist/skills/tap-goal/SKILL.md +13 -2
  7. package/dist/skills/tap-loop/SKILL.md +6 -0
  8. package/dist/skills/tap-monitor/SKILL.md +19 -3
  9. package/dist/skills/tap-orchestrate/SKILL.md +81 -0
  10. package/dist/version.json +1 -1
  11. package/docs/adr/0001-persistent-config-default-ownership.md +33 -0
  12. package/docs/adr/0002-local-provider-gateway-runtime-security.md +36 -0
  13. package/docs/adr/0003-emitter-delivery-lifecycle.md +68 -0
  14. package/docs/adr/0004-persistent-config-canonical-streams.md +86 -0
  15. package/docs/adr/0005-provider-sdk-push-and-dynamic-tools.md +48 -0
  16. package/docs/adr/0006-command-emitter-cwd-workspace-boundary.md +46 -0
  17. package/docs/adr/0007-runtime-session-workspace-context.md +62 -0
  18. package/docs/evals.md +41 -0
  19. package/docs/evolution-of-tap-icon.html +989 -0
  20. package/docs/providers.md +242 -0
  21. package/docs/recipes/adaptive-agent.md +303 -0
  22. package/docs/recipes/agent-brainstorm/100-extension-ideas.md +288 -0
  23. package/docs/recipes/agent-brainstorm/deep-ideas.md +216 -0
  24. package/docs/recipes/ambient-guardian.md +314 -0
  25. package/docs/recipes/browser-bridge.md +162 -0
  26. package/docs/recipes/codex-goals-for-tap-goal.md +136 -0
  27. package/docs/recipes/copilot-sdk-canvas.md +147 -0
  28. package/docs/recipes/deferred-cognition.md +310 -0
  29. package/docs/recipes/provider-integration-patterns.md +93 -0
  30. package/docs/recipes/provider-interface-advanced.md +1364 -0
  31. package/docs/recipes/provider-interface-core-profile.md +568 -0
  32. package/docs/recipes/tap-control-plane-roadmap.md +60 -0
  33. package/docs/recipes/universal-tool-gateway.md +202 -0
  34. package/docs/reference.md +229 -0
  35. package/docs/use-cases.md +348 -0
  36. package/package.json +4 -1
  37. package/providers/detour/README.md +84 -0
  38. package/providers/detour/bridge.js +219 -0
  39. package/providers/detour/index.mjs +322 -0
  40. package/providers/detour/package-lock.json +577 -0
  41. package/providers/detour/package.json +19 -0
  42. package/providers/detour/scripts/build.mjs +31 -0
  43. package/providers/detour/src/bridge.js +256 -0
  44. package/providers/detour/src/contracts.js +40 -0
  45. package/providers/detour/src/inspector.js +260 -0
  46. package/providers/detour/src/inspector.test.mjs +53 -0
  47. package/providers/detour/src/panel.js +465 -0
  48. package/providers/detour/src/provider-core.js +233 -0
  49. package/providers/detour/src/provider-core.test.mjs +185 -0
  50. package/providers/detour/src/react-context-core.js +143 -0
  51. package/providers/detour/src/react-context.js +44 -0
  52. package/providers/detour/src/react-context.test.mjs +41 -0
  53. package/providers/templates/README.md +23 -0
  54. package/providers/templates/ci-review-provider.mjs +46 -0
  55. package/providers/templates/detour-workflow-provider.mjs +41 -0
  56. package/providers/templates/jira-github-provider.mjs +42 -0
  57. package/providers/templates/provider-utils.mjs +45 -0
  58. 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.7",
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
+ })();