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 +1 -1
- package/bin/install.mjs +91 -16
- package/dist/extension.mjs +117 -19
- package/dist/skills/loop/SKILL.md +3 -4
- package/dist/version.json +3 -0
- package/package.json +2 -2
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](
|
|
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
|
-
--
|
|
25
|
-
--help,
|
|
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",
|
|
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
|
-
|
|
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
|
|
88
|
+
return getCopilotHome();
|
|
67
89
|
}
|
|
68
90
|
return path.join(process.cwd(), ".github");
|
|
69
91
|
}
|
|
70
92
|
|
|
71
|
-
function copyArtifact(src, dest, label
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/extension.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// https://github.com/amitse/copilot-tap-extension
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
//
|
|
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 =
|
|
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
|
-
|
|
948
|
+
await sessionPort.send(emitter.prompt);
|
|
955
949
|
if (emitter.stopRequested) {
|
|
956
950
|
return { ok: true };
|
|
957
951
|
}
|
|
958
|
-
|
|
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
|
-
//
|
|
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
|
|
70
|
+
PromptEmitter output is delivered through a single path:
|
|
71
71
|
|
|
72
|
-
1. **`session.
|
|
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
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-tap-extension",
|
|
3
|
-
"version": "0.
|
|
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
|
|
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",
|