@interactive-inc/claude-funnel 0.37.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhLi
4
4
  import { a as ScheduleStateStore, i as FunnelScheduleListener, n as scheduleConnectorSchema, o as NodeFunnelFileSystem, r as scheduleEntrySchema, s as FunnelFileSystem, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-CM-sRkac.js";
5
5
  import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-DDbSGPZn.js";
6
6
  import { dirname, join, resolve } from "node:path";
7
+ import { hc } from "hono/client";
7
8
  import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
8
9
  import { z } from "zod";
9
10
  import { homedir, tmpdir } from "node:os";
@@ -1739,7 +1740,14 @@ var MemoryFunnelClock = class extends FunnelClock {
1739
1740
  const publishRequestSchema = z.object({
1740
1741
  content: z.string().min(1),
1741
1742
  meta: z.record(z.string(), z.string()).optional(),
1742
- connector: z.string().min(1).optional()
1743
+ connector: z.string().min(1).optional(),
1744
+ /**
1745
+ * Address the event to a single subscriber. When set, only the WS client that
1746
+ * declared this id at upgrade time (`?id=<subscriberId>`) receives it among the
1747
+ * channel's regular subscribers; tap=all observers still see it. Omit for the
1748
+ * default fanout. The route surfaces it to subscribers as `meta.target`.
1749
+ */
1750
+ target: z.string().min(1).optional()
1743
1751
  });
1744
1752
  const publishResponseSchema = z.object({
1745
1753
  ok: z.literal(true),
@@ -2100,6 +2108,8 @@ var FunnelBroadcaster = class {
2100
2108
  }
2101
2109
  matchesClient(event, data) {
2102
2110
  if (data.tapAll) return true;
2111
+ const target = event.meta?.target;
2112
+ if (target && target !== data.subscriberId) return false;
2103
2113
  const channelId = event.meta?.channelId;
2104
2114
  if (channelId && channelId !== data.channel) return false;
2105
2115
  const connector = event.meta?.connector;
@@ -2111,6 +2121,11 @@ var FunnelBroadcaster = class {
2111
2121
  * receive (passive observation). For each per-channel group:
2112
2122
  * - fanout → every matching client receives
2113
2123
  * - exclusive → exactly one client receives, picked round-robin per channel
2124
+ *
2125
+ * `meta.target` narrows the regular (non-tap) recipient set first via
2126
+ * `matchesClient`: only the subscriber whose `subscriberId` equals `target`
2127
+ * stays in the running, so a targeted event reaches one named instance while
2128
+ * still being observable by tap=all clients.
2114
2129
  */
2115
2130
  pickRecipients(event) {
2116
2131
  const exclusiveByChannel = /* @__PURE__ */ new Map();
@@ -2982,13 +2997,17 @@ const channelsPublishHandler$1 = factory$1.createHandlers(zParam(z.object({ chan
2982
2997
  }), (c) => {
2983
2998
  const param = c.req.valid("param");
2984
2999
  const body = c.req.valid("json");
3000
+ const meta = body.target ? {
3001
+ ...body.meta,
3002
+ target: body.target
3003
+ } : body.meta;
2985
3004
  const response = {
2986
3005
  ok: true,
2987
3006
  offset: c.var.deps.emit({
2988
3007
  channel: param.channel,
2989
3008
  connector: body.connector,
2990
3009
  content: body.content,
2991
- meta: body.meta
3010
+ meta
2992
3011
  }).offset
2993
3012
  };
2994
3013
  return c.json(response);
@@ -3076,7 +3095,7 @@ const connectionViewSql = `CREATE TEMP VIEW connection AS SELECT
3076
3095
  FROM connectiondb.leuco_log`;
3077
3096
  //#endregion
3078
3097
  //#region lib/gateway/routes/debug.ts
3079
- const extractPreview$1 = (payload) => {
3098
+ const extractPreview = (payload) => {
3080
3099
  if (typeof payload !== "string" || payload.length === 0) return null;
3081
3100
  try {
3082
3101
  const parsed = JSON.parse(payload);
@@ -3182,7 +3201,7 @@ const debugHandler$1 = factory$1.createHandlers(async (c) => {
3182
3201
  outcome: typeof row.outcome === "string" ? row.outcome : "?",
3183
3202
  payload: rawPayload,
3184
3203
  payloadParsed,
3185
- preview: extractPreview$1(row.payload)
3204
+ preview: extractPreview(row.payload)
3186
3205
  });
3187
3206
  }
3188
3207
  if (listener && (!listener.alive || listener.errors > 0) || !listener) {
@@ -3294,13 +3313,10 @@ const statusHandler$1 = factory$1.createHandlers((c) => {
3294
3313
  });
3295
3314
  //#endregion
3296
3315
  //#region lib/gateway/routes/index.ts
3297
- /**
3298
- * Top-level Hono app for the gateway daemon. Mounts every HTTP endpoint flat
3299
- * (the WebSocket /ws upgrade is handled directly by `Bun.serve`). Deps come
3300
- * from the `deps` variable set by `FunnelGatewayServer`'s middleware — same
3301
- * shape as CLI's `c.var.funnel`.
3302
- */
3303
- const gatewayRoutes = factory$1.createApp().get("/health", ...healthHandler).get("/status", ...statusHandler$1).get("/debug", ...debugHandler$1).get("/listeners", ...listenersListHandler).post("/listeners/:channel/:connector/start", ...listenersStartHandler).delete("/listeners/:channel/:connector", ...listenersStopHandler).post("/listeners/:channel/:connector/restart", ...listenersRestartHandler).post("/channels/:channel/connectors/:connector/call", ...channelsConnectorsCallHandler).post("/channels/:channel/publish", ...channelsPublishHandler$1);
3316
+ function buildGatewayRoutes() {
3317
+ return factory$1.createApp().get("/health", ...healthHandler).get("/status", ...statusHandler$1).get("/debug", ...debugHandler$1).get("/listeners", ...listenersListHandler).post("/listeners/:channel/:connector/start", ...listenersStartHandler).delete("/listeners/:channel/:connector", ...listenersStopHandler).post("/listeners/:channel/:connector/restart", ...listenersRestartHandler).post("/channels/:channel/connectors/:connector/call", ...channelsConnectorsCallHandler).post("/channels/:channel/publish", ...channelsPublishHandler$1);
3318
+ }
3319
+ const gatewayRoutes = buildGatewayRoutes();
3304
3320
  //#endregion
3305
3321
  //#region lib/gateway/gateway-server.ts
3306
3322
  const DEFAULT_HOST = "127.0.0.1";
@@ -3455,12 +3471,14 @@ var FunnelGatewayServer = class {
3455
3471
  const sinceRaw = url.searchParams.get("since");
3456
3472
  const sinceParsed = sinceRaw === null ? NaN : Number.parseInt(sinceRaw, 10);
3457
3473
  const since = Number.isFinite(sinceParsed) && sinceParsed >= 0 ? sinceParsed : void 0;
3474
+ const subscriberId = url.searchParams.get("id") ?? void 0;
3458
3475
  if (server.upgrade(request, { data: {
3459
3476
  channel: channelId,
3460
3477
  channelName,
3461
3478
  connectors,
3462
3479
  tapAll,
3463
3480
  delivery,
3481
+ subscriberId,
3464
3482
  since
3465
3483
  } })) return void 0;
3466
3484
  return new Response("WebSocket upgrade failed", { status: 400 });
@@ -4100,6 +4118,10 @@ var Funnel = class Funnel {
4100
4118
  tmpDir: this.paths.tmpDir
4101
4119
  }, channelName ?? null);
4102
4120
  }
4121
+ gatewayClient() {
4122
+ const { port } = this.gateway.getStatus();
4123
+ return hc(`http://127.0.0.1:${port}`);
4124
+ }
4103
4125
  };
4104
4126
  //#endregion
4105
4127
  //#region lib/engine/mcp/channel-subscriber.ts
@@ -5072,6 +5094,17 @@ const queryToCliArgs = (url, reservedKeys = []) => {
5072
5094
  return args;
5073
5095
  };
5074
5096
  //#endregion
5097
+ //#region lib/cli/routes/channels.add.ts
5098
+ const help$17 = `funnel channels add — add a channel
5099
+
5100
+ usage: funnel channels add <name> [--delivery fanout|exclusive]
5101
+
5102
+ options:
5103
+ --delivery routing mode (default fanout):
5104
+ fanout every connected client receives every event
5105
+ exclusive each event delivered to exactly one client (round-robin)`;
5106
+ const channelsAddHelpHandler = factory.createHandlers((c) => c.text(help$17));
5107
+ //#endregion
5075
5108
  //#region lib/cli/router/validator.ts
5076
5109
  const zValidator$1 = (target, schema, helpText) => zValidator(target, schema, (result, c) => {
5077
5110
  if (helpText && c.req.query("help")) return c.text(helpText);
@@ -5083,18 +5116,10 @@ const zValidator$1 = (target, schema, helpText) => zValidator(target, schema, (r
5083
5116
  });
5084
5117
  //#endregion
5085
5118
  //#region lib/cli/routes/channels.add.$channel.ts
5086
- const addHelp$3 = `funnel channels add add a channel
5087
-
5088
- usage: funnel channels add <name> [--delivery fanout|exclusive]
5089
-
5090
- options:
5091
- --delivery routing mode (default fanout):
5092
- fanout every connected client receives every event
5093
- exclusive each event delivered to exactly one client (round-robin)`;
5094
- const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({ delivery: channelDeliveryModeSchema.optional() }), addHelp$3), (c) => {
5119
+ const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({ delivery: channelDeliveryModeSchema.optional() })), (c) => {
5095
5120
  const param = c.req.valid("param");
5096
5121
  const query = c.req.valid("query");
5097
- const created = c.var.funnel.channels.add({
5122
+ const created = c.env.funnel.channels.add({
5098
5123
  name: param.channel,
5099
5124
  delivery: query.delivery
5100
5125
  });
@@ -5104,14 +5129,14 @@ const channelsConnectorsGroupHandler = factory.createHandlers(zValidator$1("para
5104
5129
 
5105
5130
  usage: funnel channels <channel> connectors`), (c) => {
5106
5131
  const param = c.req.valid("param");
5107
- const channel = c.var.funnel.channels.get(param.channel);
5132
+ const channel = c.env.funnel.channels.get(param.channel);
5108
5133
  if (!channel) throw new HTTPException(404, { message: `channel "${param.channel}" not found` });
5109
5134
  if (channel.connectors.length === 0) return c.text(`no connectors in channel "${channel.name}"`);
5110
5135
  return c.text(channel.connectors.map((c) => `${c.name} (${c.type}, id: ${c.id})`).join("\n"));
5111
5136
  });
5112
5137
  //#endregion
5113
- //#region lib/cli/routes/channels.$channel.connectors.add.$connector.ts
5114
- const addHelp$2 = `funnel channels <channel> connectors add <connector> — add a connector to a channel
5138
+ //#region lib/cli/routes/channels.$channel.connectors.add.ts
5139
+ const help$16 = `funnel channels <channel> connectors add <connector> — add a connector to a channel
5115
5140
 
5116
5141
  usage:
5117
5142
  funnel channels <channel> connectors add <connector> --type=slack --bot-token=xoxb-... --app-token=xapp-...
@@ -5120,6 +5145,9 @@ usage:
5120
5145
  funnel channels <channel> connectors add <connector> --type=schedule
5121
5146
 
5122
5147
  Token uniqueness is enforced across all channels.`;
5148
+ const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$16));
5149
+ //#endregion
5150
+ //#region lib/cli/routes/channels.$channel.connectors.add.$connector.ts
5123
5151
  const slackBody = z.object({
5124
5152
  type: z.literal("slack"),
5125
5153
  "bot-token": z.string().startsWith("xoxb-"),
@@ -5143,10 +5171,10 @@ const addBody = z.discriminatedUnion("type", [
5143
5171
  const channelsConnectorsAddHandler = factory.createHandlers(zValidator$1("param", z.object({
5144
5172
  channel: z.string(),
5145
5173
  connector: z.string()
5146
- })), zValidator$1("query", addBody, addHelp$2), async (c) => {
5174
+ })), zValidator$1("query", addBody), async (c) => {
5147
5175
  const param = c.req.valid("param");
5148
5176
  const query = c.req.valid("query");
5149
- const funnel = c.var.funnel;
5177
+ const funnel = c.env.funnel;
5150
5178
  if (query.type === "slack") {
5151
5179
  const created = funnel.channels.addConnector(param.channel, {
5152
5180
  type: "slack",
@@ -5184,28 +5212,34 @@ const channelsConnectorsAddHandler = factory.createHandlers(zValidator$1("param"
5184
5212
  return c.text(`added schedule connector "${created.name}" to channel "${param.channel}"`);
5185
5213
  });
5186
5214
  //#endregion
5187
- //#region lib/cli/routes/channels.$channel.connectors.remove.$connector.ts
5188
- const removeHelp$3 = `funnel channels <channel> connectors remove <connector> — remove a connector
5215
+ //#region lib/cli/routes/channels.$channel.connectors.remove.ts
5216
+ const help$15 = `funnel channels <channel> connectors remove <connector> — remove a connector
5189
5217
 
5190
5218
  usage: funnel channels <channel> connectors remove <connector>`;
5219
+ const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$15));
5220
+ //#endregion
5221
+ //#region lib/cli/routes/channels.$channel.connectors.remove.$connector.ts
5191
5222
  const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
5192
5223
  channel: z.string(),
5193
5224
  connector: z.string()
5194
- })), zValidator$1("query", z.object({}), removeHelp$3), async (c) => {
5225
+ })), zValidator$1("query", z.object({})), async (c) => {
5195
5226
  const param = c.req.valid("param");
5196
- const funnel = c.var.funnel;
5227
+ const funnel = c.env.funnel;
5197
5228
  await funnel.listeners.stop(param.channel, param.connector);
5198
5229
  funnel.channels.removeConnector(param.channel, param.connector);
5199
5230
  return c.text(`removed connector "${param.connector}" from channel "${param.channel}"`);
5200
5231
  });
5201
5232
  //#endregion
5202
- //#region lib/cli/routes/channels.$channel.connectors.set.$connector.ts
5203
- const setHelp$1 = `funnel channels <channel> connectors set <connector> — update connector fields
5233
+ //#region lib/cli/routes/channels.$channel.connectors.set.ts
5234
+ const help$14 = `funnel channels <channel> connectors set <connector> — update connector fields
5204
5235
 
5205
5236
  usage:
5206
5237
  funnel channels <ch> connectors set <conn> [--bot-token=...] [--app-token=...] # slack
5207
5238
  funnel channels <ch> connectors set <conn> [--bot-token=...] # discord
5208
5239
  funnel channels <ch> connectors set <conn> [--poll-interval=N] # gh`;
5240
+ const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$14));
5241
+ //#endregion
5242
+ //#region lib/cli/routes/channels.$channel.connectors.set.$connector.ts
5209
5243
  const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param", z.object({
5210
5244
  channel: z.string(),
5211
5245
  connector: z.string()
@@ -5213,10 +5247,10 @@ const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param"
5213
5247
  "bot-token": z.string().optional(),
5214
5248
  "app-token": z.string().optional(),
5215
5249
  "poll-interval": z.coerce.number().int().positive().optional()
5216
- }).passthrough(), setHelp$1), async (c) => {
5250
+ }).passthrough()), async (c) => {
5217
5251
  const param = c.req.valid("param");
5218
5252
  const query = c.req.valid("query");
5219
- const funnel = c.var.funnel;
5253
+ const funnel = c.env.funnel;
5220
5254
  const existing = funnel.channels.getConnector(param.channel, param.connector);
5221
5255
  if (!existing) throw new HTTPException(404, { message: `connector "${param.connector}" not found in channel "${param.channel}"` });
5222
5256
  if (existing.type === "slack") funnel.channels.updateSlackConnector(param.channel, param.connector, {
@@ -5236,27 +5270,36 @@ const channelsConnectorsShowHandler = factory.createHandlers(zValidator$1("param
5236
5270
 
5237
5271
  usage: funnel channels <channel> connectors show <connector>`), (c) => {
5238
5272
  const param = c.req.valid("param");
5239
- const connector = c.var.funnel.channels.getConnector(param.channel, param.connector);
5273
+ const connector = c.env.funnel.channels.getConnector(param.channel, param.connector);
5240
5274
  if (!connector) throw new HTTPException(404, { message: `connector "${param.connector}" not found in channel "${param.channel}"` });
5241
5275
  return c.text(JSON.stringify(connector, null, 2));
5242
5276
  });
5243
5277
  //#endregion
5244
- //#region lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts
5245
- const renameHelp$2 = `funnel channels <channel> connectors rename <connector> <new-name>
5278
+ //#region lib/cli/routes/channels.$channel.connectors.rename.ts
5279
+ const help$13 = `funnel channels <channel> connectors rename <connector> <new-name>
5246
5280
 
5247
5281
  usage: funnel channels <channel> connectors rename <connector> <new-name>`;
5282
+ const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$13));
5283
+ //#endregion
5284
+ //#region lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts
5248
5285
  const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
5249
5286
  channel: z.string(),
5250
5287
  connector: z.string(),
5251
5288
  newName: z.string()
5252
- })), zValidator$1("query", z.object({}), renameHelp$2), async (c) => {
5289
+ })), zValidator$1("query", z.object({})), async (c) => {
5253
5290
  const param = c.req.valid("param");
5254
- const funnel = c.var.funnel;
5291
+ const funnel = c.env.funnel;
5255
5292
  await funnel.listeners.stop(param.channel, param.connector);
5256
5293
  funnel.channels.renameConnector(param.channel, param.connector, param.newName);
5257
5294
  await funnel.listeners.start(param.channel, param.newName);
5258
5295
  return c.text(`renamed connector "${param.connector}" to "${param.newName}"`);
5259
5296
  });
5297
+ //#endregion
5298
+ //#region lib/cli/routes/channels.$channel.connectors.$connector.rename.ts
5299
+ const help$12 = `funnel channels <channel> connectors rename <connector> <new-name>
5300
+
5301
+ usage: funnel channels <channel> connectors rename <connector> <new-name>`;
5302
+ const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$12));
5260
5303
  const channelsConnectorsRequestHandler = factory.createHandlers(zValidator$1("param", z.object({
5261
5304
  channel: z.string(),
5262
5305
  connector: z.string()
@@ -5265,7 +5308,7 @@ const channelsConnectorsRequestHandler = factory.createHandlers(zValidator$1("pa
5265
5308
  usage: funnel channels <channel> connectors <connector> request --method=<api.method> [--key=value ...]`), async (c) => {
5266
5309
  const param = c.req.valid("param");
5267
5310
  const query = c.req.valid("query");
5268
- const funnel = c.var.funnel;
5311
+ const funnel = c.env.funnel;
5269
5312
  const passthrough = {};
5270
5313
  for (const [k, v] of new URL(c.req.url).searchParams) {
5271
5314
  if (k === "method") continue;
@@ -5285,15 +5328,18 @@ const channelsConnectorsSchedulesGroupHandler = factory.createHandlers(zValidato
5285
5328
 
5286
5329
  usage: funnel channels <ch> connectors <conn> schedules`), (c) => {
5287
5330
  const param = c.req.valid("param");
5288
- const entries = c.var.funnel.channels.listScheduleEntries(param.channel, param.connector);
5331
+ const entries = c.env.funnel.channels.listScheduleEntries(param.channel, param.connector);
5289
5332
  if (entries.length === 0) return c.text("no schedule entries");
5290
5333
  return c.text(entries.map((e) => `${e.id}\t${e.cron}\t${e.enabled ? "on" : "off"}\t${e.prompt}`).join("\n"));
5291
5334
  });
5292
5335
  //#endregion
5293
- //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts
5294
- const addHelp$1 = `funnel channels <ch> connectors <conn> schedules add <id> — add a schedule entry
5336
+ //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.ts
5337
+ const help$11 = `funnel channels <ch> connectors <conn> schedules add <id> — add a schedule entry
5295
5338
 
5296
5339
  usage: funnel channels <ch> connectors <conn> schedules add <id> --cron="*/5 * * * *" --prompt="..." [--enabled=true] [--catchup-policy=latest|all|skip]`;
5340
+ const channelsConnectorSchedulesAddHelpHandler = factory.createHandlers((c) => c.text(help$11));
5341
+ //#endregion
5342
+ //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts
5297
5343
  const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$1("param", z.object({
5298
5344
  channel: z.string(),
5299
5345
  connector: z.string(),
@@ -5303,10 +5349,10 @@ const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$
5303
5349
  prompt: z.string(),
5304
5350
  enabled: z.coerce.boolean().optional(),
5305
5351
  "catchup-policy": scheduleCatchupPolicySchema.optional()
5306
- }), addHelp$1), async (c) => {
5352
+ })), async (c) => {
5307
5353
  const param = c.req.valid("param");
5308
5354
  const query = c.req.valid("query");
5309
- const funnel = c.var.funnel;
5355
+ const funnel = c.env.funnel;
5310
5356
  const entry = funnel.channels.addScheduleEntry(param.channel, param.connector, {
5311
5357
  id: param.id,
5312
5358
  cron: query.cron,
@@ -5318,24 +5364,27 @@ const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$
5318
5364
  return c.text(`added schedule entry "${entry.id}"`);
5319
5365
  });
5320
5366
  //#endregion
5321
- //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts
5322
- const removeHelp$2 = `funnel channels <ch> connectors <conn> schedules remove <id>
5367
+ //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.ts
5368
+ const help$10 = `funnel channels <ch> connectors <conn> schedules remove <id>
5323
5369
 
5324
5370
  usage: funnel channels <ch> connectors <conn> schedules remove <id>`;
5371
+ const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$10));
5372
+ //#endregion
5373
+ //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts
5325
5374
  const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
5326
5375
  channel: z.string(),
5327
5376
  connector: z.string(),
5328
5377
  id: z.string()
5329
- })), zValidator$1("query", z.object({}), removeHelp$2), async (c) => {
5378
+ })), zValidator$1("query", z.object({})), async (c) => {
5330
5379
  const param = c.req.valid("param");
5331
- const funnel = c.var.funnel;
5380
+ const funnel = c.env.funnel;
5332
5381
  funnel.channels.removeScheduleEntry(param.channel, param.connector, param.id);
5333
5382
  await funnel.listeners.restart(param.channel, param.connector);
5334
5383
  return c.text(`removed schedule entry "${param.id}"`);
5335
5384
  });
5336
5385
  //#endregion
5337
- //#region lib/cli/routes/channels.$channel.publish.ts
5338
- const publishHelp = `funnel channels <channel> publish — push arbitrary content into a channel
5386
+ //#region lib/cli/routes/channels.publish.ts
5387
+ const help$9 = `funnel channels <channel> publish — push arbitrary content into a channel
5339
5388
 
5340
5389
  usage: funnel channels <channel> publish --content="<text>" [--connector=<name>] [--meta-<key>=<value> ...]
5341
5390
 
@@ -5343,14 +5392,17 @@ options:
5343
5392
  --content Required. The event body delivered to subscribers.
5344
5393
  --connector Optional. Stamp the event with a connector name (resolved to id when found).
5345
5394
  --meta-<key> Optional. Repeatable. Added to meta. Example: --meta-source=cron`;
5395
+ const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$9));
5396
+ //#endregion
5397
+ //#region lib/cli/routes/channels.$channel.publish.ts
5346
5398
  const querySchema = z.object({
5347
5399
  content: z.string().min(1, { message: "--content is required" }),
5348
5400
  connector: z.string().min(1).optional()
5349
5401
  }).passthrough();
5350
- const channelsPublishHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", querySchema, publishHelp), async (c) => {
5402
+ const channelsPublishHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", querySchema), async (c) => {
5351
5403
  const param = c.req.valid("param");
5352
5404
  const query = c.req.valid("query");
5353
- const funnel = c.var.funnel;
5405
+ const funnel = c.env.funnel;
5354
5406
  const meta = {};
5355
5407
  for (const [k, v] of new URL(c.req.url).searchParams) if (k.startsWith("meta-")) meta[k.slice(5)] = v;
5356
5408
  const result = await funnel.publisher.publish(param.channel, {
@@ -5363,28 +5415,42 @@ const channelsPublishHandler = factory.createHandlers(zValidator$1("param", z.ob
5363
5415
  return c.text(`published (offset=${result.offset})`);
5364
5416
  });
5365
5417
  //#endregion
5366
- //#region lib/cli/routes/channels.remove.$channel.ts
5367
- const removeHelp$1 = `funnel channels remove — remove a channel
5418
+ //#region lib/cli/routes/channels.remove.ts
5419
+ const help$8 = `funnel channels remove — remove a channel
5368
5420
 
5369
5421
  usage: funnel channels remove <name>`;
5370
- const channelsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({}), removeHelp$1), (c) => {
5422
+ const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$8));
5423
+ //#endregion
5424
+ //#region lib/cli/routes/channels.remove.$channel.ts
5425
+ const channelsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
5371
5426
  const param = c.req.valid("param");
5372
- c.var.funnel.channels.remove(param.channel);
5427
+ c.env.funnel.channels.remove(param.channel);
5373
5428
  return c.text(`removed channel "${param.channel}"`);
5374
5429
  });
5375
5430
  //#endregion
5376
- //#region lib/cli/routes/channels.$channel.rename.$newName.ts
5377
- const renameHelp$1 = `funnel channels rename — rename a channel
5431
+ //#region lib/cli/routes/channels.rename.ts
5432
+ const help$7 = `funnel channels rename — rename a channel
5433
+
5434
+ usage:
5435
+ funnel channels rename <old> <new>
5436
+ funnel channels <old> rename <new>`;
5437
+ const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(help$7));
5438
+ //#endregion
5439
+ //#region lib/cli/routes/channels.$channel.rename.ts
5440
+ const help$6 = `funnel channels rename — rename a channel
5378
5441
 
5379
5442
  usage:
5380
5443
  funnel channels rename <old> <new>
5381
5444
  funnel channels <old> rename <new>`;
5445
+ const channelsChannelRenameHelpHandler = factory.createHandlers((c) => c.text(help$6));
5446
+ //#endregion
5447
+ //#region lib/cli/routes/channels.$channel.rename.$newName.ts
5382
5448
  const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
5383
5449
  channel: z.string(),
5384
5450
  newName: z.string()
5385
- })), zValidator$1("query", z.object({}), renameHelp$1), (c) => {
5451
+ })), zValidator$1("query", z.object({})), (c) => {
5386
5452
  const param = c.req.valid("param");
5387
- c.var.funnel.channels.rename(param.channel, param.newName);
5453
+ c.env.funnel.channels.rename(param.channel, param.newName);
5388
5454
  return c.text(`renamed channel "${param.channel}" to "${param.newName}"`);
5389
5455
  });
5390
5456
  const channelsSetDeliveryHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -5401,12 +5467,12 @@ modes:
5401
5467
  tap=all clients (TUI dashboard, debugging) always receive regardless of mode.
5402
5468
  `), (c) => {
5403
5469
  const param = c.req.valid("param");
5404
- c.var.funnel.channels.setDelivery(param.channel, param.mode);
5470
+ c.env.funnel.channels.setDelivery(param.channel, param.mode);
5405
5471
  return c.text(`channel "${param.channel}" delivery set to ${param.mode}`);
5406
5472
  });
