copilot-tap-extension 0.2.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/LICENSE +21 -0
- package/README.md +207 -0
- package/bin/install.mjs +133 -0
- package/dist/copilot-instructions.md +187 -0
- package/dist/extension.mjs +1682 -0
- package/dist/skills/loop/SKILL.md +89 -0
- package/package.json +44 -0
|
@@ -0,0 +1,1682 @@
|
|
|
1
|
+
// ※ tap — copilot-tap-extension (bundled)
|
|
2
|
+
// https://github.com/amitse/copilot-tap-extension
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// .github/extensions/tap/extension.mjs
|
|
6
|
+
import { joinSession } from "@github/copilot-sdk/extension";
|
|
7
|
+
|
|
8
|
+
// src/consts.mjs
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
var GITHUB_DIR = ".github";
|
|
11
|
+
var CONFIG_FILENAME = "tap.config.json";
|
|
12
|
+
var CONFIG_LOCATIONS = [
|
|
13
|
+
CONFIG_FILENAME,
|
|
14
|
+
`${GITHUB_DIR}${path.sep}${CONFIG_FILENAME}`
|
|
15
|
+
];
|
|
16
|
+
var COPILOT_INSTRUCTIONS_PATH = `${GITHUB_DIR}/copilot-instructions.md`;
|
|
17
|
+
var MAX_STREAM_ENTRIES = 200;
|
|
18
|
+
var DEFAULT_STREAM = "main";
|
|
19
|
+
var DEFAULT_STREAM_DESCRIPTION = "Extension events";
|
|
20
|
+
var RUN_INTERVAL_PATTERN = /^\s*(?:every\s+)?(\d+)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)\s*$/i;
|
|
21
|
+
var NOTIFICATION_BATCH_SIZE = 4;
|
|
22
|
+
var BRAND = "\u203B tap";
|
|
23
|
+
var LOG_PREFIX = `${BRAND}:`;
|
|
24
|
+
var LIFESPAN = Object.freeze({
|
|
25
|
+
TEMPORARY: "temporary",
|
|
26
|
+
PERSISTENT: "persistent"
|
|
27
|
+
});
|
|
28
|
+
var OWNERSHIP = Object.freeze({
|
|
29
|
+
USER_OWNED: "userOwned",
|
|
30
|
+
MODEL_OWNED: "modelOwned"
|
|
31
|
+
});
|
|
32
|
+
var EVENT_OUTCOME = Object.freeze({
|
|
33
|
+
DROP: "drop",
|
|
34
|
+
KEEP: "keep",
|
|
35
|
+
SURFACE: "surface",
|
|
36
|
+
INJECT: "inject"
|
|
37
|
+
});
|
|
38
|
+
var EMITTER_TYPE = Object.freeze({
|
|
39
|
+
COMMAND: "command",
|
|
40
|
+
PROMPT: "prompt"
|
|
41
|
+
});
|
|
42
|
+
var RUN_SCHEDULE = Object.freeze({
|
|
43
|
+
CONTINUOUS: "continuous",
|
|
44
|
+
TIMED: "timed",
|
|
45
|
+
ONE_TIME: "oneTime",
|
|
46
|
+
IDLE: "idle"
|
|
47
|
+
});
|
|
48
|
+
var IDLE_PROMPT_DELAY_MS = 2e3;
|
|
49
|
+
var IDLE_PROMPT_BACKOFF_MS = 5e3;
|
|
50
|
+
var EMITTER_STATUS = Object.freeze({
|
|
51
|
+
QUEUED: "queued",
|
|
52
|
+
WAITING: "waiting",
|
|
53
|
+
RUNNING: "running",
|
|
54
|
+
STOPPING: "stopping",
|
|
55
|
+
STOPPED: "stopped",
|
|
56
|
+
EXITED: "exited",
|
|
57
|
+
COMPLETED: "completed",
|
|
58
|
+
ERROR: "error"
|
|
59
|
+
});
|
|
60
|
+
var RUN_STATUS = Object.freeze({
|
|
61
|
+
SUCCESS: "success",
|
|
62
|
+
FAILURE: "failure"
|
|
63
|
+
});
|
|
64
|
+
var EMITTER_OPERATION_STATUS = Object.freeze({
|
|
65
|
+
REMOVED_FROM_CONFIG: "removed-from-config",
|
|
66
|
+
CONFIGURED: "configured"
|
|
67
|
+
});
|
|
68
|
+
var TERMINAL_EMITTER_STATUSES = Object.freeze([
|
|
69
|
+
EMITTER_STATUS.STOPPED,
|
|
70
|
+
EMITTER_STATUS.EXITED,
|
|
71
|
+
EMITTER_STATUS.COMPLETED,
|
|
72
|
+
EMITTER_STATUS.ERROR
|
|
73
|
+
]);
|
|
74
|
+
var STREAM = Object.freeze({
|
|
75
|
+
STDOUT: "stdout",
|
|
76
|
+
STDERR: "stderr",
|
|
77
|
+
PROMPT: "prompt",
|
|
78
|
+
SYSTEM: "system"
|
|
79
|
+
});
|
|
80
|
+
var SOURCE = Object.freeze({
|
|
81
|
+
SYSTEM: "system",
|
|
82
|
+
TOOL: "tool",
|
|
83
|
+
EMITTER: "emitter",
|
|
84
|
+
EMITTER_STDERR: "emitter:stderr",
|
|
85
|
+
EMITTER_PROMPT: "emitter:prompt"
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// src/session/port.mjs
|
|
89
|
+
function createSessionPort(initialSession = null) {
|
|
90
|
+
let session2 = initialSession;
|
|
91
|
+
function attach(nextSession) {
|
|
92
|
+
session2 = nextSession ?? null;
|
|
93
|
+
return session2;
|
|
94
|
+
}
|
|
95
|
+
function current() {
|
|
96
|
+
return session2;
|
|
97
|
+
}
|
|
98
|
+
async function safeLog(message, options) {
|
|
99
|
+
if (!session2) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await session2.log(message, options);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function log(message, options = {}) {
|
|
108
|
+
await safeLog(`${LOG_PREFIX} ${message}`, {
|
|
109
|
+
ephemeral: true,
|
|
110
|
+
...options
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function send(prompt) {
|
|
114
|
+
if (!session2) {
|
|
115
|
+
throw new Error("Session is not attached; cannot send prompt.");
|
|
116
|
+
}
|
|
117
|
+
return session2.send({ prompt });
|
|
118
|
+
}
|
|
119
|
+
async function sendAndWait(prompt) {
|
|
120
|
+
if (!session2) {
|
|
121
|
+
throw new Error("Session is not attached; cannot send prompt.");
|
|
122
|
+
}
|
|
123
|
+
return session2.sendAndWait({ prompt });
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
attach,
|
|
127
|
+
current,
|
|
128
|
+
log,
|
|
129
|
+
send,
|
|
130
|
+
sendAndWait
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/util/normalize.mjs
|
|
135
|
+
function normalizeName(value, fallback = "") {
|
|
136
|
+
const normalized = String(value ?? "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
137
|
+
return normalized || fallback;
|
|
138
|
+
}
|
|
139
|
+
function normalizeLifespan(value, fallback = LIFESPAN.TEMPORARY) {
|
|
140
|
+
return String(value ?? fallback).trim().toLowerCase() === LIFESPAN.PERSISTENT ? LIFESPAN.PERSISTENT : LIFESPAN.TEMPORARY;
|
|
141
|
+
}
|
|
142
|
+
function normalizeOwnership(value, fallback = OWNERSHIP.MODEL_OWNED) {
|
|
143
|
+
return String(value ?? fallback).trim().toLowerCase() === OWNERSHIP.USER_OWNED ? OWNERSHIP.USER_OWNED : OWNERSHIP.MODEL_OWNED;
|
|
144
|
+
}
|
|
145
|
+
function normalizeOutcome(value, fallback = EVENT_OUTCOME.SURFACE) {
|
|
146
|
+
return String(value ?? fallback).trim().toLowerCase() === EVENT_OUTCOME.DROP ? EVENT_OUTCOME.DROP : fallback;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/util/text.mjs
|
|
150
|
+
function toText(value) {
|
|
151
|
+
if (typeof value === "string") {
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
return value.map((item) => toText(item)).filter(Boolean).join("\n");
|
|
156
|
+
}
|
|
157
|
+
if (value && typeof value === "object") {
|
|
158
|
+
if (typeof value.text === "string") {
|
|
159
|
+
return value.text;
|
|
160
|
+
}
|
|
161
|
+
if (typeof value.content === "string") {
|
|
162
|
+
return value.content;
|
|
163
|
+
}
|
|
164
|
+
return JSON.stringify(value, null, 2);
|
|
165
|
+
}
|
|
166
|
+
return String(value ?? "");
|
|
167
|
+
}
|
|
168
|
+
function previewText(value, maxLength = 120) {
|
|
169
|
+
const text = String(value ?? "").trim();
|
|
170
|
+
if (text.length <= maxLength) {
|
|
171
|
+
return text;
|
|
172
|
+
}
|
|
173
|
+
return `${text.slice(0, Math.max(maxLength - 3, 1))}...`;
|
|
174
|
+
}
|
|
175
|
+
function splitTextLines(value) {
|
|
176
|
+
return toText(value).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
177
|
+
}
|
|
178
|
+
function clampLimit(value, fallback = 20) {
|
|
179
|
+
const parsed = Number.parseInt(String(value ?? fallback), 10);
|
|
180
|
+
if (Number.isNaN(parsed)) {
|
|
181
|
+
return fallback;
|
|
182
|
+
}
|
|
183
|
+
return Math.min(Math.max(parsed, 1), 100);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/util/time.mjs
|
|
187
|
+
function nowIso() {
|
|
188
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
189
|
+
}
|
|
190
|
+
function parseLoopInterval(value) {
|
|
191
|
+
if (value === void 0 || value === null || String(value).trim() === "") {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const trimmed = String(value).trim().toLowerCase();
|
|
195
|
+
if (trimmed === "idle") {
|
|
196
|
+
return { text: "idle", ms: 0, idle: true };
|
|
197
|
+
}
|
|
198
|
+
const match = String(value).trim().match(RUN_INTERVAL_PATTERN);
|
|
199
|
+
if (!match) {
|
|
200
|
+
throw new Error(`Invalid every interval '${value}'. Use values like 30s, 5m, 2h, or 1d.`);
|
|
201
|
+
}
|
|
202
|
+
const amount = Number.parseInt(match[1], 10);
|
|
203
|
+
if (Number.isNaN(amount) || amount < 1) {
|
|
204
|
+
throw new Error(`Invalid every interval '${value}'. The number must be 1 or greater.`);
|
|
205
|
+
}
|
|
206
|
+
const unitToken = match[2].toLowerCase();
|
|
207
|
+
let unit = "m";
|
|
208
|
+
let multiplier = 60 * 1e3;
|
|
209
|
+
if (unitToken.startsWith("s")) {
|
|
210
|
+
unit = "s";
|
|
211
|
+
multiplier = 1e3;
|
|
212
|
+
} else if (unitToken.startsWith("h")) {
|
|
213
|
+
unit = "h";
|
|
214
|
+
multiplier = 60 * 60 * 1e3;
|
|
215
|
+
} else if (unitToken.startsWith("d")) {
|
|
216
|
+
unit = "d";
|
|
217
|
+
multiplier = 24 * 60 * 60 * 1e3;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
text: `${amount}${unit}`,
|
|
221
|
+
ms: amount * multiplier
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/util/policy.mjs
|
|
226
|
+
function assertMutable(ownership, force, label) {
|
|
227
|
+
if (normalizeOwnership(ownership, OWNERSHIP.MODEL_OWNED) === OWNERSHIP.USER_OWNED && !force) {
|
|
228
|
+
throw new Error(`${label} is user-controlled. Pass force=true only when the user explicitly wants to override it.`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function isTerminalEmitterStatus(status) {
|
|
232
|
+
return TERMINAL_EMITTER_STATUSES.includes(status);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/streams/store.mjs
|
|
236
|
+
function createSessionInjector(overrides = {}) {
|
|
237
|
+
return {
|
|
238
|
+
enabled: Boolean(overrides.enabled),
|
|
239
|
+
delivery: normalizeOutcome(overrides.delivery, EVENT_OUTCOME.SURFACE),
|
|
240
|
+
lifespan: normalizeLifespan(overrides.scope ?? overrides.lifespan, LIFESPAN.TEMPORARY),
|
|
241
|
+
ownership: normalizeOwnership(overrides.managedBy ?? overrides.ownership, OWNERSHIP.MODEL_OWNED)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function createStreamStore() {
|
|
245
|
+
const streams = /* @__PURE__ */ new Map();
|
|
246
|
+
function ensure(rawName, description = "") {
|
|
247
|
+
const name = normalizeName(rawName, DEFAULT_STREAM);
|
|
248
|
+
let stream = streams.get(name);
|
|
249
|
+
if (!stream) {
|
|
250
|
+
stream = {
|
|
251
|
+
name,
|
|
252
|
+
description: String(description ?? "").trim(),
|
|
253
|
+
createdAt: nowIso(),
|
|
254
|
+
entries: [],
|
|
255
|
+
sessionInjector: createSessionInjector()
|
|
256
|
+
};
|
|
257
|
+
streams.set(name, stream);
|
|
258
|
+
} else if (description && !stream.description) {
|
|
259
|
+
stream.description = String(description).trim();
|
|
260
|
+
}
|
|
261
|
+
return stream;
|
|
262
|
+
}
|
|
263
|
+
function append(rawStream, entry) {
|
|
264
|
+
const stream = ensure(rawStream);
|
|
265
|
+
const normalizedEntry = {
|
|
266
|
+
timestamp: entry.timestamp ?? nowIso(),
|
|
267
|
+
source: entry.source ?? SOURCE.SYSTEM,
|
|
268
|
+
text: toText(entry.text).trim(),
|
|
269
|
+
monitorName: entry.monitorName ?? null,
|
|
270
|
+
stream: entry.stream ?? null
|
|
271
|
+
};
|
|
272
|
+
if (!normalizedEntry.text) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
stream.entries.push(normalizedEntry);
|
|
276
|
+
if (stream.entries.length > MAX_STREAM_ENTRIES) {
|
|
277
|
+
stream.entries.splice(0, stream.entries.length - MAX_STREAM_ENTRIES);
|
|
278
|
+
}
|
|
279
|
+
return normalizedEntry;
|
|
280
|
+
}
|
|
281
|
+
function get(rawName) {
|
|
282
|
+
return streams.get(normalizeName(rawName));
|
|
283
|
+
}
|
|
284
|
+
function list() {
|
|
285
|
+
return [...streams.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
286
|
+
}
|
|
287
|
+
function size() {
|
|
288
|
+
return streams.size;
|
|
289
|
+
}
|
|
290
|
+
function configureSessionInjector(rawName, options = {}) {
|
|
291
|
+
const stream = ensure(rawName, options.description ?? "");
|
|
292
|
+
assertMutable(stream.sessionInjector.ownership, options.force, `Session injector for stream '${stream.name}'`);
|
|
293
|
+
stream.sessionInjector = createSessionInjector({
|
|
294
|
+
enabled: options.enabled,
|
|
295
|
+
delivery: options.delivery ?? stream.sessionInjector.delivery,
|
|
296
|
+
lifespan: options.scope ?? stream.sessionInjector.lifespan,
|
|
297
|
+
ownership: options.managedBy ?? stream.sessionInjector.ownership
|
|
298
|
+
});
|
|
299
|
+
return stream;
|
|
300
|
+
}
|
|
301
|
+
function applyPersistentStream(entry) {
|
|
302
|
+
const stream = ensure(entry.name, entry.description ?? "");
|
|
303
|
+
const configInjector = entry.sessionInjector ?? entry.subscription ?? {};
|
|
304
|
+
stream.sessionInjector = createSessionInjector({
|
|
305
|
+
enabled: configInjector.enabled === true,
|
|
306
|
+
delivery: configInjector.delivery ?? EVENT_OUTCOME.SURFACE,
|
|
307
|
+
lifespan: LIFESPAN.PERSISTENT,
|
|
308
|
+
ownership: configInjector.ownership ?? configInjector.managedBy ?? OWNERSHIP.USER_OWNED
|
|
309
|
+
});
|
|
310
|
+
return stream;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
ensure,
|
|
314
|
+
append,
|
|
315
|
+
get,
|
|
316
|
+
list,
|
|
317
|
+
size,
|
|
318
|
+
configureSessionInjector,
|
|
319
|
+
applyPersistentStream
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/streams/notifications.mjs
|
|
324
|
+
function buildNotificationPrompt(batch) {
|
|
325
|
+
return [
|
|
326
|
+
`${BRAND} \u2014 background event stream update:`,
|
|
327
|
+
...batch.map((item) => {
|
|
328
|
+
const streamLabel = item.stream ? `/${item.stream}` : "";
|
|
329
|
+
return `- stream=${item.channel} emitter=${item.monitorName}${streamLabel}: ${item.text}`;
|
|
330
|
+
}),
|
|
331
|
+
"Only react if the update matters to the current task."
|
|
332
|
+
].join("\n");
|
|
333
|
+
}
|
|
334
|
+
function createNotificationDispatcher({ sessionPort }) {
|
|
335
|
+
const queue = [];
|
|
336
|
+
let inFlight = false;
|
|
337
|
+
async function flush() {
|
|
338
|
+
if (inFlight || queue.length === 0) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
inFlight = true;
|
|
342
|
+
const batch = queue.splice(0, NOTIFICATION_BATCH_SIZE);
|
|
343
|
+
try {
|
|
344
|
+
await sessionPort.send(buildNotificationPrompt(batch));
|
|
345
|
+
} catch (error) {
|
|
346
|
+
await sessionPort.log(`Failed to dispatch monitor update: ${error.message}`, { level: "warning" });
|
|
347
|
+
} finally {
|
|
348
|
+
inFlight = false;
|
|
349
|
+
if (queue.length > 0) {
|
|
350
|
+
void flush();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function enqueue(notification) {
|
|
355
|
+
queue.push(notification);
|
|
356
|
+
void flush();
|
|
357
|
+
}
|
|
358
|
+
return { enqueue };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/config/store.mjs
|
|
362
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
363
|
+
import path2 from "node:path";
|
|
364
|
+
function emptyConfig() {
|
|
365
|
+
return { streams: [], emitters: [] };
|
|
366
|
+
}
|
|
367
|
+
function ensureShape(config) {
|
|
368
|
+
if (!config || typeof config !== "object") {
|
|
369
|
+
return emptyConfig();
|
|
370
|
+
}
|
|
371
|
+
if (!Array.isArray(config.streams)) {
|
|
372
|
+
config.streams = [];
|
|
373
|
+
}
|
|
374
|
+
if (!Array.isArray(config.emitters)) {
|
|
375
|
+
config.emitters = [];
|
|
376
|
+
}
|
|
377
|
+
return config;
|
|
378
|
+
}
|
|
379
|
+
function serializeStream(stream) {
|
|
380
|
+
const entry = { name: stream.name };
|
|
381
|
+
if (stream.description) {
|
|
382
|
+
entry.description = stream.description;
|
|
383
|
+
}
|
|
384
|
+
if (stream.sessionInjector.lifespan === LIFESPAN.PERSISTENT || stream.sessionInjector.enabled) {
|
|
385
|
+
entry.sessionInjector = {
|
|
386
|
+
enabled: stream.sessionInjector.enabled,
|
|
387
|
+
delivery: stream.sessionInjector.delivery,
|
|
388
|
+
ownership: stream.sessionInjector.ownership
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return entry;
|
|
392
|
+
}
|
|
393
|
+
function serializeEmitter(emitter) {
|
|
394
|
+
const entry = {
|
|
395
|
+
name: emitter.name,
|
|
396
|
+
stream: emitter.channel,
|
|
397
|
+
autoStart: emitter.autoStart,
|
|
398
|
+
includeStderr: emitter.includeStderr,
|
|
399
|
+
ownership: emitter.managedBy
|
|
400
|
+
};
|
|
401
|
+
if (emitter.command) {
|
|
402
|
+
entry.command = emitter.command;
|
|
403
|
+
}
|
|
404
|
+
if (emitter.prompt) {
|
|
405
|
+
entry.prompt = emitter.prompt;
|
|
406
|
+
}
|
|
407
|
+
if (emitter.every) {
|
|
408
|
+
entry.every = emitter.every;
|
|
409
|
+
}
|
|
410
|
+
if (emitter.description) {
|
|
411
|
+
entry.description = emitter.description;
|
|
412
|
+
}
|
|
413
|
+
if (emitter.requestedCwd) {
|
|
414
|
+
entry.cwd = emitter.requestedCwd;
|
|
415
|
+
}
|
|
416
|
+
entry.eventFilter = {};
|
|
417
|
+
if (emitter.classifier.includePattern) {
|
|
418
|
+
entry.eventFilter.includePattern = emitter.classifier.includePattern;
|
|
419
|
+
}
|
|
420
|
+
if (emitter.classifier.excludePattern) {
|
|
421
|
+
entry.eventFilter.excludePattern = emitter.classifier.excludePattern;
|
|
422
|
+
}
|
|
423
|
+
if (emitter.classifier.notifyPattern) {
|
|
424
|
+
entry.eventFilter.notifyPattern = emitter.classifier.notifyPattern;
|
|
425
|
+
}
|
|
426
|
+
if (emitter.classifier.managedBy !== emitter.managedBy) {
|
|
427
|
+
entry.eventFilter.ownership = emitter.classifier.managedBy;
|
|
428
|
+
}
|
|
429
|
+
if (Object.keys(entry.eventFilter).length === 0) {
|
|
430
|
+
delete entry.eventFilter;
|
|
431
|
+
}
|
|
432
|
+
return entry;
|
|
433
|
+
}
|
|
434
|
+
function createConfigStore(options = {}) {
|
|
435
|
+
const fs = options.fs ?? { existsSync, readFileSync, writeFileSync };
|
|
436
|
+
const state = {
|
|
437
|
+
cwd: options.cwd ?? process.cwd(),
|
|
438
|
+
filePath: null,
|
|
439
|
+
config: emptyConfig()
|
|
440
|
+
};
|
|
441
|
+
function defaultPath(baseCwd) {
|
|
442
|
+
return path2.join(baseCwd, CONFIG_FILENAME);
|
|
443
|
+
}
|
|
444
|
+
function load(baseCwd) {
|
|
445
|
+
state.cwd = baseCwd;
|
|
446
|
+
state.filePath = defaultPath(baseCwd);
|
|
447
|
+
state.config = emptyConfig();
|
|
448
|
+
for (const relativePath of CONFIG_LOCATIONS) {
|
|
449
|
+
const filePath = path2.join(baseCwd, relativePath);
|
|
450
|
+
if (!fs.existsSync(filePath)) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
state.filePath = filePath;
|
|
454
|
+
state.config = ensureShape(JSON.parse(fs.readFileSync(filePath, "utf8")));
|
|
455
|
+
return { found: true, filePath };
|
|
456
|
+
}
|
|
457
|
+
ensureShape(state.config);
|
|
458
|
+
return { found: false, filePath: state.filePath };
|
|
459
|
+
}
|
|
460
|
+
function save() {
|
|
461
|
+
ensureShape(state.config);
|
|
462
|
+
if (!state.filePath) {
|
|
463
|
+
state.filePath = defaultPath(state.cwd);
|
|
464
|
+
}
|
|
465
|
+
const payload = {
|
|
466
|
+
streams: [...state.config.streams].sort(
|
|
467
|
+
(left, right) => normalizeName(left.name).localeCompare(normalizeName(right.name))
|
|
468
|
+
),
|
|
469
|
+
emitters: [...state.config.emitters].sort(
|
|
470
|
+
(left, right) => normalizeName(left.name).localeCompare(normalizeName(right.name))
|
|
471
|
+
)
|
|
472
|
+
};
|
|
473
|
+
fs.writeFileSync(state.filePath, `${JSON.stringify(payload, null, 2)}
|
|
474
|
+
`, "utf8");
|
|
475
|
+
}
|
|
476
|
+
function findStreamIndex(name) {
|
|
477
|
+
return state.config.streams.findIndex((stream) => normalizeName(stream.name) === name);
|
|
478
|
+
}
|
|
479
|
+
function findEmitterIndex(name) {
|
|
480
|
+
return state.config.emitters.findIndex((emitter) => normalizeName(emitter.name) === name);
|
|
481
|
+
}
|
|
482
|
+
function upsertStream(stream) {
|
|
483
|
+
ensureShape(state.config);
|
|
484
|
+
const entry = serializeStream(stream);
|
|
485
|
+
const index = findStreamIndex(stream.name);
|
|
486
|
+
if (index === -1) {
|
|
487
|
+
state.config.streams.push(entry);
|
|
488
|
+
} else {
|
|
489
|
+
state.config.streams[index] = entry;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function upsertEmitter(emitter) {
|
|
493
|
+
ensureShape(state.config);
|
|
494
|
+
const entry = serializeEmitter(emitter);
|
|
495
|
+
const index = findEmitterIndex(emitter.name);
|
|
496
|
+
if (index === -1) {
|
|
497
|
+
state.config.emitters.push(entry);
|
|
498
|
+
} else {
|
|
499
|
+
state.config.emitters[index] = entry;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function removeEmitter(name, force = false) {
|
|
503
|
+
const normalized = normalizeName(name);
|
|
504
|
+
const index = findEmitterIndex(normalized);
|
|
505
|
+
if (index === -1) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
const entry = state.config.emitters[index];
|
|
509
|
+
assertMutable(normalizeOwnership(entry.ownership, OWNERSHIP.USER_OWNED), force, `Emitter '${normalized}'`);
|
|
510
|
+
state.config.emitters.splice(index, 1);
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
function getStreams() {
|
|
514
|
+
ensureShape(state.config);
|
|
515
|
+
return state.config.streams;
|
|
516
|
+
}
|
|
517
|
+
function getEmitters() {
|
|
518
|
+
ensureShape(state.config);
|
|
519
|
+
return state.config.emitters;
|
|
520
|
+
}
|
|
521
|
+
function findEmitter(name) {
|
|
522
|
+
const index = findEmitterIndex(normalizeName(name));
|
|
523
|
+
return index === -1 ? null : state.config.emitters[index];
|
|
524
|
+
}
|
|
525
|
+
function getPath() {
|
|
526
|
+
return state.filePath;
|
|
527
|
+
}
|
|
528
|
+
function getCwd() {
|
|
529
|
+
return state.cwd;
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
load,
|
|
533
|
+
save,
|
|
534
|
+
upsertStream,
|
|
535
|
+
upsertEmitter,
|
|
536
|
+
removeEmitter,
|
|
537
|
+
getStreams,
|
|
538
|
+
getEmitters,
|
|
539
|
+
findEmitter,
|
|
540
|
+
getPath,
|
|
541
|
+
getCwd
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/util/regex.mjs
|
|
546
|
+
function compileRegex(pattern, label) {
|
|
547
|
+
if (pattern === void 0 || pattern === null || pattern === "") {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
return new RegExp(String(pattern), "i");
|
|
552
|
+
} catch (error) {
|
|
553
|
+
throw new Error(`Invalid ${label} regex '${pattern}': ${error.message}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/format/event-filter.mjs
|
|
558
|
+
function legacyToRules(source) {
|
|
559
|
+
const rules = [];
|
|
560
|
+
if (source.excludePattern) {
|
|
561
|
+
rules.push({ match: String(source.excludePattern), outcome: EVENT_OUTCOME.DROP });
|
|
562
|
+
}
|
|
563
|
+
if (source.notifyPattern) {
|
|
564
|
+
rules.push({ match: String(source.notifyPattern), outcome: EVENT_OUTCOME.INJECT });
|
|
565
|
+
}
|
|
566
|
+
if (source.includePattern) {
|
|
567
|
+
rules.push({ match: String(source.includePattern), outcome: EVENT_OUTCOME.KEEP });
|
|
568
|
+
rules.push({ match: ".*", outcome: EVENT_OUTCOME.DROP });
|
|
569
|
+
}
|
|
570
|
+
return rules;
|
|
571
|
+
}
|
|
572
|
+
function compileRules(rules) {
|
|
573
|
+
return rules.map((r) => ({
|
|
574
|
+
match: r.match,
|
|
575
|
+
regex: compileRegex(r.match, "rule.match"),
|
|
576
|
+
outcome: r.outcome
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
function createEventFilter(source = {}, fallbackOwnership = OWNERSHIP.MODEL_OWNED, fallbackLifespan = LIFESPAN.TEMPORARY) {
|
|
580
|
+
const rawRules = Array.isArray(source.rules) ? source.rules : legacyToRules(source);
|
|
581
|
+
return {
|
|
582
|
+
rules: compileRules(rawRules),
|
|
583
|
+
ownership: normalizeOwnership(source.ownership ?? source.managedBy, fallbackOwnership),
|
|
584
|
+
lifespan: normalizeLifespan(source.lifespan ?? source.scope, fallbackLifespan)
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function evaluateEventFilter(filter, text) {
|
|
588
|
+
if (!filter || !filter.rules) {
|
|
589
|
+
return EVENT_OUTCOME.KEEP;
|
|
590
|
+
}
|
|
591
|
+
for (const rule of filter.rules) {
|
|
592
|
+
if (rule.regex && rule.regex.test(text)) {
|
|
593
|
+
return rule.outcome;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return EVENT_OUTCOME.KEEP;
|
|
597
|
+
}
|
|
598
|
+
function getEventFilterInput(source = {}) {
|
|
599
|
+
if (source.eventFilter && typeof source.eventFilter === "object") {
|
|
600
|
+
return source.eventFilter;
|
|
601
|
+
}
|
|
602
|
+
if (source.classifier && typeof source.classifier === "object") {
|
|
603
|
+
return source.classifier;
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
rules: source.rules,
|
|
607
|
+
includePattern: source.includePattern,
|
|
608
|
+
excludePattern: source.excludePattern,
|
|
609
|
+
notifyPattern: source.notifyPattern,
|
|
610
|
+
ownership: source.filterOwnership ?? source.classifierManagedBy ?? source.ownership ?? source.managedBy,
|
|
611
|
+
lifespan: source.lifespan ?? source.scope
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function formatEventFilter(filter) {
|
|
615
|
+
if (!filter || !filter.rules || filter.rules.length === 0) {
|
|
616
|
+
return `rules=<none> lifespan=${filter?.lifespan ?? "?"} ownership=${filter?.ownership ?? "?"}`;
|
|
617
|
+
}
|
|
618
|
+
const rulesSummary = filter.rules.map((r) => `${r.outcome}:${JSON.stringify(r.match)}`).join(", ");
|
|
619
|
+
return `rules=[${rulesSummary}] lifespan=${filter.lifespan} ownership=${filter.ownership}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/util/path.mjs
|
|
623
|
+
import path3 from "node:path";
|
|
624
|
+
function resolveRequestedCwd(baseCwd, requestedCwd) {
|
|
625
|
+
if (!requestedCwd) {
|
|
626
|
+
return baseCwd;
|
|
627
|
+
}
|
|
628
|
+
return path3.resolve(baseCwd, requestedCwd);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/emitter/state.mjs
|
|
632
|
+
function buildEmitterState(spec, baseCwd, defaults = {}) {
|
|
633
|
+
const name = normalizeName(spec.name);
|
|
634
|
+
if (!name) {
|
|
635
|
+
throw new Error("Emitter name is required.");
|
|
636
|
+
}
|
|
637
|
+
const command = String(spec.command ?? "").trim();
|
|
638
|
+
const prompt = String(spec.prompt ?? "").trim();
|
|
639
|
+
if (!command && !prompt) {
|
|
640
|
+
throw new Error(`Emitter '${name}' must define either a command or a prompt.`);
|
|
641
|
+
}
|
|
642
|
+
if (command && prompt) {
|
|
643
|
+
throw new Error(`Emitter '${name}' cannot define both command and prompt. Choose one emitter type.`);
|
|
644
|
+
}
|
|
645
|
+
const interval = parseLoopInterval(spec.every);
|
|
646
|
+
const lifespan = normalizeLifespan(spec.scope, defaults.scope ?? LIFESPAN.TEMPORARY);
|
|
647
|
+
const ownership = normalizeOwnership(spec.managedBy, defaults.managedBy ?? OWNERSHIP.MODEL_OWNED);
|
|
648
|
+
const eventFilter = createEventFilter(
|
|
649
|
+
getEventFilterInput(spec),
|
|
650
|
+
spec.classifier?.managedBy ?? ownership,
|
|
651
|
+
lifespan
|
|
652
|
+
);
|
|
653
|
+
const emitterType = prompt ? EMITTER_TYPE.PROMPT : EMITTER_TYPE.COMMAND;
|
|
654
|
+
let runSchedule;
|
|
655
|
+
if (interval?.idle) {
|
|
656
|
+
if (!prompt) {
|
|
657
|
+
throw new Error(`Emitter '${name}': every='idle' is only valid for prompt emitters, not command emitters.`);
|
|
658
|
+
}
|
|
659
|
+
runSchedule = RUN_SCHEDULE.IDLE;
|
|
660
|
+
} else if (interval) {
|
|
661
|
+
runSchedule = RUN_SCHEDULE.TIMED;
|
|
662
|
+
} else if (prompt) {
|
|
663
|
+
runSchedule = RUN_SCHEDULE.ONE_TIME;
|
|
664
|
+
} else {
|
|
665
|
+
runSchedule = RUN_SCHEDULE.CONTINUOUS;
|
|
666
|
+
}
|
|
667
|
+
const maxRuns = spec.maxRuns != null ? Math.max(1, Math.floor(Number(spec.maxRuns))) : null;
|
|
668
|
+
return {
|
|
669
|
+
name,
|
|
670
|
+
description: String(spec.description ?? "").trim(),
|
|
671
|
+
command: command || null,
|
|
672
|
+
prompt: prompt || null,
|
|
673
|
+
emitterType,
|
|
674
|
+
runSchedule,
|
|
675
|
+
every: interval?.text ?? null,
|
|
676
|
+
everyMs: interval?.ms ?? null,
|
|
677
|
+
requestedCwd: spec.cwd ?? null,
|
|
678
|
+
cwd: resolveRequestedCwd(baseCwd, spec.cwd),
|
|
679
|
+
stream: normalizeName(spec.channel, name),
|
|
680
|
+
autoStart: spec.autoStart !== false,
|
|
681
|
+
includeStderr: spec.includeStderr !== false,
|
|
682
|
+
lifespan,
|
|
683
|
+
ownership,
|
|
684
|
+
eventFilter,
|
|
685
|
+
maxRuns,
|
|
686
|
+
startedAt: nowIso(),
|
|
687
|
+
stoppedAt: null,
|
|
688
|
+
lineCount: 0,
|
|
689
|
+
droppedLineCount: 0,
|
|
690
|
+
status: runSchedule === RUN_SCHEDULE.CONTINUOUS ? EMITTER_STATUS.RUNNING : EMITTER_STATUS.QUEUED,
|
|
691
|
+
stopRequested: false,
|
|
692
|
+
timer: null,
|
|
693
|
+
inFlight: false,
|
|
694
|
+
runCount: 0,
|
|
695
|
+
lastRunAt: null,
|
|
696
|
+
lastRunStatus: null,
|
|
697
|
+
process: null,
|
|
698
|
+
stdoutReader: null,
|
|
699
|
+
stderrReader: null,
|
|
700
|
+
exitCode: null
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/emitter/line-router.mjs
|
|
705
|
+
function createLineRouter({ streams, notifications, sessionPort }) {
|
|
706
|
+
function appendSystemMessage(emitter, text, notify = false) {
|
|
707
|
+
streams.append(emitter.stream, {
|
|
708
|
+
source: SOURCE.SYSTEM,
|
|
709
|
+
text,
|
|
710
|
+
monitorName: emitter.name
|
|
711
|
+
});
|
|
712
|
+
if (notify && streams.ensure(emitter.stream).sessionInjector.enabled) {
|
|
713
|
+
notifications.enqueue({
|
|
714
|
+
channel: emitter.stream,
|
|
715
|
+
monitorName: emitter.name,
|
|
716
|
+
stream: STREAM.SYSTEM,
|
|
717
|
+
text
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function handleLine(emitter, rawText, stream, source) {
|
|
722
|
+
const text = String(rawText ?? "").trim();
|
|
723
|
+
if (!text) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const outcome = evaluateEventFilter(emitter.eventFilter, text);
|
|
727
|
+
if (outcome === EVENT_OUTCOME.DROP) {
|
|
728
|
+
emitter.droppedLineCount += 1;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
emitter.lineCount += 1;
|
|
732
|
+
streams.append(emitter.stream, {
|
|
733
|
+
source,
|
|
734
|
+
text,
|
|
735
|
+
monitorName: emitter.name,
|
|
736
|
+
stream
|
|
737
|
+
});
|
|
738
|
+
if (outcome === EVENT_OUTCOME.SURFACE) {
|
|
739
|
+
if (sessionPort && sessionPort.log) {
|
|
740
|
+
sessionPort.log(`${BRAND} ${emitter.name}: ${text}`);
|
|
741
|
+
}
|
|
742
|
+
} else if (outcome === EVENT_OUTCOME.INJECT) {
|
|
743
|
+
notifications.enqueue({
|
|
744
|
+
channel: emitter.stream,
|
|
745
|
+
monitorName: emitter.name,
|
|
746
|
+
stream,
|
|
747
|
+
text
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function handleTextBlock(emitter, value, stream, source) {
|
|
752
|
+
for (const line of splitTextLines(value)) {
|
|
753
|
+
handleLine(emitter, line, stream, source);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function handlePromptResult(emitter, value) {
|
|
757
|
+
for (const line of splitTextLines(value)) {
|
|
758
|
+
const text = String(line ?? "").trim();
|
|
759
|
+
if (!text) {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
emitter.lineCount += 1;
|
|
763
|
+
streams.append(emitter.stream, {
|
|
764
|
+
source: SOURCE.EMITTER_PROMPT,
|
|
765
|
+
text,
|
|
766
|
+
monitorName: emitter.name,
|
|
767
|
+
stream: STREAM.PROMPT
|
|
768
|
+
});
|
|
769
|
+
notifications.enqueue({
|
|
770
|
+
channel: emitter.stream,
|
|
771
|
+
monitorName: emitter.name,
|
|
772
|
+
stream: STREAM.PROMPT,
|
|
773
|
+
text
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return { handleLine, handleTextBlock, handlePromptResult, appendSystemMessage };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/format/emitter.mjs
|
|
781
|
+
function describeEmitterWork(emitter) {
|
|
782
|
+
if (emitter.command) {
|
|
783
|
+
return `command=${emitter.command}`;
|
|
784
|
+
}
|
|
785
|
+
return `prompt=${JSON.stringify(previewText(emitter.prompt, 90))}`;
|
|
786
|
+
}
|
|
787
|
+
function formatRunningEmitter(emitter, stream) {
|
|
788
|
+
return [
|
|
789
|
+
`- ${emitter.name}:`,
|
|
790
|
+
` status=${emitter.status}`,
|
|
791
|
+
` scope=${emitter.scope}`,
|
|
792
|
+
` managedBy=${emitter.managedBy}`,
|
|
793
|
+
` emitterType=${emitter.emitterType}`,
|
|
794
|
+
` runSchedule=${emitter.runSchedule}`,
|
|
795
|
+
` stream=${emitter.channel}`,
|
|
796
|
+
` sessionInjector=${stream?.sessionInjector?.enabled ? "on" : "off"}`,
|
|
797
|
+
` cwd=${emitter.cwd}`,
|
|
798
|
+
` ${describeEmitterWork(emitter)}`,
|
|
799
|
+
emitter.every ? ` every=${emitter.every}` : null,
|
|
800
|
+
emitter.maxRuns ? ` maxRuns=${emitter.maxRuns}` : null,
|
|
801
|
+
` autoStart=${emitter.autoStart}`,
|
|
802
|
+
` includeStderr=${emitter.includeStderr}`,
|
|
803
|
+
` runs=${emitter.runCount}`,
|
|
804
|
+
` acceptedLines=${emitter.lineCount}`,
|
|
805
|
+
` droppedLines=${emitter.droppedLineCount}`,
|
|
806
|
+
` eventFilter=${formatEventFilter(emitter.eventFilter)}`,
|
|
807
|
+
emitter.description ? ` description=${emitter.description}` : null,
|
|
808
|
+
emitter.lastRunAt ? ` lastRunAt=${emitter.lastRunAt}` : null,
|
|
809
|
+
emitter.lastRunStatus ? ` lastRunStatus=${emitter.lastRunStatus}` : null,
|
|
810
|
+
emitter.exitCode !== null && emitter.exitCode !== void 0 ? ` exitCode=${emitter.exitCode}` : null
|
|
811
|
+
].filter(Boolean).join("\n");
|
|
812
|
+
}
|
|
813
|
+
function formatConfiguredEmitter(entry) {
|
|
814
|
+
const eventFilter = createEventFilter(
|
|
815
|
+
getEventFilterInput(entry),
|
|
816
|
+
entry.eventFilter?.managedBy ?? entry.classifier?.managedBy ?? entry.managedBy ?? OWNERSHIP.USER_OWNED,
|
|
817
|
+
LIFESPAN.PERSISTENT
|
|
818
|
+
);
|
|
819
|
+
const prompt = entry.prompt ? ` prompt=${JSON.stringify(previewText(entry.prompt, 90))}` : null;
|
|
820
|
+
const command = entry.command ? ` command=${entry.command}` : null;
|
|
821
|
+
const every = entry.every ? ` every=${entry.every}` : null;
|
|
822
|
+
const emitterType = entry.prompt ? EMITTER_TYPE.PROMPT : EMITTER_TYPE.COMMAND;
|
|
823
|
+
const runSchedule = entry.every ? entry.every === "idle" && entry.prompt ? RUN_SCHEDULE.IDLE : RUN_SCHEDULE.TIMED : entry.prompt ? RUN_SCHEDULE.ONE_TIME : RUN_SCHEDULE.CONTINUOUS;
|
|
824
|
+
return [
|
|
825
|
+
`- ${normalizeName(entry.name)}:`,
|
|
826
|
+
" status=configured",
|
|
827
|
+
` scope=${LIFESPAN.PERSISTENT}`,
|
|
828
|
+
` managedBy=${normalizeOwnership(entry.managedBy, OWNERSHIP.USER_OWNED)}`,
|
|
829
|
+
` emitterType=${emitterType}`,
|
|
830
|
+
` runSchedule=${runSchedule}`,
|
|
831
|
+
` stream=${normalizeName(entry.channel, normalizeName(entry.name))}`,
|
|
832
|
+
` autoStart=${entry.autoStart !== false}`,
|
|
833
|
+
` includeStderr=${entry.includeStderr !== false}`,
|
|
834
|
+
entry.cwd ? ` cwd=${entry.cwd}` : null,
|
|
835
|
+
command,
|
|
836
|
+
prompt,
|
|
837
|
+
every,
|
|
838
|
+
` eventFilter=${formatEventFilter(eventFilter)}`,
|
|
839
|
+
entry.description ? ` description=${entry.description}` : null
|
|
840
|
+
].filter(Boolean).join("\n");
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/emitter/spawn.mjs
|
|
844
|
+
import { spawn } from "node:child_process";
|
|
845
|
+
import readline from "node:readline";
|
|
846
|
+
function spawnEmitterProcess(command, cwd) {
|
|
847
|
+
if (process.platform === "win32") {
|
|
848
|
+
return spawn("powershell.exe", ["-NoLogo", "-NoProfile", "-Command", command], {
|
|
849
|
+
cwd,
|
|
850
|
+
env: process.env,
|
|
851
|
+
windowsHide: true
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
return spawn("bash", ["-lc", command], {
|
|
855
|
+
cwd,
|
|
856
|
+
env: process.env
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
function readLines(input, onLine) {
|
|
860
|
+
const reader = readline.createInterface({ input });
|
|
861
|
+
reader.on("line", onLine);
|
|
862
|
+
return reader;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/emitter/lifecycle.mjs
|
|
866
|
+
function createLifecycle({ lineRouter, sessionPort }) {
|
|
867
|
+
function wireStreams(emitter) {
|
|
868
|
+
const child = emitter.process;
|
|
869
|
+
emitter.stdoutReader = readLines(child.stdout, (line) => {
|
|
870
|
+
lineRouter.handleLine(emitter, line, STREAM.STDOUT, SOURCE.EMITTER);
|
|
871
|
+
});
|
|
872
|
+
emitter.stderrReader = readLines(child.stderr, (line) => {
|
|
873
|
+
lineRouter.handleLine(emitter, line, STREAM.STDERR, SOURCE.EMITTER_STDERR);
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
function closeStreams(emitter) {
|
|
877
|
+
if (emitter.stdoutReader) {
|
|
878
|
+
emitter.stdoutReader.close();
|
|
879
|
+
emitter.stdoutReader = null;
|
|
880
|
+
}
|
|
881
|
+
if (emitter.stderrReader) {
|
|
882
|
+
emitter.stderrReader.close();
|
|
883
|
+
emitter.stderrReader = null;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function startContinuousProcess(emitter) {
|
|
887
|
+
let child;
|
|
888
|
+
try {
|
|
889
|
+
child = spawnEmitterProcess(emitter.command, emitter.cwd);
|
|
890
|
+
} catch (error) {
|
|
891
|
+
throw new Error(`Failed to start emitter '${emitter.name}': ${error.message}`);
|
|
892
|
+
}
|
|
893
|
+
emitter.process = child;
|
|
894
|
+
emitter.status = EMITTER_STATUS.RUNNING;
|
|
895
|
+
wireStreams(emitter);
|
|
896
|
+
child.on("error", (error) => {
|
|
897
|
+
emitter.status = EMITTER_STATUS.ERROR;
|
|
898
|
+
emitter.process = null;
|
|
899
|
+
lineRouter.appendSystemMessage(emitter, `Emitter '${emitter.name}' failed: ${error.message}`, true);
|
|
900
|
+
void sessionPort.log(`Emitter '${emitter.name}' failed: ${error.message}`, { level: "warning" });
|
|
901
|
+
});
|
|
902
|
+
child.on("exit", (code, signal) => {
|
|
903
|
+
emitter.status = emitter.stopRequested ? EMITTER_STATUS.STOPPED : EMITTER_STATUS.EXITED;
|
|
904
|
+
emitter.exitCode = code;
|
|
905
|
+
emitter.stoppedAt = nowIso();
|
|
906
|
+
emitter.process = null;
|
|
907
|
+
emitter.stdoutReader = null;
|
|
908
|
+
emitter.stderrReader = null;
|
|
909
|
+
const exitMessage = emitter.stopRequested ? `Emitter '${emitter.name}' stopped.` : `Emitter '${emitter.name}' exited with code ${code ?? "null"}${signal ? ` (${signal})` : ""}.`;
|
|
910
|
+
lineRouter.appendSystemMessage(emitter, exitMessage, !emitter.stopRequested);
|
|
911
|
+
void sessionPort.log(exitMessage);
|
|
912
|
+
});
|
|
913
|
+
lineRouter.appendSystemMessage(
|
|
914
|
+
emitter,
|
|
915
|
+
`Emitter '${emitter.name}' started with ${describeEmitterWork(emitter)}.`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
async function runCommandLoopIteration(emitter) {
|
|
919
|
+
let child;
|
|
920
|
+
try {
|
|
921
|
+
child = spawnEmitterProcess(emitter.command, emitter.cwd);
|
|
922
|
+
} catch (error) {
|
|
923
|
+
return { ok: false, error: error.message };
|
|
924
|
+
}
|
|
925
|
+
emitter.process = child;
|
|
926
|
+
wireStreams(emitter);
|
|
927
|
+
return await new Promise((resolve) => {
|
|
928
|
+
let settled = false;
|
|
929
|
+
const finish = (result) => {
|
|
930
|
+
if (settled) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
settled = true;
|
|
934
|
+
closeStreams(emitter);
|
|
935
|
+
emitter.process = null;
|
|
936
|
+
emitter.exitCode = result.code ?? emitter.exitCode;
|
|
937
|
+
resolve(result);
|
|
938
|
+
};
|
|
939
|
+
child.on("error", (error) => {
|
|
940
|
+
finish({ ok: false, error: error.message });
|
|
941
|
+
});
|
|
942
|
+
child.on("exit", (code, signal) => {
|
|
943
|
+
finish({
|
|
944
|
+
ok: (code ?? 0) === 0 || emitter.stopRequested,
|
|
945
|
+
code,
|
|
946
|
+
signal,
|
|
947
|
+
error: (code ?? 0) === 0 || emitter.stopRequested ? null : `Command iteration exited with code ${code ?? "null"}${signal ? ` (${signal})` : ""}`
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
async function runPromptIteration(emitter) {
|
|
953
|
+
try {
|
|
954
|
+
const response = await sessionPort.sendAndWait(emitter.prompt);
|
|
955
|
+
if (emitter.stopRequested) {
|
|
956
|
+
return { ok: true };
|
|
957
|
+
}
|
|
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
|
+
}
|
|
967
|
+
return { ok: true };
|
|
968
|
+
} catch (error) {
|
|
969
|
+
return {
|
|
970
|
+
ok: false,
|
|
971
|
+
error: error.message,
|
|
972
|
+
deferred: (emitter.runSchedule === RUN_SCHEDULE.TIMED || emitter.runSchedule === RUN_SCHEDULE.IDLE) && /\bsession\.idle\b/i.test(String(error?.message ?? ""))
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function scheduleIteration(emitter, delayMs = 0) {
|
|
977
|
+
if (emitter.stopRequested) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (emitter.timer) {
|
|
981
|
+
clearTimeout(emitter.timer);
|
|
982
|
+
}
|
|
983
|
+
emitter.status = delayMs > 0 ? EMITTER_STATUS.WAITING : EMITTER_STATUS.QUEUED;
|
|
984
|
+
emitter.timer = setTimeout(() => {
|
|
985
|
+
emitter.timer = null;
|
|
986
|
+
void runScheduledIteration(emitter);
|
|
987
|
+
}, delayMs);
|
|
988
|
+
}
|
|
989
|
+
async function runScheduledIteration(emitter) {
|
|
990
|
+
if (emitter.stopRequested || emitter.inFlight) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
emitter.inFlight = true;
|
|
994
|
+
emitter.status = EMITTER_STATUS.RUNNING;
|
|
995
|
+
emitter.runCount += 1;
|
|
996
|
+
emitter.lastRunAt = nowIso();
|
|
997
|
+
const result = emitter.emitterType === EMITTER_TYPE.PROMPT ? await runPromptIteration(emitter) : await runCommandLoopIteration(emitter);
|
|
998
|
+
emitter.inFlight = false;
|
|
999
|
+
if (emitter.stopRequested) {
|
|
1000
|
+
emitter.status = EMITTER_STATUS.STOPPED;
|
|
1001
|
+
emitter.stoppedAt = nowIso();
|
|
1002
|
+
lineRouter.appendSystemMessage(emitter, `Emitter '${emitter.name}' stopped.`);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (result.ok) {
|
|
1006
|
+
emitter.lastRunStatus = RUN_STATUS.SUCCESS;
|
|
1007
|
+
if (emitter.runSchedule === RUN_SCHEDULE.ONE_TIME) {
|
|
1008
|
+
emitter.status = EMITTER_STATUS.COMPLETED;
|
|
1009
|
+
emitter.stoppedAt = nowIso();
|
|
1010
|
+
lineRouter.appendSystemMessage(
|
|
1011
|
+
emitter,
|
|
1012
|
+
`Emitter '${emitter.name}' completed one run of ${emitter.emitterType} work.`
|
|
1013
|
+
);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (emitter.maxRuns && emitter.runCount >= emitter.maxRuns) {
|
|
1017
|
+
emitter.status = EMITTER_STATUS.COMPLETED;
|
|
1018
|
+
emitter.stoppedAt = nowIso();
|
|
1019
|
+
lineRouter.appendSystemMessage(
|
|
1020
|
+
emitter,
|
|
1021
|
+
`Emitter '${emitter.name}' completed ${emitter.runCount} of ${emitter.maxRuns} runs.`
|
|
1022
|
+
);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
emitter.status = EMITTER_STATUS.WAITING;
|
|
1026
|
+
const delay = emitter.runSchedule === RUN_SCHEDULE.IDLE ? IDLE_PROMPT_DELAY_MS : emitter.everyMs;
|
|
1027
|
+
scheduleIteration(emitter, delay);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (result.deferred) {
|
|
1031
|
+
emitter.status = EMITTER_STATUS.WAITING;
|
|
1032
|
+
const retryDelay = emitter.runSchedule === RUN_SCHEDULE.IDLE ? IDLE_PROMPT_BACKOFF_MS : emitter.everyMs;
|
|
1033
|
+
if (emitter.runSchedule !== RUN_SCHEDULE.IDLE) {
|
|
1034
|
+
lineRouter.appendSystemMessage(
|
|
1035
|
+
emitter,
|
|
1036
|
+
`Emitter '${emitter.name}' deferred this prompt run because the session was still busy. Next attempt in ${emitter.every}.`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
scheduleIteration(emitter, retryDelay);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
emitter.lastRunStatus = RUN_STATUS.FAILURE;
|
|
1043
|
+
lineRouter.appendSystemMessage(
|
|
1044
|
+
emitter,
|
|
1045
|
+
`Emitter '${emitter.name}' iteration failed: ${result.error ?? "unknown error"}.`,
|
|
1046
|
+
true
|
|
1047
|
+
);
|
|
1048
|
+
void sessionPort.log(
|
|
1049
|
+
`Emitter '${emitter.name}' iteration failed: ${result.error ?? "unknown error"}.`,
|
|
1050
|
+
{ level: "warning" }
|
|
1051
|
+
);
|
|
1052
|
+
if (emitter.runSchedule === RUN_SCHEDULE.ONE_TIME) {
|
|
1053
|
+
emitter.status = EMITTER_STATUS.ERROR;
|
|
1054
|
+
emitter.stoppedAt = nowIso();
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
emitter.status = EMITTER_STATUS.WAITING;
|
|
1058
|
+
const failRetryDelay = emitter.runSchedule === RUN_SCHEDULE.IDLE ? IDLE_PROMPT_BACKOFF_MS : emitter.everyMs;
|
|
1059
|
+
scheduleIteration(emitter, failRetryDelay);
|
|
1060
|
+
}
|
|
1061
|
+
function startScheduled(emitter) {
|
|
1062
|
+
const scheduleLabel = emitter.runSchedule === RUN_SCHEDULE.TIMED ? `every ${emitter.every}` : emitter.runSchedule === RUN_SCHEDULE.IDLE ? "when idle" : RUN_SCHEDULE.ONE_TIME;
|
|
1063
|
+
const initialDelayMs = 0;
|
|
1064
|
+
const firstRunLabel = "";
|
|
1065
|
+
lineRouter.appendSystemMessage(
|
|
1066
|
+
emitter,
|
|
1067
|
+
`Emitter '${emitter.name}' queued ${emitter.emitterType} work (${scheduleLabel}) with ${describeEmitterWork(emitter)}.${firstRunLabel}`
|
|
1068
|
+
);
|
|
1069
|
+
scheduleIteration(emitter, initialDelayMs);
|
|
1070
|
+
}
|
|
1071
|
+
function start(emitter) {
|
|
1072
|
+
if (emitter.runSchedule === RUN_SCHEDULE.CONTINUOUS) {
|
|
1073
|
+
startContinuousProcess(emitter);
|
|
1074
|
+
} else {
|
|
1075
|
+
startScheduled(emitter);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
async function stop(emitter) {
|
|
1079
|
+
if (isTerminalEmitterStatus(emitter.status)) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
emitter.stopRequested = true;
|
|
1083
|
+
void sessionPort.log(`Stop requested for emitter '${emitter.name}'.`);
|
|
1084
|
+
if (emitter.timer) {
|
|
1085
|
+
clearTimeout(emitter.timer);
|
|
1086
|
+
emitter.timer = null;
|
|
1087
|
+
}
|
|
1088
|
+
if (!emitter.process && !emitter.inFlight) {
|
|
1089
|
+
emitter.status = EMITTER_STATUS.STOPPED;
|
|
1090
|
+
emitter.stoppedAt = nowIso();
|
|
1091
|
+
lineRouter.appendSystemMessage(emitter, `Emitter '${emitter.name}' stopped.`);
|
|
1092
|
+
void sessionPort.log(`Emitter '${emitter.name}' stopped.`);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
emitter.status = EMITTER_STATUS.STOPPING;
|
|
1096
|
+
closeStreams(emitter);
|
|
1097
|
+
if (emitter.process) {
|
|
1098
|
+
emitter.process.kill();
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return { start, stop };
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/emitter/supervisor.mjs
|
|
1105
|
+
function createEmitterSupervisor({ streams, configStore, notifications, sessionPort, getBaseCwd, persist }) {
|
|
1106
|
+
const emitters = /* @__PURE__ */ new Map();
|
|
1107
|
+
const lineRouter = createLineRouter({ streams, notifications });
|
|
1108
|
+
const lifecycle = createLifecycle({ lineRouter, sessionPort });
|
|
1109
|
+
async function start(spec, options = {}) {
|
|
1110
|
+
const baseCwd = options.baseCwd ?? getBaseCwd();
|
|
1111
|
+
const emitter = buildEmitterState(spec, baseCwd, options);
|
|
1112
|
+
const existing = emitters.get(emitter.name);
|
|
1113
|
+
if (existing && !isTerminalEmitterStatus(existing.status)) {
|
|
1114
|
+
throw new Error(`Emitter '${emitter.name}' is already active.`);
|
|
1115
|
+
}
|
|
1116
|
+
if (existing) {
|
|
1117
|
+
assertMutable(existing.ownership, options.force, `Emitter '${emitter.name}'`);
|
|
1118
|
+
}
|
|
1119
|
+
streams.ensure(emitter.stream, emitter.description || `Events for ${emitter.name}`);
|
|
1120
|
+
emitters.set(emitter.name, emitter);
|
|
1121
|
+
try {
|
|
1122
|
+
lifecycle.start(emitter);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
emitters.delete(emitter.name);
|
|
1125
|
+
throw error;
|
|
1126
|
+
}
|
|
1127
|
+
if (options.subscribe === true) {
|
|
1128
|
+
const stream = streams.configureSessionInjector(emitter.stream, {
|
|
1129
|
+
enabled: true,
|
|
1130
|
+
delivery: options.delivery ?? EVENT_OUTCOME.SURFACE,
|
|
1131
|
+
scope: options.scope ?? emitter.lifespan,
|
|
1132
|
+
managedBy: options.managedBy ?? emitter.ownership,
|
|
1133
|
+
description: spec.channelDescription ?? emitter.description,
|
|
1134
|
+
force: options.force
|
|
1135
|
+
});
|
|
1136
|
+
void sessionPort.log(
|
|
1137
|
+
`${stream.sessionInjector.enabled ? "Subscribed" : "Unsubscribed"} stream '${stream.name}' with delivery=${stream.sessionInjector.delivery} lifespan=${stream.sessionInjector.lifespan} ownership=${stream.sessionInjector.ownership}.`
|
|
1138
|
+
);
|
|
1139
|
+
if (stream.sessionInjector.lifespan === LIFESPAN.PERSISTENT) {
|
|
1140
|
+
configStore.upsertStream(stream);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (emitter.lifespan === LIFESPAN.PERSISTENT) {
|
|
1144
|
+
configStore.upsertEmitter(emitter);
|
|
1145
|
+
persist();
|
|
1146
|
+
} else if (options.subscribe === true && streams.ensure(emitter.stream).sessionInjector.lifespan === LIFESPAN.PERSISTENT) {
|
|
1147
|
+
persist();
|
|
1148
|
+
}
|
|
1149
|
+
await sessionPort.log(
|
|
1150
|
+
`Started emitter '${emitter.name}' (${emitter.emitterType}, ${emitter.runSchedule}) on stream '${emitter.stream}' in ${emitter.cwd}.`
|
|
1151
|
+
);
|
|
1152
|
+
return emitter;
|
|
1153
|
+
}
|
|
1154
|
+
async function stop(name, options = {}) {
|
|
1155
|
+
const normalized = normalizeName(name);
|
|
1156
|
+
const lifespan = normalizeLifespan(options.scope, LIFESPAN.TEMPORARY);
|
|
1157
|
+
const emitter = emitters.get(normalized);
|
|
1158
|
+
if (emitter) {
|
|
1159
|
+
assertMutable(emitter.ownership, options.force, `Emitter '${normalized}'`);
|
|
1160
|
+
await lifecycle.stop(emitter);
|
|
1161
|
+
}
|
|
1162
|
+
if (lifespan === LIFESPAN.PERSISTENT) {
|
|
1163
|
+
const removed = configStore.removeEmitter(normalized, options.force);
|
|
1164
|
+
if (removed) {
|
|
1165
|
+
persist();
|
|
1166
|
+
void sessionPort.log(`Removed persistent emitter '${normalized}' from config.`);
|
|
1167
|
+
}
|
|
1168
|
+
if (!emitter && !removed) {
|
|
1169
|
+
throw new Error(`Emitter '${normalized}' was not found in the session or persistent config.`);
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
name: normalized,
|
|
1173
|
+
status: removed ? EMITTER_OPERATION_STATUS.REMOVED_FROM_CONFIG : emitter?.status ?? EMITTER_STATUS.STOPPED
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
if (!emitter) {
|
|
1177
|
+
throw new Error(`Emitter '${normalized}' is not running in this session.`);
|
|
1178
|
+
}
|
|
1179
|
+
return emitter;
|
|
1180
|
+
}
|
|
1181
|
+
function updateEventFilter(name, input, options = {}) {
|
|
1182
|
+
const normalized = normalizeName(name);
|
|
1183
|
+
const lifespan = normalizeLifespan(options.scope, LIFESPAN.TEMPORARY);
|
|
1184
|
+
const ownership = normalizeOwnership(options.managedBy, OWNERSHIP.MODEL_OWNED);
|
|
1185
|
+
const emitter = emitters.get(normalized);
|
|
1186
|
+
const configEntry = configStore.findEmitter(normalized);
|
|
1187
|
+
if (emitter) {
|
|
1188
|
+
assertMutable(emitter.eventFilter.managedBy, options.force, `Event filter for emitter '${normalized}'`);
|
|
1189
|
+
emitter.eventFilter = createEventFilter(
|
|
1190
|
+
{
|
|
1191
|
+
includePattern: input.includePattern ?? emitter.eventFilter.includePattern,
|
|
1192
|
+
excludePattern: input.excludePattern ?? emitter.eventFilter.excludePattern,
|
|
1193
|
+
notifyPattern: input.notifyPattern ?? emitter.eventFilter.notifyPattern,
|
|
1194
|
+
managedBy: options.managedBy ?? emitter.eventFilter.managedBy,
|
|
1195
|
+
scope: lifespan
|
|
1196
|
+
},
|
|
1197
|
+
ownership,
|
|
1198
|
+
lifespan
|
|
1199
|
+
);
|
|
1200
|
+
if (lifespan === LIFESPAN.PERSISTENT) {
|
|
1201
|
+
emitter.lifespan = LIFESPAN.PERSISTENT;
|
|
1202
|
+
configStore.upsertEmitter(emitter);
|
|
1203
|
+
persist();
|
|
1204
|
+
}
|
|
1205
|
+
void sessionPort.log(`Updated event filter for emitter '${normalized}': ${formatEventFilter(emitter.eventFilter)}`);
|
|
1206
|
+
return emitter;
|
|
1207
|
+
}
|
|
1208
|
+
if (lifespan !== LIFESPAN.PERSISTENT || !configEntry) {
|
|
1209
|
+
throw new Error(`Emitter '${normalized}' is not running, so only a persistent event filter update is possible when it exists in config.`);
|
|
1210
|
+
}
|
|
1211
|
+
assertMutable(
|
|
1212
|
+
normalizeOwnership(configEntry.eventFilter?.managedBy ?? configEntry.classifier?.managedBy ?? configEntry.managedBy, OWNERSHIP.USER_OWNED),
|
|
1213
|
+
options.force,
|
|
1214
|
+
`Event filter for emitter '${normalized}'`
|
|
1215
|
+
);
|
|
1216
|
+
configEntry.eventFilter = {
|
|
1217
|
+
includePattern: input.includePattern ?? configEntry.eventFilter?.includePattern ?? configEntry.classifier?.includePattern,
|
|
1218
|
+
excludePattern: input.excludePattern ?? configEntry.eventFilter?.excludePattern ?? configEntry.classifier?.excludePattern,
|
|
1219
|
+
notifyPattern: input.notifyPattern ?? configEntry.eventFilter?.notifyPattern ?? configEntry.classifier?.notifyPattern,
|
|
1220
|
+
managedBy: ownership
|
|
1221
|
+
};
|
|
1222
|
+
persist();
|
|
1223
|
+
void sessionPort.log(`Updated persistent event filter for emitter '${normalized}': ${formatEventFilter(configEntry.eventFilter)}`);
|
|
1224
|
+
return {
|
|
1225
|
+
name: normalized,
|
|
1226
|
+
status: EMITTER_OPERATION_STATUS.CONFIGURED,
|
|
1227
|
+
eventFilter: createEventFilter(configEntry.eventFilter, ownership, LIFESPAN.PERSISTENT)
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
async function stopAll() {
|
|
1231
|
+
const active = [...emitters.values()].filter((emitter) => !isTerminalEmitterStatus(emitter.status));
|
|
1232
|
+
await Promise.allSettled(active.map((emitter) => lifecycle.stop(emitter)));
|
|
1233
|
+
}
|
|
1234
|
+
function list() {
|
|
1235
|
+
return [...emitters.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
1236
|
+
}
|
|
1237
|
+
function has(name) {
|
|
1238
|
+
return emitters.has(normalizeName(name));
|
|
1239
|
+
}
|
|
1240
|
+
function get(name) {
|
|
1241
|
+
return emitters.get(normalizeName(name));
|
|
1242
|
+
}
|
|
1243
|
+
return {
|
|
1244
|
+
start,
|
|
1245
|
+
stop,
|
|
1246
|
+
stopAll,
|
|
1247
|
+
updateEventFilter,
|
|
1248
|
+
list,
|
|
1249
|
+
has,
|
|
1250
|
+
get
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/format/stream.mjs
|
|
1255
|
+
function formatSessionInjector(stream) {
|
|
1256
|
+
const sessionInjector = stream.sessionInjector;
|
|
1257
|
+
const state = sessionInjector.enabled ? "on" : "off";
|
|
1258
|
+
return `sessionInjector=${state} delivery=${sessionInjector.delivery} lifespan=${sessionInjector.lifespan} ownership=${sessionInjector.ownership}`;
|
|
1259
|
+
}
|
|
1260
|
+
function formatStream(stream) {
|
|
1261
|
+
const latest = stream.entries[stream.entries.length - 1];
|
|
1262
|
+
const latestSummary = latest ? ` latest=${JSON.stringify(latest.text.slice(0, 80))}` : "";
|
|
1263
|
+
const description = stream.description ? ` description=${JSON.stringify(stream.description)}` : "";
|
|
1264
|
+
return `- ${stream.name}: messages=${stream.entries.length}${description} ${formatSessionInjector(stream)}${latestSummary}`;
|
|
1265
|
+
}
|
|
1266
|
+
function formatStreamHistory(stream, limit) {
|
|
1267
|
+
const entries = stream.entries.slice(-limit);
|
|
1268
|
+
if (entries.length === 0) {
|
|
1269
|
+
return `Stream '${stream.name}' is empty.`;
|
|
1270
|
+
}
|
|
1271
|
+
return [
|
|
1272
|
+
`Stream '${stream.name}' (${entries.length} of ${stream.entries.length} entries):`,
|
|
1273
|
+
...entries.map((entry) => {
|
|
1274
|
+
const emitterLabel = entry.monitorName ? ` emitter=${entry.monitorName}` : "";
|
|
1275
|
+
const streamLabel = entry.stream ? ` stream=${entry.stream}` : "";
|
|
1276
|
+
return `[${entry.timestamp}] source=${entry.source}${emitterLabel}${streamLabel} ${entry.text}`;
|
|
1277
|
+
})
|
|
1278
|
+
].join("\n");
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/tools/channels.mjs
|
|
1282
|
+
function applySessionInjector({ streams, configStore, sessionPort, persist }, rawName, options) {
|
|
1283
|
+
const stream = streams.configureSessionInjector(rawName, options);
|
|
1284
|
+
void sessionPort.log(
|
|
1285
|
+
`${stream.sessionInjector.enabled ? "Subscribed" : "Unsubscribed"} stream '${stream.name}' with delivery=${stream.sessionInjector.delivery} lifespan=${stream.sessionInjector.lifespan} ownership=${stream.sessionInjector.ownership}.`
|
|
1286
|
+
);
|
|
1287
|
+
if (stream.sessionInjector.lifespan === LIFESPAN.PERSISTENT) {
|
|
1288
|
+
configStore.upsertStream(stream);
|
|
1289
|
+
persist();
|
|
1290
|
+
}
|
|
1291
|
+
return stream;
|
|
1292
|
+
}
|
|
1293
|
+
function renderStreamList(streams) {
|
|
1294
|
+
streams.ensure(DEFAULT_STREAM, DEFAULT_STREAM_DESCRIPTION);
|
|
1295
|
+
const values = streams.list();
|
|
1296
|
+
return [
|
|
1297
|
+
`Streams (${values.length}):`,
|
|
1298
|
+
...values.map((stream) => formatStream(stream))
|
|
1299
|
+
].join("\n");
|
|
1300
|
+
}
|
|
1301
|
+
function createStreamTools(deps) {
|
|
1302
|
+
const { streams, sessionPort } = deps;
|
|
1303
|
+
return [
|
|
1304
|
+
{
|
|
1305
|
+
name: "tap_list_streams",
|
|
1306
|
+
description: "Lists event streams, session injector state, and recent metadata.",
|
|
1307
|
+
handler: async () => renderStreamList(streams)
|
|
1308
|
+
},
|
|
1309
|
+
{
|
|
1310
|
+
name: "tap_post",
|
|
1311
|
+
description: "Posts a note into a named event stream for later retrieval.",
|
|
1312
|
+
parameters: {
|
|
1313
|
+
type: "object",
|
|
1314
|
+
properties: {
|
|
1315
|
+
channel: { type: "string", description: "EventStream name." },
|
|
1316
|
+
message: { type: "string", description: "Text to append." },
|
|
1317
|
+
source: { type: "string", description: "Optional source label." },
|
|
1318
|
+
description: { type: "string", description: "Optional stream description when creating it." }
|
|
1319
|
+
},
|
|
1320
|
+
required: ["channel", "message"]
|
|
1321
|
+
},
|
|
1322
|
+
handler: async (args) => {
|
|
1323
|
+
const stream = streams.ensure(args.channel, args.description ?? "");
|
|
1324
|
+
streams.append(stream.name, {
|
|
1325
|
+
source: args.source || SOURCE.TOOL,
|
|
1326
|
+
text: args.message
|
|
1327
|
+
});
|
|
1328
|
+
void sessionPort.log(`Posted message to stream '${stream.name}'.`);
|
|
1329
|
+
return `Posted to stream '${stream.name}'.`;
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
name: "tap_stream_history",
|
|
1334
|
+
description: "Returns recent entries from a named event stream.",
|
|
1335
|
+
parameters: {
|
|
1336
|
+
type: "object",
|
|
1337
|
+
properties: {
|
|
1338
|
+
channel: { type: "string", description: "EventStream name to inspect." },
|
|
1339
|
+
limit: { type: "number", description: "How many recent entries to return." }
|
|
1340
|
+
},
|
|
1341
|
+
required: ["channel"]
|
|
1342
|
+
},
|
|
1343
|
+
handler: async (args) => {
|
|
1344
|
+
const streamName = normalizeName(args.channel);
|
|
1345
|
+
const stream = streams.get(streamName);
|
|
1346
|
+
if (!stream) {
|
|
1347
|
+
throw new Error(`Stream '${streamName}' does not exist.`);
|
|
1348
|
+
}
|
|
1349
|
+
return formatStreamHistory(stream, clampLimit(args.limit, 20));
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
name: "tap_enable_injector",
|
|
1354
|
+
description: "Attaches a session injector to an event stream for this session or persistently.",
|
|
1355
|
+
parameters: {
|
|
1356
|
+
type: "object",
|
|
1357
|
+
properties: {
|
|
1358
|
+
channel: { type: "string", description: "EventStream name." },
|
|
1359
|
+
description: { type: "string", description: "Optional stream description." },
|
|
1360
|
+
delivery: { type: "string", description: "Event outcome mode: 'important' or 'all'." },
|
|
1361
|
+
scope: { type: "string", description: "Use 'temporary' for session-only or 'persistent' to write config." },
|
|
1362
|
+
managedBy: { type: "string", description: "Ownership label: 'userOwned' or 'modelOwned'." },
|
|
1363
|
+
force: { type: "boolean", description: "Required only when transferring ownership of a protected session injector." }
|
|
1364
|
+
},
|
|
1365
|
+
required: ["channel"]
|
|
1366
|
+
},
|
|
1367
|
+
handler: async (args) => {
|
|
1368
|
+
const stream = applySessionInjector(deps, args.channel, {
|
|
1369
|
+
enabled: true,
|
|
1370
|
+
delivery: args.delivery ?? EVENT_OUTCOME.SURFACE,
|
|
1371
|
+
scope: args.scope ?? LIFESPAN.TEMPORARY,
|
|
1372
|
+
managedBy: args.managedBy ?? OWNERSHIP.MODEL_OWNED,
|
|
1373
|
+
description: args.description ?? "",
|
|
1374
|
+
force: args.force === true
|
|
1375
|
+
});
|
|
1376
|
+
return `Attached session injector to stream '${stream.name}' with delivery=${stream.sessionInjector.delivery} lifespan=${stream.sessionInjector.lifespan} ownership=${stream.sessionInjector.ownership}.`;
|
|
1377
|
+
}
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
name: "tap_disable_injector",
|
|
1381
|
+
description: "Disables the session injector for an event stream.",
|
|
1382
|
+
parameters: {
|
|
1383
|
+
type: "object",
|
|
1384
|
+
properties: {
|
|
1385
|
+
channel: { type: "string", description: "EventStream name." },
|
|
1386
|
+
scope: { type: "string", description: "Use 'temporary' or 'persistent'." },
|
|
1387
|
+
managedBy: { type: "string", description: "Ownership label after the update: 'userOwned' or 'modelOwned'." },
|
|
1388
|
+
force: { type: "boolean", description: "Required only when transferring ownership of a protected session injector." }
|
|
1389
|
+
},
|
|
1390
|
+
required: ["channel"]
|
|
1391
|
+
},
|
|
1392
|
+
handler: async (args) => {
|
|
1393
|
+
const stream = applySessionInjector(deps, args.channel, {
|
|
1394
|
+
enabled: false,
|
|
1395
|
+
delivery: args.delivery ?? EVENT_OUTCOME.SURFACE,
|
|
1396
|
+
scope: args.scope ?? LIFESPAN.TEMPORARY,
|
|
1397
|
+
managedBy: args.managedBy ?? OWNERSHIP.MODEL_OWNED,
|
|
1398
|
+
force: args.force === true
|
|
1399
|
+
});
|
|
1400
|
+
return `Disabled session injector for stream '${stream.name}' with lifespan=${stream.sessionInjector.lifespan} ownership=${stream.sessionInjector.ownership}.`;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
];
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/tools/monitors.mjs
|
|
1407
|
+
function renderEmitterList(streams, configStore, supervisor) {
|
|
1408
|
+
const running = supervisor.list();
|
|
1409
|
+
const configured = configStore.getEmitters().filter((entry) => !supervisor.has(entry.name)).sort((left, right) => normalizeName(left.name).localeCompare(normalizeName(right.name)));
|
|
1410
|
+
if (running.length === 0 && configured.length === 0) {
|
|
1411
|
+
return "No emitters have been defined for this session.";
|
|
1412
|
+
}
|
|
1413
|
+
return [
|
|
1414
|
+
`Session emitters (${running.length}):`,
|
|
1415
|
+
...running.length > 0 ? running.map((emitter) => formatRunningEmitter(emitter, streams.ensure(emitter.stream))) : ["- <none>"],
|
|
1416
|
+
"",
|
|
1417
|
+
`Persistent emitter definitions (${configured.length}):`,
|
|
1418
|
+
...configured.length > 0 ? configured.map((entry) => formatConfiguredEmitter(entry)) : ["- <none>"]
|
|
1419
|
+
].join("\n");
|
|
1420
|
+
}
|
|
1421
|
+
function createEmitterTools({ streams, configStore, supervisor, getBaseCwd }) {
|
|
1422
|
+
return [
|
|
1423
|
+
{
|
|
1424
|
+
name: "tap_list_emitters",
|
|
1425
|
+
description: "Lists session event emitters, their run schedules, and persistent definitions.",
|
|
1426
|
+
handler: async () => renderEmitterList(streams, configStore, supervisor)
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
name: "tap_start_emitter",
|
|
1430
|
+
description: "Starts a command emitter, prompt emitter, or timed work item with event filter rules and optional session injector.",
|
|
1431
|
+
parameters: {
|
|
1432
|
+
type: "object",
|
|
1433
|
+
properties: {
|
|
1434
|
+
name: { type: "string", description: "Unique emitter name." },
|
|
1435
|
+
command: { type: "string", description: "Shell command to run. Optional when prompt is provided." },
|
|
1436
|
+
prompt: { type: "string", description: "Prompt to send to the agent. Optional when command is provided." },
|
|
1437
|
+
description: { type: "string", description: "Short summary." },
|
|
1438
|
+
channel: { type: "string", description: "EventStream to receive accepted events." },
|
|
1439
|
+
cwd: { type: "string", description: "Optional working directory relative to the session cwd." },
|
|
1440
|
+
every: { type: "string", description: "Optional repeat interval like 30s, 5m, 2h, or 1d. Use 'idle' for prompts that re-run whenever the session is idle. When omitted, commands run continuously and prompts run once." },
|
|
1441
|
+
scope: { type: "string", description: "Use 'temporary' for session-only or 'persistent' to write config." },
|
|
1442
|
+
managedBy: { type: "string", description: "Ownership label: 'userOwned' or 'modelOwned'." },
|
|
1443
|
+
autoStart: { type: "boolean", description: "When persistent, whether the emitter should auto-start next session." },
|
|
1444
|
+
includeStderr: { type: "boolean", description: "Whether stderr lines are eligible for event outcome evaluation." },
|
|
1445
|
+
includePattern: { type: "string", description: "Only matching lines are admitted into the stream. (Legacy: prefer eventFilter rules.)" },
|
|
1446
|
+
excludePattern: { type: "string", description: "Matching lines are dropped before they reach the stream. (Legacy: prefer eventFilter rules.)" },
|
|
1447
|
+
notifyPattern: { type: "string", description: "Matching lines trigger session injection when delivery='important'. (Legacy: prefer eventFilter rules.)" },
|
|
1448
|
+
subscribe: { type: "boolean", description: "Whether to attach a session injector to the stream as part of emitter creation." },
|
|
1449
|
+
delivery: { type: "string", description: "Session injector event outcome mode: 'important' or 'all'." },
|
|
1450
|
+
maxRuns: { type: "integer", description: "Maximum number of iterations before the emitter auto-completes. Useful for idle and timed loops." },
|
|
1451
|
+
force: { type: "boolean", description: "Required only when transferring ownership of a protected emitter." }
|
|
1452
|
+
},
|
|
1453
|
+
required: ["name"]
|
|
1454
|
+
},
|
|
1455
|
+
handler: async (args) => {
|
|
1456
|
+
const lifespan = args.scope ?? LIFESPAN.TEMPORARY;
|
|
1457
|
+
const ownership = args.managedBy ?? OWNERSHIP.MODEL_OWNED;
|
|
1458
|
+
const emitter = await supervisor.start(
|
|
1459
|
+
{ ...args, scope: lifespan, managedBy: ownership },
|
|
1460
|
+
{
|
|
1461
|
+
baseCwd: getBaseCwd(),
|
|
1462
|
+
scope: lifespan,
|
|
1463
|
+
managedBy: ownership,
|
|
1464
|
+
subscribe: args.subscribe !== false,
|
|
1465
|
+
delivery: args.delivery ?? EVENT_OUTCOME.SURFACE,
|
|
1466
|
+
force: args.force === true
|
|
1467
|
+
}
|
|
1468
|
+
);
|
|
1469
|
+
return [
|
|
1470
|
+
`Started emitter '${emitter.name}'.`,
|
|
1471
|
+
`lifespan=${emitter.lifespan}`,
|
|
1472
|
+
`ownership=${emitter.ownership}`,
|
|
1473
|
+
`emitterType=${emitter.emitterType}`,
|
|
1474
|
+
`runSchedule=${emitter.runSchedule}`,
|
|
1475
|
+
emitter.every ? `every=${emitter.every}` : null,
|
|
1476
|
+
emitter.maxRuns ? `maxRuns=${emitter.maxRuns}` : null,
|
|
1477
|
+
`stream=${emitter.stream}`,
|
|
1478
|
+
`sessionInjector=${streams.ensure(emitter.stream).sessionInjector.enabled ? "on" : "off"}`,
|
|
1479
|
+
`eventFilter=${formatEventFilter(emitter.eventFilter)}`
|
|
1480
|
+
].filter(Boolean).join("\n");
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
{
|
|
1484
|
+
name: "tap_set_event_filter",
|
|
1485
|
+
description: "Updates the event filter rules that determine event outcomes (drop, keep, surface, inject) for an emitter.",
|
|
1486
|
+
parameters: {
|
|
1487
|
+
type: "object",
|
|
1488
|
+
properties: {
|
|
1489
|
+
name: { type: "string", description: "Emitter name." },
|
|
1490
|
+
includePattern: { type: "string", description: "Only matching lines are admitted into the stream." },
|
|
1491
|
+
excludePattern: { type: "string", description: "Matching lines are removed from the stream." },
|
|
1492
|
+
notifyPattern: { type: "string", description: "Matching lines trigger session injection when delivery='important'." },
|
|
1493
|
+
scope: { type: "string", description: "Use 'temporary' for session-only or 'persistent' to write config." },
|
|
1494
|
+
managedBy: { type: "string", description: "Ownership label: 'userOwned' or 'modelOwned'." },
|
|
1495
|
+
force: { type: "boolean", description: "Required only when transferring ownership of a protected emitter." }
|
|
1496
|
+
},
|
|
1497
|
+
required: ["name"]
|
|
1498
|
+
},
|
|
1499
|
+
handler: async (args) => {
|
|
1500
|
+
const result = supervisor.updateEventFilter(args.name, args, {
|
|
1501
|
+
scope: args.scope ?? LIFESPAN.TEMPORARY,
|
|
1502
|
+
managedBy: args.managedBy ?? OWNERSHIP.MODEL_OWNED,
|
|
1503
|
+
force: args.force === true
|
|
1504
|
+
});
|
|
1505
|
+
const eventFilter = result.eventFilter ?? supervisor.get(args.name)?.eventFilter;
|
|
1506
|
+
return `Updated event filter for emitter '${normalizeName(args.name)}': ${formatEventFilter(eventFilter)}`;
|
|
1507
|
+
}
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
name: "tap_stop_emitter",
|
|
1511
|
+
description: "Stops a running event emitter. With lifespan='persistent', also removes the stored definition from config.",
|
|
1512
|
+
parameters: {
|
|
1513
|
+
type: "object",
|
|
1514
|
+
properties: {
|
|
1515
|
+
name: { type: "string", description: "Emitter name." },
|
|
1516
|
+
scope: { type: "string", description: "Use 'temporary' or 'persistent'." },
|
|
1517
|
+
force: { type: "boolean", description: "Required only when transferring ownership of a protected emitter." }
|
|
1518
|
+
},
|
|
1519
|
+
required: ["name"]
|
|
1520
|
+
},
|
|
1521
|
+
handler: async (args) => {
|
|
1522
|
+
const result = await supervisor.stop(args.name, {
|
|
1523
|
+
scope: args.scope ?? LIFESPAN.TEMPORARY,
|
|
1524
|
+
force: args.force === true
|
|
1525
|
+
});
|
|
1526
|
+
return `Stop requested for emitter '${normalizeName(args.name)}' (status=${result.status}).`;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
];
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// src/tools/index.mjs
|
|
1533
|
+
function createTools(deps) {
|
|
1534
|
+
return [
|
|
1535
|
+
...createStreamTools(deps),
|
|
1536
|
+
...createEmitterTools(deps)
|
|
1537
|
+
];
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// src/hooks.mjs
|
|
1541
|
+
function sessionInjectorSummary(streams) {
|
|
1542
|
+
const subscribed = streams.list().filter((stream) => stream.sessionInjector.enabled);
|
|
1543
|
+
if (subscribed.length === 0) {
|
|
1544
|
+
return "";
|
|
1545
|
+
}
|
|
1546
|
+
return [
|
|
1547
|
+
"Session injectors:",
|
|
1548
|
+
...subscribed.map(
|
|
1549
|
+
(stream) => `- ${stream.name} delivery=${stream.sessionInjector.delivery} lifespan=${stream.sessionInjector.lifespan} ownership=${stream.sessionInjector.ownership}`
|
|
1550
|
+
)
|
|
1551
|
+
].join("\n");
|
|
1552
|
+
}
|
|
1553
|
+
async function applyPersistentConfig({ baseCwd, streams, configStore, supervisor, sessionPort, setBaseCwd }) {
|
|
1554
|
+
setBaseCwd(baseCwd);
|
|
1555
|
+
const configLoad = configStore.load(baseCwd);
|
|
1556
|
+
for (const entry of configStore.getStreams()) {
|
|
1557
|
+
streams.applyPersistentStream(entry);
|
|
1558
|
+
}
|
|
1559
|
+
let started = 0;
|
|
1560
|
+
for (const entry of configStore.getEmitters()) {
|
|
1561
|
+
if (entry.autoStart === false) {
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
await supervisor.start(
|
|
1566
|
+
{
|
|
1567
|
+
...entry,
|
|
1568
|
+
scope: LIFESPAN.PERSISTENT,
|
|
1569
|
+
managedBy: entry.ownership ?? OWNERSHIP.USER_OWNED
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
baseCwd,
|
|
1573
|
+
scope: LIFESPAN.PERSISTENT,
|
|
1574
|
+
managedBy: entry.ownership ?? OWNERSHIP.USER_OWNED,
|
|
1575
|
+
subscribe: false,
|
|
1576
|
+
force: true
|
|
1577
|
+
}
|
|
1578
|
+
);
|
|
1579
|
+
started += 1;
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
await sessionPort.log(`Failed to auto-start emitter '${entry.name}': ${error.message}`, {
|
|
1582
|
+
level: "warning"
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return configLoad.found ? `Loaded ${configStore.getStreams().length} event streams and ${configStore.getEmitters().length} persistent emitter definitions from ${configLoad.filePath}. Auto-started ${started}.` : "No copilot-channels config file found.";
|
|
1587
|
+
}
|
|
1588
|
+
function createHooks({ streams, configStore, supervisor, sessionPort, setBaseCwd }) {
|
|
1589
|
+
return {
|
|
1590
|
+
onSessionStart: async (input) => {
|
|
1591
|
+
streams.ensure(DEFAULT_STREAM, DEFAULT_STREAM_DESCRIPTION);
|
|
1592
|
+
let configSummary = "No config loaded.";
|
|
1593
|
+
try {
|
|
1594
|
+
configSummary = await applyPersistentConfig({
|
|
1595
|
+
baseCwd: input.cwd,
|
|
1596
|
+
streams,
|
|
1597
|
+
configStore,
|
|
1598
|
+
supervisor,
|
|
1599
|
+
sessionPort,
|
|
1600
|
+
setBaseCwd
|
|
1601
|
+
});
|
|
1602
|
+
await sessionPort.log(configSummary);
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
configSummary = `Config load failed: ${error.message}`;
|
|
1605
|
+
await sessionPort.log(configSummary, { level: "warning" });
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
additionalContext: [
|
|
1609
|
+
`${BRAND} is active.`,
|
|
1610
|
+
"Use event emitters to run background commands or prompts; use event filters to control which events are kept, surfaced, or injected; use session injectors when you want events surfaced or injected into the session.",
|
|
1611
|
+
"Session injector updates are sent immediately from emitter output and do not wait for transcript events.",
|
|
1612
|
+
`Repo guidance is available at ${COPILOT_INSTRUCTIONS_PATH} if you want to read the project-specific instructions.`,
|
|
1613
|
+
configSummary,
|
|
1614
|
+
sessionInjectorSummary(streams)
|
|
1615
|
+
].filter(Boolean).join("\n")
|
|
1616
|
+
};
|
|
1617
|
+
},
|
|
1618
|
+
onUserPromptSubmitted: async () => {
|
|
1619
|
+
const summary = sessionInjectorSummary(streams);
|
|
1620
|
+
if (!summary) {
|
|
1621
|
+
return void 0;
|
|
1622
|
+
}
|
|
1623
|
+
return { additionalContext: summary };
|
|
1624
|
+
},
|
|
1625
|
+
onSessionEnd: async () => {
|
|
1626
|
+
await supervisor.stopAll();
|
|
1627
|
+
return {
|
|
1628
|
+
sessionSummary: `${BRAND} tracked ${streams.size()} event streams and ${configStore.getEmitters().length} persistent emitter definitions.`,
|
|
1629
|
+
cleanupActions: [`Stopped session emitters managed by ${BRAND}.`]
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/tap-runtime.mjs
|
|
1636
|
+
function createCopilotChannelsRuntime(options = {}) {
|
|
1637
|
+
let baseCwd = options.cwd ?? process.cwd();
|
|
1638
|
+
const getBaseCwd = () => baseCwd;
|
|
1639
|
+
const setBaseCwd = (next) => {
|
|
1640
|
+
baseCwd = next;
|
|
1641
|
+
};
|
|
1642
|
+
const sessionPort = createSessionPort(options.session ?? null);
|
|
1643
|
+
const streams = createStreamStore();
|
|
1644
|
+
const configStore = createConfigStore({ cwd: baseCwd });
|
|
1645
|
+
const notifications = createNotificationDispatcher({ sessionPort });
|
|
1646
|
+
const persist = () => configStore.save();
|
|
1647
|
+
const supervisor = createEmitterSupervisor({
|
|
1648
|
+
streams,
|
|
1649
|
+
configStore,
|
|
1650
|
+
notifications,
|
|
1651
|
+
sessionPort,
|
|
1652
|
+
getBaseCwd,
|
|
1653
|
+
persist
|
|
1654
|
+
});
|
|
1655
|
+
const tools = createTools({ streams, configStore, supervisor, sessionPort, getBaseCwd, persist });
|
|
1656
|
+
const hooks = createHooks({ streams, configStore, supervisor, sessionPort, setBaseCwd });
|
|
1657
|
+
return {
|
|
1658
|
+
attachSession: (nextSession) => sessionPort.attach(nextSession),
|
|
1659
|
+
tools,
|
|
1660
|
+
hooks,
|
|
1661
|
+
stopAllEmitters: () => supervisor.stopAll(),
|
|
1662
|
+
appendStreamMessage: (name, entry) => streams.append(name, entry),
|
|
1663
|
+
DEFAULT_STREAM
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// .github/extensions/tap/extension.mjs
|
|
1668
|
+
var runtime = createCopilotChannelsRuntime({
|
|
1669
|
+
cwd: process.cwd()
|
|
1670
|
+
});
|
|
1671
|
+
var session = await joinSession({
|
|
1672
|
+
tools: runtime.tools,
|
|
1673
|
+
hooks: runtime.hooks
|
|
1674
|
+
});
|
|
1675
|
+
runtime.attachSession(session);
|
|
1676
|
+
runtime.appendStreamMessage(runtime.DEFAULT_STREAM, {
|
|
1677
|
+
source: "system",
|
|
1678
|
+
text: "\u203B tap loaded."
|
|
1679
|
+
});
|
|
1680
|
+
session.on("session.shutdown", () => {
|
|
1681
|
+
void runtime.stopAllEmitters();
|
|
1682
|
+
});
|