copilot-tap-extension 0.2.5 → 1.0.0
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 +66 -16
- package/dist/extension.mjs +112 -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,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, mkdirSync, copyFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
@@ -11,6 +11,14 @@ const distDir = path.join(pkgRoot, "dist");
|
|
|
11
11
|
const BRAND = "※ tap";
|
|
12
12
|
const EXT_DIR_NAME = "tap";
|
|
13
13
|
|
|
14
|
+
function getPackageVersion() {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(path.join(distDir, "version.json"), "utf8")).version;
|
|
17
|
+
} catch {
|
|
18
|
+
return JSON.parse(readFileSync(path.join(pkgRoot, "package.json"), "utf8")).version;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
function usage() {
|
|
15
23
|
console.log(`
|
|
16
24
|
${BRAND} — Copilot CLI extension installer
|
|
@@ -18,14 +26,18 @@ ${BRAND} — Copilot CLI extension installer
|
|
|
18
26
|
Usage:
|
|
19
27
|
npx copilot-tap-extension [options]
|
|
20
28
|
|
|
29
|
+
If ※ tap is already installed, updates core files (extension + version)
|
|
30
|
+
and preserves customizable artifacts. If fresh, does a full install.
|
|
31
|
+
|
|
21
32
|
Options:
|
|
22
33
|
--global, -g Install to ~/.copilot/ (default)
|
|
23
34
|
--local, -l Install to .github/ (project-scoped)
|
|
24
|
-
--
|
|
25
|
-
--help,
|
|
35
|
+
--full Force a full install even if already installed
|
|
36
|
+
--help, -h Show this help message
|
|
26
37
|
|
|
27
38
|
Installs:
|
|
28
39
|
extensions/tap/extension.mjs The bundled ※ tap extension
|
|
40
|
+
extensions/tap/version.json Installed version metadata
|
|
29
41
|
skills/loop/SKILL.md The /loop skill for prompt-based loops
|
|
30
42
|
copilot-instructions.md Agent instructions for using ※ tap
|
|
31
43
|
`);
|
|
@@ -33,7 +45,7 @@ Installs:
|
|
|
33
45
|
|
|
34
46
|
function parseArgs(argv) {
|
|
35
47
|
const args = argv.slice(2);
|
|
36
|
-
const flags = { scope: "global",
|
|
48
|
+
const flags = { scope: "global", full: false, help: false };
|
|
37
49
|
for (const arg of args) {
|
|
38
50
|
switch (arg) {
|
|
39
51
|
case "--global":
|
|
@@ -44,9 +56,14 @@ function parseArgs(argv) {
|
|
|
44
56
|
case "-l":
|
|
45
57
|
flags.scope = "local";
|
|
46
58
|
break;
|
|
59
|
+
case "--full":
|
|
60
|
+
flags.full = true;
|
|
61
|
+
break;
|
|
62
|
+
// Keep legacy flags working
|
|
47
63
|
case "--force":
|
|
48
64
|
case "-f":
|
|
49
|
-
|
|
65
|
+
case "--update":
|
|
66
|
+
case "-u":
|
|
50
67
|
break;
|
|
51
68
|
case "--help":
|
|
52
69
|
case "-h":
|
|
@@ -68,33 +85,63 @@ function getTargetRoot(scope) {
|
|
|
68
85
|
return path.join(process.cwd(), ".github");
|
|
69
86
|
}
|
|
70
87
|
|
|
71
|
-
function copyArtifact(src, dest, label
|
|
88
|
+
function copyArtifact(src, dest, label) {
|
|
72
89
|
if (!existsSync(src)) {
|
|
73
90
|
console.error(` ✗ ${label}: source not found (${src})`);
|
|
74
91
|
return false;
|
|
75
92
|
}
|
|
76
|
-
if (existsSync(dest) && !flags.force) {
|
|
77
|
-
console.log(` ⊘ ${label}: already exists, skipping (use --force to overwrite)`);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
93
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
81
94
|
copyFileSync(src, dest);
|
|
82
95
|
console.log(` ✓ ${label}`);
|
|
83
96
|
return true;
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
function getInstalledVersion(targetRoot) {
|
|
100
|
+
try {
|
|
101
|
+
const versionFile = path.join(targetRoot, "extensions", EXT_DIR_NAME, "version.json");
|
|
102
|
+
return JSON.parse(readFileSync(versionFile, "utf8")).version;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isAlreadyInstalled(targetRoot) {
|
|
109
|
+
return existsSync(path.join(targetRoot, "extensions", EXT_DIR_NAME, "extension.mjs"));
|
|
110
|
+
}
|
|
111
|
+
|
|
86
112
|
function install(flags) {
|
|
87
113
|
const targetRoot = getTargetRoot(flags.scope);
|
|
88
114
|
const scopeLabel = flags.scope === "global" ? "global (~/.copilot)" : "local (.github)";
|
|
115
|
+
const packageVersion = getPackageVersion();
|
|
116
|
+
const installed = isAlreadyInstalled(targetRoot);
|
|
117
|
+
const isUpdate = installed && !flags.full;
|
|
118
|
+
|
|
119
|
+
if (isUpdate) {
|
|
120
|
+
const installedVersion = getInstalledVersion(targetRoot);
|
|
121
|
+
if (installedVersion && installedVersion === packageVersion) {
|
|
122
|
+
console.log(`\n${BRAND} — already up to date (v${installedVersion})\n`);
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
126
|
+
console.log(`\n${BRAND} — updating ${fromLabel} → v${packageVersion} (${scopeLabel})\n`);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(`\n${BRAND} — installing v${packageVersion} (${scopeLabel})\n`);
|
|
129
|
+
}
|
|
89
130
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const artifacts = [
|
|
131
|
+
const coreArtifacts = [
|
|
93
132
|
{
|
|
94
133
|
src: path.join(distDir, "extension.mjs"),
|
|
95
134
|
dest: path.join(targetRoot, "extensions", EXT_DIR_NAME, "extension.mjs"),
|
|
96
135
|
label: "extensions/tap/extension.mjs"
|
|
97
136
|
},
|
|
137
|
+
{
|
|
138
|
+
src: path.join(distDir, "version.json"),
|
|
139
|
+
dest: path.join(targetRoot, "extensions", EXT_DIR_NAME, "version.json"),
|
|
140
|
+
label: "extensions/tap/version.json"
|
|
141
|
+
}
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const ancillaryArtifacts = [
|
|
98
145
|
{
|
|
99
146
|
src: path.join(distDir, "skills", "loop", "SKILL.md"),
|
|
100
147
|
dest: path.join(targetRoot, "skills", "loop", "SKILL.md"),
|
|
@@ -107,18 +154,21 @@ function install(flags) {
|
|
|
107
154
|
}
|
|
108
155
|
];
|
|
109
156
|
|
|
157
|
+
const artifacts = isUpdate ? coreArtifacts : [...coreArtifacts, ...ancillaryArtifacts];
|
|
158
|
+
|
|
110
159
|
let allOk = true;
|
|
111
160
|
for (const { src, dest, label } of artifacts) {
|
|
112
|
-
if (!copyArtifact(src, dest, label
|
|
161
|
+
if (!copyArtifact(src, dest, label)) {
|
|
113
162
|
allOk = false;
|
|
114
163
|
}
|
|
115
164
|
}
|
|
116
165
|
|
|
117
166
|
console.log();
|
|
118
167
|
if (allOk) {
|
|
119
|
-
|
|
168
|
+
const verb = isUpdate ? "updated" : "installed";
|
|
169
|
+
console.log(`✓ ${BRAND} ${verb} to ${targetRoot}`);
|
|
120
170
|
} else {
|
|
121
|
-
console.error(`⚠ Some artifacts could not be installed.`);
|
|
171
|
+
console.error(`⚠ Some artifacts could not be ${isUpdate ? "updated" : "installed"}.`);
|
|
122
172
|
process.exit(1);
|
|
123
173
|
}
|
|
124
174
|
}
|
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,111 @@ 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
|
+
var UPDATE_STATE_DIR = path4.join(os.homedir(), ".copilot");
|
|
1534
|
+
var UPDATE_STATE_FILE = path4.join(UPDATE_STATE_DIR, ".tap-update-state.json");
|
|
1535
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1536
|
+
function getInstalledVersion() {
|
|
1537
|
+
try {
|
|
1538
|
+
const extensionDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1539
|
+
const candidates = [
|
|
1540
|
+
path4.join(extensionDir, "version.json"),
|
|
1541
|
+
path4.join(extensionDir, "..", "version.json"),
|
|
1542
|
+
path4.join(extensionDir, "..", "..", "dist", "version.json")
|
|
1543
|
+
];
|
|
1544
|
+
for (const candidate of candidates) {
|
|
1545
|
+
if (existsSync2(candidate)) {
|
|
1546
|
+
return JSON.parse(readFileSync2(candidate, "utf8")).version;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return null;
|
|
1550
|
+
} catch {
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
function readUpdateState() {
|
|
1555
|
+
try {
|
|
1556
|
+
return JSON.parse(readFileSync2(UPDATE_STATE_FILE, "utf8"));
|
|
1557
|
+
} catch {
|
|
1558
|
+
return {};
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function writeUpdateState(state) {
|
|
1562
|
+
try {
|
|
1563
|
+
mkdirSync(UPDATE_STATE_DIR, { recursive: true });
|
|
1564
|
+
writeFileSync2(UPDATE_STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
function shouldCheck() {
|
|
1569
|
+
const state = readUpdateState();
|
|
1570
|
+
if (!state.lastCheckAt) {
|
|
1571
|
+
return true;
|
|
1572
|
+
}
|
|
1573
|
+
return Date.now() - state.lastCheckAt > CHECK_INTERVAL_MS;
|
|
1574
|
+
}
|
|
1575
|
+
function recordCheck(latest) {
|
|
1576
|
+
const state = readUpdateState();
|
|
1577
|
+
state.lastCheckAt = Date.now();
|
|
1578
|
+
if (latest) {
|
|
1579
|
+
state.latestVersion = latest;
|
|
1580
|
+
}
|
|
1581
|
+
writeUpdateState(state);
|
|
1582
|
+
}
|
|
1583
|
+
async function fetchLatestVersion() {
|
|
1584
|
+
const res = await fetch(REGISTRY_URL);
|
|
1585
|
+
if (!res.ok) {
|
|
1586
|
+
return null;
|
|
1587
|
+
}
|
|
1588
|
+
const data = await res.json();
|
|
1589
|
+
return data.version ?? null;
|
|
1590
|
+
}
|
|
1591
|
+
function isNewer(installed, latest) {
|
|
1592
|
+
if (!installed || !latest) {
|
|
1593
|
+
return !!latest;
|
|
1594
|
+
}
|
|
1595
|
+
const pa = installed.split(".").map(Number);
|
|
1596
|
+
const pb = latest.split(".").map(Number);
|
|
1597
|
+
for (let i = 0; i < 3; i++) {
|
|
1598
|
+
if ((pb[i] || 0) > (pa[i] || 0)) {
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1601
|
+
if ((pb[i] || 0) < (pa[i] || 0)) {
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return false;
|
|
1606
|
+
}
|
|
1607
|
+
async function checkForUpdate(sessionPort) {
|
|
1608
|
+
try {
|
|
1609
|
+
if (!shouldCheck()) {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const installed = getInstalledVersion();
|
|
1613
|
+
if (!installed) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
const latest = await fetchLatestVersion();
|
|
1617
|
+
if (!latest) {
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
recordCheck(latest);
|
|
1621
|
+
if (!isNewer(installed, latest)) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
await sessionPort.log(
|
|
1625
|
+
`Update available: v${installed} \u2192 v${latest}. Run \`npx ${PKG_NAME}\` to update.`
|
|
1626
|
+
);
|
|
1627
|
+
} catch {
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1540
1631
|
// src/hooks.mjs
|
|
1541
1632
|
function sessionInjectorSummary(streams) {
|
|
1542
1633
|
const subscribed = streams.list().filter((stream) => stream.sessionInjector.enabled);
|
|
@@ -1589,6 +1680,8 @@ function createHooks({ streams, configStore, supervisor, sessionPort, setBaseCwd
|
|
|
1589
1680
|
return {
|
|
1590
1681
|
onSessionStart: async (input) => {
|
|
1591
1682
|
streams.ensure(DEFAULT_STREAM, DEFAULT_STREAM_DESCRIPTION);
|
|
1683
|
+
checkForUpdate(sessionPort).catch(() => {
|
|
1684
|
+
});
|
|
1592
1685
|
let configSummary = "No config loaded.";
|
|
1593
1686
|
try {
|
|
1594
1687
|
configSummary = await applyPersistentConfig({
|
|
@@ -1664,7 +1757,7 @@ function createCopilotChannelsRuntime(options = {}) {
|
|
|
1664
1757
|
};
|
|
1665
1758
|
}
|
|
1666
1759
|
|
|
1667
|
-
//
|
|
1760
|
+
// src/extension.mjs
|
|
1668
1761
|
var runtime = createCopilotChannelsRuntime({
|
|
1669
1762
|
cwd: process.cwd()
|
|
1670
1763
|
});
|
|
@@ -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.0",
|
|
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",
|