5407
5473
  const channelsShowHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({}), `funnel channels <name> — show channel details`), (c) => {
5408
5474
  const param = c.req.valid("param");
5409
- const channel = c.var.funnel.channels.get(param.channel);
5475
+ const channel = c.env.funnel.channels.get(param.channel);
5410
5476
  if (!channel) throw new HTTPException(404, { message: `channel "${param.channel}" not found` });
5411
5477
  const connectorLines = channel.connectors.length ? channel.connectors.map((c) => ` - ${c.name} (${c.type}, id: ${c.id})`) : [" (none)"];
5412
5478
  const lines = [
@@ -5444,7 +5510,7 @@ examples:
5444
5510
  funnel channels prod-inbox connectors add prod-slack --type=slack --bot-token=xoxb-... --app-token=xapp-...
5445
5511
  funnel channels prod-inbox`), (c) => {
5446
5512
  const query = c.req.valid("query");
5447
- const channels = c.var.funnel.channels.list();
5513
+ const channels = c.env.funnel.channels.list();
5448
5514
  if (query.json === "true" || query.json === "") return c.json(channels.map((ch) => ({
5449
5515
  id: ch.id,
5450
5516
  name: ch.name,
@@ -5464,6 +5530,106 @@ examples:
5464
5530
  return c.text(lines.join("\n"));
5465
5531
  });
5466
5532
  //#endregion
5533
+ //#region lib/cli/routes/channels.validate.ts
5534
+ const help$5 = `funnel channels <channel> validate — check connector configuration
5535
+
5536
+ usage: funnel channels <channel> validate [--json]
5537
+
5538
+ options:
5539
+ --json output as JSON
5540
+
5541
+ Checks that each connector has the required tokens and fields set.
5542
+ Does not make any network calls — static config check only.
5543
+
5544
+ examples:
5545
+ funnel channels open-karte validate
5546
+ funnel channels open-karte validate --json`;
5547
+ const channelsValidateHelpHandler = factory.createHandlers((c) => c.text(help$5));
5548
+ //#endregion
5549
+ //#region lib/cli/routes/channels.$channel.validate.ts
5550
+ const validateConnector = (connector) => {
5551
+ const issues = [];
5552
+ if (connector.type === "slack") {
5553
+ if (!(connector.botToken || connector.botTokenEnv)) issues.push({
5554
+ connector: connector.name,
5555
+ field: "botToken",
5556
+ message: "missing botToken (xoxb-...) or botTokenEnv"
5557
+ });
5558
+ if (!(connector.appToken || connector.appTokenEnv)) issues.push({
5559
+ connector: connector.name,
5560
+ field: "appToken",
5561
+ message: "missing appToken (xapp-...) or appTokenEnv"
5562
+ });
5563
+ if (connector.botToken && typeof connector.botToken === "string" && !connector.botToken.startsWith("xoxb-")) issues.push({
5564
+ connector: connector.name,
5565
+ field: "botToken",
5566
+ message: `botToken must start with xoxb- (got: ${connector.botToken.slice(0, 8)}...)`
5567
+ });
5568
+ if (connector.appToken && typeof connector.appToken === "string" && !connector.appToken.startsWith("xapp-")) issues.push({
5569
+ connector: connector.name,
5570
+ field: "appToken",
5571
+ message: `appToken must start with xapp- (got: ${connector.appToken.slice(0, 8)}...)`
5572
+ });
5573
+ }
5574
+ if (connector.type === "gh") {
5575
+ if (!(connector.token || connector.tokenEnv)) issues.push({
5576
+ connector: connector.name,
5577
+ field: "token",
5578
+ message: "missing token or tokenEnv for GitHub connector"
5579
+ });
5580
+ if (!connector.repo) issues.push({
5581
+ connector: connector.name,
5582
+ field: "repo",
5583
+ message: "missing repo (expected owner/repo format)"
5584
+ });
5585
+ }
5586
+ if (connector.type === "discord") {
5587
+ if (!(connector.botToken || connector.botTokenEnv)) issues.push({
5588
+ connector: connector.name,
5589
+ field: "botToken",
5590
+ message: "missing botToken or botTokenEnv for Discord connector"
5591
+ });
5592
+ }
5593
+ return issues;
5594
+ };
5595
+ const channelsValidateHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({ json: z.enum([
5596
+ "true",
5597
+ "false",
5598
+ ""
5599
+ ]).optional() })), (c) => {
5600
+ const param = c.req.valid("param");
5601
+ const query = c.req.valid("query");
5602
+ const funnel = c.env.funnel;
5603
+ const isJson = query.json === "true" || query.json === "";
5604
+ const channel = funnel.channels.get(param.channel);
5605
+ if (!channel) throw new HTTPException(404, { message: `channel "${param.channel}" not found` });
5606
+ if (channel.connectors.length === 0) {
5607
+ if (isJson) return c.json({
5608
+ channel: channel.name,
5609
+ valid: false,
5610
+ issues: [{
5611
+ connector: "(none)",
5612
+ field: "connectors",
5613
+ message: "no connectors configured"
5614
+ }]
5615
+ });
5616
+ return c.text(`⚠ ${channel.name}: no connectors configured`);
5617
+ }
5618
+ const allIssues = [];
5619
+ for (const connector of channel.connectors) {
5620
+ const issues = validateConnector(connector);
5621
+ allIssues.push(...issues);
5622
+ }
5623
+ if (isJson) return c.json({
5624
+ channel: channel.name,
5625
+ valid: allIssues.length === 0,
5626
+ issues: allIssues
5627
+ });
5628
+ if (allIssues.length === 0) return c.text(`✓ ${channel.name}: all connectors valid`);
5629
+ const lines = allIssues.map((issue) => `✗ ${channel.name}/${issue.connector}: ${issue.message}`);
5630
+ return c.text(lines.join("\n"));
5631
+ });
5632
+ //#endregion
5467
5633
  //#region lib/cli/routes/claude.ts
5468
5634
  const claudeHelp = `funnel claude — launch Claude Code
5469
5635
 
@@ -5495,7 +5661,7 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
5495
5661
  channel: z.string().optional()
5496
5662
  }).passthrough(), claudeHelp), async (c) => {
5497
5663
  const query = c.req.valid("query");
5498
- const funnel = c.var.funnel;
5664
+ const funnel = c.env.funnel;
5499
5665
  const userArgs = queryToCliArgs(c.req.url, RESERVED_KEYS$1);
5500
5666
  if (query.channel && !query.profile) {
5501
5667
  const exitCode = await funnel.claude.launch({
@@ -5548,6 +5714,71 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
5548
5714
  process.exit(exitCode);
5549
5715
  });
5550
5716
  //#endregion
5717
+ //#region lib/cli/routes/debug-row.ts
5718
+ const stringOrNull = (value) => typeof value === "string" && value.length > 0 ? value : null;
5719
+ const numberOrNull = (value) => typeof value === "number" ? value : null;
5720
+ const stringOr = (value, fallback) => typeof value === "string" ? value : fallback;
5721
+ /**
5722
+ * Parse a payload string as a JSON object. Returns null for non-strings,
5723
+ * malformed JSON, or any non-object JSON (arrays, primitives) — the callers
5724
+ * only ever want the object form.
5725
+ */
5726
+ const parsePayloadObject = (payload) => {
5727
+ if (payload === null) return null;
5728
+ try {
5729
+ const parsed = JSON.parse(payload);
5730
+ if (isStringKeyedObject(parsed)) return parsed;
5731
+ } catch {
5732
+ return null;
5733
+ }
5734
+ return null;
5735
+ };
5736
+ const isStringKeyedObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
5737
+ const truncate = (text, max) => text.length <= max ? text : `${text.slice(0, max)}…`;
5738
+ /**
5739
+ * A short human preview of a payload: the `text` field when the payload is a
5740
+ * JSON object that has one, otherwise the raw payload, both truncated.
5741
+ */
5742
+ const previewOf = (payload) => {
5743
+ if (typeof payload !== "string" || payload.length === 0) return null;
5744
+ const parsed = parsePayloadObject(payload);
5745
+ if (parsed !== null && "text" in parsed) return truncate(String(parsed.text), 60);
5746
+ return truncate(payload, 60);
5747
+ };
5748
+ /** Narrow one processed-table row into a `DebugEvent`. */
5749
+ const toDebugEvent = (row) => {
5750
+ const payload = stringOrNull(row.payload);
5751
+ return {
5752
+ seq: numberOrNull(row.seq),
5753
+ ts: numberOrNull(row.ts),
5754
+ type: stringOr(row.type, "?"),
5755
+ outcome: stringOr(row.outcome, "?"),
5756
+ eventId: stringOrNull(row.event_id),
5757
+ payload,
5758
+ payloadParsed: parsePayloadObject(payload),
5759
+ preview: previewOf(row.payload)
5760
+ };
5761
+ };
5762
+ /** Narrow one connection-table row into a `DebugConnectionError`. */
5763
+ const toDebugConnectionError = (row) => ({
5764
+ seq: numberOrNull(row.seq),
5765
+ ts: numberOrNull(row.ts),
5766
+ type: stringOr(row.type, "?"),
5767
+ status: stringOr(row.status, "?"),
5768
+ detail: stringOrNull(row.detail)
5769
+ });
5770
+ /**
5771
+ * Open a reader, run one query, and always close — the shared shape behind
5772
+ * every diagnostic lookup. Returns the rows or the reader's `Error`.
5773
+ */
5774
+ const queryRows = (reader, sql, params) => {
5775
+ try {
5776
+ return reader.query(sql, params);
5777
+ } finally {
5778
+ reader.close();
5779
+ }
5780
+ };
5781
+ //#endregion
5551
5782
  //#region lib/cli/routes/debug.ts
5552
5783
  const debugHelp = `funnel debug — diagnose why Claude is not receiving events
5553
5784
 
@@ -5639,17 +5870,6 @@ const formatTs = (epochMs) => {
5639
5870
  if (typeof epochMs !== "number") return "?";
5640
5871
  return new Date(epochMs).toISOString().slice(11, 19);
5641
5872
  };
5642
- const truncate = (text, max) => text.length <= max ? text : `${text.slice(0, max)}…`;
5643
- const extractPreview = (payload) => {
5644
- if (typeof payload !== "string" || payload.length === 0) return null;
5645
- try {
5646
- const parsed = JSON.parse(payload);
5647
- if (parsed !== null && typeof parsed === "object" && "text" in parsed) return truncate(String(parsed.text), 60);
5648
- } catch {
5649
- return truncate(payload, 60);
5650
- }
5651
- return truncate(payload, 60);
5652
- };
5653
5873
  const buildDiagnosis = (report) => {
5654
5874
  const rootCause = (report.connectionErrors[report.connectionErrors.length - 1] ?? null)?.detail ?? null;
5655
5875
  if (!report.gateway.running) return {
@@ -5762,6 +5982,16 @@ const resolveStoreOrNull = () => {
5762
5982
  connectionPath
5763
5983
  };
5764
5984
  };
5985
+ /**
5986
+ * Resolve the connector name for a connector id on a channel, used to attribute
5987
+ * a replayed event back to its source connector. Returns undefined when the id
5988
+ * is null or no longer present (connectors can be removed after an event was
5989
+ * logged).
5990
+ */
5991
+ const connectorOf = (channel, connectorId) => {
5992
+ if (connectorId === null) return void 0;
5993
+ return channel.connectors?.find((connector) => connector.id === connectorId)?.name;
5994
+ };
5765
5995
  const resolveChannelId = (channels, channelName) => {
5766
5996
  if (channelName) {
5767
5997
  const match = channels.find((ch) => ch.name === channelName);
@@ -5799,7 +6029,7 @@ const debugEventsHandler = factory.createHandlers(zValidator$1("query", z.object
5799
6029
  ]).optional()
5800
6030
  }), debugEventsHelp), async (c) => {
5801
6031
  const query = c.req.valid("query");
5802
- const channels = c.var.funnel.channels.list();
6032
+ const channels = c.env.funnel.channels.list();
5803
6033
  const isJson = query.json === "true" || query.json === "";
5804
6034
  const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
5805
6035
  const store = resolveStoreOrNull();
@@ -5828,34 +6058,9 @@ const debugEventsHandler = factory.createHandlers(zValidator$1("query", z.object
5828
6058
  }
5829
6059
  const channel = resolved.channel;
5830
6060
  const reader = new ConnectorDiagnosticSqlReader(store);
5831
- const rows = (() => {
5832
- try {
5833
- if (channel) return reader.query("SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT ?", [channel.id, limit]);
5834
- return reader.query("SELECT seq, ts, type, outcome, payload FROM processed ORDER BY seq DESC LIMIT ?", [limit]);
5835
- } finally {
5836
- reader.close();
5837
- }
5838
- })();
6061
+ const rows = channel ? queryRows(reader, "SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT ?", [channel.id, limit]) : queryRows(reader, "SELECT seq, ts, type, outcome, payload FROM processed ORDER BY seq DESC LIMIT ?", [limit]);
5839
6062
  if (rows instanceof Error) return c.text(`error: ${rows.message}`);
5840
- const events = [...rows].reverse().map((row) => {
5841
- const rawPayload = typeof row.payload === "string" ? row.payload : null;
5842
- let payloadParsed = null;
5843
- if (rawPayload) try {
5844
- const parsed = JSON.parse(rawPayload);
5845
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) payloadParsed = parsed;
5846
- } catch {
5847
- payloadParsed = null;
5848
- }
5849
- return {
5850
- seq: typeof row.seq === "number" ? row.seq : null,
5851
- ts: typeof row.ts === "number" ? row.ts : null,
5852
- type: typeof row.type === "string" ? row.type : "?",
5853
- outcome: typeof row.outcome === "string" ? row.outcome : "?",
5854
- payload: rawPayload,
5855
- payloadParsed,
5856
- preview: extractPreview(row.payload)
5857
- };
5858
- });
6063
+ const events = rows.reverse().map(toDebugEvent);
5859
6064
  if (isJson) return c.json(events);
5860
6065
  if (events.length === 0) return c.text("no events recorded");
5861
6066
  const lines = events.map((ev) => {
@@ -5877,7 +6082,7 @@ const debugDroppedHandler = factory.createHandlers(zValidator$1("query", z.objec
5877
6082
  ]).optional()
5878
6083
  }), debugDroppedHelp), async (c) => {
5879
6084
  const query = c.req.valid("query");
5880
- const channels = c.var.funnel.channels.list();
6085
+ const channels = c.env.funnel.channels.list();
5881
6086
  const isJson = query.json === "true" || query.json === "";
5882
6087
  const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
5883
6088
  const store = resolveStoreOrNull();
@@ -5906,39 +6111,13 @@ const debugDroppedHandler = factory.createHandlers(zValidator$1("query", z.objec
5906
6111
  }
5907
6112
  const channel = resolvedDropped.channel;
5908
6113
  const reader = new ConnectorDiagnosticSqlReader(store);
5909
- const rows = (() => {
5910
- try {
5911
- if (channel) return reader.query("SELECT p.seq, p.ts, p.type, p.outcome, p.payload, p.event_id FROM processed p WHERE p.channel_id = ? AND p.outcome LIKE 'skip:%' ORDER BY p.seq DESC LIMIT ?", [channel.id, limit]);
5912
- return reader.query("SELECT seq, ts, type, outcome, payload, event_id FROM processed WHERE outcome LIKE 'skip:%' ORDER BY seq DESC LIMIT ?", [limit]);
5913
- } finally {
5914
- reader.close();
5915
- }
5916
- })();
6114
+ const rows = channel ? queryRows(reader, "SELECT p.seq, p.ts, p.type, p.outcome, p.payload, p.event_id FROM processed p WHERE p.channel_id = ? AND p.outcome LIKE 'skip:%' ORDER BY p.seq DESC LIMIT ?", [channel.id, limit]) : queryRows(reader, "SELECT seq, ts, type, outcome, payload, event_id FROM processed WHERE outcome LIKE 'skip:%' ORDER BY seq DESC LIMIT ?", [limit]);
5917
6115
  if (rows instanceof Error) return c.text(`error: ${rows.message}`);
5918
- const events = [...rows].reverse().map((row) => {
5919
- const rawPayload = typeof row.payload === "string" ? row.payload : null;
5920
- let payloadParsed = null;
5921
- if (rawPayload) try {
5922
- const parsed = JSON.parse(rawPayload);
5923
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) payloadParsed = parsed;
5924
- } catch {
5925
- payloadParsed = null;
5926
- }
5927
- return {
5928
- seq: typeof row.seq === "number" ? row.seq : null,
5929
- ts: typeof row.ts === "number" ? row.ts : null,
5930
- type: typeof row.type === "string" ? row.type : "?",
5931
- outcome: typeof row.outcome === "string" ? row.outcome : "?",
5932
- event_id: typeof row.event_id === "string" ? row.event_id : null,
5933
- payload: rawPayload,
5934
- payloadParsed,
5935
- preview: extractPreview(row.payload)
5936
- };
5937
- });
6116
+ const events = rows.reverse().map(toDebugEvent);
5938
6117
  if (isJson) return c.json(events);
5939
6118
  if (events.length === 0) return c.text("no dropped events recorded");
5940
6119
  const lines = events.map((ev) => {
5941
- return `${formatTs(ev.ts)} ${ev.type.padEnd(8)} ${ev.outcome.padEnd(20)}${ev.seq !== null ? ` seq=${ev.seq}` : ""}${ev.event_id ? ` event_id=${ev.event_id.slice(0, 8)}` : ""}${ev.preview ? ` "${ev.preview}"` : ""}`;
6120
+ return `${formatTs(ev.ts)} ${ev.type.padEnd(8)} ${ev.outcome.padEnd(20)}${ev.seq !== null ? ` seq=${ev.seq}` : ""}${ev.eventId ? ` event_id=${ev.eventId.slice(0, 8)}` : ""}${ev.preview ? ` "${ev.preview}"` : ""}`;
5942
6121
  });
5943
6122
  return c.text(lines.join("\n"));
5944
6123
  });
@@ -5952,7 +6131,7 @@ const debugErrorsHandler = factory.createHandlers(zValidator$1("query", z.object
5952
6131
  ]).optional()
5953
6132
  }), debugErrorsHelp), async (c) => {
5954
6133
  const query = c.req.valid("query");
5955
- const channels = c.var.funnel.channels.list();
6134
+ const channels = c.env.funnel.channels.list();
5956
6135
  const isJson = query.json === "true" || query.json === "";
5957
6136
  const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
5958
6137
  const store = resolveStoreOrNull();
@@ -5981,22 +6160,9 @@ const debugErrorsHandler = factory.createHandlers(zValidator$1("query", z.object
5981
6160
  }
5982
6161
  const channel = resolvedErrors.channel;
5983
6162
  const reader = new ConnectorDiagnosticSqlReader(store);
5984
- const rows = (() => {
5985
- try {
5986
- if (channel) return reader.query("SELECT seq, ts, type, status, detail FROM connection WHERE channel_id = ? AND status IN ('auth-failed','error') ORDER BY seq DESC LIMIT ?", [channel.id, limit]);
5987
- return reader.query("SELECT seq, ts, type, status, detail FROM connection WHERE status IN ('auth-failed','error') ORDER BY seq DESC LIMIT ?", [limit]);
5988
- } finally {
5989
- reader.close();
5990
- }
5991
- })();
6163
+ const rows = channel ? queryRows(reader, "SELECT seq, ts, type, status, detail FROM connection WHERE channel_id = ? AND status IN ('auth-failed','error') ORDER BY seq DESC LIMIT ?", [channel.id, limit]) : queryRows(reader, "SELECT seq, ts, type, status, detail FROM connection WHERE status IN ('auth-failed','error') ORDER BY seq DESC LIMIT ?", [limit]);
5992
6164
  if (rows instanceof Error) return c.text(`error: ${rows.message}`);
5993
- const errors = [...rows].reverse().map((row) => ({
5994
- seq: typeof row.seq === "number" ? row.seq : null,
5995
- ts: typeof row.ts === "number" ? row.ts : null,
5996
- type: typeof row.type === "string" ? row.type : "?",
5997
- status: typeof row.status === "string" ? row.status : "?",
5998
- detail: typeof row.detail === "string" && row.detail.length > 0 ? row.detail : null
5999
- }));
6165
+ const errors = rows.reverse().map(toDebugConnectionError);
6000
6166
  if (isJson) return c.json(errors);
6001
6167
  if (errors.length === 0) return c.text("no connection errors recorded");
6002
6168
  const lines = errors.map((ev) => {
@@ -6032,50 +6198,13 @@ const buildChannelReport = async (targetChannel, gatewayStatus, gatewayBodyOrNul
6032
6198
  baseReport.claudeClients = gatewayBodyOrNull.clients.filter((cl) => !cl.tapAll && cl.channelName === targetChannelName).length;
6033
6199
  }
6034
6200
  if (store) {
6035
- const reader = new ConnectorDiagnosticSqlReader(store);
6036
- const evRows = (() => {
6037
- try {
6038
- return reader.query("SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT ?", [targetChannel.id, limit]);
6039
- } finally {
6040
- reader.close();
6041
- }
6042
- })();
6043
- if (!(evRows instanceof Error)) baseReport.recentEvents = evRows.reverse().map((row) => {
6044
- const rawPayload = typeof row.payload === "string" ? row.payload : null;
6045
- let payloadParsed = null;
6046
- if (rawPayload) try {
6047
- const parsed = JSON.parse(rawPayload);
6048
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) payloadParsed = parsed;
6049
- } catch {
6050
- payloadParsed = null;
6051
- }
6052
- return {
6053
- seq: typeof row.seq === "number" ? row.seq : null,
6054
- ts: typeof row.ts === "number" ? row.ts : null,
6055
- type: typeof row.type === "string" ? row.type : "?",
6056
- outcome: typeof row.outcome === "string" ? row.outcome : "?",
6057
- payload: rawPayload,
6058
- payloadParsed,
6059
- preview: extractPreview(row.payload)
6060
- };
6061
- });
6201
+ const evRows = queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT ?", [targetChannel.id, limit]);
6202
+ if (!(evRows instanceof Error)) baseReport.recentEvents = evRows.reverse().map(toDebugEvent);
6062
6203
  const hasDeadListeners = baseReport.listeners.some((l) => !l.alive);
6063
6204
  const hasListenerErrors = baseReport.listeners.some((l) => l.errors > 0);
6064
6205
  if (hasDeadListeners || hasListenerErrors) {
6065
- const errReader = new ConnectorDiagnosticSqlReader(store);
6066
- const errRows = (() => {
6067
- try {
6068
- return errReader.query("SELECT ts, type, status, detail FROM connection WHERE channel_id = ? AND status IN ('auth-failed','error') ORDER BY seq DESC LIMIT 3", [targetChannel.id]);
6069
- } finally {
6070
- errReader.close();
6071
- }
6072
- })();
6073
- if (!(errRows instanceof Error)) baseReport.connectionErrors = errRows.reverse().map((row) => ({
6074
- ts: typeof row.ts === "number" ? row.ts : null,
6075
- type: typeof row.type === "string" ? row.type : "?",
6076
- status: typeof row.status === "string" ? row.status : "?",
6077
- detail: typeof row.detail === "string" && row.detail.length > 0 ? row.detail : null
6078
- }));
6206
+ const errRows = queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT ts, type, status, detail FROM connection WHERE channel_id = ? AND status IN ('auth-failed','error') ORDER BY seq DESC LIMIT 3", [targetChannel.id]);
6207
+ if (!(errRows instanceof Error)) baseReport.connectionErrors = errRows.reverse().map(toDebugConnectionError);
6079
6208
  }
6080
6209
  }
6081
6210
  return {
@@ -6098,7 +6227,7 @@ const debugHandler = factory.createHandlers(zValidator$1("query", z.object({
6098
6227
  limit: z.string().optional()
6099
6228
  }), debugHelp), async (c) => {
6100
6229
  const query = c.req.valid("query");
6101
- const funnel = c.var.funnel;
6230
+ const funnel = c.env.funnel;
6102
6231
  const channels = funnel.channels.list();
6103
6232
  const gatewayStatus = funnel.gateway.getStatus();
6104
6233
  const isJson = query.json === "true" || query.json === "";
@@ -6193,7 +6322,7 @@ examples:
6193
6322
  fnl debug replay --channel open-karte --seq 412
6194
6323
  fnl debug replay --channel open-karte --json`), async (c) => {
6195
6324
  const query = c.req.valid("query");
6196
- const funnel = c.var.funnel;
6325
+ const funnel = c.env.funnel;
6197
6326
  const channels = funnel.channels.list();
6198
6327
  const isJson = query.json === "true" || query.json === "";
6199
6328
  const resolved = resolveChannelId(channels, query.channel);
@@ -6221,15 +6350,7 @@ examples:
6221
6350
  if (isJson) return c.json({ error: "no diagnostic store yet (start the gateway first)" });
6222
6351
  return c.text("no diagnostic store yet (start the gateway first)");
6223
6352
  }
6224
- const reader = new ConnectorDiagnosticSqlReader(store);
6225
- const rows = (() => {
6226
- try {
6227
- if (query.seq) return reader.query("SELECT seq, event_id, type, payload, connector_id, channel_id FROM processed WHERE channel_id = ? AND seq = ? LIMIT 1", [targetChannel.id, Number(query.seq)]);
6228
- return reader.query("SELECT seq, event_id, type, payload, connector_id, channel_id FROM processed WHERE channel_id = ? AND outcome LIKE 'emitted%' ORDER BY seq DESC LIMIT 1", [targetChannel.id]);
6229
- } finally {
6230
- reader.close();
6231
- }
6232
- })();
6353
+ const rows = query.seq ? queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT seq, event_id, type, payload, connector_id, channel_id FROM processed WHERE channel_id = ? AND seq = ? LIMIT 1", [targetChannel.id, Number(query.seq)]) : queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT seq, event_id, type, payload, connector_id, channel_id FROM processed WHERE channel_id = ? AND outcome LIKE 'emitted%' ORDER BY seq DESC LIMIT 1", [targetChannel.id]);
6233
6354
  if (rows instanceof Error) {
6234
6355
  if (isJson) return c.json({ error: rows.message });
6235
6356
  return c.text(`error: ${rows.message}`);
@@ -6244,24 +6365,15 @@ examples:
6244
6365
  const connectorId = typeof firstRow.connector_id === "string" ? firstRow.connector_id : null;
6245
6366
  let content = typeof firstRow.payload === "string" ? firstRow.payload : null;
6246
6367
  if ((!content || content.length === 0) && eventId) {
6247
- const rawReader = new ConnectorDiagnosticSqlReader(store);
6248
- const rawRows = (() => {
6249
- try {
6250
- return rawReader.query("SELECT payload FROM raw WHERE event_id = ? LIMIT 1", [eventId]);
6251
- } finally {
6252
- rawReader.close();
6253
- }
6254
- })();
6255
- if (!(rawRows instanceof Error) && rawRows[0]) {
6256
- const rawRow = rawRows[0];
6257
- content = typeof rawRow.payload === "string" ? rawRow.payload : null;
6258
- }
6368
+ const rawRows = queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT payload FROM raw WHERE event_id = ? LIMIT 1", [eventId]);
6369
+ const rawRow = rawRows instanceof Error ? null : rawRows[0];
6370
+ if (rawRow) content = typeof rawRow.payload === "string" ? rawRow.payload : null;
6259
6371
  }
6260
6372
  if (!content) {
6261
6373
  if (isJson) return c.json({ error: "event has no payload to replay" });
6262
6374
  return c.text("event has no payload to replay");
6263
6375
  }
6264
- const connectorName = connectorId ? targetChannel.connectors?.find((c) => c.id === connectorId)?.name : void 0;
6376
+ const connectorName = connectorOf(targetChannel, connectorId);
6265
6377
  const result = await funnel.publisher.publish(targetChannel.name, {
6266
6378
  content,
6267
6379
  connector: connectorName
@@ -6277,7 +6389,7 @@ examples:
6277
6389
  if (isJson) return c.json({ error: result.reason });
6278
6390
  return c.text(`error: ${result.reason}`);
6279
6391
  }
6280
- const preview = extractPreview(content);
6392
+ const preview = previewOf(content);
6281
6393
  if (isJson) return c.json({
6282
6394
  replayed: true,
6283
6395
  seq,
@@ -6287,103 +6399,6 @@ examples:
6287
6399
  return c.text(`replayed seq=${seq ?? "?"} → offset=${result.offset}${preview ? ` "${preview}"` : ""}`);
6288
6400
  });
6289
6401
  //#endregion
6290
- //#region lib/cli/routes/channels.$channel.validate.ts
6291
- const validateHelp = `funnel channels <channel> validate — check connector configuration
6292
-
6293
- usage: funnel channels <channel> validate [--json]
6294
-
6295
- options:
6296
- --json output as JSON
6297
-
6298
- Checks that each connector has the required tokens and fields set.
6299
- Does not make any network calls — static config check only.
6300
-
6301
- examples:
6302
- funnel channels open-karte validate
6303
- funnel channels open-karte validate --json`;
6304
- const validateConnector = (connector) => {
6305
- const issues = [];
6306
- if (connector.type === "slack") {
6307
- if (!(connector.botToken || connector.botTokenEnv)) issues.push({
6308
- connector: connector.name,
6309
- field: "botToken",
6310
- message: "missing botToken (xoxb-...) or botTokenEnv"
6311
- });
6312
- if (!(connector.appToken || connector.appTokenEnv)) issues.push({
6313
- connector: connector.name,
6314
- field: "appToken",
6315
- message: "missing appToken (xapp-...) or appTokenEnv"
6316
- });
6317
- if (connector.botToken && typeof connector.botToken === "string" && !connector.botToken.startsWith("xoxb-")) issues.push({
6318
- connector: connector.name,
6319
- field: "botToken",
6320
- message: `botToken must start with xoxb- (got: ${connector.botToken.slice(0, 8)}...)`
6321
- });
6322
- if (connector.appToken && typeof connector.appToken === "string" && !connector.appToken.startsWith("xapp-")) issues.push({
6323
- connector: connector.name,
6324
- field: "appToken",
6325
- message: `appToken must start with xapp- (got: ${connector.appToken.slice(0, 8)}...)`
6326
- });
6327
- }
6328
- if (connector.type === "gh") {
6329
- if (!(connector.token || connector.tokenEnv)) issues.push({
6330
- connector: connector.name,
6331
- field: "token",
6332
- message: "missing token or tokenEnv for GitHub connector"
6333
- });
6334
- if (!connector.repo) issues.push({
6335
- connector: connector.name,
6336
- field: "repo",
6337
- message: "missing repo (expected owner/repo format)"
6338
- });
6339
- }
6340
- if (connector.type === "discord") {
6341
- if (!(connector.botToken || connector.botTokenEnv)) issues.push({
6342
- connector: connector.name,
6343
- field: "botToken",
6344
- message: "missing botToken or botTokenEnv for Discord connector"
6345
- });
6346
- }
6347
- return issues;
6348
- };
6349
- const channelsValidateHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({ json: z.enum([
6350
- "true",
6351
- "false",
6352
- ""
6353
- ]).optional() }), validateHelp), (c) => {
6354
- const param = c.req.valid("param");
6355
- const query = c.req.valid("query");
6356
- const funnel = c.var.funnel;
6357
- const isJson = query.json === "true" || query.json === "";
6358
- const channel = funnel.channels.get(param.channel);
6359
- if (!channel) throw new HTTPException(404, { message: `channel "${param.channel}" not found` });
6360
- if (channel.connectors.length === 0) {
6361
- if (isJson) return c.json({
6362
- channel: channel.name,
6363
- valid: false,
6364
- issues: [{
6365
- connector: "(none)",
6366
- field: "connectors",
6367
- message: "no connectors configured"
6368
- }]
6369
- });
6370
- return c.text(`⚠ ${channel.name}: no connectors configured`);
6371
- }
6372
- const allIssues = [];
6373
- for (const connector of channel.connectors) {
6374
- const issues = validateConnector(connector);
6375
- allIssues.push(...issues);
6376
- }
6377
- if (isJson) return c.json({
6378
- channel: channel.name,
6379
- valid: allIssues.length === 0,
6380
- issues: allIssues
6381
- });
6382
- if (allIssues.length === 0) return c.text(`✓ ${channel.name}: all connectors valid`);
6383
- const lines = allIssues.map((issue) => `✗ ${channel.name}/${issue.connector}: ${issue.message}`);
6384
- return c.text(lines.join("\n"));
6385
- });
6386
- //#endregion
6387
6402
  //#region lib/cli/routes/gateway.ts
