@a5c-ai/channels-adapter 5.1.1-staging.5ee7f5f98b91

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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +661 -0
  3. package/dist/backend.d.ts +7 -0
  4. package/dist/backend.d.ts.map +1 -0
  5. package/dist/backend.js +22 -0
  6. package/dist/backend.js.map +1 -0
  7. package/dist/backends/github.d.ts +16 -0
  8. package/dist/backends/github.d.ts.map +1 -0
  9. package/dist/backends/github.js +357 -0
  10. package/dist/backends/github.js.map +1 -0
  11. package/dist/backends/jira.d.ts +18 -0
  12. package/dist/backends/jira.d.ts.map +1 -0
  13. package/dist/backends/jira.js +256 -0
  14. package/dist/backends/jira.js.map +1 -0
  15. package/dist/backends/webhook.d.ts +25 -0
  16. package/dist/backends/webhook.d.ts.map +1 -0
  17. package/dist/backends/webhook.js +206 -0
  18. package/dist/backends/webhook.js.map +1 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +28 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/config.d.ts +10 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +323 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/dedup.d.ts +26 -0
  28. package/dist/dedup.d.ts.map +1 -0
  29. package/dist/dedup.js +68 -0
  30. package/dist/dedup.js.map +1 -0
  31. package/dist/filter.d.ts +9 -0
  32. package/dist/filter.d.ts.map +1 -0
  33. package/dist/filter.js +115 -0
  34. package/dist/filter.js.map +1 -0
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +21 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/poller.d.ts +63 -0
  40. package/dist/poller.d.ts.map +1 -0
  41. package/dist/poller.js +195 -0
  42. package/dist/poller.js.map +1 -0
  43. package/dist/registry.d.ts +22 -0
  44. package/dist/registry.d.ts.map +1 -0
  45. package/dist/registry.js +59 -0
  46. package/dist/registry.js.map +1 -0
  47. package/dist/relay.d.ts +58 -0
  48. package/dist/relay.d.ts.map +1 -0
  49. package/dist/relay.js +172 -0
  50. package/dist/relay.js.map +1 -0
  51. package/dist/runtime.d.ts +27 -0
  52. package/dist/runtime.d.ts.map +1 -0
  53. package/dist/runtime.js +198 -0
  54. package/dist/runtime.js.map +1 -0
  55. package/dist/server.d.ts +64 -0
  56. package/dist/server.d.ts.map +1 -0
  57. package/dist/server.js +217 -0
  58. package/dist/server.js.map +1 -0
  59. package/dist/spawner.d.ts +96 -0
  60. package/dist/spawner.d.ts.map +1 -0
  61. package/dist/spawner.js +368 -0
  62. package/dist/spawner.js.map +1 -0
  63. package/dist/state.d.ts +43 -0
  64. package/dist/state.d.ts.map +1 -0
  65. package/dist/state.js +92 -0
  66. package/dist/state.js.map +1 -0
  67. package/dist/types.d.ts +140 -0
  68. package/dist/types.d.ts.map +1 -0
  69. package/dist/types.js +10 -0
  70. package/dist/types.js.map +1 -0
  71. package/package.json +78 -0
