copilot-tap-extension 0.2.5 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -204,7 +204,7 @@ PLAN.md # ubiquitous language and design decisions
204
204
  | [Reference](./docs/reference.md) | Look up tool parameters, config fields, or the event pipeline |
205
205
  | [Use cases and patterns](./docs/use-cases.md) | Recipes for deploy watchers, PR monitors, log tailers, and more |
206
206
  | [Evals](./docs/evals.md) | Run or extend the automated test suite |
207
- | [Copilot instructions](./.github/copilot-instructions.md) | Understand or customize how the agent uses this extension |
207
+ | [Copilot instructions](./src/copilot-instructions.md) | Understand or customize how the agent uses this extension |
208
208
  | [Implementation plan](./PLAN.md) | Ubiquitous language and naming conventions for contributors |
209
209
  | [Evolution of the ※ icon](./docs/evolution-of-tap-icon.html) | 20 metaphors, 10 variants, one mark — the design story behind ※ tap |
210
210
 
package/bin/install.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, copyFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, copyFileSync, readFileSync } from "node:fs";
3
+ import { execFileSync } from "node:child_process";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import path from "node:path";
5
6
  import os from "node:os";
@@ -11,6 +12,14 @@ const distDir = path.join(pkgRoot, "dist");
11
12
  const BRAND = "※ tap";
12
13
  const EXT_DIR_NAME = "tap";
13
14
 
