@interactive-inc/claude-funnel 0.19.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 +60 -7
- package/dist/index.js +130 -5
- 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.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ type SlackListenerOptions = {
|
|
|
63
63
|
type ScheduleListenerOptions = {
|
|
64
64
|
onFired?: ScheduleOnFired;
|
|
65
65
|
};
|
|
66
|
-
type Deps$
|
|
66
|
+
type Deps$16 = {
|
|
67
67
|
fs?: FunnelFileSystem;
|
|
68
68
|
process?: FunnelProcessRunner;
|
|
69
69
|
logger?: FunnelLogger;
|
|
@@ -90,7 +90,7 @@ declare class FunnelConnectorFactory {
|
|
|
90
90
|
private readonly dir;
|
|
91
91
|
private readonly slackListenerOptions;
|
|
92
92
|
private readonly scheduleListenerOptions;
|
|
93
|
-
constructor(deps?: Deps$
|
|
93
|
+
constructor(deps?: Deps$16);
|
|
94
94
|
createListener(channelId: string, config: ConnectorConfig): FunnelConnectorListener;
|
|
95
95
|
createAdapter(config: ConnectorConfig): FunnelConnectorAdapter | null;
|
|
96
96
|
connectorDir(channelId: string, connectorId: string): string;
|
|
@@ -152,6 +152,7 @@ declare const channelConfigSchema: z.ZodObject<{
|
|
|
152
152
|
}>>;
|
|
153
153
|
options: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
154
154
|
env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
155
|
+
resume: z.ZodDefault<z.ZodBoolean>;
|
|
155
156
|
connectors: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
156
157
|
id: z.ZodString;
|
|
157
158
|
name: z.ZodString;
|
|
@@ -212,6 +213,7 @@ declare const settingsSchema: z.ZodObject<{
|
|
|
212
213
|
}>>;
|
|
213
214
|
options: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
214
215
|
env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
216
|
+
resume: z.ZodDefault<z.ZodBoolean>;
|
|
215
217
|
connectors: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
216
218
|
id: z.ZodString;
|
|
217
219
|
name: z.ZodString;
|
|
@@ -268,7 +270,7 @@ declare abstract class FunnelSettingsReader {
|
|
|
268
270
|
}
|
|
269
271
|
//#endregion
|
|
270
272
|
//#region lib/engine/channels/channels.d.ts
|
|
271
|
-
type Deps$
|
|
273
|
+
type Deps$15 = {
|
|
272
274
|
store: FunnelSettingsReader;
|
|
273
275
|
factory: FunnelConnectorFactory;
|
|
274
276
|
profileChecker: ProfileChannelChecker;
|
|
@@ -311,7 +313,7 @@ declare class FunnelChannels {
|
|
|
311
313
|
private readonly profileChecker;
|
|
312
314
|
private readonly clock;
|
|
313
315
|
private readonly idGenerator;
|
|
314
|
-
constructor(deps: Deps$
|
|
316
|
+
constructor(deps: Deps$15);
|
|
315
317
|
list(): ChannelConfig[];
|
|
316
318
|
get(name: string): ChannelConfig | null;
|
|
317
319
|
getById(id: string): ChannelConfig | null;
|
|
@@ -320,8 +322,10 @@ declare class FunnelChannels {
|
|
|
320
322
|
delivery?: ChannelDeliveryMode;
|
|
321
323
|
options?: string[];
|
|
322
324
|
env?: Record<string, string>;
|
|
325
|
+
resume?: boolean;
|
|
323
326
|
}): ChannelConfig;
|
|
324
327
|
setDelivery(name: string, delivery: ChannelDeliveryMode): void;
|
|
328
|
+
setResume(name: string, resume: boolean): void;
|
|
325
329
|
setOptions(name: string, options: string[]): void;
|
|
326
330
|
setEnv(name: string, env: Record<string, string>): void;
|
|
327
331
|
remove(name: string): void;
|
|
@@ -373,7 +377,7 @@ type GatewayController = {
|
|
|
373
377
|
//#region lib/engine/mcp/mcp.d.ts
|
|
374
378
|
declare const FUNNEL_MCP_COMMAND = "funnel";
|
|
375
379
|
declare const FUNNEL_MCP_NAME = "funnel";
|
|
376
|
-
type Deps$
|
|
380
|
+
type Deps$14 = {
|
|
377
381
|
fs?: FunnelFileSystem;
|
|
378
382
|
};
|
|
379
383
|
/**
|
|
@@ -383,7 +387,7 @@ type Deps$13 = {
|
|
|
383
387
|
*/
|
|
384
388
|
declare class FunnelMcp {
|
|
385
389
|
private readonly fs;
|
|
386
|
-
constructor(deps?: Deps$
|
|
390
|
+
constructor(deps?: Deps$14);
|
|
387
391
|
install(repoPath: string): void;
|
|
388
392
|
uninstall(repoPath: string): void;
|
|
389
393
|
findInstalledName(cwd: string): string | null;
|
|
@@ -392,6 +396,43 @@ declare class FunnelMcp {
|
|
|
392
396
|
private writeConfig;
|
|
393
397
|
}
|
|
394
398
|
//#endregion
|
|
399
|
+
//#region lib/engine/sessions/sessions.d.ts
|
|
400
|
+
type Deps$13 = {
|
|
401
|
+
fs: FunnelFileSystem;
|
|
402
|
+
idGenerator: FunnelIdGenerator;
|
|
403
|
+
dir: string;
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* Per-channel persistent Claude Code session IDs, keyed by the cwd the
|
|
407
|
+
* channel was launched from. The whole point is to give each (channel, cwd)
|
|
408
|
+
* its own stable conversation: relaunching from the same path picks up the
|
|
409
|
+
* previous claude session via `--session-id <uuid>`, while a different cwd
|
|
410
|
+
* (or a different channel) gets an independent one — so sessions never
|
|
411
|
+
* silently bleed across workspaces the way claude's `-c` does.
|
|
412
|
+
*
|
|
413
|
+
* Storage lives under `<dir>/channels/<channel-id>/sessions.json` (channel
|
|
414
|
+
* id, not name, so renames don't lose history). The file is a flat
|
|
415
|
+
* `{ cwd: uuid }` map; the channel directory itself is created lazily.
|
|
416
|
+
*/
|
|
417
|
+
declare class FunnelSessions {
|
|
418
|
+
private readonly fs;
|
|
419
|
+
private readonly idGenerator;
|
|
420
|
+
private readonly dir;
|
|
421
|
+
constructor(deps: Deps$13);
|
|
422
|
+
/** Returns the existing session id for (channelId, cwd) or generates and persists a new one. */
|
|
423
|
+
getOrCreate(channelId: string, cwd: string): string;
|
|
424
|
+
/** Returns the existing session id for (channelId, cwd) or null. */
|
|
425
|
+
get(channelId: string, cwd: string): string | null;
|
|
426
|
+
/** Drops the recorded session id for (channelId, cwd). No-op if absent. */
|
|
427
|
+
clear(channelId: string, cwd: string): void;
|
|
428
|
+
/** Drops the whole session map for the channel (e.g. when the channel is deleted). */
|
|
429
|
+
clearAll(channelId: string): void;
|
|
430
|
+
private readMap;
|
|
431
|
+
private writeMap;
|
|
432
|
+
private channelDir;
|
|
433
|
+
private pathFor;
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
395
436
|
//#region lib/engine/claude/claude.d.ts
|
|
396
437
|
type LaunchOptions = {
|
|
397
438
|
channel: string;
|
|
@@ -411,6 +452,7 @@ type Deps$12 = {
|
|
|
411
452
|
channels: FunnelChannels;
|
|
412
453
|
mcp: FunnelMcp;
|
|
413
454
|
gateway: GatewayController;
|
|
455
|
+
sessions: FunnelSessions;
|
|
414
456
|
process?: FunnelProcessRunner;
|
|
415
457
|
fs?: FunnelFileSystem;
|
|
416
458
|
logger?: FunnelLogger;
|
|
@@ -426,6 +468,7 @@ declare class FunnelClaude {
|
|
|
426
468
|
private readonly channels;
|
|
427
469
|
private readonly mcp;
|
|
428
470
|
private readonly gateway;
|
|
471
|
+
private readonly sessions;
|
|
429
472
|
private readonly process;
|
|
430
473
|
private readonly fs;
|
|
431
474
|
private readonly logger;
|
|
@@ -440,6 +483,12 @@ declare class FunnelClaude {
|
|
|
440
483
|
private installCleanup;
|
|
441
484
|
private isProcessAlive;
|
|
442
485
|
private buildArgs;
|
|
486
|
+
/**
|
|
487
|
+
* Decides whether funnel should inject `--session-id`. We back off when
|
|
488
|
+
* the user already passed a session-shaping flag, since combining them
|
|
489
|
+
* would either confuse claude or override the explicit user intent.
|
|
490
|
+
*/
|
|
491
|
+
private resolveSessionId;
|
|
443
492
|
private buildEnv;
|
|
444
493
|
}
|
|
445
494
|
//#endregion
|
|
@@ -489,6 +538,7 @@ declare const channelSpecSchema: z.ZodObject<{
|
|
|
489
538
|
name: z.ZodString;
|
|
490
539
|
options: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
491
540
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
541
|
+
resume: z.ZodOptional<z.ZodBoolean>;
|
|
492
542
|
connectors: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
493
543
|
type: z.ZodLiteral<"slack">;
|
|
494
544
|
name: z.ZodString;
|
|
@@ -521,6 +571,7 @@ declare const localConfigSchema: z.ZodObject<{
|
|
|
521
571
|
name: z.ZodString;
|
|
522
572
|
options: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
523
573
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
574
|
+
resume: z.ZodOptional<z.ZodBoolean>;
|
|
524
575
|
connectors: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
525
576
|
type: z.ZodLiteral<"slack">;
|
|
526
577
|
name: z.ZodString;
|
|
@@ -1326,6 +1377,8 @@ declare class Funnel {
|
|
|
1326
1377
|
get channels(): FunnelChannels;
|
|
1327
1378
|
/** Launch profiles (named presets for `fnl claude`: path + sub-agent + channel id). */
|
|
1328
1379
|
get profiles(): FunnelProfiles;
|
|
1380
|
+
/** Per-(channel, cwd) claude session-id store. Backs `--session-id` injection on launch. */
|
|
1381
|
+
get sessions(): FunnelSessions;
|
|
1329
1382
|
/** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
|
|
1330
1383
|
get localConfig(): FunnelLocalConfig;
|
|
1331
1384
|
/** Parses `.env.local` from a cwd (used by sync to back $VAR references). */
|
|
@@ -4210,4 +4263,4 @@ ${string}`;
|
|
|
4210
4263
|
//#region lib/tui/tui.d.ts
|
|
4211
4264
|
declare function launchTui(funnel: Funnel): Promise<void>;
|
|
4212
4265
|
//#endregion
|
|
4213
|
-
export { AttachOptions, BroadcastEvent, BroadcastSubscriber, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, ConnectorConfig, ConnectorSpec, ConnectorSyncOutcome, ConnectorType, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEvent, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, LaunchOptions, ListListenersResult, ListenerEntry, ListenerOpResult, LocalConfig, LocalConfigSyncResult, LogEntry, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, NotifyFn, ProfileConfig, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, 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 };
|
|
4266
|
+
export { AttachOptions, BroadcastEvent, BroadcastSubscriber, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, ConnectorConfig, ConnectorSpec, ConnectorSyncOutcome, ConnectorType, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEvent, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSessions, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, LaunchOptions, ListListenersResult, ListenerEntry, ListenerOpResult, LocalConfig, LocalConfigSyncResult, LogEntry, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, NotifyFn, ProfileConfig, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, 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 };
|
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;
|
|
@@ -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({
|
|
@@ -936,16 +973,19 @@ var FunnelLocalConfigSync = class {
|
|
|
936
973
|
}
|
|
937
974
|
async ensure(channel, cwd) {
|
|
938
975
|
const existing = this.channels.get(channel.name);
|
|
976
|
+
const nextResume = channel.resume ?? true;
|
|
939
977
|
if (!existing) this.channels.add({
|
|
940
978
|
name: channel.name,
|
|
941
979
|
options: channel.options ?? [],
|
|
942
|
-
env: channel.env ?? {}
|
|
980
|
+
env: channel.env ?? {},
|
|
981
|
+
resume: nextResume
|
|
943
982
|
});
|
|
944
983
|
else {
|
|
945
984
|
const nextOptions = channel.options ?? [];
|
|
946
985
|
const nextEnv = channel.env ?? {};
|
|
947
986
|
if (!arraysEqual(existing.options, nextOptions)) this.channels.setOptions(channel.name, nextOptions);
|
|
948
987
|
if (!recordsEqual(existing.env, nextEnv)) this.channels.setEnv(channel.name, nextEnv);
|
|
988
|
+
if (existing.resume !== nextResume) this.channels.setResume(channel.name, nextResume);
|
|
949
989
|
}
|
|
950
990
|
if (channel.connectors === void 0) return {
|
|
951
991
|
touched: [],
|
|
@@ -1429,6 +1469,81 @@ var FunnelProfiles = class {
|
|
|
1429
1469
|
}
|
|
1430
1470
|
};
|
|
1431
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
|
|
1432
1547
|
//#region lib/engine/token-prompter/node-token-prompter.ts
|
|
1433
1548
|
const STAR = "*";
|
|
1434
1549
|
const CR = "\r";
|
|
@@ -3361,6 +3476,15 @@ var Funnel = class Funnel {
|
|
|
3361
3476
|
if (!this.memos.profiles) this.memos.profiles = new FunnelProfiles({ store: this.store });
|
|
3362
3477
|
return this.memos.profiles;
|
|
3363
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
|
+
}
|
|
3364
3488
|
/** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
|
|
3365
3489
|
get localConfig() {
|
|
3366
3490
|
if (!this.memos.localConfig) this.memos.localConfig = new FunnelLocalConfig({ fs: this.fs });
|
|
@@ -3396,6 +3520,7 @@ var Funnel = class Funnel {
|
|
|
3396
3520
|
channels: this.channels,
|
|
3397
3521
|
mcp: this.mcp,
|
|
3398
3522
|
gateway: this.gateway,
|
|
3523
|
+
sessions: this.sessions,
|
|
3399
3524
|
fs: this.fs,
|
|
3400
3525
|
process: this.process,
|
|
3401
3526
|
logger: this.logger,
|
|
@@ -6930,4 +7055,4 @@ async function launchTui(funnel) {
|
|
|
6930
7055
|
});
|
|
6931
7056
|
}
|
|
6932
7057
|
//#endregion
|
|
6933
|
-
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