@@ -0,0 +1,368 @@
1
+ // Event-triggered session spawner (SPEC §10 / DESIGN §7).
2
+ //
3
+ // A surviving event (post filter + dedup) can spawn a fresh agent session via
4
+ // `@a5c-ai/adapters` instead of — or in addition to — emitting into the current
5
+ // session. The spawned session is SELF-ASSOCIATED: it is re-launched with THIS
6
+ // MCP server over stdio (same config path) and handed the event context + the
7
+ // same `reply_to` token, so it can post back to the SAME origin by calling the
8
+ // `reply` tool.
9
+ //
10
+ // Two seams:
11
+ // - `buildSpawnRunOptions(source, event, { configPath, replySecret })` is a
12
+ // PURE function mapping the effective spawn config + event -> adapters
13
+ // `RunOptions`. No I/O, no client — the cheapest, most stable unit target.
14
+ // - `SessionSpawner` calls an INJECTED adapters-like client's `run(opts)` with
15
+ // bounded concurrency + error isolation. `@a5c-ai/adapters` is lazy-loaded
16
+ // only when no client is injected (so the suite stays fully offline and the
17
+ // dep is never imported in CI).
18
+ import { fileURLToPath } from 'node:url';
19
+ import { isAbsolute, resolve } from 'node:path';
20
+ /**
21
+ * The canonical adapters agent id for Claude Code, as registered by
22
+ * `registerBuiltInAdapters(client)` in `@a5c-ai/adapters` (verified empirically:
23
+ * `client.adapters.get('claude')` resolves; `'claude-code'` does NOT). SPEC §10
24
+ * examples and configs write `claude-code` as a friendly alias — we normalize it
25
+ * to the canonical id so `client.run({ agent })` actually resolves the adapter.
26
+ */
27
+ const DEFAULT_AGENT = 'claude';
28
+ /**
29
+ * Map of friendly/catalog agent aliases -> the canonical adapter key the
30
+ * `@a5c-ai/adapters` registry resolves. `'claude-code'` is the documented friendly
31
+ * name (SPEC §10.3) but is an UNNORMALIZED catalog alias the registry does not
32
+ * resolve; the registered id is `'claude'`. A user may write either.
33
+ */
34
+ const AGENT_ALIASES = { 'claude-code': 'claude' };
35
+ /**
36
+ * Normalize a configured agent id to the canonical adapter key, so a friendly
37
+ * alias (e.g. `claude-code`) resolves against the real adapters registry. An
38
+ * unknown/already-canonical id passes through unchanged.
39
+ */
40
+ export function normalizeAgentId(agent) {
41
+ if (agent == null || agent === '')
42
+ return DEFAULT_AGENT;
43
+ return AGENT_ALIASES[agent] || agent;
44
+ }
45
+ /** Absolute path to THIS framework's cli.js, resolved from this module so it is
46
+ * correct regardless of the child's cwd (Windows-safe; no process.cwd reliance). */
47
+ function defaultCliPath() {
48
+ return fileURLToPath(new URL('./cli.js', import.meta.url));
49
+ }
50
+ /**
51
+ * Render a `promptTemplate` with literal `{{…}}` substitution. Supported
52
+ * placeholders: `{{content}}`, `{{reply_to}}`, `{{source_id}}`, `{{meta.KEY}}`.
53
+ * Substitution is a SINGLE pass with literal values, so untrusted event text in
54
+ * `content`/`meta` can never inject a *new* placeholder (an unknown `{{meta.X}}`
55
+ * or missing key expands to the empty string).
56
+ */
57
+ function renderTemplate(template, { content, reply_to, source_id, meta }) {
58
+ return String(template).replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_m, key) => {
59
+ if (key === 'content')
60
+ return content == null ? '' : String(content);
61
+ if (key === 'reply_to')
62
+ return reply_to == null ? '' : String(reply_to);
63
+ if (key === 'source_id')
64
+ return source_id == null ? '' : String(source_id);
65
+ if (key.startsWith('meta.')) {
66
+ const metaKey = key.slice('meta.'.length);
67
+ const val = meta ? meta[metaKey] : undefined;
68
+ return val == null ? '' : String(val);
69
+ }
70
+ // Unknown placeholder -> empty string (boring, predictable).
71
+ return '';
72
+ });
73
+ }
74
+ /**
75
+ * Build the default spawned prompt (AC-23): the event content, the routing-
76
+ * relevant meta (rendered as `key: value` lines), the source id, the `reply_to`
77
+ * token verbatim, and an explicit instruction to call the `reply` tool with it.
78
+ */
79
+ function buildDefaultPrompt(source, event) {
80
+ const meta = event.meta || {};
81
+ const reply_to = meta.reply_to;
82
+ const lines = [];
83
+ lines.push('A new channel event was triggered and needs your attention.');
84
+ lines.push('');
85
+ lines.push(`source_id: ${source.id}`);
86
+ // Routing-relevant meta first (whatever identifies origin for that backend),
87
+ // then the remaining identifier meta (kind/author/etc.), excluding reply_to
88
+ // (surfaced separately, verbatim, below).
89
+ const routingKeys = ['repo', 'issue_number', 'project', 'issue_key', 'kind', 'author'];
90
+ const seen = new Set();
91
+ for (const key of routingKeys) {
92
+ if (key in meta && meta[key] != null && meta[key] !== '') {
93
+ lines.push(`${key}: ${meta[key]}`);
94
+ seen.add(key);
95
+ }
96
+ }
97
+ for (const [key, value] of Object.entries(meta)) {
98
+ if (key === 'reply_to' || seen.has(key))
99
+ continue;
100
+ if (value == null || value === '')
101
+ continue;
102
+ lines.push(`${key}: ${value}`);
103
+ }
104
+ lines.push('');
105
+ lines.push('Event content:');
106
+ lines.push(event.content == null ? '' : String(event.content));
107
+ lines.push('');
108
+ lines.push(`reply_to: ${reply_to == null ? '' : reply_to}`);
109
+ lines.push('');
110
+ lines.push('To respond to the origin of this event, call the `reply` tool with `reply_to` ' +
111
+ 'set to the token above and your message as `text`. Echo the token exactly; ' +
112
+ 'do not modify it.');
113
+ return lines.join('\n');
114
+ }
115
+ /**
116
+ * PURE mapping of (source, event, ctx) -> adapters `RunOptions` (SPEC §10.2).
117
+ * Reads the source's EFFECTIVE spawn config (`source.spawn`, already merged over
118
+ * the global defaults by config.js / the caller). Optional fields are OMITTED
119
+ * when unset (never emitted as `undefined`) so the adapter's own defaults apply.
120
+ */
121
+ export function buildSpawnRunOptions(source, event, ctx = {}) {
122
+ const spawn = (source && source.spawn) || {};
123
+ const { configPath, replySecret, resolveCliPath } = ctx;
124
+ // --- prompt -------------------------------------------------------------
125
+ let prompt;
126
+ if (typeof spawn.promptTemplate === 'string') {
127
+ prompt = renderTemplate(spawn.promptTemplate, {
128
+ content: event.content,
129
+ reply_to: event.meta ? event.meta.reply_to : undefined,
130
+ source_id: source.id,
131
+ meta: event.meta || {}
132
+ });
133
+ }
134
+ else {
135
+ prompt = buildDefaultPrompt(source, event);
136
+ }
137
+ const opts = {
138
+ // Normalize the configured agent to the canonical adapters key so a friendly
139
+ // alias (`claude-code`) resolves against the real registry (`claude`). An
140
+ // unset agent falls back to the canonical default.
141
+ agent: normalizeAgentId(spawn.agent),
142
+ prompt
143
+ };
144
+ // --- mode mapping (exactly one of nonInteractive/interactive) -----------
145
+ const mode = spawn.mode || 'headless';
146
+ if (mode === 'interactive') {
147
+ opts.interactive = true;
148
+ }
149
+ else {
150
+ opts.nonInteractive = true;
151
+ }
152
+ // --- optional passthroughs (omitted when unset) -------------------------
153
+ if (spawn.model != null && spawn.model !== '')
154
+ opts.model = spawn.model;
155
+ if (spawn.systemPrompt != null && spawn.systemPrompt !== '') {
156
+ opts.systemPrompt = spawn.systemPrompt;
157
+ }
158
+ // approvalMode defaults to 'yolo' (autonomous reply).
159
+ opts.approvalMode = spawn.approvalMode || 'yolo';
160
+ // cwd resolved to an absolute path against the config's baseDir when set.
161
+ if (spawn.cwd != null && spawn.cwd !== '') {
162
+ const base = (source && source.baseDir) || process.cwd();
163
+ opts.cwd = isAbsolute(spawn.cwd) ? spawn.cwd : resolve(base, spawn.cwd);
164
+ }
165
+ if (spawn.env && typeof spawn.env === 'object') {
166
+ opts.env = { ...spawn.env };
167
+ }
168
+ // --- self-association mcpServers entry (AC-20) --------------------------
169
+ const cliPath = (typeof resolveCliPath === 'function' && resolveCliPath()) || defaultCliPath();
170
+ const selfMcp = {
171
+ name: spawn.selfMcpName || 'mcp-channels',
172
+ transport: 'stdio',
173
+ command: 'node',
174
+ args: [cliPath, configPath],
175
+ env: replySecret ? { MCP_CHANNELS_REPLY_SECRET: replySecret } : {}
176
+ };
177
+ opts.mcpServers = [selfMcp];
178
+ return opts;
179
+ }
180
+ /**
181
+ * The default lazy loader for the `@a5c-ai/adapters` client. Dynamic-imports the
182
+ * package, creates a client via the real `createClient()` factory, registers the
183
+ * built-in adapters ONTO that client via the real `registerBuiltInAdapters(client)`
184
+ * (so `agent:'claude'` resolves — the core `AdapterRegistry` starts empty), and
185
+ * returns it. NEVER imported at module load — only invoked when no client is
186
+ * injected (so tests that inject a fake stay fully offline).
187
+ *
188
+ * Bound HONESTLY to the in-repo `@a5c-ai/adapters` API: `createClient` and
189
+ * `registerBuiltInAdapters` are real exported symbols (re-exported from
190
+ * `@a5c-ai/comm-adapter` + `@a5c-ai/adapters-cli`), and the returned
191
+ * `AgentMuxClient.run(options)` is the real launch entry. The dynamic `import(...)`
192
+ * type is the package's own module type, so the factory/registration call sites are
193
+ * genuinely type-checked against the real signatures rather than cast through
194
+ * `unknown`. The structural `AdaptersClientLike` seam is preserved ONLY at the
195
+ * injection boundary (so the offline suite can supply a fake), and the real
196
+ * `AgentMuxClient` structurally satisfies it (it exposes `run(options): RunHandle`).
197
+ */
198
+ /* c8 ignore start -- the real `@a5c-ai/adapters` launch path is environment-gated
199
+ (needs the claude CLI + auth) and out of scope for the offline suite (DESIGN §7.7);
200
+ tests always inject a fake client or loadClient. */
201
+ async function defaultLoadClient() {
202
+ const mod = await import('@a5c-ai/adapters');
203
+ const client = mod.createClient();
204
+ mod.registerBuiltInAdapters(client);
205
+ return client;
206
+ }
207
+ /**
208
+ * Launches sessions for surviving events via an injected adapters-like client.
209
+ * Bounded concurrency (a tiny promise-queue semaphore), error isolation (a
210
+ * throwing/rejecting `run` is caught + logged, never thrown to the caller), and
211
+ * an `onDispatch` hook so the poller records seen-on-dispatch exactly once.
212
+ */
213
+ export class SessionSpawner {
214
+ client;
215
+ configPath;
216
+ replySecret;
217
+ maxConcurrent;
218
+ log;
219
+ resolveCliPath;
220
+ loadClient;
221
+ onDispatch;
222
+ _free;
223
+ _waiters;
224
+ _clientPromise;
225
+ constructor({ client, configPath, replySecret, maxConcurrent = 4, log, resolveCliPath, loadClient, onDispatch } = {}) {
226
+ this.client = client || null;
227
+ this.configPath = configPath;
228
+ this.replySecret = replySecret;
229
+ this.maxConcurrent = Math.max(1, Number(maxConcurrent) || 4);
230
+ this.log = typeof log === 'function' ? log : () => { };
231
+ this.resolveCliPath = resolveCliPath;
232
+ this.loadClient = typeof loadClient === 'function' ? loadClient : defaultLoadClient;
233
+ this.onDispatch = typeof onDispatch === 'function' ? onDispatch : null;
234
+ // Concurrency semaphore: count of free slots + a FIFO queue of waiters.
235
+ this._free = this.maxConcurrent;
236
+ this._waiters = [];
237
+ // Memoize a lazily-loaded client so we resolve the dep at most once.
238
+ this._clientPromise = null;
239
+ }
240
+ /**
241
+ * Resolve the adapters client: the injected one if present, else the lazily
242
+ * loaded one. A failure to obtain a client is surfaced as a CLEAR, actionable
243
+ * error (AC-25). Memoized so the dep is resolved at most once.
244
+ */
245
+ async _getClient() {
246
+ if (this.client)
247
+ return this.client;
248
+ if (!this._clientPromise) {
249
+ this._clientPromise = (async () => {
250
+ let loaded;
251
+ try {
252
+ loaded = await this.loadClient();
253
+ }
254
+ catch (err) {
255
+ throw new Error("Spawn is configured but no client was injected and '@a5c-ai/adapters' " +
256
+ `could not be loaded (${err?.message || err}); install it or inject a client.`);
257
+ }
258
+ if (!loaded || typeof loaded.run !== 'function') {
259
+ throw new Error("Spawn is configured but no usable adapters client is available; install " +
260
+ "'@a5c-ai/adapters' or inject a client with a run(options) method.");
261
+ }
262
+ return loaded;
263
+ })();
264
+ // Don't cache a rejected promise — let a later validate()/spawn retry.
265
+ this._clientPromise.catch(() => {
266
+ this._clientPromise = null;
267
+ });
268
+ }
269
+ return this._clientPromise;
270
+ }
271
+ /**
272
+ * Startup validation hook (AC-25): if no client is injected, eagerly attempt
273
+ * to obtain one so a missing dep fails FAST and LOUDLY at startup rather than
274
+ * silently at event time. Throws a clear error when no client can be obtained.
275
+ * A no-op when a client is injected (stays offline).
276
+ */
277
+ async validate() {
278
+ if (this.client)
279
+ return;
280
+ await this._getClient();
281
+ }
282
+ /** Acquire a concurrency slot (awaits when the cap is reached). */
283
+ _acquire() {
284
+ if (this._free > 0) {
285
+ this._free -= 1;
286
+ return Promise.resolve();
287
+ }
288
+ return new Promise((res) => {
289
+ this._waiters.push(res);
290
+ });
291
+ }
292
+ /** Release a concurrency slot, handing it to the next waiter if any. */
293
+ _release() {
294
+ const next = this._waiters.shift();
295
+ if (next) {
296
+ next();
297
+ }
298
+ else {
299
+ this._free += 1;
300
+ }
301
+ }
302
+ /**
303
+ * Spawn a session for one surviving event. Builds RunOptions, resolves the
304
+ * client, acquires a concurrency slot, calls `client.run(opts)`, and returns
305
+ * the launch outcome. NEVER throws to the caller (the poller): a launch error
306
+ * is caught + logged and converted to `{ ok:false, error }`. The event is
307
+ * recorded as dispatched EXACTLY ONCE (AC-24), regardless of launch outcome.
308
+ *
309
+ * The missing-client/dep error (AC-25) is the one case allowed to surface as a
310
+ * rejection (it is a misconfiguration, not a per-launch failure).
311
+ */
312
+ async spawn(source, event) {
313
+ // Resolve the client FIRST. A missing client/dep is a hard configuration
314
+ // error (AC-25) and is allowed to surface. (When a client is injected this
315
+ // never throws.)
316
+ const client = await this._getClient();
317
+ // Record the event as dispatched exactly once, BEFORE the launch settles, so
318
+ // a launch failure does not roll back dedup (at-most-once stance, AC-24).
319
+ if (this.onDispatch) {
320
+ try {
321
+ this.onDispatch(source, event);
322
+ }
323
+ catch {
324
+ // an onDispatch hook failure must not affect the spawn outcome
325
+ }
326
+ }
327
+ const opts = buildSpawnRunOptions(source, event, {
328
+ configPath: this.configPath,
329
+ replySecret: this.replySecret,
330
+ resolveCliPath: this.resolveCliPath
331
+ });
332
+ await this._acquire();
333
+ // `client.run` may throw SYNCHRONOUSLY (bad options) -> caught here -> the
334
+ // slot is released and the failure reported. This is a launch-start failure.
335
+ let handle;
336
+ try {
337
+ handle = client.run(opts);
338
+ }
339
+ catch (err) {
340
+ this._release();
341
+ this.log(`spawner: spawn failed for source "${source?.id}": ${err?.message || err}`);
342
+ return { ok: false, error: err };
343
+ }
344
+ // The slot is bound to the HANDLE's lifetime: the cap is on in-flight launches
345
+ // (DESIGN §7.2), so the slot stays occupied while the handle is open, and is
346
+ // freed the moment it settles (success OR failure).
347
+ let rejected = false;
348
+ let launchError;
349
+ Promise.resolve(handle).then(() => {
350
+ this._release();
351
+ }, (err) => {
352
+ rejected = true;
353
+ launchError = err;
354
+ this._release();
355
+ });
356
+ // Give an IMMEDIATE rejection a chance to settle, so it is reported as
357
+ // `{ ok:false }` and logged (AC-24). A pending (long-lived) handle does not
358
+ // reject in this window and is reported as a started launch.
359
+ await Promise.resolve();
360
+ await Promise.resolve();
361
+ if (rejected) {
362
+ this.log(`spawner: spawn failed for source "${source?.id}": ${launchError?.message || launchError}`);
363
+ return { ok: false, error: launchError };
364
+ }
365
+ return { ok: true, handle };
366
+ }
367
+ }
368
+ //# sourceMappingURL=spawner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawner.js","sourceRoot":"","sources":["../src/spawner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ export interface SourceState {
2
+ cursor: unknown;
3
+ seen: string[];
4
+ }
5
+ export interface StateSetArgs {
6
+ cursor?: unknown;
7
+ seen?: string[];
8
+ keepSeen?: string[];
9
+ }
10
+ export interface StateStoreLike {
11
+ get(id: string): SourceState;
12
+ set(id: string, state: StateSetArgs): void | Promise<void>;
13
+ }
14
+ /**
15
+ * File-backed state store. One JSON file per source under `dir`.
16
+ */
17
+ export declare class StateStore implements StateStoreLike {
18
+ dir: string;
19
+ maxSeenPerSource?: number;
20
+ constructor({ dir, maxSeenPerSource }?: {
21
+ dir: string;
22
+ maxSeenPerSource?: number;
23
+ });
24
+ _pathFor(id: string): string;
25
+ get(id: string): SourceState;
26
+ /**
27
+ * Atomically persist state for a source (temp file + rename). `state.keepSeen`
28
+ * (optional) lists boundary ids the FIFO prune must never drop (finding §3).
29
+ */
30
+ set(id: string, state: StateSetArgs): Promise<void>;
31
+ }
32
+ /**
33
+ * In-memory state store with the same contract (pluggable for tests).
34
+ */
35
+ export declare class MemoryStateStore implements StateStoreLike {
36
+ maxSeenPerSource?: number;
37
+ _map: Map<string, SourceState>;
38
+ constructor({ maxSeenPerSource }?: {
39
+ maxSeenPerSource?: number;
40
+ });
41
+ get(id: string): SourceState;
42
+ set(id: string, state: StateSetArgs): Promise<void>;
43
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,CAAC;IAC7B,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AASD;;GAEG;AACH,qBAAa,UAAW,YAAW,cAAc;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,MAAM,CAAC;gBAEd,EAAE,GAAG,EAAE,gBAAgB,EAAE,GAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAqD;IAOpI,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM;IAI5B,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW;IAiB5B;;;OAGG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAU1D;AAED;;GAEG;AACH,qBAAa,gBAAiB,YAAW,cAAc;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;gBAEnB,EAAE,gBAAgB,EAAE,GAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAO;IAKpE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW;IAMtB,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAM1D"}
package/dist/state.js ADDED
@@ -0,0 +1,92 @@
1
+ // State persistence (SPEC §3, DESIGN §1).
2
+ //
3
+ // Each source has state `{ cursor, seen[] }`. The cursor narrows the next API
4
+ // query to "since last time"; the seen-set is the correctness backstop that
5
+ // guarantees at-most-once across overlapping windows. `seen` is FIFO-bounded by
6
+ // `maxSeenPerSource` so state files stay small.
7
+ //
8
+ // Two implementations:
9
+ // - StateStore: file-backed JSON, one file per source, written atomically
10
+ // (temp file + rename) so a crash never leaves a half-written state file.
11
+ // - MemoryStateStore: in-memory, for tests / ephemeral runs.
12
+ import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { boundSeen } from './dedup.js';
15
+ const DEFAULT_STATE = () => ({ cursor: null, seen: [] });
16
+ /** Turn a source id into a filesystem-safe file name. */
17
+ function safeFileName(id) {
18
+ return `${String(id).replace(/[^A-Za-z0-9._-]/g, '_')}.json`;
19
+ }
20
+ /**
21
+ * File-backed state store. One JSON file per source under `dir`.
22
+ */
23
+ export class StateStore {
24
+ dir;
25
+ maxSeenPerSource;
26
+ constructor({ dir, maxSeenPerSource } = {}) {
27
+ if (!dir)
28
+ throw new Error('StateStore requires a `dir`');
29
+ this.dir = dir;
30
+ this.maxSeenPerSource = maxSeenPerSource;
31
+ mkdirSync(this.dir, { recursive: true });
32
+ }
33
+ _pathFor(id) {
34
+ return join(this.dir, safeFileName(id));
35
+ }
36
+ get(id) {
37
+ const file = this._pathFor(id);
38
+ if (!existsSync(file))
39
+ return DEFAULT_STATE();
40
+ try {
41
+ const raw = readFileSync(file, 'utf8');
42
+ const parsed = JSON.parse(raw);
43
+ return {
44
+ cursor: parsed?.cursor ?? null,
45
+ seen: Array.isArray(parsed?.seen) ? parsed.seen : []
46
+ };
47
+ }
48
+ catch {
49
+ // A corrupt/unreadable state file degrades to empty state rather than
50
+ // crashing a poll; the cursor simply restarts.
51
+ return DEFAULT_STATE();
52
+ }
53
+ }
54
+ /**
55
+ * Atomically persist state for a source (temp file + rename). `state.keepSeen`
56
+ * (optional) lists boundary ids the FIFO prune must never drop (finding §3).
57
+ */
58
+ async set(id, state) {
59
+ const next = {
60
+ cursor: state?.cursor ?? null,
61
+ seen: boundSeen(state?.seen, { max: this.maxSeenPerSource, keep: state?.keepSeen })
62
+ };
63
+ const file = this._pathFor(id);
64
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
65
+ writeFileSync(tmp, JSON.stringify(next), 'utf8');
66
+ renameSync(tmp, file);
67
+ }
68
+ }
69
+ /**
70
+ * In-memory state store with the same contract (pluggable for tests).
71
+ */
72
+ export class MemoryStateStore {
73
+ maxSeenPerSource;
74
+ _map;
75
+ constructor({ maxSeenPerSource } = {}) {
76
+ this.maxSeenPerSource = maxSeenPerSource;
77
+ this._map = new Map();
78
+ }
79
+ get(id) {
80
+ const v = this._map.get(id);
81
+ if (!v)
82
+ return DEFAULT_STATE();
83
+ return { cursor: v.cursor, seen: [...v.seen] };
84
+ }
85
+ async set(id, state) {
86
+ this._map.set(id, {
87
+ cursor: state?.cursor ?? null,
88
+ seen: boundSeen(state?.seen, { max: this.maxSeenPerSource, keep: state?.keepSeen })
89
+ });
90
+ }
91
+ }
92
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,140 @@
1
+ export interface ChannelEvent {
2
+ /** Stable dedup id for this event. */
3
+ id: string;
4
+ /** Text Claude sees inside <channel>…</channel>. */
5
+ content: string;
6
+ /** Identifier-keyed channel attributes ([A-Za-z0-9_]+). */
7
+ meta: Record<string, string>;
8
+ /** Raw upstream object (so declarative dot-path filters can match). */
9
+ payload: Record<string, unknown>;
10
+ /** Origin coordinates a reply needs to reach back to. */
11
+ routing: Record<string, unknown>;
12
+ }
13
+ export interface PollContext {
14
+ source: Record<string, unknown>;
15
+ state: {
16
+ cursor: unknown;
17
+ seen: string[];
18
+ };
19
+ http: (url: string | URL, opts?: Record<string, unknown>) => Promise<unknown>;
20
+ log: (...args: unknown[]) => void;
21
+ now: Date;
22
+ }
23
+ export interface PollResult {
24
+ events: ChannelEvent[];
25
+ state: {
26
+ cursor: unknown;
27
+ seen?: string[];
28
+ };
29
+ }
30
+ export interface ReplyArgs {
31
+ routing: Record<string, unknown>;
32
+ text: string;
33
+ source: Record<string, unknown>;
34
+ http: (url: string | URL, opts?: Record<string, unknown>) => Promise<unknown>;
35
+ }
36
+ export interface ReplyResult {
37
+ ok: boolean;
38
+ ref?: string;
39
+ }
40
+ export interface Backend {
41
+ type: string;
42
+ validateConfig?: (source: Record<string, unknown>) => string[];
43
+ init?: (source: Record<string, unknown>) => void | Promise<void>;
44
+ poll: (ctx: PollContext) => Promise<PollResult>;
45
+ reply: (a: ReplyArgs) => Promise<ReplyResult>;
46
+ }
47
+ export interface ReplyToken {
48
+ sourceId: string;
49
+ backendType: string;
50
+ routing: Record<string, unknown>;
51
+ }
52
+ /** Effective spawn config (global defaults merged with per-source overrides). */
53
+ export interface SpawnConfig {
54
+ agent?: string;
55
+ mode?: 'headless' | 'interactive';
56
+ approvalMode?: 'yolo' | 'prompt' | 'deny';
57
+ selfMcpName?: string;
58
+ maxConcurrent?: number;
59
+ model?: string;
60
+ cwd?: string;
61
+ systemPrompt?: string;
62
+ env?: Record<string, string>;
63
+ promptTemplate?: string;
64
+ }
65
+ /** Adapters McpServerConfig (the self-association entry). */
66
+ export interface McpServerConfig {
67
+ name: string;
68
+ transport: 'stdio';
69
+ command: string;
70
+ args?: string[];
71
+ env?: Record<string, string>;
72
+ url?: string;
73
+ headers?: Record<string, string>;
74
+ }
75
+ /** Adapters RunOptions produced by `buildSpawnRunOptions` (SPEC §10.2). The
76
+ * package's OWN structural type — deliberately NOT imported from the adapters
77
+ * SDK so `buildSpawnRunOptions` stays a pure mapping duck-typed by `client.run()`. */
78
+ export interface RunOptions {
79
+ agent: string;
80
+ prompt: string;
81
+ mcpServers: McpServerConfig[];
82
+ nonInteractive?: boolean;
83
+ interactive?: boolean;
84
+ cwd?: string;
85
+ model?: string;
86
+ approvalMode?: 'yolo' | 'prompt' | 'deny';
87
+ systemPrompt?: string;
88
+ env?: Record<string, string>;
89
+ }
90
+ /** The leaf-clause operators the filter engine understands (SPEC §4). */
91
+ export type FilterOp = 'eq' | 'ne' | 'in' | 'nin' | 'includes' | 'contains' | 'regex' | 'exists' | 'gt' | 'gte' | 'lt' | 'lte';
92
+ /** A leaf clause: a dot-path `field`, an `op`, a comparison `value`. */
93
+ export interface FilterLeaf {
94
+ field?: string;
95
+ op?: FilterOp;
96
+ value?: unknown;
97
+ ignoreCase?: boolean;
98
+ }
99
+ /** A combinator clause: `all` (AND), `any` (OR), or `not` (negation). */
100
+ export type FilterCombinator = {
101
+ all?: FilterSpec[];
102
+ } | {
103
+ any?: FilterSpec[];
104
+ } | {
105
+ not?: FilterSpec;
106
+ };
107
+ /** A filter spec is either a leaf clause or a combinator (recursively). */
108
+ export type FilterSpec = FilterLeaf | FilterCombinator;
109
+ /** A normalized source after `loadConfig`: defaults applied + spawn merged. */
110
+ export interface NormalizedSource {
111
+ id: unknown;
112
+ backend: unknown;
113
+ pollIntervalSeconds: number;
114
+ auth: Record<string, unknown>;
115
+ config: Record<string, unknown>;
116
+ filter: unknown;
117
+ routing: Record<string, unknown>;
118
+ onEvent: 'emit' | 'spawn' | 'both';
119
+ spawn: SpawnConfig;
120
+ baseDir: string;
121
+ }
122
+ /** The validated channels config returned by `loadConfig`. */
123
+ export interface ChannelsConfig {
124
+ server: {
125
+ name?: unknown;
126
+ instructions?: unknown;
127
+ permissionRelay: boolean;
128
+ replySecret?: string;
129
+ };
130
+ state: {
131
+ dir?: unknown;
132
+ maxSeenPerSource: number;
133
+ };
134
+ defaults: {
135
+ pollIntervalSeconds: number;
136
+ };
137
+ spawn: SpawnConfig;
138
+ sources: NormalizedSource[];
139
+ baseDir: string;
140
+ }