15
+ function getPackageVersion() {
16
+ try {
17
+ return JSON.parse(readFileSync(path.join(distDir, "version.json"), "utf8")).version;
18
+ } catch {
19
+ return JSON.parse(readFileSync(path.join(pkgRoot, "package.json"), "utf8")).version;
20
+ }
21
+ }
22
+
14
23
  function usage() {
15
24
  console.log(`
16
25
  ${BRAND} — Copilot CLI extension installer
@@ -18,14 +27,18 @@ ${BRAND} — Copilot CLI extension installer
18
27
  Usage:
19
28
  npx copilot-tap-extension [options]
20
29
 
30
+ If ※ tap is already installed, updates core files (extension + version)
31
+ and preserves customizable artifacts. If fresh, does a full install.
32
+
21
33
  Options:
22
34
  --global, -g Install to ~/.copilot/ (default)
23
35
  --local, -l Install to .github/ (project-scoped)
24
- --force, -f Overwrite existing files without prompting
25
- --help, -h Show this help message
36
+ --full Force a full install even if already installed
37
+ --help, -h Show this help message
26
38
 
27
39
  Installs:
28
40
  extensions/tap/extension.mjs The bundled ※ tap extension
41
+ extensions/tap/version.json Installed version metadata
29
42
  skills/loop/SKILL.md The /loop skill for prompt-based loops
30
43
  copilot-instructions.md Agent instructions for using ※ tap
31
44
  `);
@@ -33,7 +46,7 @@ Installs:
33
46
 
34
47
  function parseArgs(argv) {
35
48
  const args = argv.slice(2);
36
- const flags = { scope: "global", force: false, help: false };
49
+ const flags = { scope: "global", full: false, help: false };
37
50
  for (const arg of args) {
38
51
  switch (arg) {
39
52
  case "--global":
@@ -44,9 +57,14 @@ function parseArgs(argv) {
44
57
  case "-l":
45
58
  flags.scope = "local";
46
59
  break;
60
+ case "--full":
61
+ flags.full = true;
62
+ break;
63
+ // Keep legacy flags working
47
64
  case "--force":
48
65
  case "-f":
49
- flags.force = true;
66
+ case "--update":
67
+ case "-u":
50
68
  break;
51
69
  case "--help":
52
70
  case "-h":
@@ -61,40 +79,94 @@ function parseArgs(argv) {
61
79
  return flags;
62
80
  }
63
81
 
82
+ function getCopilotHome() {
83
+ return process.env.COPILOT_HOME || path.join(os.homedir(), ".copilot");
84
+ }
85
+
64
86
  function getTargetRoot(scope) {
65
87
  if (scope === "global") {
66
- return path.join(os.homedir(), ".copilot");
88
+ return getCopilotHome();
67
89
  }
68
90
  return path.join(process.cwd(), ".github");
69
91
  }
70
92
 
71
- function copyArtifact(src, dest, label, flags) {
93
+ function copyArtifact(src, dest, label) {
72
94
  if (!existsSync(src)) {
73
95
  console.error(` ✗ ${label}: source not found (${src})`);
74
96
  return false;
75
97
  }
76
- if (existsSync(dest) && !flags.force) {
77
- console.log(` ⊘ ${label}: already exists, skipping (use --force to overwrite)`);
78
- return true;
79
- }
80
98
  mkdirSync(path.dirname(dest), { recursive: true });
81
99
  copyFileSync(src, dest);
82
100
  console.log(` ✓ ${label}`);
83
101
  return true;
84
102
  }
85
103
 
104
+ function getInstalledVersion(targetRoot) {
105
+ try {
106
+ const versionFile = path.join(targetRoot, "extensions", EXT_DIR_NAME, "version.json");
107
+ return JSON.parse(readFileSync(versionFile, "utf8")).version;
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ function isAlreadyInstalled(targetRoot) {
114
+ return existsSync(path.join(targetRoot, "extensions", EXT_DIR_NAME, "extension.mjs"));
115
+ }
116
+
117
+ function isCopilotCliInstalled() {
118
+ if (existsSync(getCopilotHome())) {
119
+ return true;
120
+ }
121
+ try {
122
+ execFileSync("copilot", ["--version"], { stdio: "ignore", timeout: 5000 });
123
+ return true;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
86
129
  function install(flags) {
87
130
  const targetRoot = getTargetRoot(flags.scope);
88
131
  const scopeLabel = flags.scope === "global" ? "global (~/.copilot)" : "local (.github)";
132
+ const packageVersion = getPackageVersion();
133
+
134
+ if (flags.scope === "global" && !isCopilotCliInstalled()) {
135
+ console.log(`\n⚠ Copilot CLI does not appear to be installed.`);
136
+ console.log(` Install it first: https://docs.github.com/en/copilot/github-copilot-in-the-cli`);
137
+ console.log(` Then re-run: npx copilot-tap-extension\n`);
138
+ process.exit(1);
139
+ }
140
+
141
+ const installed = isAlreadyInstalled(targetRoot);
142
+ const isUpdate = installed && !flags.full;
89
143
 
90
- console.log(`\n${BRAND} — installing (${scopeLabel})\n`);
144
+ if (isUpdate) {
145
+ const installedVersion = getInstalledVersion(targetRoot);
146
+ if (installedVersion && installedVersion === packageVersion) {
147
+ console.log(`\n${BRAND} — already up to date (v${installedVersion})\n`);
148
+ process.exit(0);
149
+ }
150
+ const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
151
+ console.log(`\n${BRAND} — updating ${fromLabel} → v${packageVersion} (${scopeLabel})\n`);
152
+ } else {
153
+ console.log(`\n${BRAND} — installing v${packageVersion} (${scopeLabel})\n`);
154
+ }
91
155
 
92
- const artifacts = [
156
+ const coreArtifacts = [
93
157
  {
94
158
  src: path.join(distDir, "extension.mjs"),
95
159
  dest: path.join(targetRoot, "extensions", EXT_DIR_NAME, "extension.mjs"),
96
160
  label: "extensions/tap/extension.mjs"
97
161
  },
162
+ {
163
+ src: path.join(distDir, "version.json"),
164
+ dest: path.join(targetRoot, "extensions", EXT_DIR_NAME, "version.json"),
165
+ label: "extensions/tap/version.json"
166
+ }
167
+ ];
168
+
169
+ const ancillaryArtifacts = [
98
170
  {
99
171
  src: path.join(distDir, "skills", "loop", "SKILL.md"),
100
172
  dest: path.join(targetRoot, "skills", "loop", "SKILL.md"),
@@ -107,18 +179,21 @@ function install(flags) {
107
179
  }
108
180
  ];
109
181
 
182
+ const artifacts = isUpdate ? coreArtifacts : [...coreArtifacts, ...ancillaryArtifacts];
183
+
110
184
  let allOk = true;
111
185
  for (const { src, dest, label } of artifacts) {
112
- if (!copyArtifact(src, dest, label, flags)) {
186
+ if (!copyArtifact(src, dest, label)) {
113
187
  allOk = false;
114
188
  }
115
189
  }
116
190
 
117
191
  console.log();
118
192
  if (allOk) {
119
- console.log(`✓ ${BRAND} installed to ${targetRoot}`);
193
+ const verb = isUpdate ? "updated" : "installed";
194
+ console.log(`✓ ${BRAND} ${verb} to ${targetRoot}`);
120
195
  } else {
121
- console.error(`⚠ Some artifacts could not be installed.`);
196
+ console.error(`⚠ Some artifacts could not be ${isUpdate ? "updated" : "installed"}.`);
122
197
  process.exit(1);
123
198
  }
124
199
  }
@@ -2,7 +2,7 @@
2
2
  // https://github.com/amitse/copilot-tap-extension
3
3
 
4
4
 
5
- // .github/extensions/tap/extension.mjs
5
+ // src/extension.mjs
6
6
  import { joinSession } from "@github/copilot-sdk/extension";
7
7
 
8
8
  // src/consts.mjs
@@ -13,7 +13,7 @@ var CONFIG_LOCATIONS = [
13
13
  CONFIG_FILENAME,
14
14
  `${GITHUB_DIR}${path.sep}${CONFIG_FILENAME}`
15
15
  ];
16
- var COPILOT_INSTRUCTIONS_PATH = `${GITHUB_DIR}/copilot-instructions.md`;
16
+ var COPILOT_INSTRUCTIONS_PATH = "src/copilot-instructions.md";
17
17
  var MAX_STREAM_ENTRIES = 200;
18
18
  var DEFAULT_STREAM = "main";
19
19
  var DEFAULT_STREAM_DESCRIPTION = "Extension events";
@@ -766,12 +766,6 @@ function createLineRouter({ streams, notifications, sessionPort }) {
766
766
  monitorName: emitter.name,
767
767
  stream: STREAM.PROMPT
768
768
  });
769
- notifications.enqueue({
770
- channel: emitter.stream,
771
- monitorName: emitter.name,
772
- stream: STREAM.PROMPT,
773
- text
774
- });
775
769
  }
776
770
  }
777
771
  return { handleLine, handleTextBlock, handlePromptResult, appendSystemMessage };
@@ -951,19 +945,11 @@ function createLifecycle({ lineRouter, sessionPort }) {
951
945
  }
952
946
  async function runPromptIteration(emitter) {
953
947
  try {
954
- const response = await sessionPort.sendAndWait(emitter.prompt);
948
+ await sessionPort.send(emitter.prompt);
955
949
  if (emitter.stopRequested) {
956
950
  return { ok: true };
957
951
  }
958
- const responseText = toText(response?.data?.content ?? response?.data ?? response);
959
- if (responseText.trim()) {
960
- lineRouter.handlePromptResult(emitter, responseText);
961
- } else {
962
- lineRouter.appendSystemMessage(
963
- emitter,
964
- `Emitter '${emitter.name}' received an empty response from prompt work.`
965
- );
966
- }
952
+ emitter.lineCount += 1;
967
953
  return { ok: true };
968
954
  } catch (error) {
969
955
  return {
@@ -1537,6 +1523,116 @@ function createTools(deps) {
1537
1523
  ];
1538
1524
  }
1539
1525
 
1526
+ // src/update/checker.mjs
1527
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync } from "node:fs";
1528
+ import { fileURLToPath } from "node:url";
1529
+ import path4 from "node:path";
1530
+ import os from "node:os";
1531
+ var PKG_NAME = "copilot-tap-extension";
1532
+ var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
1533
+ function getCopilotHome() {
1534
+ return process.env.COPILOT_HOME || path4.join(os.homedir(), ".copilot");
1535
+ }
1536
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1537
+ function getInstalledVersion() {
1538
+ try {
1539
+ const extensionDir = path4.dirname(fileURLToPath(import.meta.url));
1540
+ const candidates = [
1541
+ path4.join(extensionDir, "version.json"),
1542
+ path4.join(extensionDir, "..", "version.json"),
1543
+ path4.join(extensionDir, "..", "..", "dist", "version.json")
1544
+ ];
1545
+ for (const candidate of candidates) {
1546
+ if (existsSync2(candidate)) {
1547
+ return JSON.parse(readFileSync2(candidate, "utf8")).version;
1548
+ }
1549
+ }
1550
+ return null;
1551
+ } catch {
1552
+ return null;
1553
+ }
1554
+ }
1555
+ function getUpdateStateFile() {
1556
+ return path4.join(getCopilotHome(), ".tap-update-state.json");
1557
+ }
1558
+ function readUpdateState() {
1559
+ try {
1560
+ return JSON.parse(readFileSync2(getUpdateStateFile(), "utf8"));
1561
+ } catch {
1562
+ return {};
1563
+ }
1564
+ }
1565
+ function writeUpdateState(state) {
1566
+ try {
1567
+ const stateDir = getCopilotHome();
1568
+ mkdirSync(stateDir, { recursive: true });
1569
+ writeFileSync2(getUpdateStateFile(), JSON.stringify(state, null, 2) + "\n");
1570
+ } catch {
1571
+ }
1572
+ }
1573
+ function shouldCheck() {
1574
+ const state = readUpdateState();
1575
+ if (!state.lastCheckAt) {
1576
+ return true;
1577
+ }
1578
+ return Date.now() - state.lastCheckAt > CHECK_INTERVAL_MS;
1579
+ }
1580
+ function recordCheck(latest) {
1581
+ const state = readUpdateState();
1582
+ state.lastCheckAt = Date.now();
1583
+ if (latest) {
1584
+ state.latestVersion = latest;
1585
+ }
1586
+ writeUpdateState(state);
1587
+ }
1588
+ async function fetchLatestVersion() {
1589
+ const res = await fetch(REGISTRY_URL);
1590
+ if (!res.ok) {
1591
+ return null;
1592
+ }
1593
+ const data = await res.json();
1594
+ return data.version ?? null;
1595
+ }
1596
+ function isNewer(installed, latest) {
1597
+ if (!installed || !latest) {
1598
+ return !!latest;
1599
+ }
1600
+ const pa = installed.split(".").map(Number);
1601
+ const pb = latest.split(".").map(Number);
1602
+ for (let i = 0; i < 3; i++) {
1603
+ if ((pb[i] || 0) > (pa[i] || 0)) {
1604
+ return true;
1605
+ }
1606
+ if ((pb[i] || 0) < (pa[i] || 0)) {
1607
+ return false;
1608
+ }
1609
+ }
1610
+ return false;
1611
+ }
1612
+ async function checkForUpdate(sessionPort) {
1613
+ try {
1614
+ if (!shouldCheck()) {
1615
+ return;
1616
+ }
1617
+ const installed = getInstalledVersion();
1618
+ if (!installed) {
1619
+ return;
1620
+ }
1621
+ const latest = await fetchLatestVersion();
1622
+ if (!latest) {
1623
+ return;
1624
+ }
1625
+ recordCheck(latest);
1626
+ if (!isNewer(installed, latest)) {
1627
+ return;
1628
+ }
1629
+ await sessionPort.log(
1630
+ `Update available: v${installed} \u2192 v${latest}. Run \`npx ${PKG_NAME}\` to update.`
1631
+ );
1632
+ } catch {
1633
+ }
1634
+ }
1635
+
1540
1636
  // src/hooks.mjs
1541
1637
  function sessionInjectorSummary(streams) {
1542
1638
  const subscribed = streams.list().filter((stream) => stream.sessionInjector.enabled);
@@ -1589,6 +1685,8 @@ function createHooks({ streams, configStore, supervisor, sessionPort, setBaseCwd
1589
1685
  return {
1590
1686
  onSessionStart: async (input) => {
1591
1687
  streams.ensure(DEFAULT_STREAM, DEFAULT_STREAM_DESCRIPTION);
1688
+ checkForUpdate(sessionPort).catch(() => {
1689
+ });
1592
1690
  let configSummary = "No config loaded.";
1593
1691
  try {
1594
1692
  configSummary = await applyPersistentConfig({
@@ -1664,7 +1762,7 @@ function createCopilotChannelsRuntime(options = {}) {
1664
1762
  };
1665
1763
  }
1666
1764
 
1667
- // .github/extensions/tap/extension.mjs
1765
+ // src/extension.mjs
1668
1766
  var runtime = createCopilotChannelsRuntime({
1669
1767
  cwd: process.cwd()
1670
1768
  });
@@ -67,12 +67,11 @@ When this skill is invoked:
67
67
 
68
68
  ## Why subscribe defaults to false
69
69
 
70
- PromptEmitter output is already delivered through two paths:
70
+ PromptEmitter output is delivered through a single path:
71
71
 
72
- 1. **`session.sendAndWait()`** -- the prompt runs inside the session, so Copilot processes and responds to it directly.
73
- 2. **Notification dispatcher** -- each result line is also enqueued via `handlePromptResult` and injected as a background event stream update via `session.send()`.
72
+ 1. **`session.send()`** -- the prompt is dispatched fire-and-forget; Copilot processes and responds to it directly inside the session.
74
73
 
75
- The `subscribe` flag controls a third layer: the **SessionInjector**. When enabled, it additionally pushes system-level messages (emitter started, stopped, errored) into the session.
74
+ The `subscribe` flag controls the **SessionInjector**. When enabled, it additionally pushes system-level messages (emitter started, stopped, errored) into the session.
76
75
 
77
76
  For PromptEmitters, the main results already reach the session without the SessionInjector. Setting `subscribe = true` adds system noise on top of content that is already being delivered. Default to `false` to keep things clean.
78
77
 
@@ -0,0 +1,3 @@
1
+ {
2
+ "version": "1.0.1"
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-tap-extension",
3
- "version": "0.2.5",
3
+ "version": "1.0.1",
4
4
  "description": "Copilot CLI extension for background event emitters, event streams, and session injection.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "node scripts/build.mjs",
22
22
  "prepublishOnly": "npm run build",
23
- "check": "node --check ./src/tap-runtime.mjs && node --check ./.github/extensions/tap/extension.mjs && node --check ./evals/run.mjs && node --check ./examples/heartbeat.mjs",
23
+ "check": "node --check ./src/tap-runtime.mjs && node --check ./src/extension.mjs && node --check ./evals/run.mjs && node --check ./examples/heartbeat.mjs",
24
24
  "demo:heartbeat": "node ./examples/heartbeat.mjs",
25
25
  "evals:list": "node ./evals/run.mjs list",
26
26
  "evals:smoke": "node ./evals/run.mjs smoke",