6388
6403
  const groupHelp$1 = `funnel gateway — manage the funnel daemon
6389
6404
 
@@ -6411,7 +6426,7 @@ examples:
6411
6426
 
6412
6427
  see also: fnl debug --channel <name> (higher-level diagnosis with next-action hints)`;
6413
6428
  const renderGatewayStatus = async (c) => {
6414
- const status = c.var.funnel.gateway.getStatus();
6429
+ const status = c.env.funnel.gateway.getStatus();
6415
6430
  if (!status.running) throw new HTTPException(503, { message: "funnel gateway: not running" });
6416
6431
  const res = await fetch(`http://127.0.0.1:${status.port}/status`).catch(() => null);
6417
6432
  if (!res) return c.text(`funnel gateway: running (pid ${status.pid}) — health check failed`);
@@ -6453,7 +6468,7 @@ Reads /listeners from the running gateway daemon and prints the live registry.
6453
6468
 
6454
6469
  examples:
6455
6470
  funnel gateway listeners`), async (c) => {
6456
- const result = await c.var.funnel.listeners.list();
6471
+ const result = await c.env.funnel.listeners.list();
6457
6472
  if (result.state === "offline") throw new HTTPException(503, { message: "funnel gateway: not running" });
6458
6473
  if (result.state === "error") throw new HTTPException(503, { message: `funnel gateway: ${result.reason}` });
6459
6474
  if (result.listeners.length === 0) return c.text("funnel gateway: no running listeners");
@@ -6619,7 +6634,7 @@ const gatewaySqlHandler = factory.createHandlers(zValidator$1("query", z.object(
6619
6634
  limit: z.string().optional()
6620
6635
  }), sqlHelp), async (c) => {
6621
6636
  const query = c.req.valid("query");
6622
- const funnel = c.var.funnel;
6637
+ const funnel = c.env.funnel;
6623
6638
  let sql = null;
6624
6639
  let params = [];
6625
6640
  let resolvedChannelId = null;
@@ -6669,7 +6684,7 @@ examples:
6669
6684
  funnel gateway restart
6670
6685
  funnel gateway restart --no-caffeine`), async (c) => {
6671
6686
  const query = c.req.valid("query");
6672
- const result = await c.var.funnel.gateway.restart({ caffeinate: query["no-caffeine"] !== "true" });
6687
+ const result = await c.env.funnel.gateway.restart({ caffeinate: query["no-caffeine"] !== "true" });
6673
6688
  const lines = [];
6674
6689
  if (result.wasRunning) lines.push(result.stopped ? "funnel gateway: stopped" : "funnel gateway: failed to stop");
6675
6690
  if (result.stopped) lines.push(result.started ? "funnel gateway: started" : "funnel gateway: failed to start");
@@ -6690,7 +6705,7 @@ examples:
6690
6705
  funnel gateway run
6691
6706
  funnel gateway run --no-caffeine`), async (c) => {
6692
6707
  const query = c.req.valid("query");
6693
- const funnel = c.var.funnel;
6708
+ const funnel = c.env.funnel;
6694
6709
  const gatewayScript = resolveDaemonScript();
6695
6710
  const command = query["no-caffeine"] !== "true" && process.platform === "darwin" ? [
6696
6711
  "caffeinate",
@@ -6720,7 +6735,7 @@ examples:
6720
6735
  funnel gateway start --no-caffeine`;
6721
6736
  const gatewayStartHandler = factory.createHandlers(zValidator$1("query", z.object({ "no-caffeine": z.string().optional() }), startHelp), async (c) => {
6722
6737
  const query = c.req.valid("query");
6723
- const funnel = c.var.funnel;
6738
+ const funnel = c.env.funnel;
6724
6739
  if (funnel.gateway.isRunning()) {
6725
6740
  const status = funnel.gateway.getStatus();
6726
6741
  return c.text(`funnel gateway: already running (pid ${status.pid})`);
@@ -6747,7 +6762,7 @@ examples:
6747
6762
  funnel gateway status --json`), async (c) => {
6748
6763
  const query = c.req.valid("query");
6749
6764
  if (!(query.json === "true" || query.json === "")) return renderGatewayStatus(c);
6750
- const status = c.var.funnel.gateway.getStatus();
6765
+ const status = c.env.funnel.gateway.getStatus();
6751
6766
  if (!status.running) throw new HTTPException(503, { message: "funnel gateway: not running" });
6752
6767
  const res = await fetch(`http://127.0.0.1:${status.port}/status`).catch(() => null);
6753
6768
  if (!res) return c.json({
@@ -6771,12 +6786,29 @@ Terminates the process whose PID is stored in ~/.funnel/gateway.pid.
6771
6786
 
6772
6787
  examples:
6773
6788
  funnel gateway stop`), async (c) => {
6774
- const funnel = c.var.funnel;
6789
+ const funnel = c.env.funnel;
6775
6790
  if (!funnel.gateway.isRunning()) return c.text("funnel gateway: no running process");
6776
6791
  if (!await funnel.gateway.stop()) throw new HTTPException(500, { message: "funnel gateway: failed to stop" });
6777
6792
  return c.text("funnel gateway: stopped");
6778
6793
  });
6779
6794
  //#endregion
6795
+ //#region lib/cli/routes/profiles.add.ts
6796
+ const help$4 = `funnel profiles add — add a profile
6797
+
6798
+ usage: funnel profiles add <name> --path <path> --channel <channel-name> [recipe]
6799
+
6800
+ options:
6801
+ --path working directory passed to claude as cwd
6802
+ --channel channel name (resolved to channel id internally)
6803
+ --agent sub-agent name, prepended to the launch argv as --agent <name>
6804
+ --options extra launch argv as one whitespace-split string (e.g. "--brief")
6805
+ --env env vars layered under the process, as "KEY=VAL,KEY2=VAL2"
6806
+ --no-resume start a fresh claude session every launch (default resumes)
6807
+
6808
+ The launch recipe (--agent / --options / --env / --resume) lives on the
6809
+ profile; the channel only declares transport (connectors / delivery).`;
6810
+ const profilesAddHelpHandler = factory.createHandlers((c) => c.text(help$4));
6811
+ //#endregion
6780
6812
  //#region lib/cli/routes/parse-profile-recipe.ts
6781
6813
  /**
6782
6814
  * Turns the single-string CLI flags (`--agent`, `--options "<argv>"`,
@@ -6813,20 +6845,6 @@ const parseProfileRecipe = (query) => {
6813
6845
  };
6814
6846
  //#endregion
6815
6847
  //#region lib/cli/routes/profiles.add.$profile.ts
6816
- const addHelp = `funnel profiles add — add a profile
6817
-
6818
- usage: funnel profiles add <name> --path <path> --channel <channel-name> [recipe]
6819
-
6820
- options:
6821
- --path working directory passed to claude as cwd
6822
- --channel channel name (resolved to channel id internally)
6823
- --agent sub-agent name, prepended to the launch argv as --agent <name>
6824
- --options extra launch argv as one whitespace-split string (e.g. "--brief")
6825
- --env env vars layered under the process, as "KEY=VAL,KEY2=VAL2"
6826
- --no-resume start a fresh claude session every launch (default resumes)
6827
-
6828
- The launch recipe (--agent / --options / --env / --resume) lives on the
6829
- profile; the channel only declares transport (connectors / delivery).`;
6830
6848
  const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({
6831
6849
  path: z.string(),
6832
6850
  channel: z.string(),
@@ -6835,10 +6853,10 @@ const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object
6835
6853
  env: z.string().optional(),
6836
6854
  resume: z.string().optional(),
6837
6855
  "no-resume": z.string().optional()
6838
- }), addHelp), (c) => {
6856
+ })), (c) => {
6839
6857
  const param = c.req.valid("param");
6840
6858
  const query = c.req.valid("query");
6841
- const funnel = c.var.funnel;
6859
+ const funnel = c.env.funnel;
6842
6860
  const channel = funnel.channels.get(query.channel);
6843
6861
  if (!channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
6844
6862
  const recipe = parseProfileRecipe(query);
@@ -6858,22 +6876,33 @@ usage: funnel profiles <name> as-default
6858
6876
 
6859
6877
  the first profile in the list is treated as the default for fnl claude.`), (c) => {
6860
6878
  const param = c.req.valid("param");
6861
- c.var.funnel.profiles.asDefault(param.profile);
6879
+ c.env.funnel.profiles.asDefault(param.profile);
6862
6880
  return c.text(`profile "${param.profile}" is now the default`);
6863
6881
  });
6864
6882
  //#endregion
6865
- //#region lib/cli/routes/profiles.$profile.rename.$newName.ts
6866
- const renameHelp = `funnel profiles rename — rename a profile
6883
+ //#region lib/cli/routes/profiles.rename.ts
6884
+ const help$3 = `funnel profiles rename — rename a profile
6885
+
6886
+ usage:
6887
+ funnel profiles rename <old> <new>
6888
+ funnel profiles <old> rename <new>`;
6889
+ const profilesRenameHelpHandler = factory.createHandlers((c) => c.text(help$3));
6890
+ //#endregion
6891
+ //#region lib/cli/routes/profiles.$profile.rename.ts
6892
+ const help$2 = `funnel profiles rename — rename a profile
6867
6893
 
6868
6894
  usage:
6869
6895
  funnel profiles rename <old> <new>
6870
6896
  funnel profiles <old> rename <new>`;
6897
+ const profilesProfileRenameHelpHandler = factory.createHandlers((c) => c.text(help$2));
6898
+ //#endregion
6899
+ //#region lib/cli/routes/profiles.$profile.rename.$newName.ts
6871
6900
  const profilesRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
6872
6901
  profile: z.string(),
6873
6902
  newName: z.string()
6874
- })), zValidator$1("query", z.object({}), renameHelp), (c) => {
6903
+ })), zValidator$1("query", z.object({})), (c) => {
6875
6904
  const param = c.req.valid("param");
6876
- c.var.funnel.profiles.rename(param.profile, param.newName);
6905
+ c.env.funnel.profiles.rename(param.profile, param.newName);
6877
6906
  return c.text(`renamed profile "${param.profile}" to "${param.newName}"`);
6878
6907
  });
6879
6908
  //#endregion
@@ -6885,7 +6914,7 @@ usage: funnel profiles <name> run [additional claude args...]
6885
6914
  const RESERVED_KEYS = [];
6886
6915
  const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({}).passthrough(), launchHelp), async (c) => {
6887
6916
  const param = c.req.valid("param");
6888
- const funnel = c.var.funnel;
6917
+ const funnel = c.env.funnel;
6889
6918
  const profile = funnel.profiles.get(param.profile);
6890
6919
  if (!profile) throw new HTTPException(404, { message: `profile "${param.profile}" not found` });
6891
6920
  const exitCode = await funnel.claude.launch({
@@ -6900,18 +6929,21 @@ const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.obj
6900
6929
  process.exit(exitCode);
6901
6930
  });
6902
6931
  //#endregion
6903
- //#region lib/cli/routes/profiles.remove.$profile.ts
6904
- const removeHelp = `funnel profiles remove — remove a profile
6932
+ //#region lib/cli/routes/profiles.remove.ts
6933
+ const help$1 = `funnel profiles remove — remove a profile
6905
6934
 
6906
6935
  usage: funnel profiles remove <name>`;
6907
- const profilesRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({}), removeHelp), (c) => {
6936
+ const profilesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$1));
6937
+ //#endregion
6938
+ //#region lib/cli/routes/profiles.remove.$profile.ts
6939
+ const profilesRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({})), (c) => {
6908
6940
  const param = c.req.valid("param");
6909
- c.var.funnel.profiles.remove(param.profile);
6941
+ c.env.funnel.profiles.remove(param.profile);
6910
6942
  return c.text(`removed profile "${param.profile}"`);
6911
6943
  });
