@interactive-inc/claude-funnel 0.10.0 → 0.15.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/README.md +106 -56
- package/dist/bin.js +557 -530
- package/dist/connectors/schedule.d.ts +2 -49
- package/dist/connectors/schedule.js +1 -1
- package/dist/connectors/slack.d.ts +4 -48
- package/dist/connectors/slack.js +1 -1
- package/dist/gateway/daemon.js +213 -211
- package/dist/index.d.ts +465 -173
- package/dist/index.js +692 -154
- package/dist/{schedule-connector-schema-CkuIQ0JQ.js → schedule-connector-schema-FxP7LPlx.js} +11 -0
- package/dist/{file-system-Co60LrmR.d.ts → schedule-listener-BPodvbld.d.ts} +56 -1
- package/dist/{slack-connector-schema-Cd22WiHB.js → slack-connector-schema-B4hsf3AY.js} +10 -1
- package/dist/slack-listener-CHj6uMY-.d.ts +74 -0
- package/package.json +2 -6
- package/schemas/funnel.schema.json +144 -0
- package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
- package/lib/bin.ts +0 -3
- package/lib/cli/factory.ts +0 -10
- package/lib/cli/index.ts +0 -85
- package/lib/cli/router/query-to-cli-args.ts +0 -20
- package/lib/cli/router/to-request.ts +0 -113
- package/lib/cli/router/validator.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
- package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
- package/lib/cli/routes/channels.$channel.publish.ts +0 -52
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
- package/lib/cli/routes/channels.$channel.ts +0 -34
- package/lib/cli/routes/channels.add.$channel.ts +0 -33
- package/lib/cli/routes/channels.remove.$channel.ts +0 -20
- package/lib/cli/routes/channels.ts +0 -39
- package/lib/cli/routes/claude.ts +0 -70
- package/lib/cli/routes/gateway.listeners.ts +0 -41
- package/lib/cli/routes/gateway.logs.ts +0 -123
- package/lib/cli/routes/gateway.restart.ts +0 -50
- package/lib/cli/routes/gateway.run.ts +0 -41
- package/lib/cli/routes/gateway.start.ts +0 -50
- package/lib/cli/routes/gateway.status.ts +0 -19
- package/lib/cli/routes/gateway.stop.ts +0 -32
- package/lib/cli/routes/gateway.ts +0 -55
- package/lib/cli/routes/index.ts +0 -219
- package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
- package/lib/cli/routes/profiles.$profile.run.ts +0 -36
- package/lib/cli/routes/profiles.add.$profile.ts +0 -49
- package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
- package/lib/cli/routes/profiles.set.$profile.ts +0 -45
- package/lib/cli/routes/profiles.ts +0 -40
- package/lib/cli/routes/status.ts +0 -93
- package/lib/cli/routes/update.ts +0 -27
- package/lib/connectors/connector-adapter.ts +0 -9
- package/lib/connectors/connector-config-schema.ts +0 -16
- package/lib/connectors/connector-factory.ts +0 -94
- package/lib/connectors/connector-listener.ts +0 -20
- package/lib/connectors/discord-adapter.ts +0 -51
- package/lib/connectors/discord-connector-schema.ts +0 -12
- package/lib/connectors/discord-event-processor.ts +0 -48
- package/lib/connectors/discord-listener.ts +0 -111
- package/lib/connectors/discord.ts +0 -4
- package/lib/connectors/gh-adapter.ts +0 -48
- package/lib/connectors/gh-connector-schema.ts +0 -12
- package/lib/connectors/gh-listener.ts +0 -137
- package/lib/connectors/gh.ts +0 -3
- package/lib/connectors/match-cron.ts +0 -78
- package/lib/connectors/schedule-connector-schema.ts +0 -33
- package/lib/connectors/schedule-listener.ts +0 -207
- package/lib/connectors/schedule-state-store.ts +0 -54
- package/lib/connectors/schedule.ts +0 -4
- package/lib/connectors/slack-adapter.ts +0 -36
- package/lib/connectors/slack-connector-schema.ts +0 -13
- package/lib/connectors/slack-event-processor.ts +0 -97
- package/lib/connectors/slack-listener.ts +0 -97
- package/lib/connectors/slack.ts +0 -4
- package/lib/engine/channels/channels.ts +0 -520
- package/lib/engine/claude/claude.ts +0 -205
- package/lib/engine/claude/gateway-controller.ts +0 -4
- package/lib/engine/fs/file-system.ts +0 -23
- package/lib/engine/fs/memory-file-system.ts +0 -102
- package/lib/engine/fs/node-file-system.ts +0 -68
- package/lib/engine/http/http-client.ts +0 -17
- package/lib/engine/http/memory-http-client.ts +0 -36
- package/lib/engine/http/node-http-client.ts +0 -23
- package/lib/engine/id/id-generator.ts +0 -7
- package/lib/engine/id/memory-id-generator.ts +0 -20
- package/lib/engine/id/node-id-generator.ts +0 -7
- package/lib/engine/logger/logger.ts +0 -11
- package/lib/engine/logger/memory-logger.ts +0 -28
- package/lib/engine/logger/node-logger.ts +0 -49
- package/lib/engine/logger/noop-logger.ts +0 -9
- package/lib/engine/mcp/channel-server.ts +0 -123
- package/lib/engine/mcp/channel-subscriber.ts +0 -82
- package/lib/engine/mcp/mcp.ts +0 -126
- package/lib/engine/mcp/read-channel-connectors.ts +0 -34
- package/lib/engine/mcp/read-gateway-token.ts +0 -16
- package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
- package/lib/engine/process/memory-process-runner.ts +0 -88
- package/lib/engine/process/node-process-runner.ts +0 -91
- package/lib/engine/process/process-runner.ts +0 -33
- package/lib/engine/profiles/profile-channel-checker.ts +0 -7
- package/lib/engine/profiles/profiles.ts +0 -126
- package/lib/engine/settings/mock-settings-reader.ts +0 -27
- package/lib/engine/settings/settings-reader.ts +0 -6
- package/lib/engine/settings/settings-schema.ts +0 -48
- package/lib/engine/settings/settings-store.ts +0 -110
- package/lib/engine/time/clock.ts +0 -15
- package/lib/engine/time/memory-clock.ts +0 -26
- package/lib/engine/time/node-clock.ts +0 -7
- package/lib/funnel.ts +0 -294
- package/lib/gateway/auth-middleware.ts +0 -44
- package/lib/gateway/broadcaster.ts +0 -319
- package/lib/gateway/channel-publisher.ts +0 -67
- package/lib/gateway/daemon.ts +0 -47
- package/lib/gateway/factory.ts +0 -10
- package/lib/gateway/funnel-event-store.ts +0 -155
- package/lib/gateway/gateway-server.ts +0 -426
- package/lib/gateway/gateway-token.ts +0 -79
- package/lib/gateway/gateway.ts +0 -209
- package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
- package/lib/gateway/listener-supervisor.ts +0 -339
- package/lib/gateway/listeners-client.ts +0 -128
- package/lib/gateway/publish-schema.ts +0 -27
- package/lib/gateway/resolve-daemon-script.ts +0 -26
- package/lib/gateway/routes/channels.connectors.call.ts +0 -39
- package/lib/gateway/routes/channels.publish.ts +0 -44
- package/lib/gateway/routes/health.ts +0 -13
- package/lib/gateway/routes/index.ts +0 -26
- package/lib/gateway/routes/listeners.list.ts +0 -6
- package/lib/gateway/routes/listeners.restart.ts +0 -15
- package/lib/gateway/routes/listeners.start.ts +0 -15
- package/lib/gateway/routes/listeners.stop.ts +0 -15
- package/lib/gateway/routes/route-deps.ts +0 -19
- package/lib/gateway/routes/status.ts +0 -15
- package/lib/gateway/routes/validator.ts +0 -17
- package/lib/index.ts +0 -67
- package/lib/logger/leuco-human-file-writer.ts +0 -65
- package/lib/logger/leuco-human-logger.ts +0 -98
- package/lib/logger/leuco-human-record.ts +0 -16
- package/lib/logger/leuco-human-stdout-writer.ts +0 -26
- package/lib/logger/leuco-human-writer.ts +0 -14
- package/lib/logger/leuco-logger-memory-sink.ts +0 -67
- package/lib/logger/leuco-logger-record.ts +0 -13
- package/lib/logger/leuco-logger-sink.ts +0 -33
- package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
- package/lib/logger/leuco-logger.ts +0 -135
- package/lib/tui/app.tsx +0 -357
- package/lib/tui/components/add-row.tsx +0 -18
- package/lib/tui/components/brand.tsx +0 -27
- package/lib/tui/components/card.tsx +0 -44
- package/lib/tui/components/detail-bar.tsx +0 -46
- package/lib/tui/components/editable-field.tsx +0 -33
- package/lib/tui/components/empty-state.tsx +0 -11
- package/lib/tui/components/gateway-status.tsx +0 -66
- package/lib/tui/components/keymap.tsx +0 -29
- package/lib/tui/components/menu-item.tsx +0 -73
- package/lib/tui/components/menu.tsx +0 -26
- package/lib/tui/components/panel-header.tsx +0 -22
- package/lib/tui/components/readonly-field.tsx +0 -18
- package/lib/tui/components/section-header.tsx +0 -25
- package/lib/tui/components/selection-accent.tsx +0 -32
- package/lib/tui/components/session-item.tsx +0 -33
- package/lib/tui/components/session-list.tsx +0 -33
- package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
- package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
- package/lib/tui/components/ui/hascii/badge.tsx +0 -51
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
- package/lib/tui/components/ui/hascii/button.tsx +0 -194
- package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/card.tsx +0 -27
- package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
- package/lib/tui/components/ui/hascii/command.tsx +0 -159
- package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
- package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
- package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
- package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
- package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
- package/lib/tui/components/ui/hascii/input.tsx +0 -130
- package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
- package/lib/tui/components/ui/hascii/progress.tsx +0 -28
- package/lib/tui/components/ui/hascii/select.tsx +0 -131
- package/lib/tui/components/ui/hascii/separator.tsx +0 -35
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
- package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
- package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
- package/lib/tui/components/ui/hascii/slider.tsx +0 -91
- package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
- package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
- package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
- package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
- package/lib/tui/components/ui/hascii/switch.tsx +0 -66
- package/lib/tui/components/ui/hascii/table.tsx +0 -95
- package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
- package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
- package/lib/tui/components/ui/hascii/tree.tsx +0 -104
- package/lib/tui/components/view-shell.tsx +0 -44
- package/lib/tui/filter-input.tsx +0 -33
- package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
- package/lib/tui/parse-comma-list.ts +0 -14
- package/lib/tui/profile-launcher.tsx +0 -61
- package/lib/tui/scrollbar-options.ts +0 -19
- package/lib/tui/sidebar.tsx +0 -50
- package/lib/tui/theme.ts +0 -40
- package/lib/tui/tui.tsx +0 -20
- package/lib/tui/types.ts +0 -38
- package/lib/tui/unique-name.ts +0 -18
- package/lib/tui/use-event-stream.ts +0 -133
- package/lib/tui/use-snapshot.ts +0 -99
- package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
- package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
- package/lib/tui/utils/hascii/theme-context.tsx +0 -26
- package/lib/tui/utils/hascii/theme.ts +0 -176
- package/lib/tui/views/channels-view.tsx +0 -108
- package/lib/tui/views/connectors-view.tsx +0 -164
- package/lib/tui/views/events-view.tsx +0 -160
- package/lib/tui/views/listeners-view.tsx +0 -80
- package/lib/tui/views/profiles-view.tsx +0 -152
package/dist/{schedule-connector-schema-CkuIQ0JQ.js → schedule-connector-schema-FxP7LPlx.js}
RENAMED
|
@@ -160,6 +160,7 @@ var FunnelScheduleListener = class extends FunnelConnectorListener {
|
|
|
160
160
|
lastFiredStore;
|
|
161
161
|
logger;
|
|
162
162
|
now;
|
|
163
|
+
onFired;
|
|
163
164
|
timer = null;
|
|
164
165
|
stopped = false;
|
|
165
166
|
constructor(deps) {
|
|
@@ -168,6 +169,7 @@ var FunnelScheduleListener = class extends FunnelConnectorListener {
|
|
|
168
169
|
this.lastFiredStore = deps.lastFiredStore;
|
|
169
170
|
this.logger = deps.logger ?? defaultLogger;
|
|
170
171
|
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
172
|
+
this.onFired = deps.onFired ?? null;
|
|
171
173
|
}
|
|
172
174
|
async start(notify) {
|
|
173
175
|
this.stopped = false;
|
|
@@ -243,6 +245,15 @@ var FunnelScheduleListener = class extends FunnelConnectorListener {
|
|
|
243
245
|
};
|
|
244
246
|
if (catchup) meta.catchup = "true";
|
|
245
247
|
await notify(entry.prompt, meta);
|
|
248
|
+
if (this.onFired) try {
|
|
249
|
+
await this.onFired(entry, firedAt);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.logger.error("schedule onFired callback failed", {
|
|
252
|
+
connector: this.config.name,
|
|
253
|
+
id: entry.id,
|
|
254
|
+
error: error instanceof Error ? error.message : String(error)
|
|
255
|
+
});
|
|
256
|
+
}
|
|
246
257
|
}
|
|
247
258
|
findMostRecentMatch(cron, from, until, entryId) {
|
|
248
259
|
const maxIterations = Math.min(MAX_CATCHUP_MINUTES, Math.floor((until.getTime() - from.getTime()) / 6e4) + 1);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as FunnelConnectorListener, r as NotifyFn, t as FunnelLogger } from "./logger-CTlXs7z4.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
//#region lib/connectors/schedule-connector-schema.d.ts
|
|
@@ -71,4 +72,58 @@ declare abstract class FunnelFileSystem {
|
|
|
71
72
|
abstract statSync(path: string): FileStat;
|
|
72
73
|
}
|
|
73
74
|
//#endregion
|
|
74
|
-
|
|
75
|
+
//#region lib/connectors/schedule-state-store.d.ts
|
|
76
|
+
type Deps$1 = {
|
|
77
|
+
path: string;
|
|
78
|
+
fs?: FunnelFileSystem;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Per-connector lastFiredAt persistence for the schedule listener. The path is
|
|
82
|
+
* passed in by FunnelConnectorFactory so this store does not know about the
|
|
83
|
+
* funnel directory layout (`channels/<id>/connectors/<id>/state.json` lives
|
|
84
|
+
* outside this class).
|
|
85
|
+
*/
|
|
86
|
+
declare class ScheduleStateStore {
|
|
87
|
+
private readonly path;
|
|
88
|
+
private readonly fs;
|
|
89
|
+
constructor(deps: Deps$1);
|
|
90
|
+
load(): Map<string, Date>;
|
|
91
|
+
save(state: Map<string, Date>): void;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region lib/connectors/schedule-listener.d.ts
|
|
95
|
+
type ScheduleOnFired = (entry: ScheduleEntry, firedAt: Date) => void | Promise<void>;
|
|
96
|
+
type Deps = {
|
|
97
|
+
config: ScheduleConnectorConfig;
|
|
98
|
+
lastFiredStore: ScheduleStateStore;
|
|
99
|
+
logger?: FunnelLogger;
|
|
100
|
+
now?: () => Date;
|
|
101
|
+
/**
|
|
102
|
+
* Invoked after a schedule entry fires successfully. Use to remove one-shot
|
|
103
|
+
* entries from the connector config, or to log per-fire side effects.
|
|
104
|
+
* Errors from this callback are caught and logged; they do not abort the tick.
|
|
105
|
+
*/
|
|
106
|
+
onFired?: ScheduleOnFired;
|
|
107
|
+
};
|
|
108
|
+
declare class FunnelScheduleListener extends FunnelConnectorListener {
|
|
109
|
+
private readonly config;
|
|
110
|
+
private readonly lastFiredStore;
|
|
111
|
+
private readonly logger;
|
|
112
|
+
private readonly now;
|
|
113
|
+
private readonly onFired;
|
|
114
|
+
private timer;
|
|
115
|
+
private stopped;
|
|
116
|
+
constructor(deps: Deps);
|
|
117
|
+
start(notify: NotifyFn): Promise<void>;
|
|
118
|
+
stop(): Promise<void>;
|
|
119
|
+
isAlive(): boolean;
|
|
120
|
+
tick(notify: NotifyFn): Promise<void>;
|
|
121
|
+
private fireEntry;
|
|
122
|
+
private notifyOne;
|
|
123
|
+
private findMostRecentMatch;
|
|
124
|
+
private findAllMatches;
|
|
125
|
+
private logInvalidCron;
|
|
126
|
+
private truncateToMinute;
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { FunnelFileSystem as a, ScheduleEntry as c, scheduleEntrySchema as d, FileStat as i, scheduleCatchupPolicySchema as l, ScheduleOnFired as n, ScheduleCatchupPolicy as o, ScheduleStateStore as r, ScheduleConnectorConfig as s, FunnelScheduleListener as t, scheduleConnectorSchema as u };
|
|
@@ -85,11 +85,15 @@ const defaultLogger = new NodeFunnelLogger();
|
|
|
85
85
|
var FunnelSlackListener = class extends FunnelConnectorListener {
|
|
86
86
|
config;
|
|
87
87
|
logger;
|
|
88
|
+
onAppCreated;
|
|
89
|
+
preprocessEvent;
|
|
88
90
|
app = null;
|
|
89
91
|
constructor(deps) {
|
|
90
92
|
super();
|
|
91
93
|
this.config = deps.config;
|
|
92
94
|
this.logger = deps.logger ?? defaultLogger;
|
|
95
|
+
this.onAppCreated = deps.onAppCreated ?? null;
|
|
96
|
+
this.preprocessEvent = deps.preprocessEvent ?? null;
|
|
93
97
|
}
|
|
94
98
|
async start(notify) {
|
|
95
99
|
const app = new App({
|
|
@@ -103,10 +107,14 @@ var FunnelSlackListener = class extends FunnelConnectorListener {
|
|
|
103
107
|
ownBotUserId: authResult.user_id ?? "",
|
|
104
108
|
ownBotId: authResult.bot_id ?? ""
|
|
105
109
|
});
|
|
110
|
+
const preprocess = this.preprocessEvent;
|
|
106
111
|
app.use(async (args) => {
|
|
107
112
|
const parsed = middlewareArgsSchema.safeParse(args);
|
|
108
113
|
if (!parsed.success || !parsed.data.event) return;
|
|
109
|
-
const
|
|
114
|
+
const rawEvent = parsed.data.event;
|
|
115
|
+
const event = preprocess ? preprocess(rawEvent) : rawEvent;
|
|
116
|
+
if (event === null) return;
|
|
117
|
+
const result = processor.process(event);
|
|
110
118
|
if (result.skip) return;
|
|
111
119
|
if (result.shouldReact) try {
|
|
112
120
|
await app.client.reactions.add({
|
|
@@ -121,6 +129,7 @@ var FunnelSlackListener = class extends FunnelConnectorListener {
|
|
|
121
129
|
app.error(async (error) => {
|
|
122
130
|
this.logger.error("Slack error", { error: error instanceof Error ? error.message : String(error) });
|
|
123
131
|
});
|
|
132
|
+
if (this.onAppCreated) await this.onAppCreated(app);
|
|
124
133
|
await app.start();
|
|
125
134
|
this.app = app;
|
|
126
135
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { n as FunnelConnectorListener, r as NotifyFn, t as FunnelLogger } from "./logger-CTlXs7z4.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { App } from "@slack/bolt";
|
|
4
|
+
|
|
5
|
+
//#region lib/connectors/slack-connector-schema.d.ts
|
|
6
|
+
declare const slackConnectorSchema: z.ZodObject<{
|
|
7
|
+
id: z.ZodString;
|
|
8
|
+
name: z.ZodString;
|
|
9
|
+
type: z.ZodLiteral<"slack">;
|
|
10
|
+
botToken: z.ZodString;
|
|
11
|
+
appToken: z.ZodString;
|
|
12
|
+
createdAt: z.ZodOptional<z.ZodString>;
|
|
13
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
type SlackConnectorConfig = z.infer<typeof slackConnectorSchema>;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region lib/connectors/slack-event-processor.d.ts
|
|
18
|
+
type SlackRawEvent = Record<string, unknown>;
|
|
19
|
+
type SlackProcessedSkip = {
|
|
20
|
+
skip: true;
|
|
21
|
+
};
|
|
22
|
+
type SlackProcessedEmit = {
|
|
23
|
+
skip: false;
|
|
24
|
+
content: string;
|
|
25
|
+
meta: Record<string, string>;
|
|
26
|
+
shouldReact: boolean;
|
|
27
|
+
channel: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
};
|
|
30
|
+
type SlackProcessed = SlackProcessedSkip | SlackProcessedEmit;
|
|
31
|
+
type Props = {
|
|
32
|
+
ownBotUserId: string;
|
|
33
|
+
ownBotId: string;
|
|
34
|
+
now?: () => number;
|
|
35
|
+
};
|
|
36
|
+
declare class FunnelSlackEventProcessor {
|
|
37
|
+
private readonly ownBotUserId;
|
|
38
|
+
private readonly ownBotId;
|
|
39
|
+
private readonly now;
|
|
40
|
+
private readonly dedup;
|
|
41
|
+
constructor(props: Props);
|
|
42
|
+
process(event: SlackRawEvent): SlackProcessed;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region lib/connectors/slack-listener.d.ts
|
|
46
|
+
type SlackOnAppCreated = (app: App) => void | Promise<void>;
|
|
47
|
+
type SlackPreprocessEvent = (event: SlackRawEvent) => SlackRawEvent | null;
|
|
48
|
+
type Deps = {
|
|
49
|
+
config: SlackConnectorConfig;
|
|
50
|
+
logger?: FunnelLogger;
|
|
51
|
+
/**
|
|
52
|
+
* Invoked after the Bolt App is constructed, before it starts.
|
|
53
|
+
* Use to attach app.action handlers, custom middleware, etc.
|
|
54
|
+
*/
|
|
55
|
+
onAppCreated?: SlackOnAppCreated;
|
|
56
|
+
/**
|
|
57
|
+
* Transform or drop the raw Slack event before the built-in processor sees it.
|
|
58
|
+
* Return null to drop the event entirely.
|
|
59
|
+
*/
|
|
60
|
+
preprocessEvent?: SlackPreprocessEvent;
|
|
61
|
+
};
|
|
62
|
+
declare class FunnelSlackListener extends FunnelConnectorListener {
|
|
63
|
+
private readonly config;
|
|
64
|
+
private readonly logger;
|
|
65
|
+
private readonly onAppCreated;
|
|
66
|
+
private readonly preprocessEvent;
|
|
67
|
+
private app;
|
|
68
|
+
constructor(deps: Deps);
|
|
69
|
+
start(notify: NotifyFn): Promise<void>;
|
|
70
|
+
stop(): Promise<void>;
|
|
71
|
+
isAlive(): boolean;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { SlackProcessed as a, SlackRawEvent as c, FunnelSlackEventProcessor as i, SlackConnectorConfig as l, SlackOnAppCreated as n, SlackProcessedEmit as o, SlackPreprocessEvent as r, SlackProcessedSkip as s, FunnelSlackListener as t, slackConnectorSchema as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interactive-inc/claude-funnel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Hub CLI that routes external events (Slack / GitHub / Discord) to Claude Code agents through subscription channels over MCP.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -27,12 +27,8 @@
|
|
|
27
27
|
"funnel": "./dist/bin.js"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
-
"lib/**/*.ts",
|
|
31
|
-
"lib/**/*.tsx",
|
|
32
|
-
"!lib/**/*.test.ts",
|
|
33
|
-
"!lib/**/*.test.tsx",
|
|
34
|
-
"!lib/**/*.bun-test.ts",
|
|
35
30
|
"dist/**/*",
|
|
31
|
+
"schemas/**/*",
|
|
36
32
|
"README.md",
|
|
37
33
|
"LICENSE"
|
|
38
34
|
],
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"$schema": {
|
|
6
|
+
"type": "string"
|
|
7
|
+
},
|
|
8
|
+
"channel": {
|
|
9
|
+
"type": "string"
|
|
10
|
+
},
|
|
11
|
+
"options": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"env": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"propertyNames": {
|
|
20
|
+
"type": "string"
|
|
21
|
+
},
|
|
22
|
+
"additionalProperties": {
|
|
23
|
+
"type": "string"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"connectors": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"oneOf": [
|
|
30
|
+
{
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"type": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"const": "slack"
|
|
36
|
+
},
|
|
37
|
+
"name": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"botToken": {
|
|
41
|
+
"type": "string"
|
|
42
|
+
},
|
|
43
|
+
"appToken": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"env": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"botToken": {
|
|
50
|
+
"type": "string"
|
|
51
|
+
},
|
|
52
|
+
"appToken": {
|
|
53
|
+
"type": "string"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"additionalProperties": false
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"required": [
|
|
60
|
+
"type",
|
|
61
|
+
"name"
|
|
62
|
+
],
|
|
63
|
+
"additionalProperties": false
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"type": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"const": "discord"
|
|
71
|
+
},
|
|
72
|
+
"name": {
|
|
73
|
+
"type": "string"
|
|
74
|
+
},
|
|
75
|
+
"botToken": {
|
|
76
|
+
"type": "string"
|
|
77
|
+
},
|
|
78
|
+
"env": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"botToken": {
|
|
82
|
+
"type": "string"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"additionalProperties": false
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"required": [
|
|
89
|
+
"type",
|
|
90
|
+
"name"
|
|
91
|
+
],
|
|
92
|
+
"additionalProperties": false
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"type": "object",
|
|
96
|
+
"properties": {
|
|
97
|
+
"type": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"const": "gh"
|
|
100
|
+
},
|
|
101
|
+
"name": {
|
|
102
|
+
"type": "string"
|
|
103
|
+
},
|
|
104
|
+
"pollInterval": {
|
|
105
|
+
"type": "integer",
|
|
106
|
+
"exclusiveMinimum": 0,
|
|
107
|
+
"maximum": 9007199254740991
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"required": [
|
|
111
|
+
"type",
|
|
112
|
+
"name"
|
|
113
|
+
],
|
|
114
|
+
"additionalProperties": false
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": {
|
|
119
|
+
"type": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"const": "schedule"
|
|
122
|
+
},
|
|
123
|
+
"name": {
|
|
124
|
+
"type": "string"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"required": [
|
|
128
|
+
"type",
|
|
129
|
+
"name"
|
|
130
|
+
],
|
|
131
|
+
"additionalProperties": false
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"required": [
|
|
138
|
+
"channel"
|
|
139
|
+
],
|
|
140
|
+
"additionalProperties": false,
|
|
141
|
+
"title": "Funnel per-repo launch config",
|
|
142
|
+
"description": "Used by `fnl claude` when no --profile / --channel is given. Declares the channel to subscribe to, optional sub-agent and brief flag, environment variables to layer under process.env, and optional connectors to materialize into ~/.funnel/settings.json on launch."
|
|
143
|
+
}
|
|
144
|
+
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
//#region lib/connectors/slack-connector-schema.d.ts
|
|
4
|
-
declare const slackConnectorSchema: z.ZodObject<{
|
|
5
|
-
id: z.ZodString;
|
|
6
|
-
name: z.ZodString;
|
|
7
|
-
type: z.ZodLiteral<"slack">;
|
|
8
|
-
botToken: z.ZodString;
|
|
9
|
-
appToken: z.ZodString;
|
|
10
|
-
createdAt: z.ZodOptional<z.ZodString>;
|
|
11
|
-
updatedAt: z.ZodOptional<z.ZodString>;
|
|
12
|
-
}, z.core.$strip>;
|
|
13
|
-
type SlackConnectorConfig = z.infer<typeof slackConnectorSchema>;
|
|
14
|
-
//#endregion
|
|
15
|
-
export { slackConnectorSchema as n, SlackConnectorConfig as t };
|
package/lib/bin.ts
DELETED
package/lib/cli/factory.ts
DELETED
package/lib/cli/index.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import pkg from "@/../package.json" with { type: "json" }
|
|
2
|
-
import { startChannelServer } from "@/engine/mcp/channel-server"
|
|
3
|
-
import { toRequest } from "@/cli/router/to-request"
|
|
4
|
-
import { launchTui } from "@/tui/tui"
|
|
5
|
-
import { createCliApp } from "@/cli/routes"
|
|
6
|
-
import { Funnel } from "@/funnel"
|
|
7
|
-
|
|
8
|
-
process.title = "funnel"
|
|
9
|
-
|
|
10
|
-
const funnel = new Funnel()
|
|
11
|
-
|
|
12
|
-
const app = createCliApp(funnel)
|
|
13
|
-
|
|
14
|
-
const HELP = `funnel — Open Claude Funnel
|
|
15
|
-
|
|
16
|
-
usage: funnel [command]
|
|
17
|
-
|
|
18
|
-
commands:
|
|
19
|
-
(none) launch TUI
|
|
20
|
-
claude launch Claude Code (default profile or --profile)
|
|
21
|
-
channels manage subscription boxes (and their nested connectors)
|
|
22
|
-
profiles manage launch profiles
|
|
23
|
-
gateway manage the gateway daemon (HTTP + WS)
|
|
24
|
-
status show overall connection status
|
|
25
|
-
update update funnel to the latest version
|
|
26
|
-
mcp run as an MCP server (invoked from .mcp.json)
|
|
27
|
-
|
|
28
|
-
options:
|
|
29
|
-
--help, -h show help
|
|
30
|
-
--version, -v show version
|
|
31
|
-
|
|
32
|
-
more: funnel <command> --help`
|
|
33
|
-
|
|
34
|
-
const args = process.argv.slice(2)
|
|
35
|
-
|
|
36
|
-
if (args.length === 0) {
|
|
37
|
-
await launchTui(funnel)
|
|
38
|
-
process.exit(0)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (args[0] === "--version" || args[0] === "-v") {
|
|
42
|
-
process.stdout.write(`${pkg.version}\n`)
|
|
43
|
-
process.exit(0)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (args[0] === "mcp") {
|
|
47
|
-
await startChannelServer({ dir: funnel.paths.dir })
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (args[0] !== "mcp") {
|
|
51
|
-
const { method, url } = toRequest(args)
|
|
52
|
-
|
|
53
|
-
const parsed = new URL(url)
|
|
54
|
-
|
|
55
|
-
const wantsHelp = parsed.searchParams.has("help")
|
|
56
|
-
|
|
57
|
-
if (wantsHelp && parsed.pathname === "/") {
|
|
58
|
-
process.stdout.write(`${HELP}\n`)
|
|
59
|
-
process.exit(0)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const res = await app.request(url, { method })
|
|
63
|
-
|
|
64
|
-
if (res.ok) {
|
|
65
|
-
const body = await res.text()
|
|
66
|
-
if (body) process.stdout.write(`${body}\n`)
|
|
67
|
-
process.exit(0)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (wantsHelp) {
|
|
71
|
-
const segments = parsed.pathname.split("/").filter(Boolean)
|
|
72
|
-
const group = segments[0]
|
|
73
|
-
const fallback = group
|
|
74
|
-
? await app.request(`http://localhost/${group}?help=true`, { method: "GET" })
|
|
75
|
-
: null
|
|
76
|
-
|
|
77
|
-
const text = fallback?.ok ? await fallback.text() : HELP
|
|
78
|
-
process.stdout.write(`${text}\n`)
|
|
79
|
-
process.exit(0)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const text = await res.text()
|
|
83
|
-
if (text) process.stderr.write(`${text}\n`)
|
|
84
|
-
process.exit(1)
|
|
85
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
const BUILTIN_SKIP = new Set(["help"])
|
|
2
|
-
|
|
3
|
-
export const queryToCliArgs = (url: string, reservedKeys: string[] = []): string[] => {
|
|
4
|
-
const skipped = new Set([...BUILTIN_SKIP, ...reservedKeys])
|
|
5
|
-
const args: string[] = []
|
|
6
|
-
const searchParams = new URL(url).searchParams
|
|
7
|
-
|
|
8
|
-
for (const entry of searchParams.entries()) {
|
|
9
|
-
const key = entry[0]
|
|
10
|
-
const value = entry[1]
|
|
11
|
-
|
|
12
|
-
if (skipped.has(key)) continue
|
|
13
|
-
|
|
14
|
-
args.push(`--${key}`)
|
|
15
|
-
|
|
16
|
-
if (value !== "true") args.push(value)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return args
|
|
20
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
const SHORT_FLAGS: Record<string, string> = {
|
|
2
|
-
h: "help",
|
|
3
|
-
n: "name",
|
|
4
|
-
p: "profile",
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
// All CLI verbs map to POST and stay in the URL (no method-stripping).
|
|
8
|
-
// Hono routes disambiguate by URL segment (e.g. /channels/add/:channel vs /channels/remove/:channel).
|
|
9
|
-
const METHOD_KEYWORDS = new Set([
|
|
10
|
-
"add",
|
|
11
|
-
"set",
|
|
12
|
-
"remove",
|
|
13
|
-
"rename",
|
|
14
|
-
"as-default",
|
|
15
|
-
"request",
|
|
16
|
-
"publish",
|
|
17
|
-
])
|
|
18
|
-
|
|
19
|
-
const API_CALL_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "options"])
|
|
20
|
-
|
|
21
|
-
const isValue = (arg: string | undefined): arg is string => {
|
|
22
|
-
return typeof arg === "string" && !arg.startsWith("-")
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const consumeApiCall = (args: string[], i: number, params: URLSearchParams): number => {
|
|
26
|
-
const nextPath = args[i + 1]
|
|
27
|
-
|
|
28
|
-
if (!isValue(nextPath)) return 1
|
|
29
|
-
|
|
30
|
-
params.set("path", nextPath)
|
|
31
|
-
|
|
32
|
-
const nextBody = args[i + 2]
|
|
33
|
-
|
|
34
|
-
if (!isValue(nextBody)) return 2
|
|
35
|
-
|
|
36
|
-
params.set("body", nextBody)
|
|
37
|
-
|
|
38
|
-
return 3
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const toRequest = (args: string[]) => {
|
|
42
|
-
const segments: string[] = []
|
|
43
|
-
const params = new URLSearchParams()
|
|
44
|
-
let method = "GET"
|
|
45
|
-
|
|
46
|
-
let i = 0
|
|
47
|
-
while (i < args.length) {
|
|
48
|
-
const arg = args[i]!
|
|
49
|
-
|
|
50
|
-
if (arg.startsWith("--")) {
|
|
51
|
-
const key = arg.slice(2)
|
|
52
|
-
const next = args[i + 1]
|
|
53
|
-
|
|
54
|
-
if (isValue(next)) {
|
|
55
|
-
params.set(key, next)
|
|
56
|
-
i += 2
|
|
57
|
-
} else {
|
|
58
|
-
params.set(key, "true")
|
|
59
|
-
i++
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (arg.startsWith("-") && arg.length === 2) {
|
|
66
|
-
const long = SHORT_FLAGS[arg[1]!]
|
|
67
|
-
|
|
68
|
-
if (!long) {
|
|
69
|
-
i++
|
|
70
|
-
continue
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const next = args[i + 1]
|
|
74
|
-
|
|
75
|
-
if (isValue(next)) {
|
|
76
|
-
params.set(long, next)
|
|
77
|
-
i += 2
|
|
78
|
-
} else {
|
|
79
|
-
params.set(long, "true")
|
|
80
|
-
i++
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
continue
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (METHOD_KEYWORDS.has(arg)) {
|
|
87
|
-
method = "POST"
|
|
88
|
-
segments.push(arg)
|
|
89
|
-
i++
|
|
90
|
-
continue
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (API_CALL_METHODS.has(arg) && !params.has("path")) {
|
|
94
|
-
segments.push(arg)
|
|
95
|
-
i += consumeApiCall(args, i, params)
|
|
96
|
-
continue
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (arg.includes("/") && !params.has("path")) {
|
|
100
|
-
params.set("path", arg)
|
|
101
|
-
i++
|
|
102
|
-
continue
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
segments.push(arg)
|
|
106
|
-
i++
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const path = segments.length > 0 ? `/${segments.join("/")}` : "/"
|
|
110
|
-
const query = params.size > 0 ? `?${params}` : ""
|
|
111
|
-
|
|
112
|
-
return { method, path, url: `http://localhost${path}${query}` }
|
|
113
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { zValidator as zv } from "@hono/zod-validator"
|
|
2
|
-
import { HTTPException } from "hono/http-exception"
|
|
3
|
-
import type { ZodType } from "zod"
|
|
4
|
-
|
|
5
|
-
export const zValidator = <Target extends "param" | "query" | "json", T extends ZodType>(
|
|
6
|
-
target: Target,
|
|
7
|
-
schema: T,
|
|
8
|
-
helpText?: string,
|
|
9
|
-
) =>
|
|
10
|
-
zv(target, schema, (result, c) => {
|
|
11
|
-
if (helpText && c.req.query("help")) {
|
|
12
|
-
return c.text(helpText)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (result.success) return
|
|
16
|
-
|
|
17
|
-
const issue = result.error.issues[0]
|
|
18
|
-
|
|
19
|
-
if (!issue) {
|
|
20
|
-
throw new HTTPException(400, { message: "invalid request" })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const path = issue.path.join(".")
|
|
24
|
-
const message = path ? `${path}: ${issue.message}` : issue.message
|
|
25
|
-
|
|
26
|
-
throw new HTTPException(400, { message })
|
|
27
|
-
})
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { z } from "zod"
|
|
2
|
-
import { factory } from "@/cli/factory"
|
|
3
|
-
import { zValidator } from "@/cli/router/validator"
|
|
4
|
-
|
|
5
|
-
export const renameHelp = `funnel channels <channel> connectors rename <connector> <new-name>
|
|
6
|
-
|
|
7
|
-
usage: funnel channels <channel> connectors rename <connector> <new-name>`
|
|
8
|
-
|
|
9
|
-
export const channelsConnectorsRenameHandler = factory.createHandlers(
|
|
10
|
-
zValidator(
|
|
11
|
-
"param",
|
|
12
|
-
z.object({ channel: z.string(), connector: z.string(), newName: z.string() }),
|
|
13
|
-
),
|
|
14
|
-
zValidator("query", z.object({}), renameHelp),
|
|
15
|
-
async (c) => {
|
|
16
|
-
const param = c.req.valid("param")
|
|
17
|
-
const funnel = c.var.funnel
|
|
18
|
-
|
|
19
|
-
await funnel.listeners.stop(param.channel, param.connector)
|
|
20
|
-
|
|
21
|
-
funnel.channels.renameConnector(param.channel, param.connector, param.newName)
|
|
22
|
-
|
|
23
|
-
await funnel.listeners.start(param.channel, param.newName)
|
|
24
|
-
|
|
25
|
-
return c.text(`renamed connector "${param.connector}" to "${param.newName}"`)
|
|
26
|
-
},
|
|
27
|
-
)
|