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.
@@ -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
+ });