@interactive-inc/claude-funnel 0.18.0 → 0.20.1
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/bin.js +337 -336
- package/dist/connectors/slack.js +1 -1
- package/dist/gateway/daemon.js +163 -162
- package/dist/index.d.ts +76 -8
- package/dist/index.js +246 -42
- package/dist/{slack-connector-schema-B4hsf3AY.js → slack-connector-schema-BM9xshol.js} +25 -1
- package/funnel.schema.json +3 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConn
|
|
|
2
2
|
import { n as FunnelLogger, r as FunnelConnectorListener, t as NodeFunnelLogger } from "./node-logger-DQz_BGOD.js";
|
|
3
3
|
import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-CD5HIkrd.js";
|
|
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-FxP7LPlx.js";
|
|
5
|
-
import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-
|
|
5
|
+
import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-BM9xshol.js";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
8
8
|
import { z } from "zod";
|
|
@@ -54,6 +54,12 @@ const channelConfigSchema = z.object({
|
|
|
54
54
|
options: z.array(z.string()).default([]),
|
|
55
55
|
/** Env vars layered under the launched claude process. process.env wins on collision. */
|
|
56
56
|
env: z.record(z.string(), z.string()).default({}),
|
|
57
|
+
/**
|
|
58
|
+
* When true (the default), funnel injects `--session-id <uuid>` so that
|
|
59
|
+
* relaunching from the same cwd resumes the previous claude session.
|
|
60
|
+
* Set to false for channels that should always start a fresh session.
|
|
61
|
+
*/
|
|
62
|
+
resume: z.boolean().default(true),
|
|
57
63
|
connectors: z.array(connectorConfigSchema).default([])
|
|
58
64
|
});
|
|
59
65
|
const profileConfigSchema = z.object({
|
|
@@ -308,6 +314,7 @@ var FunnelChannels = class {
|
|
|
308
314
|
delivery: input.delivery ?? "fanout",
|
|
309
315
|
options: input.options ?? [],
|
|
310
316
|
env: input.env ?? {},
|
|
317
|
+
resume: input.resume ?? true,
|
|
311
318
|
connectors: []
|
|
312
319
|
};
|
|
313
320
|
settings.channels.push(channel);
|
|
@@ -320,6 +327,12 @@ var FunnelChannels = class {
|
|
|
320
327
|
channel.delivery = delivery;
|
|
321
328
|
this.store.write(settings);
|
|
322
329
|
}
|
|
330
|
+
setResume(name, resume) {
|
|
331
|
+
const settings = this.store.read();
|
|
332
|
+
const channel = this.requireChannel(settings, name);
|
|
333
|
+
channel.resume = resume;
|
|
334
|
+
this.store.write(settings);
|
|
335
|
+
}
|
|
323
336
|
setOptions(name, options) {
|
|
324
337
|
const settings = this.store.read();
|
|
325
338
|
const channel = this.requireChannel(settings, name);
|
|
@@ -551,6 +564,7 @@ var FunnelClaude = class {
|
|
|
551
564
|
channels;
|
|
552
565
|
mcp;
|
|
553
566
|
gateway;
|
|
567
|
+
sessions;
|
|
554
568
|
process;
|
|
555
569
|
fs;
|
|
556
570
|
logger;
|
|
@@ -559,6 +573,7 @@ var FunnelClaude = class {
|
|
|
559
573
|
this.channels = deps.channels;
|
|
560
574
|
this.mcp = deps.mcp;
|
|
561
575
|
this.gateway = deps.gateway;
|
|
576
|
+
this.sessions = deps.sessions;
|
|
562
577
|
this.process = deps.process ?? defaultProcess$2;
|
|
563
578
|
this.fs = deps.fs ?? defaultFs$3;
|
|
564
579
|
this.logger = deps.logger ?? defaultLogger$4;
|
|
@@ -570,7 +585,7 @@ var FunnelClaude = class {
|
|
|
570
585
|
if (!channel) throw new Error(`channel "${options.channel}" not found`);
|
|
571
586
|
if (options.profileName && this.isRunning(options.profileName)) throw new Error(`profile "${options.profileName}" is already running`);
|
|
572
587
|
const cwd = options.cwd ?? globalThis.process.cwd();
|
|
573
|
-
if (!this.mcp.findInstalledName(cwd)) {
|
|
588
|
+
if ((options.installMcp ?? true) && !this.mcp.findInstalledName(cwd)) {
|
|
574
589
|
this.mcp.install(cwd);
|
|
575
590
|
this.logger.info(`added funnel MCP to .mcp.json`, { cwd });
|
|
576
591
|
}
|
|
@@ -582,7 +597,8 @@ var FunnelClaude = class {
|
|
|
582
597
|
this.writePidFile(options.profileName);
|
|
583
598
|
this.installCleanup(options.profileName);
|
|
584
599
|
}
|
|
585
|
-
const
|
|
600
|
+
const sessionId = channel.resume ? this.resolveSessionId(channel.id, cwd, options.userArgs ?? []) : null;
|
|
601
|
+
const claudeArgs = this.buildArgs(channel.options, options.userArgs ?? [], cwd, sessionId);
|
|
586
602
|
const env = this.buildEnv(channel.id, channel.env);
|
|
587
603
|
this.logger.info(`claude launch`, {
|
|
588
604
|
channel: options.channel,
|
|
@@ -643,12 +659,26 @@ var FunnelClaude = class {
|
|
|
643
659
|
if (!state) return false;
|
|
644
660
|
return !state.startsWith("Z");
|
|
645
661
|
}
|
|
646
|
-
buildArgs(channelOptions, userArgs, cwd) {
|
|
662
|
+
buildArgs(channelOptions, userArgs, cwd, sessionId) {
|
|
647
663
|
const result = [...channelOptions, ...userArgs];
|
|
664
|
+
if (sessionId !== null) result.push("--session-id", sessionId);
|
|
648
665
|
const mcpName = this.mcp.findInstalledName(cwd);
|
|
649
666
|
if (mcpName && !result.includes("--dangerously-load-development-channels") && !result.includes("--channels")) result.push("--dangerously-load-development-channels", `server:${mcpName}`);
|
|
650
667
|
return result;
|
|
651
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* Decides whether funnel should inject `--session-id`. We back off when
|
|
671
|
+
* the user already passed a session-shaping flag, since combining them
|
|
672
|
+
* would either confuse claude or override the explicit user intent.
|
|
673
|
+
*/
|
|
674
|
+
resolveSessionId(channelId, cwd, userArgs) {
|
|
675
|
+
for (const arg of userArgs) {
|
|
676
|
+
if (arg === "-c" || arg === "--continue") return null;
|
|
677
|
+
if (arg === "--resume" || arg.startsWith("--resume=")) return null;
|
|
678
|
+
if (arg === "--session-id" || arg.startsWith("--session-id=")) return null;
|
|
679
|
+
}
|
|
680
|
+
return this.sessions.getOrCreate(channelId, cwd);
|
|
681
|
+
}
|
|
652
682
|
buildEnv(channelId, channelEnv) {
|
|
653
683
|
const env = {};
|
|
654
684
|
for (const [key, value] of Object.entries(channelEnv)) env[key] = value;
|
|
@@ -800,6 +830,13 @@ const channelSpecSchema = z.object({
|
|
|
800
830
|
options: z.array(z.string()).optional(),
|
|
801
831
|
/** Env vars layered under the launched claude process. process.env wins on collision. */
|
|
802
832
|
env: z.record(z.string(), z.string()).optional(),
|
|
833
|
+
/**
|
|
834
|
+
* When true (the default), funnel injects `--session-id <uuid>` so that
|
|
835
|
+
* relaunching from the same cwd resumes the previous claude session
|
|
836
|
+
* without bleeding into other channels or workspaces. Set to false for
|
|
837
|
+
* channels that should always start a fresh session.
|
|
838
|
+
*/
|
|
839
|
+
resume: z.boolean().optional(),
|
|
803
840
|
connectors: z.array(connectorSpecSchema).optional()
|
|
804
841
|
});
|
|
805
842
|
const localConfigSchema = z.object({
|
|
@@ -918,6 +955,9 @@ const recordsEqual = (a, b) => {
|
|
|
918
955
|
* absent field means "do not manage connectors from here" and leaves
|
|
919
956
|
* everything in `~/.funnel` alone. Other channels in funnel.json (not
|
|
920
957
|
* passed to this call) are untouched.
|
|
958
|
+
*
|
|
959
|
+
* Returns the per-connector change set so callers (e.g. the claude launcher)
|
|
960
|
+
* can drive listener hot-reload on the gateway after settings are written.
|
|
921
961
|
*/
|
|
922
962
|
var FunnelLocalConfigSync = class {
|
|
923
963
|
channels;
|
|
@@ -933,25 +973,39 @@ var FunnelLocalConfigSync = class {
|
|
|
933
973
|
}
|
|
934
974
|
async ensure(channel, cwd) {
|
|
935
975
|
const existing = this.channels.get(channel.name);
|
|
976
|
+
const nextResume = channel.resume ?? true;
|
|
936
977
|
if (!existing) this.channels.add({
|
|
937
978
|
name: channel.name,
|
|
938
979
|
options: channel.options ?? [],
|
|
939
|
-
env: channel.env ?? {}
|
|
980
|
+
env: channel.env ?? {},
|
|
981
|
+
resume: nextResume
|
|
940
982
|
});
|
|
941
983
|
else {
|
|
942
984
|
const nextOptions = channel.options ?? [];
|
|
943
985
|
const nextEnv = channel.env ?? {};
|
|
944
986
|
if (!arraysEqual(existing.options, nextOptions)) this.channels.setOptions(channel.name, nextOptions);
|
|
945
987
|
if (!recordsEqual(existing.env, nextEnv)) this.channels.setEnv(channel.name, nextEnv);
|
|
988
|
+
if (existing.resume !== nextResume) this.channels.setResume(channel.name, nextResume);
|
|
946
989
|
}
|
|
947
|
-
if (channel.connectors === void 0) return
|
|
990
|
+
if (channel.connectors === void 0) return {
|
|
991
|
+
touched: [],
|
|
992
|
+
removed: []
|
|
993
|
+
};
|
|
948
994
|
const dotenv = this.dotenv.read(cwd);
|
|
949
|
-
const touched =
|
|
995
|
+
const touched = [];
|
|
996
|
+
const touchedIds = /* @__PURE__ */ new Set();
|
|
950
997
|
for (const spec of channel.connectors) {
|
|
951
|
-
const
|
|
952
|
-
touched.
|
|
998
|
+
const outcome = await this.ensureConnector(channel.name, spec, dotenv);
|
|
999
|
+
touched.push({
|
|
1000
|
+
name: outcome.name,
|
|
1001
|
+
changed: outcome.changed
|
|
1002
|
+
});
|
|
1003
|
+
touchedIds.add(outcome.id);
|
|
953
1004
|
}
|
|
954
|
-
|
|
1005
|
+
return {
|
|
1006
|
+
touched,
|
|
1007
|
+
removed: this.removeExtras(channel.name, touchedIds)
|
|
1008
|
+
};
|
|
955
1009
|
}
|
|
956
1010
|
async ensureConnector(channelName, spec, dotenv) {
|
|
957
1011
|
if (spec.type === "slack") return await this.ensureSlack(channelName, spec, dotenv);
|
|
@@ -976,11 +1030,22 @@ var FunnelLocalConfigSync = class {
|
|
|
976
1030
|
existing: byName?.appToken
|
|
977
1031
|
});
|
|
978
1032
|
if (byName) {
|
|
979
|
-
if (byName.botToken !== botToken || byName.appToken !== appToken)
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1033
|
+
if (byName.botToken !== botToken || byName.appToken !== appToken) {
|
|
1034
|
+
this.channels.updateSlackConnector(channelName, spec.name, {
|
|
1035
|
+
botToken,
|
|
1036
|
+
appToken
|
|
1037
|
+
});
|
|
1038
|
+
return {
|
|
1039
|
+
id: byName.id,
|
|
1040
|
+
name: spec.name,
|
|
1041
|
+
changed: true
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
return {
|
|
1045
|
+
id: byName.id,
|
|
1046
|
+
name: spec.name,
|
|
1047
|
+
changed: false
|
|
1048
|
+
};
|
|
984
1049
|
}
|
|
985
1050
|
const byToken = this.findSlackByToken(channelName, [botToken, appToken]);
|
|
986
1051
|
if (byToken) {
|
|
@@ -989,14 +1054,22 @@ var FunnelLocalConfigSync = class {
|
|
|
989
1054
|
botToken,
|
|
990
1055
|
appToken
|
|
991
1056
|
});
|
|
992
|
-
return
|
|
1057
|
+
return {
|
|
1058
|
+
id: byToken.id,
|
|
1059
|
+
name: spec.name,
|
|
1060
|
+
changed: true
|
|
1061
|
+
};
|
|
993
1062
|
}
|
|
994
|
-
return
|
|
995
|
-
|
|
1063
|
+
return {
|
|
1064
|
+
id: this.channels.addConnector(channelName, {
|
|
1065
|
+
type: "slack",
|
|
1066
|
+
name: spec.name,
|
|
1067
|
+
botToken,
|
|
1068
|
+
appToken
|
|
1069
|
+
}).id,
|
|
996
1070
|
name: spec.name,
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}).id;
|
|
1071
|
+
changed: true
|
|
1072
|
+
};
|
|
1000
1073
|
}
|
|
1001
1074
|
async ensureDiscord(channelName, spec, dotenv) {
|
|
1002
1075
|
const byName = this.findExistingDiscord(channelName, spec.name);
|
|
@@ -1008,42 +1081,84 @@ var FunnelLocalConfigSync = class {
|
|
|
1008
1081
|
existing: byName?.botToken
|
|
1009
1082
|
});
|
|
1010
1083
|
if (byName) {
|
|
1011
|
-
if (byName.botToken !== botToken)
|
|
1012
|
-
|
|
1084
|
+
if (byName.botToken !== botToken) {
|
|
1085
|
+
this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
|
|
1086
|
+
return {
|
|
1087
|
+
id: byName.id,
|
|
1088
|
+
name: spec.name,
|
|
1089
|
+
changed: true
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
id: byName.id,
|
|
1094
|
+
name: spec.name,
|
|
1095
|
+
changed: false
|
|
1096
|
+
};
|
|
1013
1097
|
}
|
|
1014
1098
|
const byToken = this.findDiscordByToken(channelName, botToken);
|
|
1015
1099
|
if (byToken) {
|
|
1016
1100
|
this.channels.renameConnector(channelName, byToken.name, spec.name);
|
|
1017
1101
|
if (byToken.botToken !== botToken) this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
|
|
1018
|
-
return
|
|
1102
|
+
return {
|
|
1103
|
+
id: byToken.id,
|
|
1104
|
+
name: spec.name,
|
|
1105
|
+
changed: true
|
|
1106
|
+
};
|
|
1019
1107
|
}
|
|
1020
|
-
return
|
|
1021
|
-
|
|
1108
|
+
return {
|
|
1109
|
+
id: this.channels.addConnector(channelName, {
|
|
1110
|
+
type: "discord",
|
|
1111
|
+
name: spec.name,
|
|
1112
|
+
botToken
|
|
1113
|
+
}).id,
|
|
1022
1114
|
name: spec.name,
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1115
|
+
changed: true
|
|
1116
|
+
};
|
|
1025
1117
|
}
|
|
1026
1118
|
ensureGh(channelName, spec) {
|
|
1027
1119
|
const existing = this.channels.getConnector(channelName, spec.name);
|
|
1028
1120
|
if (existing && existing.type !== "gh") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "gh"`);
|
|
1029
1121
|
if (existing && existing.type === "gh") {
|
|
1030
|
-
if (spec.pollInterval !== void 0 && existing.pollInterval !== spec.pollInterval)
|
|
1031
|
-
|
|
1122
|
+
if (spec.pollInterval !== void 0 && existing.pollInterval !== spec.pollInterval) {
|
|
1123
|
+
this.channels.updateGhConnector(channelName, spec.name, { pollInterval: spec.pollInterval });
|
|
1124
|
+
return {
|
|
1125
|
+
id: existing.id,
|
|
1126
|
+
name: spec.name,
|
|
1127
|
+
changed: true
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
id: existing.id,
|
|
1132
|
+
name: spec.name,
|
|
1133
|
+
changed: false
|
|
1134
|
+
};
|
|
1032
1135
|
}
|
|
1033
|
-
return
|
|
1034
|
-
|
|
1136
|
+
return {
|
|
1137
|
+
id: this.channels.addConnector(channelName, {
|
|
1138
|
+
type: "gh",
|
|
1139
|
+
name: spec.name,
|
|
1140
|
+
...spec.pollInterval !== void 0 ? { pollInterval: spec.pollInterval } : {}
|
|
1141
|
+
}).id,
|
|
1035
1142
|
name: spec.name,
|
|
1036
|
-
|
|
1037
|
-
}
|
|
1143
|
+
changed: true
|
|
1144
|
+
};
|
|
1038
1145
|
}
|
|
1039
1146
|
ensureSchedule(channelName, spec) {
|
|
1040
1147
|
const existing = this.channels.getConnector(channelName, spec.name);
|
|
1041
1148
|
if (existing && existing.type !== "schedule") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "schedule"`);
|
|
1042
|
-
if (existing && existing.type === "schedule") return
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1149
|
+
if (existing && existing.type === "schedule") return {
|
|
1150
|
+
id: existing.id,
|
|
1151
|
+
name: spec.name,
|
|
1152
|
+
changed: false
|
|
1153
|
+
};
|
|
1154
|
+
return {
|
|
1155
|
+
id: this.channels.addConnector(channelName, {
|
|
1156
|
+
type: "schedule",
|
|
1157
|
+
name: spec.name
|
|
1158
|
+
}).id,
|
|
1159
|
+
name: spec.name,
|
|
1160
|
+
changed: true
|
|
1161
|
+
};
|
|
1047
1162
|
}
|
|
1048
1163
|
findExistingSlack(channelName, connectorName) {
|
|
1049
1164
|
const existing = this.channels.getConnector(channelName, connectorName);
|
|
@@ -1077,9 +1192,10 @@ var FunnelLocalConfigSync = class {
|
|
|
1077
1192
|
}
|
|
1078
1193
|
removeExtras(channelName, touched) {
|
|
1079
1194
|
const channel = this.channels.get(channelName);
|
|
1080
|
-
if (!channel) return;
|
|
1195
|
+
if (!channel) return [];
|
|
1081
1196
|
const stale = channel.connectors.filter((c) => !touched.has(c.id));
|
|
1082
1197
|
for (const connector of stale) this.channels.removeConnector(channelName, connector.name);
|
|
1198
|
+
return stale.map((c) => c.name);
|
|
1083
1199
|
}
|
|
1084
1200
|
async resolveField(input) {
|
|
1085
1201
|
if (input.literal !== void 0 && input.envVar !== void 0) throw new Error(`${input.label} is set both as a literal and as env.${input.label.split(".").pop()}; pick one`);
|
|
@@ -1353,6 +1469,81 @@ var FunnelProfiles = class {
|
|
|
1353
1469
|
}
|
|
1354
1470
|
};
|
|
1355
1471
|
//#endregion
|
|
1472
|
+
//#region lib/engine/sessions/sessions.ts
|
|
1473
|
+
const sessionsMapSchema = z.record(z.string(), z.string());
|
|
1474
|
+
/**
|
|
1475
|
+
* Per-channel persistent Claude Code session IDs, keyed by the cwd the
|
|
1476
|
+
* channel was launched from. The whole point is to give each (channel, cwd)
|
|
1477
|
+
* its own stable conversation: relaunching from the same path picks up the
|
|
1478
|
+
* previous claude session via `--session-id <uuid>`, while a different cwd
|
|
1479
|
+
* (or a different channel) gets an independent one — so sessions never
|
|
1480
|
+
* silently bleed across workspaces the way claude's `-c` does.
|
|
1481
|
+
*
|
|
1482
|
+
* Storage lives under `<dir>/channels/<channel-id>/sessions.json` (channel
|
|
1483
|
+
* id, not name, so renames don't lose history). The file is a flat
|
|
1484
|
+
* `{ cwd: uuid }` map; the channel directory itself is created lazily.
|
|
1485
|
+
*/
|
|
1486
|
+
var FunnelSessions = class {
|
|
1487
|
+
fs;
|
|
1488
|
+
idGenerator;
|
|
1489
|
+
dir;
|
|
1490
|
+
constructor(deps) {
|
|
1491
|
+
this.fs = deps.fs;
|
|
1492
|
+
this.idGenerator = deps.idGenerator;
|
|
1493
|
+
this.dir = deps.dir;
|
|
1494
|
+
Object.freeze(this);
|
|
1495
|
+
}
|
|
1496
|
+
/** Returns the existing session id for (channelId, cwd) or generates and persists a new one. */
|
|
1497
|
+
getOrCreate(channelId, cwd) {
|
|
1498
|
+
const map = this.readMap(channelId);
|
|
1499
|
+
const existing = map[cwd];
|
|
1500
|
+
if (existing) return existing;
|
|
1501
|
+
const sessionId = this.idGenerator.generate();
|
|
1502
|
+
map[cwd] = sessionId;
|
|
1503
|
+
this.writeMap(channelId, map);
|
|
1504
|
+
return sessionId;
|
|
1505
|
+
}
|
|
1506
|
+
/** Returns the existing session id for (channelId, cwd) or null. */
|
|
1507
|
+
get(channelId, cwd) {
|
|
1508
|
+
return this.readMap(channelId)[cwd] ?? null;
|
|
1509
|
+
}
|
|
1510
|
+
/** Drops the recorded session id for (channelId, cwd). No-op if absent. */
|
|
1511
|
+
clear(channelId, cwd) {
|
|
1512
|
+
const map = this.readMap(channelId);
|
|
1513
|
+
if (!(cwd in map)) return;
|
|
1514
|
+
delete map[cwd];
|
|
1515
|
+
this.writeMap(channelId, map);
|
|
1516
|
+
}
|
|
1517
|
+
/** Drops the whole session map for the channel (e.g. when the channel is deleted). */
|
|
1518
|
+
clearAll(channelId) {
|
|
1519
|
+
const path = this.pathFor(channelId);
|
|
1520
|
+
if (this.fs.existsSync(path)) this.fs.unlink(path);
|
|
1521
|
+
}
|
|
1522
|
+
readMap(channelId) {
|
|
1523
|
+
const path = this.pathFor(channelId);
|
|
1524
|
+
if (!this.fs.existsSync(path)) return {};
|
|
1525
|
+
const raw = this.fs.readFileSync(path);
|
|
1526
|
+
try {
|
|
1527
|
+
const parsed = sessionsMapSchema.safeParse(JSON.parse(raw));
|
|
1528
|
+
return parsed.success ? parsed.data : {};
|
|
1529
|
+
} catch {
|
|
1530
|
+
return {};
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
writeMap(channelId, map) {
|
|
1534
|
+
const path = this.pathFor(channelId);
|
|
1535
|
+
const channelDir = this.channelDir(channelId);
|
|
1536
|
+
if (!this.fs.existsSync(channelDir)) this.fs.mkdirSync(channelDir, { recursive: true });
|
|
1537
|
+
this.fs.writeFileSync(path, `${JSON.stringify(map, null, 2)}\n`);
|
|
1538
|
+
}
|
|
1539
|
+
channelDir(channelId) {
|
|
1540
|
+
return join(this.dir, "channels", channelId);
|
|
1541
|
+
}
|
|
1542
|
+
pathFor(channelId) {
|
|
1543
|
+
return join(this.channelDir(channelId), "sessions.json");
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
//#endregion
|
|
1356
1547
|
//#region lib/engine/token-prompter/node-token-prompter.ts
|
|
1357
1548
|
const STAR = "*";
|
|
1358
1549
|
const CR = "\r";
|
|
@@ -3285,6 +3476,15 @@ var Funnel = class Funnel {
|
|
|
3285
3476
|
if (!this.memos.profiles) this.memos.profiles = new FunnelProfiles({ store: this.store });
|
|
3286
3477
|
return this.memos.profiles;
|
|
3287
3478
|
}
|
|
3479
|
+
/** Per-(channel, cwd) claude session-id store. Backs `--session-id` injection on launch. */
|
|
3480
|
+
get sessions() {
|
|
3481
|
+
if (!this.memos.sessions) this.memos.sessions = new FunnelSessions({
|
|
3482
|
+
fs: this.fs,
|
|
3483
|
+
idGenerator: this.idGenerator,
|
|
3484
|
+
dir: this.paths.dir
|
|
3485
|
+
});
|
|
3486
|
+
return this.memos.sessions;
|
|
3487
|
+
}
|
|
3288
3488
|
/** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
|
|
3289
3489
|
get localConfig() {
|
|
3290
3490
|
if (!this.memos.localConfig) this.memos.localConfig = new FunnelLocalConfig({ fs: this.fs });
|
|
@@ -3320,6 +3520,7 @@ var Funnel = class Funnel {
|
|
|
3320
3520
|
channels: this.channels,
|
|
3321
3521
|
mcp: this.mcp,
|
|
3322
3522
|
gateway: this.gateway,
|
|
3523
|
+
sessions: this.sessions,
|
|
3323
3524
|
fs: this.fs,
|
|
3324
3525
|
process: this.process,
|
|
3325
3526
|
logger: this.logger,
|
|
@@ -4165,7 +4366,10 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
4165
4366
|
if (local) {
|
|
4166
4367
|
const picked = query.channel !== void 0 ? local.channels.find((c) => c.name === query.channel) : local.channels[0];
|
|
4167
4368
|
if (!picked) throw new HTTPException(404, { message: query.channel ? `channel "${query.channel}" is not declared in funnel.json` : `funnel.json declares no channels` });
|
|
4168
|
-
await funnel.localConfigSync.ensure(picked, cwd);
|
|
4369
|
+
const synced = await funnel.localConfigSync.ensure(picked, cwd);
|
|
4370
|
+
for (const outcome of synced.touched) if (outcome.changed) await funnel.listeners.restart(picked.name, outcome.name);
|
|
4371
|
+
else await funnel.listeners.start(picked.name, outcome.name);
|
|
4372
|
+
for (const name of synced.removed) await funnel.listeners.stop(picked.name, name);
|
|
4169
4373
|
const exitCode = await funnel.claude.launch({
|
|
4170
4374
|
channel: picked.name,
|
|
4171
4375
|
cwd,
|
|
@@ -6851,4 +7055,4 @@ async function launchTui(funnel) {
|
|
|
6851
7055
|
});
|
|
6852
7056
|
}
|
|
6853
7057
|
//#endregion
|
|
6854
|
-
export { DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|
|
7058
|
+
export { DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSessions, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|
|
@@ -9,6 +9,24 @@ const toRecord = (value) => {
|
|
|
9
9
|
for (const [key, val] of Object.entries(value)) result[key] = val;
|
|
10
10
|
return result;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Recognises errors that @slack/web-api throws for Slack-side API failures
|
|
14
|
+
* (e.g. `cant_delete_message`, `channel_not_found`, rate limits). Every such
|
|
15
|
+
* error carries `code: "slack_webapi_*"` and a `data` field holding the raw
|
|
16
|
+
* Slack response with `ok: false`. We unwrap to that response so the caller
|
|
17
|
+
* receives a structured failure instead of having the gateway translate it
|
|
18
|
+
* into an opaque HTTP 500.
|
|
19
|
+
*/
|
|
20
|
+
const slackErrorResponse = (error) => {
|
|
21
|
+
if (!error || typeof error !== "object") return null;
|
|
22
|
+
if (!("code" in error)) return null;
|
|
23
|
+
const code = error.code;
|
|
24
|
+
if (typeof code !== "string" || !code.startsWith("slack_webapi_")) return null;
|
|
25
|
+
if (!("data" in error)) return null;
|
|
26
|
+
const data = error.data;
|
|
27
|
+
if (!data || typeof data !== "object") return null;
|
|
28
|
+
return data;
|
|
29
|
+
};
|
|
12
30
|
var FunnelSlackAdapter = class extends FunnelConnectorAdapter {
|
|
13
31
|
client;
|
|
14
32
|
constructor(deps) {
|
|
@@ -18,7 +36,13 @@ var FunnelSlackAdapter = class extends FunnelConnectorAdapter {
|
|
|
18
36
|
}
|
|
19
37
|
async call(input) {
|
|
20
38
|
const body = input.body !== null && typeof input.body === "object" ? toRecord(input.body) : {};
|
|
21
|
-
|
|
39
|
+
try {
|
|
40
|
+
return await this.client.apiCall(input.path, body);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const slackResponse = slackErrorResponse(error);
|
|
43
|
+
if (slackResponse) return slackResponse;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
22
46
|
}
|
|
23
47
|
};
|
|
24
48
|
//#endregion
|
package/funnel.schema.json
CHANGED
package/package.json
CHANGED