6912
6944
  //#endregion
6913
- //#region lib/cli/routes/profiles.set.$profile.ts
6914
- const setHelp = `funnel profiles <name> set — update a profile
6945
+ //#region lib/cli/routes/profiles.set.ts
6946
+ const help = `funnel profiles <name> set — update a profile
6915
6947
 
6916
6948
  usage: funnel profiles <name> set [--path <path>] [--channel <channel-name>] [recipe]
6917
6949
 
@@ -6925,6 +6957,9 @@ options:
6925
6957
 
6926
6958
  Only the flags you pass are changed; --agent and --options together replace
6927
6959
  the profile's whole options list.`;
6960
+ const profilesSetHelpHandler = factory.createHandlers((c) => c.text(help));
6961
+ //#endregion
6962
+ //#region lib/cli/routes/profiles.set.$profile.ts
6928
6963
  const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({
6929
6964
  path: z.string().optional(),
6930
6965
  channel: z.string().optional(),
@@ -6933,10 +6968,10 @@ const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object
6933
6968
  env: z.string().optional(),
6934
6969
  resume: z.string().optional(),
6935
6970
  "no-resume": z.string().optional()
6936
- }), setHelp), (c) => {
6971
+ })), (c) => {
6937
6972
  const param = c.req.valid("param");
6938
6973
  const query = c.req.valid("query");
6939
- const funnel = c.var.funnel;
6974
+ const funnel = c.env.funnel;
6940
6975
  const channel = query.channel !== void 0 ? funnel.channels.get(query.channel) : null;
6941
6976
  if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
6942
6977
  const recipe = parseProfileRecipe(query);
@@ -6971,7 +7006,7 @@ examples:
6971
7006
  funnel profiles add cto --path /repo/myapp --channel prod-inbox --agent pm --options "--brief"
6972
7007
  funnel profiles cto as-default
6973
7008
  funnel profiles cto run`), (c) => {
6974
- const profiles = c.var.funnel.profiles.list();
7009
+ const profiles = c.env.funnel.profiles.list();
6975
7010
  if (profiles.length === 0) return c.text("no profiles");
6976
7011
  const lines = profiles.map((profile, index) => {
6977
7012
  const tag = index === 0 ? " (default)" : "";
@@ -7092,7 +7127,7 @@ const statusHandler = factory.createHandlers(zValidator$1("query", z.object({
7092
7127
  interval: z.string().optional()
7093
7128
  }), statusHelp), async (c) => {
7094
7129
  const query = c.req.valid("query");
7095
- const funnel = c.var.funnel;
7130
+ const funnel = c.env.funnel;
7096
7131
  const isWatch = query.watch === "true" || query.watch === "";
7097
7132
  const intervalSec = Math.min(60, Math.max(1, query.interval ? Number(query.interval) : 3));
7098
7133
  if (!isWatch) {
@@ -7137,30 +7172,9 @@ const updateHandler = factory.createHandlers(zValidator$1("query", z.object({}),
7137
7172
  });
7138
7173
  //#endregion
7139
7174
  //#region lib/cli/routes/index.ts
7140
- const helpRoute = (text) => factory.createHandlers((c) => c.text(text));
7141
- /**
7142
- * Build the CLI Hono app wired to a specific Funnel instance.
7143
- * Exposed so library consumers can mount the same routes their `fnl` CLI
7144
- * uses against a custom Funnel (e.g. one with sandboxed boundaries).
7145
- *
7146
- * All CLI verbs (`add` / `remove` / `set` / `rename` / `as-default` / `request`) map to POST in
7147
- * to-request.ts and stay in the URL as a literal segment. Read paths (list / show / launch) keep GET.
7148
- * Help shortcuts at parameterless URLs return the help text directly so `funnel <verb>` (no args) is
7149
- * informative instead of 404.
7150
- */
7151
- const createCliApp = (funnel) => {
7152
- const base = factory.createApp();
7153
- base.use((c, next) => {
7154
- c.set("funnel", funnel);
7155
- return next();
7156
- });
7157
- base.onError((error, c) => {
7158
- if (error instanceof HTTPException) return c.text(`error: ${error.message}`, error.status);
7159
- return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
7160
- });
7161
- return base.get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...helpRoute(addHelp$3)).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...helpRoute(removeHelp$1)).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...helpRoute(renameHelp$1)).post("/channels/:channel/rename", ...helpRoute(renameHelp$1)).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...helpRoute(publishHelp)).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel/validate", ...channelsValidateHandler).get("/channels/validate", ...helpRoute(validateHelp)).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...helpRoute(addHelp$2)).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...helpRoute(removeHelp$3)).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...helpRoute(setHelp$1)).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...helpRoute(renameHelp$2)).post("/channels/:channel/connectors/:connector/rename", ...helpRoute(renameHelp$2)).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...helpRoute(addHelp$1)).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...helpRoute(removeHelp$2)).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...helpRoute(addHelp)).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...helpRoute(setHelp)).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...helpRoute(removeHelp)).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...helpRoute(renameHelp)).post("/profiles/:profile/rename", ...helpRoute(renameHelp)).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/sql", ...gatewaySqlHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/debug", ...debugHandler).get("/debug/events", ...debugEventsHandler).get("/debug/dropped", ...debugDroppedHandler).get("/debug/errors", ...debugErrorsHandler).get("/debug/replay", ...debugReplayHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
7162
- };
7163
- /** CLI Hono app wired to a default `new Funnel()`. For embedding with a custom Funnel use `createCliApp`. */
7164
- const app = createCliApp(new Funnel());
7175
+ const routes = factory.createApp().onError((error, c) => {
7176
+ if (error instanceof HTTPException) return c.text(`error: ${error.message}`, error.status);
7177
+ return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
7178
+ }).get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...channelsAddHelpHandler).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...channelsRemoveHelpHandler).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...channelsRenameHelpHandler).post("/channels/:channel/rename", ...channelsChannelRenameHelpHandler).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...channelsPublishHelpHandler).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel/validate", ...channelsValidateHandler).get("/channels/validate", ...channelsValidateHelpHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...channelsConnectorsAddHelpHandler).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...channelsConnectorsRemoveHelpHandler).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...channelsConnectorsSetHelpHandler).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...channelsConnectorsRenameHelpHandler).post("/channels/:channel/connectors/:connector/rename", ...channelsConnectorRenameHelpHandler).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...channelsConnectorSchedulesAddHelpHandler).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...channelsConnectorSchedulesRemoveHelpHandler).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...profilesAddHelpHandler).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...profilesSetHelpHandler).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...profilesRemoveHelpHandler).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...profilesRenameHelpHandler).post("/profiles/:profile/rename", ...profilesProfileRenameHelpHandler).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/sql", ...gatewaySqlHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/debug", ...debugHandler).get("/debug/events", ...debugEventsHandler).get("/debug/dropped", ...debugDroppedHandler).get("/debug/errors", ...debugErrorsHandler).get("/debug/replay", ...debugReplayHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
7165
7179
  //#endregion
7166
- export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_ARGS, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
7180
+ export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_ARGS, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, routes as cliRoutes, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, connectorSpecSchema, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };