From 2d8f1ed722ed6378b7f316e021fa9b3c1cf22e24 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:28:53 +0000 Subject: [PATCH 01/23] channels: core package (config, secrets, allowlist, dispatch, renderer) New workspace package @redrob-code/channels-core: the transport contract, channels.json load/save, a 0600 file secret store behind SecretStore, a deny-by-default allowlist, a conversation->session dispatcher persisted in channels/sessions.json, a markdown-to-text renderer with chunking, and the ChannelManager that starts enabled transports and attaches each to the daemon as channel:. --- package.json | 1 + packages/channels/core/package.json | 25 +++ packages/channels/core/src/allowlist.ts | 46 +++++ packages/channels/core/src/config.ts | 113 ++++++++++ packages/channels/core/src/dispatch.ts | 257 +++++++++++++++++++++++ packages/channels/core/src/index.ts | 263 ++++++++++++++++++++++++ packages/channels/core/src/paths.ts | 49 +++++ packages/channels/core/src/renderer.ts | 112 ++++++++++ packages/channels/core/src/secrets.ts | 112 ++++++++++ packages/channels/core/src/transport.ts | 70 +++++++ packages/channels/core/tsconfig.json | 8 + 11 files changed, 1056 insertions(+) create mode 100644 packages/channels/core/package.json create mode 100644 packages/channels/core/src/allowlist.ts create mode 100644 packages/channels/core/src/config.ts create mode 100644 packages/channels/core/src/dispatch.ts create mode 100644 packages/channels/core/src/index.ts create mode 100644 packages/channels/core/src/paths.ts create mode 100644 packages/channels/core/src/renderer.ts create mode 100644 packages/channels/core/src/secrets.ts create mode 100644 packages/channels/core/src/transport.ts create mode 100644 packages/channels/core/tsconfig.json diff --git a/package.json b/package.json index 3a48ec46b4..5029ab96f2 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "workspaces": { "packages": [ "packages/*", + "packages/channels/*", "packages/sdk/js", "packages/slack" ], diff --git a/packages/channels/core/package.json b/packages/channels/core/package.json new file mode 100644 index 0000000000..1669fe088e --- /dev/null +++ b/packages/channels/core/package.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@redrob-code/channels-core", + "version": "0.0.0", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + ".": "./src/index.ts", + "./*": "./src/*.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@redrob-labs/sdk": "workspace:*" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/channels/core/src/allowlist.ts b/packages/channels/core/src/allowlist.ts new file mode 100644 index 0000000000..d8e08b684e --- /dev/null +++ b/packages/channels/core/src/allowlist.ts @@ -0,0 +1,46 @@ +/** + * Who may talk to the daemon through a channel: an explicit list of sender IDs per channel. + * + * Deny by default. An empty list lets nobody in, so enabling a channel without allowing anyone is + * safe: the bot connects, answers "not allowed" once per denied sender per start, and logs it. + */ +import type { ChannelConfig } from "./config" +import type { Inbound, Logger } from "./transport" + +export const DENIED_REPLY = "You are not allowed to use this bot." + +export class Allowlist { + private readonly ids: Set + private readonly warned = new Set() + + constructor(ids: readonly string[]) { + this.ids = new Set(ids.map((id) => id.trim()).filter((id) => id.length > 0)) + } + + static fromConfig(config: ChannelConfig): Allowlist { + return new Allowlist(config.allowlist) + } + + allows(senderId: string): boolean { + return this.ids.has(senderId.trim()) + } + + get size(): number { + return this.ids.size + } + + /** + * Decide about one message. Returns the reply to send when the sender is denied (only the + * first time per sender, so a chatty stranger does not get a reply per line), or undefined when + * the message may proceed. + */ + check(message: Inbound, log: Logger): { allowed: true } | { allowed: false; reply: string | undefined } { + if (this.allows(message.senderId)) return { allowed: true } + const first = !this.warned.has(message.senderId) + this.warned.add(message.senderId) + log.warn( + `denied ${message.channel} sender ${message.senderId}${message.senderName ? ` (${message.senderName})` : ""} in ${message.conversationId}`, + ) + return { allowed: false, reply: first ? DENIED_REPLY : undefined } + } +} diff --git a/packages/channels/core/src/config.ts b/packages/channels/core/src/config.ts new file mode 100644 index 0000000000..8f0d674489 --- /dev/null +++ b/packages/channels/core/src/config.ts @@ -0,0 +1,113 @@ +/** + * `$HOME/.redrob/channels.json`: which chat channels the daemon serves and who may talk to it. + * + * Non-secret fields only. Tokens live in the secret store (`secrets.ts`) so this file can be read, + * diffed and backed up without leaking anything. + * + * ```json + * { + * "version": 1, + * "directory": "/path/the/sessions/work/in", + * "channels": { + * "slack": { "enabled": true, "allowlist": ["U0123ABC"] }, + * "telegram": { "enabled": false, "allowlist": [] } + * } + * } + * ``` + */ +import * as Paths from "./paths" + +export const CHANNEL_NAMES = ["slack", "telegram", "discord", "whatsapp"] as const +export type ChannelName = (typeof CHANNEL_NAMES)[number] + +export function isChannelName(value: string): value is ChannelName { + return (CHANNEL_NAMES as readonly string[]).includes(value) +} + +export type ChannelConfig = { + readonly enabled: boolean + /** Sender IDs allowed to talk to the daemon. Empty means nobody. */ + readonly allowlist: readonly string[] + /** Project directory the channel's sessions work in; falls back to the file-level `directory`. */ + readonly directory?: string + /** Longest reply chunk this channel accepts; the renderer splits at this many characters. */ + readonly maxChunk?: number + /** Transport-specific, non-secret settings (Stage 2 transports read their own keys). */ + readonly options?: Readonly> +} + +export type ChannelsConfig = { + readonly version: 1 + /** Default project directory for every channel's sessions. Default: the user's home. */ + readonly directory?: string + readonly channels: Partial>> +} + +export const EMPTY: ChannelsConfig = { version: 1, channels: {} } + +export function defaultChannel(): ChannelConfig { + return { enabled: false, allowlist: [] } +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function channel(value: unknown): ChannelConfig | undefined { + if (!isObject(value)) return undefined + const allowlist = Array.isArray(value.allowlist) + ? value.allowlist.filter((item): item is string => typeof item === "string") + : [] + return { + enabled: value.enabled === true, + allowlist, + ...(typeof value.directory === "string" ? { directory: value.directory } : {}), + ...(typeof value.maxChunk === "number" && value.maxChunk > 0 ? { maxChunk: value.maxChunk } : {}), + ...(isObject(value.options) ? { options: value.options } : {}), + } +} + +/** Coerce anything into a config; unknown channels and malformed entries are dropped. */ +export function parse(value: unknown): ChannelsConfig { + if (!isObject(value)) return EMPTY + const channels: Partial> = {} + if (isObject(value.channels)) { + for (const [name, entry] of Object.entries(value.channels)) { + if (!isChannelName(name)) continue + const parsed = channel(entry) + if (parsed) channels[name] = parsed + } + } + return { + version: 1, + ...(typeof value.directory === "string" ? { directory: value.directory } : {}), + channels, + } +} + +export async function load(env: NodeJS.ProcessEnv = process.env): Promise { + return parse(await Paths.readJson(Paths.configFile(env))) +} + +export async function save(config: ChannelsConfig, env: NodeJS.ProcessEnv = process.env): Promise { + await Paths.writeJson(Paths.configFile(env), parse(config), 0o644) +} + +/** Read, apply, write; returns the saved config. */ +export async function update( + fn: (current: ChannelsConfig) => ChannelsConfig, + env: NodeJS.ProcessEnv = process.env, +): Promise { + const next = parse(fn(await load(env))) + await save(next, env) + return next +} + +export function setChannel(config: ChannelsConfig, name: ChannelName, patch: Partial): ChannelsConfig { + const current = config.channels[name] ?? defaultChannel() + return { ...config, channels: { ...config.channels, [name]: { ...current, ...patch } } } +} + +export function enabledChannels(config: ChannelsConfig): ChannelName[] { + return CHANNEL_NAMES.filter((name) => config.channels[name]?.enabled === true) +} diff --git a/packages/channels/core/src/dispatch.ts b/packages/channels/core/src/dispatch.ts new file mode 100644 index 0000000000..bdbc0da305 --- /dev/null +++ b/packages/channels/core/src/dispatch.ts @@ -0,0 +1,257 @@ +/** + * From an inbound chat message to an engine session and back. + * + * One conversation is one session. The map `(channel, conversationId) -> sessionID` is persisted + * in `$HOME/.redrob/channels/sessions.json` so a daemon restart continues the same sessions. + * Messages in one conversation are handled one at a time, in order; conversations run in parallel. + * + * Commands: `/new` drops the mapping and starts a fresh session on the next message; `/status` + * reports the session the conversation is bound to. Anything else is sent as a prompt. + */ +import { createRedrobClient, type RedrobClient } from "@redrob-labs/sdk" +import type { ChannelConfig } from "./config" +import * as Paths from "./paths" +import { DEFAULT_MAX_CHUNK, render } from "./renderer" +import type { DaemonConnection, Inbound, Logger, Transport } from "./transport" + +export type Binding = { + readonly sessionID: string + readonly directory: string + readonly createdAt: number + /** Last time a message went through; informational. */ + readonly lastUsedAt: number +} + +type Shape = { version: 1; bindings: Record } + +export function conversationKey(channel: string, conversationId: string): string { + return `${channel}:${conversationId}` +} + +function isBinding(value: unknown): value is Binding { + if (typeof value !== "object" || value === null) return false + const v = value as Record + return ( + typeof v.sessionID === "string" && + typeof v.directory === "string" && + typeof v.createdAt === "number" && + typeof v.lastUsedAt === "number" + ) +} + +/** The persisted conversation -> session map. One instance per daemon; writes are serialised. */ +export class SessionMap { + private bindings: Record = {} + private loaded = false + private writing: Promise = Promise.resolve() + + constructor(private readonly file: string = Paths.sessionsFile()) {} + + private async load(): Promise { + if (this.loaded) return + const value = (await Paths.readJson(this.file)) as Partial | undefined + if (value && typeof value.bindings === "object" && value.bindings !== null) { + for (const [key, binding] of Object.entries(value.bindings)) { + if (isBinding(binding)) this.bindings[key] = binding + } + } + this.loaded = true + } + + private flush(): Promise { + const snapshot: Shape = { version: 1, bindings: { ...this.bindings } } + this.writing = this.writing.then(() => Paths.writeJson(this.file, snapshot, 0o600)).catch(() => undefined) + return this.writing + } + + async get(key: string): Promise { + await this.load() + return this.bindings[key] + } + + async set(key: string, binding: Binding): Promise { + await this.load() + this.bindings[key] = binding + await this.flush() + } + + async delete(key: string): Promise { + await this.load() + const had = key in this.bindings + delete this.bindings[key] + if (had) await this.flush() + return had + } + + async touch(key: string): Promise { + await this.load() + const current = this.bindings[key] + if (!current) return + this.bindings[key] = { ...current, lastUsedAt: Date.now() } + await this.flush() + } +} + +export type DispatcherOptions = { + readonly transport: Transport + readonly config: ChannelConfig + readonly daemon: DaemonConnection + readonly sessions: SessionMap + readonly log: Logger + /** Project directory for new sessions. */ + readonly directory: string + readonly fetch?: typeof fetch +} + +export const HELP = [ + "Commands:", + "/new start a fresh session for this conversation", + "/status show which session this conversation is bound to", + "/help this text", +].join("\n") + +type Part = { type: string; text?: string } + +export class Dispatcher { + private readonly queues = new Map>() + private readonly clients = new Map() + + constructor(private readonly o: DispatcherOptions) {} + + private client(directory: string): RedrobClient { + let client = this.clients.get(directory) + if (!client) { + client = createRedrobClient({ + baseUrl: this.o.daemon.url, + headers: { ...this.o.daemon.headers }, + directory, + ...(this.o.fetch ? { fetch: this.o.fetch } : {}), + }) + this.clients.set(directory, client) + } + return client + } + + private get limit(): number { + return this.o.config.maxChunk ?? this.o.transport.maxChunk ?? DEFAULT_MAX_CHUNK + } + + /** Queue a message behind the others in its conversation. Never rejects; errors are replied. */ + handle(message: Inbound): Promise { + const key = conversationKey(message.channel, message.conversationId) + const previous = this.queues.get(key) ?? Promise.resolve() + const next = previous + .then(() => this.process(key, message)) + .catch((error: unknown) => { + this.o.log.error(`${key}: ${error instanceof Error ? error.message : String(error)}`) + return this.reply(message, `Something went wrong: ${error instanceof Error ? error.message : String(error)}`) + }) + .finally(() => { + if (this.queues.get(key) === next) this.queues.delete(key) + }) + this.queues.set(key, next) + return next + } + + private async reply(message: Inbound, markdown: string): Promise { + const chunks = render(markdown, this.limit) + if (chunks.length === 0) return + await this.o.transport.send(message.conversationId, chunks, message.replyTo) + } + + private async process(key: string, message: Inbound): Promise { + const text = message.text.trim() + if (text.length === 0) return + const command = /^\/(\w+)\b/.exec(text)?.[1]?.toLowerCase() + switch (command) { + case "new": { + await this.o.sessions.delete(key) + const binding = await this.create(key, message) + return this.reply(message, `Started a new session (${binding.sessionID}).`) + } + case "status": { + const binding = await this.o.sessions.get(key) + if (!binding) return this.reply(message, "No session yet; the next message starts one.") + return this.reply( + message, + [ + `Session: ${binding.sessionID}`, + `Directory: ${binding.directory}`, + `Started: ${new Date(binding.createdAt).toISOString()}`, + `Last used: ${new Date(binding.lastUsedAt).toISOString()}`, + ].join("\n"), + ) + } + case "help": + return this.reply(message, HELP) + default: + return this.prompt(key, message, text) + } + } + + private async create(key: string, message: Inbound): Promise { + const directory = this.o.directory + const client = this.client(directory) + const title = `${message.channel} ${message.conversationId}`.slice(0, 120) + const result = await client.session.create({ body: { title } }) + if (result.error || !result.data) { + throw new Error(`The engine refused to create a session: ${describe(result.error)}`) + } + const now = Date.now() + const binding: Binding = { sessionID: result.data.id, directory, createdAt: now, lastUsedAt: now } + await this.o.sessions.set(key, binding) + this.o.log.info(`${key} -> session ${binding.sessionID}`) + return binding + } + + private async prompt(key: string, message: Inbound, text: string, retried = false): Promise { + const binding = (await this.o.sessions.get(key)) ?? (await this.create(key, message)) + const client = this.client(binding.directory) + await this.o.transport.typing?.(message.conversationId).catch(() => undefined) + const result = await client.session.prompt({ + path: { id: binding.sessionID }, + body: { parts: [{ type: "text", text }] }, + }) + if (result.error || !result.data) { + // The session is gone (the engine's store was cleared, or another version rewrote it): bind + // the conversation to a fresh one and send the message there, once. + if (!retried && status(result.error) === 404) { + this.o.log.warn(`${key}: session ${binding.sessionID} is gone; starting a new one`) + await this.o.sessions.delete(key) + return this.prompt(key, message, text, true) + } + throw new Error(`The engine refused the prompt: ${describe(result.error)}`) + } + await this.o.sessions.touch(key) + const parts = (result.data.parts ?? []) as Part[] + const answer = parts + .filter((p) => p.type === "text" && typeof p.text === "string") + .map((p) => p.text as string) + .join("\n") + .trim() + const failure = (result.data.info as { error?: { data?: { message?: string } } } | undefined)?.error + if (answer.length === 0 && failure) { + return this.reply(message, `The engine reported an error: ${failure.data?.message ?? "unknown"}`) + } + return this.reply(message, answer.length > 0 ? answer : "(no reply)") + } +} + +function status(error: unknown): number | undefined { + if (typeof error !== "object" || error === null) return undefined + const e = error as { status?: unknown; response?: { status?: unknown } } + if (typeof e.status === "number") return e.status + if (typeof e.response?.status === "number") return e.response.status + return undefined +} + +function describe(error: unknown): string { + if (error instanceof Error) return error.message + if (typeof error === "object" && error !== null) { + const e = error as { data?: { message?: unknown }; message?: unknown; name?: unknown } + if (typeof e.data?.message === "string") return e.data.message + if (typeof e.message === "string") return e.message + if (typeof e.name === "string") return e.name + } + return String(error ?? "unknown error") +} diff --git a/packages/channels/core/src/index.ts b/packages/channels/core/src/index.ts new file mode 100644 index 0000000000..d95b32a3dd --- /dev/null +++ b/packages/channels/core/src/index.ts @@ -0,0 +1,263 @@ +/** + * @redrob-code/channels-core: run chat transports against the shared daemon. + * + * `startEnabledChannels(factories)` reads `channels.json`, starts a transport for every enabled + * channel that has a factory, attaches each to the daemon as `channel:` so it is counted as + * a client, and wires inbound messages through the allowlist to the dispatcher. `reload()` + * re-reads the file and starts, stops or restarts transports to match. `stopAll()` tears down. + * + * Plain promises, no Effect: this runs inside `redrob serve --register` next to the server, and + * must also be driveable from a test or a one-off script. + */ +import os from "node:os" +import { attach as attachToDaemon, currentDaemon, type Attachment } from "@redrob-labs/sdk/daemon" +import { Allowlist } from "./allowlist" +import * as Config from "./config" +import { Dispatcher, SessionMap } from "./dispatch" +import { FileSecretStore, scoped, type SecretStore } from "./secrets" +import { consoleLogger, type DaemonConnection, type Inbound, type Logger, type Transport, type TransportFactory } from "./transport" + +export * as Config from "./config" +export * as Paths from "./paths" +export * as Renderer from "./renderer" +export { Allowlist, DENIED_REPLY } from "./allowlist" +export { Dispatcher, SessionMap, conversationKey, HELP, type Binding, type DispatcherOptions } from "./dispatch" +export { FileSecretStore, MemorySecretStore, scoped, type SecretStore, type ChannelSecrets } from "./secrets" +export { + consoleLogger, + type DaemonConnection, + type Inbound, + type Logger, + type Transport, + type TransportContext, + type TransportFactory, +} from "./transport" +export type { ChannelConfig, ChannelName, ChannelsConfig } from "./config" + +export type ChannelStatus = { + readonly name: Config.ChannelName + readonly enabled: boolean + readonly connected: boolean + readonly lastError?: string + /** Whether a transport for this channel is compiled in at all. */ + readonly available: boolean +} + +export type ManagerOptions = { + /** The daemon transports talk to. Default: the registered daemon from `server.json`. */ + readonly daemon?: DaemonConnection + readonly secrets?: SecretStore + readonly sessions?: SessionMap + readonly env?: NodeJS.ProcessEnv + readonly log?: (name: string) => Logger + readonly fetch?: typeof fetch +} + +type Running = { + readonly name: Config.ChannelName + readonly config: Config.ChannelConfig + readonly transport: Transport + readonly dispatcher: Dispatcher + readonly allowlist: Allowlist + attachment?: Attachment + connected: boolean + lastError?: string +} + +export class ChannelManager { + private readonly running = new Map() + private readonly errors = new Map() + private factories: Partial> = {} + private config: Config.ChannelsConfig = Config.EMPTY + private readonly secrets: SecretStore + private readonly sessions: SessionMap + private readonly env: NodeJS.ProcessEnv + private readonly logger: (name: string) => Logger + private daemon?: DaemonConnection + private busy: Promise = Promise.resolve() + + constructor(private readonly options: ManagerOptions = {}) { + this.env = options.env ?? process.env + this.secrets = options.secrets ?? new FileSecretStore() + this.sessions = options.sessions ?? new SessionMap() + this.logger = options.log ?? consoleLogger + this.daemon = options.daemon + } + + /** Serialise start/stop/reload so two reloads cannot race one transport. */ + private serial(fn: () => Promise): Promise { + const next = this.busy.then(fn, fn) + this.busy = next.catch(() => undefined) + return next + } + + private async connection(): Promise { + if (this.daemon) return this.daemon + const found = await currentDaemon({ env: this.env, ...(this.options.fetch ? { fetch: this.options.fetch } : {}) }) + if (!found) throw new Error("No registered Redrob daemon is running (redrob service start)") + this.daemon = { url: found.url, headers: found.headers } + return this.daemon + } + + /** + * Read `channels.json` and start every enabled channel that has a factory. Returns the names + * that are running afterwards. A channel that fails to start is recorded in `status()` and does + * not stop the others. + */ + startEnabledChannels(factories: Partial>): Promise { + return this.serial(async () => { + this.factories = factories + this.config = await Config.load(this.env) + await this.apply() + return [...this.running.keys()] + }) + } + + /** Re-read `channels.json`; start newly enabled channels, stop disabled ones, restart changed ones. */ + reload(): Promise { + return this.serial(async () => { + this.config = await Config.load(this.env) + await this.apply() + return [...this.running.keys()] + }) + } + + stopAll(): Promise { + return this.serial(async () => { + for (const name of [...this.running.keys()]) await this.stopOne(name) + }) + } + + /** True while at least one transport is running, which is when the daemon must not idle-exit. */ + get active(): boolean { + return this.running.size > 0 + } + + status(): ChannelStatus[] { + return Config.CHANNEL_NAMES.map((name) => { + const entry = this.config.channels[name] + const run = this.running.get(name) + const lastError = run?.lastError ?? this.errors.get(name) + return { + name, + enabled: entry?.enabled === true, + connected: run?.connected === true, + available: this.factories[name] !== undefined, + ...(lastError ? { lastError } : {}), + } + }) + } + + private async apply(): Promise { + const wanted = Config.enabledChannels(this.config) + for (const name of [...this.running.keys()]) { + const run = this.running.get(name)! + const next = this.config.channels[name] + const changed = !next?.enabled || JSON.stringify(next) !== JSON.stringify(run.config) + if (changed) await this.stopOne(name) + } + for (const name of wanted) { + if (this.running.has(name)) continue + await this.startOne(name, this.config.channels[name]!) + } + } + + private async startOne(name: Config.ChannelName, config: Config.ChannelConfig): Promise { + const log = this.logger(name) + const factory = this.factories[name] + if (!factory) { + this.errors.set(name, "no transport is available for this channel in this build") + log.warn(`enabled but no transport is registered; skipping`) + return + } + let daemon: DaemonConnection + try { + daemon = await this.connection() + } catch (error) { + this.errors.set(name, error instanceof Error ? error.message : String(error)) + log.error(this.errors.get(name)!) + return + } + const transport = factory() + const allowlist = Allowlist.fromConfig(config) + const directory = config.directory ?? this.config.directory ?? os.homedir() + const dispatcher = new Dispatcher({ + transport, + config, + daemon, + sessions: this.sessions, + log, + directory, + ...(this.options.fetch ? { fetch: this.options.fetch } : {}), + }) + const run: Running = { name, config, transport, dispatcher, allowlist, connected: false } + const onMessage = (message: Inbound) => { + const verdict = allowlist.check(message, log) + if (!verdict.allowed) { + if (verdict.reply) void transport.send(message.conversationId, [verdict.reply], message.replyTo).catch(() => undefined) + return + } + void dispatcher.handle(message) + } + try { + await transport.start({ config, secrets: scoped(this.secrets, name), daemon, log, onMessage }) + run.connected = true + this.errors.delete(name) + try { + run.attachment = await attachToDaemon(daemon, `channel:${name}`, { + ...(this.options.fetch ? { fetch: this.options.fetch } : {}), + }) + } catch (error) { + // Not fatal: the daemon only uses attachments for idle accounting, and serve.ts turns idle + // exit off while channels run anyway. + log.warn(`could not attach to the daemon as a client: ${error instanceof Error ? error.message : String(error)}`) + } + if (allowlist.size === 0) log.warn("allowlist is empty: every sender will be denied") + log.info(`started (directory ${directory})`) + } catch (error) { + run.lastError = error instanceof Error ? error.message : String(error) + this.errors.set(name, run.lastError) + log.error(`failed to start: ${run.lastError}`) + await transport.stop().catch(() => undefined) + return + } + this.running.set(name, run) + } + + private async stopOne(name: Config.ChannelName): Promise { + const run = this.running.get(name) + if (!run) return + this.running.delete(name) + const log = this.logger(name) + await run.attachment?.detach().catch(() => undefined) + try { + await run.transport.stop() + log.info("stopped") + } catch (error) { + log.warn(`stop failed: ${error instanceof Error ? error.message : String(error)}`) + } + } +} + +/** The one manager a process uses; serve.ts and the server's /channels handlers share it. */ +let shared: ChannelManager | undefined + +export function manager(options?: ManagerOptions): ChannelManager { + if (!shared) shared = new ChannelManager(options) + return shared +} + +export function startEnabledChannels( + factories: Partial>, + options?: ManagerOptions, +): Promise { + return manager(options).startEnabledChannels(factories) +} + +export function stopAll(): Promise { + return shared ? shared.stopAll() : Promise.resolve() +} + +export function reload(): Promise { + return shared ? shared.reload() : Promise.resolve([]) +} diff --git a/packages/channels/core/src/paths.ts b/packages/channels/core/src/paths.ts new file mode 100644 index 0000000000..13aa62b9b8 --- /dev/null +++ b/packages/channels/core/src/paths.ts @@ -0,0 +1,49 @@ +/** + * Where the channel files live: `$HOME/.redrob/channels.json` (non-secret config), + * `$HOME/.redrob/channels/secrets.json` (tokens, mode 0600) and + * `$HOME/.redrob/channels/sessions.json` (conversation -> session map). + * + * `REDROB_CHANNELS_HOME` moves the whole set, for tests and odd homes, the way `REDROB_RUN_DIR` + * moves the daemon record. + */ +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +export function home(env: NodeJS.ProcessEnv = process.env): string { + return env.REDROB_CHANNELS_HOME?.trim() || path.join(os.homedir(), ".redrob") +} + +export function configFile(env: NodeJS.ProcessEnv = process.env): string { + return path.join(home(env), "channels.json") +} + +export function directory(env: NodeJS.ProcessEnv = process.env): string { + return path.join(home(env), "channels") +} + +export function secretsFile(env: NodeJS.ProcessEnv = process.env): string { + return path.join(directory(env), "secrets.json") +} + +export function sessionsFile(env: NodeJS.ProcessEnv = process.env): string { + return path.join(directory(env), "sessions.json") +} + +/** Parsed JSON, or undefined when the file is missing or does not parse. */ +export async function readJson(file: string): Promise { + try { + return JSON.parse(await fs.readFile(file, "utf8")) as unknown + } catch { + return undefined + } +} + +/** Write atomically (temp file + rename), creating the folder, with the given file mode. */ +export async function writeJson(file: string, value: unknown, mode: number): Promise { + await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 }) + const temp = `${file}.${process.pid}.${Date.now()}.tmp` + await fs.writeFile(temp, JSON.stringify(value, null, 2) + "\n", { mode }) + await fs.chmod(temp, mode).catch(() => undefined) + await fs.rename(temp, file) +} diff --git a/packages/channels/core/src/renderer.ts b/packages/channels/core/src/renderer.ts new file mode 100644 index 0000000000..823d2e7453 --- /dev/null +++ b/packages/channels/core/src/renderer.ts @@ -0,0 +1,112 @@ +/** + * Turn the assistant's markdown into text a chat platform shows sensibly, then split it to fit. + * + * "Plain-ish": headings become their text, emphasis markers are dropped, links become + * `text (url)`, list markers become bullets, tables are left as-is (monospace is the best any + * platform does). Code fences are kept verbatim so a transport that supports them (Slack, Discord, + * Telegram MarkdownV2) can still show code as code; splitting never cuts inside a fence when it + * can avoid it, and re-opens the fence in the next chunk when it cannot. + */ + +export const DEFAULT_MAX_CHUNK = 3900 + +export function toPlainText(markdown: string): string { + const out: string[] = [] + let inFence = false + for (const raw of markdown.replace(/\r\n?/g, "\n").split("\n")) { + if (/^\s*(```|~~~)/.test(raw)) { + inFence = !inFence + out.push(raw) + continue + } + if (inFence) { + out.push(raw) + continue + } + let line = raw + line = line.replace(/^\s{0,3}#{1,6}\s+(.*?)\s*#*\s*$/, "$1") + line = line.replace(/^\s*>\s?/, "") + line = line.replace(/^(\s*)[-*+]\s+/, "$1• ") + line = line.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1") + line = line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_, text: string, url: string) => + text === url ? url : `${text} (${url})`, + ) + line = line.replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, "$2") + line = line.replace(/(^|[^\w*])\*(?=\S)([^*]*?\S)\*(?!\w)/g, "$1$2") + line = line.replace(/(^|[^\w_])_(?=\S)([^_]*?\S)_(?!\w)/g, "$1$2") + line = line.replace(/~~(?=\S)([\s\S]*?\S)~~/g, "$1") + line = line.replace(/^\s*([-*_])(\s*\1){2,}\s*$/, "────────") + out.push(line) + } + return out + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .trim() +} + +type Fence = { open: string } | undefined + +function fenceState(text: string): Fence { + let open: Fence + for (const line of text.split("\n")) { + const m = /^\s*(```|~~~)(.*)$/.exec(line) + if (!m) continue + if (open) open = undefined + else open = { open: m[1] + m[2].trim() } + } + return open +} + +/** Where to cut `text` so the first piece is at most `limit` chars: a blank line, a newline, a space, else hard. */ +function cutPoint(text: string, limit: number): number { + if (text.length <= limit) return text.length + const window = text.slice(0, limit) + const paragraph = window.lastIndexOf("\n\n") + if (paragraph > limit * 0.4) return paragraph + const line = window.lastIndexOf("\n") + if (line > limit * 0.4) return line + const space = window.lastIndexOf(" ") + if (space > limit * 0.4) return space + return limit +} + +/** Split into chunks of at most `limit` characters, keeping code fences balanced per chunk. */ +export function split(text: string, limit = DEFAULT_MAX_CHUNK): string[] { + const max = Math.max(64, Math.floor(limit)) + const chunks: string[] = [] + let rest = text + let reopen = "" + while (rest.length > 0) { + let piece = reopen + rest + if (piece.length <= max) { + chunks.push(piece) + break + } + let at = cutPoint(piece, max) + let head = piece.slice(0, at) + const fence = fenceState(head) + if (fence) { + // Cutting inside a fence: close it here and re-open it in the next chunk. Make room for the + // closing marker; if that pushes us too far back, accept the cut and let the platform cope. + const closer = "\n" + fence.open.slice(0, 3) + if (head.length + closer.length > max) { + at = cutPoint(piece, max - closer.length) + head = piece.slice(0, at) + } + head = head.trimEnd() + closer + reopen = fence.open + "\n" + } else { + reopen = "" + } + chunks.push(head.trimEnd()) + rest = piece.slice(at).replace(/^\n+/, "") + } + return chunks.filter((c) => c.trim().length > 0) +} + +/** Render a markdown reply into platform-sized chunks. */ +export function render(markdown: string, limit = DEFAULT_MAX_CHUNK): string[] { + const text = toPlainText(markdown) + if (text.length === 0) return [] + return split(text, limit) +} diff --git a/packages/channels/core/src/secrets.ts b/packages/channels/core/src/secrets.ts new file mode 100644 index 0000000000..c7705f2507 --- /dev/null +++ b/packages/channels/core/src/secrets.ts @@ -0,0 +1,112 @@ +/** + * Channel secrets (bot tokens, app tokens, signing secrets) behind one interface. + * + * The file store writes `$HOME/.redrob/channels/secrets.json` with mode 0600 in a 0700 folder: + * `{ "slack": { "botToken": "…", "appToken": "…" } }`. A keychain-backed store can replace it + * later without the transports noticing; they only ever see `get(channel, key)`. + */ +import fs from "node:fs/promises" +import path from "node:path" +import * as Paths from "./paths" + +export interface SecretStore { + get(channel: string, key: string): Promise + set(channel: string, key: string, value: string): Promise + delete(channel: string, key: string): Promise + /** Key names stored for a channel, never the values. */ + keys(channel: string): Promise +} + +type Shape = Record> + +function isShape(value: unknown): value is Shape { + if (typeof value !== "object" || value === null || Array.isArray(value)) return false + return Object.values(value as Record).every( + (entry) => + typeof entry === "object" && + entry !== null && + !Array.isArray(entry) && + Object.values(entry as Record).every((v) => typeof v === "string"), + ) +} + +export class FileSecretStore implements SecretStore { + constructor(private readonly file: string = Paths.secretsFile()) {} + + private async read(): Promise { + const value = await Paths.readJson(this.file) + return isShape(value) ? value : {} + } + + private async write(shape: Shape): Promise { + await Paths.writeJson(this.file, shape, 0o600) + // The folder is created 0700 by writeJson on first use; an existing folder keeps whatever it + // had, so tighten it here too. + await fs.chmod(path.dirname(this.file), 0o700).catch(() => undefined) + } + + async get(channel: string, key: string): Promise { + return (await this.read())[channel]?.[key] + } + + async set(channel: string, key: string, value: string): Promise { + const shape = await this.read() + shape[channel] = { ...shape[channel], [key]: value } + await this.write(shape) + } + + async delete(channel: string, key: string): Promise { + const shape = await this.read() + if (shape[channel]?.[key] === undefined) return false + const { [key]: _, ...rest } = shape[channel] + if (Object.keys(rest).length === 0) delete shape[channel] + else shape[channel] = rest + await this.write(shape) + return true + } + + async keys(channel: string): Promise { + return Object.keys((await this.read())[channel] ?? {}).sort() + } +} + +/** In-memory store, for tests and for a transport started with secrets passed in by hand. */ +export class MemorySecretStore implements SecretStore { + constructor(private readonly shape: Shape = {}) {} + async get(channel: string, key: string) { + return this.shape[channel]?.[key] + } + async set(channel: string, key: string, value: string) { + this.shape[channel] = { ...this.shape[channel], [key]: value } + } + async delete(channel: string, key: string) { + if (this.shape[channel]?.[key] === undefined) return false + delete this.shape[channel][key] + return true + } + async keys(channel: string) { + return Object.keys(this.shape[channel] ?? {}).sort() + } +} + +/** A view of one channel's secrets, so a transport cannot read another channel's tokens. */ +export type ChannelSecrets = { + get(key: string): Promise + /** `get` that throws a readable error when the key was never set. */ + require(key: string): Promise + keys(): Promise +} + +export function scoped(store: SecretStore, channel: string): ChannelSecrets { + return { + get: (key) => store.get(channel, key), + require: async (key) => { + const value = await store.get(channel, key) + if (value === undefined || value === "") { + throw new Error(`Secret ${channel}.${key} is not set; run: redrob channels set-secret ${channel} ${key}`) + } + return value + }, + keys: () => store.keys(channel), + } +} diff --git a/packages/channels/core/src/transport.ts b/packages/channels/core/src/transport.ts new file mode 100644 index 0000000000..4e3b197c6e --- /dev/null +++ b/packages/channels/core/src/transport.ts @@ -0,0 +1,70 @@ +/** + * The contract a chat transport (Slack, Telegram, Discord, WhatsApp) implements. + * + * A transport knows its network and nothing about sessions: it turns platform events into + * `Inbound` messages and hands them to `ctx.onMessage`, and it delivers plain-text chunks the + * dispatcher gives it. Everything else -- who may talk, which session a conversation maps to, + * how markdown becomes text -- is the core's job, so the four transports stay thin and alike. + */ +import type { ChannelConfig } from "./config" +import type { ChannelSecrets } from "./secrets" + +/** A message a transport received, normalised across platforms. */ +export type Inbound = { + /** Transport name, e.g. `slack`. */ + readonly channel: string + /** + * The conversation this belongs to, stable for its lifetime: a Slack channel+thread, a Telegram + * chat id, a Discord channel id, a WhatsApp JID. One conversation is one engine session. + */ + readonly conversationId: string + /** Platform user id of the sender; this is what the allowlist matches. */ + readonly senderId: string + readonly senderName?: string + readonly text: string + /** Platform message id to reply to (a thread ts, a message id), when the platform has one. */ + readonly replyTo?: string +} + +export type Logger = { + info(message: string): void + warn(message: string): void + error(message: string): void +} + +/** The daemon the transport talks to: its base URL and the Basic header built from its secret. */ +export type DaemonConnection = { + readonly url: string + readonly headers: { readonly Authorization: string } +} + +export type TransportContext = { + readonly config: ChannelConfig + /** This channel's secrets only. */ + readonly secrets: ChannelSecrets + readonly daemon: DaemonConnection + readonly log: Logger + /** Hand every message here. The core replies through `send`; the transport never answers itself. */ + readonly onMessage: (message: Inbound) => void +} + +export interface Transport { + readonly name: string + /** Longest message the platform accepts; the renderer splits replies to fit. Default 3900. */ + readonly maxChunk?: number + /** Connect and begin delivering to `ctx.onMessage`. Resolves once connected; rejects on failure. */ + start(ctx: TransportContext): Promise + stop(): Promise + /** Deliver chunks in order to a conversation. `replyTo` is the inbound message's `replyTo`. */ + send(conversationId: string, chunks: readonly string[], replyTo?: string): Promise + /** Show a typing indicator, when the platform has one. */ + typing?(conversationId: string): Promise +} + +export type TransportFactory = () => Transport + +export const consoleLogger = (name: string): Logger => ({ + info: (m) => console.log(`[channels:${name}] ${m}`), + warn: (m) => console.warn(`[channels:${name}] ${m}`), + error: (m) => console.error(`[channels:${name}] ${m}`), +}) diff --git a/packages/channels/core/tsconfig.json b/packages/channels/core/tsconfig.json new file mode 100644 index 0000000000..00ef125468 --- /dev/null +++ b/packages/channels/core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "noUncheckedIndexedAccess": false + } +} From fdab98b95e38a8182f1c650074941baba760dde0 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:28:53 +0000 Subject: [PATCH 02/23] channels: run transports inside the daemon, /channels endpoints redrob serve --register starts every enabled channel from channels.json through the (still empty) channelFactories registry, turns idle exit off while any transport runs, and stops them on the way out. The server gains GET /channels/status and POST /channels/reload behind a controller seam the serve command fills in, following the clients group pattern. --- packages/protocol/src/api.ts | 2 ++ packages/protocol/src/groups/channels.ts | 46 ++++++++++++++++++++++++ packages/redrob/package.json | 1 + packages/redrob/src/channels/registry.ts | 15 ++++++++ packages/redrob/src/cli/cmd/serve.ts | 26 +++++++++++++- packages/server/src/channels.ts | 31 ++++++++++++++++ packages/server/src/clients.ts | 2 +- packages/server/src/handlers.ts | 2 ++ packages/server/src/handlers/channels.ts | 26 ++++++++++++++ 9 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 packages/protocol/src/groups/channels.ts create mode 100644 packages/redrob/src/channels/registry.ts create mode 100644 packages/server/src/channels.ts create mode 100644 packages/server/src/handlers/channels.ts diff --git a/packages/protocol/src/api.ts b/packages/protocol/src/api.ts index 10710bafa1..870ec82da3 100644 --- a/packages/protocol/src/api.ts +++ b/packages/protocol/src/api.ts @@ -15,6 +15,7 @@ import type { Definition } from "@redrob-code/schema/event" import { AgentGroup } from "./groups/agent" import { HealthGroup } from "./groups/health" import { ClientsGroup } from "./groups/clients" +import { ChannelsGroup } from "./groups/channels" import { PtyGroup } from "./groups/pty" import { makeBrowserRequestGroup } from "./groups/browser-request" import { makeQuestionGroup } from "./groups/question" @@ -42,6 +43,7 @@ const makeApiFromGroup = < HttpApi.make("server") .add(HealthGroup) .add(ClientsGroup) + .add(ChannelsGroup) .add(LocationGroup.middleware(locationMiddleware)) .add(AgentGroup.middleware(locationMiddleware)) .add(makeSessionGroup(sessionLocationMiddleware)) diff --git a/packages/protocol/src/groups/channels.ts b/packages/protocol/src/groups/channels.ts new file mode 100644 index 0000000000..d9398cf6ed --- /dev/null +++ b/packages/protocol/src/groups/channels.ts @@ -0,0 +1,46 @@ +import { Schema } from "effect" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +/** + * The chat channels this daemon serves (Slack, Telegram, Discord, WhatsApp). The transports run + * inside `redrob serve --register`; these endpoints let `redrob channels enable|disable|allow` + * tell a running daemon to re-read `~/.redrob/channels.json`, and let the launcher show which + * channels are connected. No location: this is about the process, not a project. + */ +export const ChannelStatus = Schema.Struct({ + name: Schema.String, + enabled: Schema.Boolean, + connected: Schema.Boolean, + /** Whether this build has a transport for the channel at all. */ + available: Schema.Boolean, + lastError: Schema.optional(Schema.String), +}).annotate({ identifier: "ChannelStatus" }) + +export const ChannelStatusList = Schema.Struct({ + channels: Schema.Array(ChannelStatus), +}).annotate({ identifier: "ChannelStatusList" }) + +export const ChannelReloadResult = Schema.Struct({ + /** Channels running after the reload. */ + running: Schema.Array(Schema.String), +}).annotate({ identifier: "ChannelReloadResult" }) + +export const ChannelsGroup = HttpApiGroup.make("server.channels") + .add( + HttpApiEndpoint.get("channels.status", "/channels/status", { success: ChannelStatusList }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.channels.status", + summary: "Chat channel status", + description: "Per channel: enabled in channels.json, connected right now, and the last error if any.", + }), + ), + ) + .add( + HttpApiEndpoint.post("channels.reload", "/channels/reload", { success: ChannelReloadResult }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.channels.reload", + summary: "Re-read channels.json", + description: "Start newly enabled channels, stop disabled ones, restart changed ones.", + }), + ), + ) diff --git a/packages/redrob/package.json b/packages/redrob/package.json index 76081db17c..9aba7ca00a 100644 --- a/packages/redrob/package.json +++ b/packages/redrob/package.json @@ -87,6 +87,7 @@ "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", "@redrob-code/codemode": "workspace:*", + "@redrob-code/channels-core": "workspace:*", "@redrob-code/llm": "workspace:*", "@redrob-code/plugin": "workspace:*", "@redrob-code/protocol": "workspace:*", diff --git a/packages/redrob/src/channels/registry.ts b/packages/redrob/src/channels/registry.ts new file mode 100644 index 0000000000..a1de3f48a5 --- /dev/null +++ b/packages/redrob/src/channels/registry.ts @@ -0,0 +1,15 @@ +/** + * The transports this build of the CLI can run, by channel name. + * + * Stage 1 ships the core and no transports, so the map is empty: an enabled channel in + * `channels.json` shows up in `redrob channels status` as "no transport is available". Stage 2 + * adds one entry per platform, each a lazy factory so a platform SDK is only loaded when its + * channel is enabled: + * + * ```ts + * slack: () => new SlackTransport(), + * ``` + */ +import type { ChannelName, TransportFactory } from "@redrob-code/channels-core" + +export const channelFactories: Partial> = {} diff --git a/packages/redrob/src/cli/cmd/serve.ts b/packages/redrob/src/cli/cmd/serve.ts index 8b09624335..c379987ba8 100644 --- a/packages/redrob/src/cli/cmd/serve.ts +++ b/packages/redrob/src/cli/cmd/serve.ts @@ -70,11 +70,34 @@ export const ServeCommand = effectCmd({ const idleExitMs = args["idle-exit"] > 0 ? args["idle-exit"] * 60_000 : undefined setIdleExitAfterMs(idleExitMs) + // Chat channels (Slack, Telegram, ...) run inside the daemon. While any is on, the daemon + // must stay up for the bot to answer, so idle exit is off for as long as one is running; + // it is restored when a reload turns the last one off. + const Channels = yield* Effect.promise(() => import("@redrob-code/channels-core")) + const ServerChannels = yield* Effect.promise(() => import("@redrob-code/server/channels")) + const { channelFactories } = yield* Effect.promise(() => import("../../channels/registry")) + const channels = Channels.manager({ daemon: { url: `http://${server.hostname}:${server.port}`, headers: Registry.headers(secret) } }) + const applyIdlePolicy = () => setIdleExitAfterMs(channels.active ? undefined : idleExitMs) + ServerChannels.setController({ + status: () => channels.status(), + reload: async () => { + const running = await channels.reload() + applyIdlePolicy() + return running + }, + }) + const started = yield* Effect.promise(() => channels.startEnabledChannels(channelFactories).catch(() => [])) + applyIdlePolicy() + if (started.length > 0) console.log(`channels: ${started.join(", ")}`) + let leaving = false const leave = (code: number) => { if (leaving) return leaving = true - void Registry.remove(id) + void channels + .stopAll() + .catch(() => undefined) + .then(() => Registry.remove(id)) .catch(() => undefined) .then(() => server.stop(true)) .finally(() => process.exit(code)) @@ -89,6 +112,7 @@ export const ServeCommand = effectCmd({ if (current && current.id !== id) return leave(0) Clients.sweep() const idle = Clients.idleFor() + if (channels.active) return if (idleExitMs !== undefined && idle !== undefined && idle >= idleExitMs) leave(0) }) }, 10_000) diff --git a/packages/server/src/channels.ts b/packages/server/src/channels.ts new file mode 100644 index 0000000000..ce610a5cd7 --- /dev/null +++ b/packages/server/src/channels.ts @@ -0,0 +1,31 @@ +/** + * The chat-channel controller the `/channels/*` handlers call. + * + * The transports themselves run in the CLI process (`redrob serve --register`, which owns the + * `@redrob-code/channels-core` manager); the server package only exposes them over HTTP. The + * serve command registers the controller after it starts the channels. Plain module state, like + * `clients.ts`: an embedded or unregistered server has no controller and answers "nothing running". + */ +export type ChannelStatus = { + readonly name: string + readonly enabled: boolean + readonly connected: boolean + readonly available: boolean + readonly lastError?: string +} + +export type Controller = { + status(): ChannelStatus[] | Promise + /** Re-read the config and reconcile; returns the channels running afterwards. */ + reload(): Promise +} + +let controller: Controller | undefined + +export function setController(value: Controller | undefined): void { + controller = value +} + +export function current(): Controller | undefined { + return controller +} diff --git a/packages/server/src/clients.ts b/packages/server/src/clients.ts index 6cecc7be35..81869fd0fb 100644 --- a/packages/server/src/clients.ts +++ b/packages/server/src/clients.ts @@ -77,7 +77,7 @@ export function list(): Client[] { let lastRequestAt: number | undefined let inFlight = 0 -const STATUS_PATHS = ["/api/health", "/api/clients"] +const STATUS_PATHS = ["/api/health", "/api/clients", "/channels/status"] /** Whether a request to this path keeps the daemon alive. */ export function countsAsActivity(path: string): boolean { diff --git a/packages/server/src/handlers.ts b/packages/server/src/handlers.ts index 5c1b7284e7..2daaebcc31 100644 --- a/packages/server/src/handlers.ts +++ b/packages/server/src/handlers.ts @@ -12,6 +12,7 @@ import { EventHandler } from "./handlers/event" import { AgentHandler } from "./handlers/agent" import { HealthHandler } from "./handlers/health" import { ClientsHandler } from "./handlers/clients" +import { ChannelsHandler } from "./handlers/channels" import { PtyHandler } from "./handlers/pty" import { BrowserRequestHandler } from "./handlers/browser-request" import { QuestionHandler } from "./handlers/question" @@ -26,6 +27,7 @@ import { ChatCompletionHandler } from "./handlers/chat-completion" export const handlers = Layer.mergeAll( HealthHandler, ClientsHandler, + ChannelsHandler, LocationHandler, AgentHandler, SessionHandler, diff --git a/packages/server/src/handlers/channels.ts b/packages/server/src/handlers/channels.ts new file mode 100644 index 0000000000..f13a278e10 --- /dev/null +++ b/packages/server/src/handlers/channels.ts @@ -0,0 +1,26 @@ +import { Effect } from "effect" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { Api } from "../api" +import * as Channels from "../channels" + +export const ChannelsHandler = HttpApiBuilder.group(Api, "server.channels", (handlers) => + handlers + .handle( + "channels.status", + Effect.fn(function* () { + const controller = Channels.current() + if (!controller) return { channels: [] } + const channels = yield* Effect.promise(async () => controller.status()) + return { channels } + }), + ) + .handle( + "channels.reload", + Effect.fn(function* () { + const controller = Channels.current() + if (!controller) return { running: [] } + const running = yield* Effect.promise(() => controller.reload()) + return { running } + }), + ), +) From 82d522b4806fa4be1e73a3e7302ba30f9deb28d0 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:28:53 +0000 Subject: [PATCH 03/23] channels: redrob channels CLI list | status | enable | disable | allow | set-secret | slack-manifest. Edits channels.json and the 0600 secrets file, then asks a running registered daemon to POST /channels/reload; without one the change waits for the next start. slack-manifest prints the Socket Mode app manifest, or with --url the api.slack.com link that creates the app from it. --- packages/redrob/src/cli/cmd/channels.ts | 255 ++++++++++++++++++++++++ packages/redrob/src/index.ts | 2 + 2 files changed, 257 insertions(+) create mode 100644 packages/redrob/src/cli/cmd/channels.ts diff --git a/packages/redrob/src/cli/cmd/channels.ts b/packages/redrob/src/cli/cmd/channels.ts new file mode 100644 index 0000000000..492776d208 --- /dev/null +++ b/packages/redrob/src/cli/cmd/channels.ts @@ -0,0 +1,255 @@ +/** + * `redrob channels ...`: configure the chat channels the shared daemon serves. + * + * Edits `~/.redrob/channels.json` (non-secret) and `~/.redrob/channels/secrets.json` (0600), then + * asks a running registered daemon to re-read the file with `POST /channels/reload`. Nothing here + * starts a daemon: enabling a channel with no daemon running takes effect on its next start. + */ +import { cmd } from "./cmd" +import { UI } from "@/cli/ui" +import { Daemon } from "@/daemon/connect" +import { Config, FileSecretStore, type ChannelName } from "@redrob-code/channels-core" + +const NAMES = Config.CHANNEL_NAMES + +function channelName(value: string): ChannelName { + if (Config.isChannelName(value)) return value + throw new Error(`Unknown channel "${value}"; one of: ${NAMES.join(", ")}`) +} + +type Status = { name: string; enabled: boolean; connected: boolean; available: boolean; lastError?: string } + +/** The running daemon's view, or undefined when none is registered. */ +async function daemonStatus(): Promise { + const connection = await Daemon.current() + if (!connection) return undefined + const response = await fetch(`${connection.url.replace(/\/+$/, "")}/channels/status`, { + headers: connection.headers, + signal: AbortSignal.timeout(5_000), + }).catch(() => undefined) + if (!response?.ok) return undefined + const body = (await response.json().catch(() => null)) as { channels?: Status[] } | null + return body?.channels +} + +/** Tell a running daemon to re-read channels.json. Quietly does nothing when none is running. */ +async function reload(): Promise { + const connection = await Daemon.current() + if (!connection) return false + const response = await fetch(`${connection.url.replace(/\/+$/, "")}/channels/reload`, { + method: "POST", + headers: connection.headers, + signal: AbortSignal.timeout(30_000), + }).catch(() => undefined) + return response?.ok === true +} + +async function readStdin(): Promise { + const chunks: Buffer[] = [] + for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + return Buffer.concat(chunks).toString("utf8") +} + +const after = (reloaded: boolean) => + UI.println( + reloaded + ? `${UI.Style.TEXT_DIM}daemon reloaded${UI.Style.TEXT_NORMAL}` + : `${UI.Style.TEXT_DIM}no running daemon; takes effect on its next start${UI.Style.TEXT_NORMAL}`, + ) + +export const SLACK_SCOPES = [ + "app_mentions:read", + "chat:write", + "im:history", + "im:read", + "im:write", + "channels:history", + "groups:history", + "mpim:history", + "users:read", +] as const + +export const SLACK_EVENTS = ["app_mention", "message.im", "message.channels"] as const + +export function slackManifest(): string { + return [ + "display_information:", + " name: Redrob", + " description: Talk to your Redrob Code engine from Slack.", + " background_color: '#1a1a1a'", + "features:", + " bot_user:", + " display_name: Redrob", + " always_online: true", + "oauth_config:", + " scopes:", + " bot:", + ...SLACK_SCOPES.map((scope) => ` - ${scope}`), + "settings:", + " event_subscriptions:", + " bot_events:", + ...SLACK_EVENTS.map((event) => ` - ${event}`), + " interactivity:", + " is_enabled: false", + " org_deploy_enabled: false", + " socket_mode_enabled: true", + " token_rotation_enabled: false", + "", + ].join("\n") +} + +export function slackManifestUrl(): string { + return `https://api.slack.com/apps?new_app=1&manifest_yaml=${encodeURIComponent(slackManifest())}` +} + +export const ChannelsCommand = cmd({ + command: "channels", + describe: "chat channels (Slack, Telegram, Discord, WhatsApp) served by the shared engine", + builder: (yargs) => + yargs + .command( + cmd({ + command: "list", + describe: "show the configured channels", + builder: (yargs) => yargs.option("json", { type: "boolean", default: false }), + handler: async (args) => { + const config = await Config.load() + const secrets = new FileSecretStore() + const rows = [] + for (const name of NAMES) { + const entry = config.channels[name] ?? Config.defaultChannel() + rows.push({ name, enabled: entry.enabled, allowlist: [...entry.allowlist], secrets: await secrets.keys(name) }) + } + if (args.json) { + process.stdout.write(JSON.stringify({ directory: config.directory, channels: rows }) + "\n") + return + } + for (const row of rows) { + const state = row.enabled + ? `${UI.Style.TEXT_SUCCESS_BOLD}enabled${UI.Style.TEXT_NORMAL}` + : `${UI.Style.TEXT_DIM}disabled${UI.Style.TEXT_NORMAL}` + UI.println( + `${row.name.padEnd(9)} ${state} allow: ${row.allowlist.length ? row.allowlist.join(", ") : "(nobody)"} secrets: ${row.secrets.length ? row.secrets.join(", ") : "(none)"}`, + ) + } + }, + }), + ) + .command( + cmd({ + command: "status", + describe: "show which channels the running engine has connected", + builder: (yargs) => yargs.option("json", { type: "boolean", default: false }), + handler: async (args) => { + const live = await daemonStatus() + if (!live) { + if (args.json) process.stdout.write(JSON.stringify(null) + "\n") + else UI.println(`${UI.Style.TEXT_DIM}no running engine${UI.Style.TEXT_NORMAL}`) + process.exitCode = 1 + return + } + if (args.json) { + process.stdout.write(JSON.stringify(live) + "\n") + return + } + for (const row of live) { + const state = !row.enabled + ? `${UI.Style.TEXT_DIM}disabled${UI.Style.TEXT_NORMAL}` + : row.connected + ? `${UI.Style.TEXT_SUCCESS_BOLD}connected${UI.Style.TEXT_NORMAL}` + : !row.available + ? `${UI.Style.TEXT_WARNING}no transport in this build${UI.Style.TEXT_NORMAL}` + : `${UI.Style.TEXT_DANGER}not connected${UI.Style.TEXT_NORMAL}` + UI.println(`${row.name.padEnd(9)} ${state}${row.lastError ? ` ${UI.Style.TEXT_DIM}${row.lastError}${UI.Style.TEXT_NORMAL}` : ""}`) + } + }, + }), + ) + .command( + cmd({ + command: "enable ", + describe: "turn a channel on", + builder: (yargs) => yargs.positional("name", { type: "string", demandOption: true, choices: NAMES }), + handler: async (args) => { + const name = channelName(args.name) + const config = await Config.update((c) => Config.setChannel(c, name, { enabled: true })) + if ((config.channels[name]?.allowlist.length ?? 0) === 0) { + UI.println(`${UI.Style.TEXT_WARNING}allowlist is empty: nobody can talk to it yet (redrob channels allow ${name} )${UI.Style.TEXT_NORMAL}`) + } + after(await reload()) + }, + }), + ) + .command( + cmd({ + command: "disable ", + describe: "turn a channel off", + builder: (yargs) => yargs.positional("name", { type: "string", demandOption: true, choices: NAMES }), + handler: async (args) => { + const name = channelName(args.name) + await Config.update((c) => Config.setChannel(c, name, { enabled: false })) + after(await reload()) + }, + }), + ) + .command( + cmd({ + command: "allow ", + describe: "let a platform user id talk to the engine through a channel", + builder: (yargs) => + yargs + .positional("name", { type: "string", demandOption: true, choices: NAMES }) + .positional("senderId", { type: "string", demandOption: true }) + .option("remove", { type: "boolean", default: false, describe: "remove the id instead" }), + handler: async (args) => { + const name = channelName(args.name) + const id = args.senderId.trim() + if (!id) throw new Error("senderId is empty") + await Config.update((c) => { + const current = c.channels[name] ?? Config.defaultChannel() + const allowlist = args.remove + ? current.allowlist.filter((x) => x !== id) + : [...new Set([...current.allowlist, id])] + return Config.setChannel(c, name, { allowlist }) + }) + after(await reload()) + }, + }), + ) + .command( + cmd({ + command: "set-secret ", + describe: "store a channel secret (bot token, app token, ...) in the 0600 secrets file", + builder: (yargs) => + yargs + .positional("name", { type: "string", demandOption: true, choices: NAMES }) + .positional("key", { type: "string", demandOption: true }) + .option("value", { type: "string", describe: "the secret; read from stdin when omitted" }), + handler: async (args) => { + const name = channelName(args.name) + const value = (args.value ?? (await readStdin())).replace(/\r?\n$/, "") + if (!value) throw new Error("empty secret; pass --value or pipe it on stdin") + await new FileSecretStore().set(name, args.key, value) + UI.println(`stored ${name}.${args.key}`) + after(await reload()) + }, + }), + ) + .command( + cmd({ + command: "slack-manifest", + describe: "print the Slack app manifest (YAML) for the Redrob bot", + builder: (yargs) => + yargs.option("url", { + type: "boolean", + default: false, + describe: "print the api.slack.com link that creates the app from this manifest", + }), + handler: (args) => { + process.stdout.write(args.url ? slackManifestUrl() + "\n" : slackManifest()) + }, + }), + ) + .demandCommand(), + handler: () => {}, +}) diff --git a/packages/redrob/src/index.ts b/packages/redrob/src/index.ts index bc82748ac8..95cdd48667 100644 --- a/packages/redrob/src/index.ts +++ b/packages/redrob/src/index.ts @@ -14,6 +14,7 @@ import { InstallationVersion } from "@redrob-code/core/installation/version" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" import { ServiceCommand } from "./cli/cmd/service" +import { ChannelsCommand } from "./cli/cmd/channels" import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" import { McpCommand } from "./cli/cmd/mcp" @@ -93,6 +94,7 @@ const cli = yargs(args) .command(UninstallCommand) .command(ServeCommand) .command(ServiceCommand) + .command(ChannelsCommand) .command(ModelsCommand) .command(ModelsLocalCommand) .command(StatsCommand) From 1829d2b3f1cf129502cc5c261d97c02ba758b1f7 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:28:53 +0000 Subject: [PATCH 04/23] docs: CHANNELS.md Files, in-chat commands, daemon behaviour, the two endpoints, the CLI and the Transport contract Stage 2 transports implement. --- docs/CHANNELS.md | 195 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 docs/CHANNELS.md diff --git a/docs/CHANNELS.md b/docs/CHANNELS.md new file mode 100644 index 0000000000..1f106c260b --- /dev/null +++ b/docs/CHANNELS.md @@ -0,0 +1,195 @@ +# Chat channels + +Talk to the shared Redrob Code engine from Slack, Telegram, Discord or WhatsApp. The bots run +*inside* the daemon (`redrob serve --register`, see `LOCAL-ENGINE-API.md` section 3), so there is +one process to keep up, one set of credentials, and the sessions a chat creates are ordinary +engine sessions every other Redrob app can see. + +Stage 1 (this document) is the core: configuration, secrets, the allowlist, the conversation to +session mapping, the renderer, the daemon wiring, the HTTP endpoints and the CLI. Stage 2 adds the +transports themselves, one per platform, against the `Transport` contract at the end. + +## Files + +| File | Mode | Holds | +| --- | --- | --- | +| `~/.redrob/channels.json` | 0644 | which channels are on, who may talk, non-secret settings | +| `~/.redrob/channels/secrets.json` | 0600 | bot tokens and other secrets, per channel | +| `~/.redrob/channels/sessions.json` | 0600 | conversation -> engine session map | + +`REDROB_CHANNELS_HOME` moves all three (default `~/.redrob`), the way `REDROB_RUN_DIR` moves the +daemon record. + +### `channels.json` + +```json +{ + "version": 1, + "directory": "/path/the/sessions/work/in", + "channels": { + "slack": { + "enabled": true, + "allowlist": ["U0123ABCDEF"], + "directory": "/an/override/for/this/channel", + "maxChunk": 3900, + "options": {} + }, + "telegram": { "enabled": false, "allowlist": [] } + } +} +``` + +- `directory` is the project directory new sessions are created in (the engine's + `x-redrob-directory`). The file-level value is the default; a channel may override it. When + neither is set, the user's home. +- `allowlist` is the list of platform sender ids allowed to talk. **Empty means nobody.** A denied + sender gets one "You are not allowed to use this bot." reply per daemon start and a warning in + the daemon log; nothing reaches the engine. +- `maxChunk` caps a reply chunk for this channel (default: the transport's own limit, else 3900). +- `options` is free-form, non-secret, read by the transport (Stage 2 documents its keys). + +Unknown channel names and malformed entries are dropped on load. The CLI edits this file; editing +it by hand and running `redrob channels enable ` (or `POST /channels/reload`) is fine too. + +### `secrets.json` + +```json +{ "slack": { "botToken": "xoxb-…", "appToken": "xapp-…" } } +``` + +Written by `redrob channels set-secret`, created `0600` in a `0700` folder, never read by the CLI +except to list key *names*. Transports see it through the `SecretStore` interface +(`get`, `set`, `delete`, `keys`), and only their own channel's slice, so the file store can be +swapped for a keychain later without touching a transport. + +### `sessions.json` + +One conversation is one engine session. The key is `:` -- a Slack +channel + thread, a Telegram chat id, a Discord channel id, a WhatsApp JID. The binding records +the `sessionID`, the `directory` it was created in and timestamps. A daemon restart continues the +same sessions; `/new` in the chat drops the binding so the next message starts a fresh one. If +the engine no longer knows the session (404), the dispatcher rebinds and resends once. + +## In the chat + +| Message | Does | +| --- | --- | +| `/new` | start a fresh session for this conversation | +| `/status` | show the session this conversation is bound to | +| `/help` | list these | +| anything else | sent as a prompt to the session; the assistant's text comes back | + +Messages within one conversation are handled strictly in order; different conversations run in +parallel. Replies are rendered from markdown to plain-ish text (headings and emphasis flattened, +links as `text (url)`, code fences kept) and split into chunks the platform accepts, never cutting +inside a code fence when a line or paragraph break is available. + +## Daemon + +`redrob serve --register` loads `channels.json` after it has written the daemon record. Every +enabled channel that has a transport in `packages/redrob/src/channels/registry.ts` is started, +attached to the daemon as a client named `channel:` (so `GET /api/clients` lists it), and +**idle exit is turned off for as long as any channel runs** -- a bot that is on must be reachable. +When a reload turns the last channel off, the `--idle-exit` policy is restored. `SIGTERM`/`SIGINT` +stop the transports before the record is removed. + +A channel enabled in the file but with no transport in the build (Stage 1: all of them) is +reported in `status` as `available: false` with `lastError: "no transport is available for this +channel in this build"`; it does not keep the daemon up. + +## HTTP + +Both are under the daemon's Basic auth (`redrob:` from `server.json`) and take no +location header. `GET /channels/status` is a status poll and does not count as daemon activity. + +``` +GET /channels/status -> { "channels": [ { "name", "enabled", "connected", "available", "lastError"? } ] } +POST /channels/reload -> { "running": ["slack"] } +``` + +`reload` re-reads `channels.json`: newly enabled channels start, disabled ones stop, a channel +whose entry changed is restarted. An embedded server (no `--register`) answers with empty lists. + +## CLI + +``` +redrob channels list # configured channels, allowlists, secret key names +redrob channels status [--json] # what the running daemon has connected (exit 1 when none) +redrob channels enable +redrob channels disable +redrob channels allow [--remove] +redrob channels set-secret [--value v] # value from stdin when --value is omitted +redrob channels slack-manifest [--url] +``` + +`` is one of `slack`, `telegram`, `discord`, `whatsapp`. `enable`, `disable`, `allow` and +`set-secret` write the file and then `POST /channels/reload` on the running registered daemon; +with no daemon the change waits for the next start. + +`slack-manifest` prints the Slack app manifest: bot named **Redrob**, Socket Mode on, bot scopes +`app_mentions:read chat:write im:history im:read im:write channels:history groups:history +mpim:history users:read`, bot events `app_mention message.im message.channels`. With `--url` it +prints `https://api.slack.com/apps?new_app=1&manifest_yaml=`, which opens the "create +app" page pre-filled. + +Setting up Slack, once the transport exists: + +```sh +redrob channels slack-manifest --url # open it, create the app, install to the workspace +redrob channels set-secret slack botToken # paste xoxb-… , Ctrl-D +redrob channels set-secret slack appToken # paste xapp-… (Socket Mode, connections:write), Ctrl-D +redrob channels allow slack U0123ABCDEF # your Slack member id +redrob channels enable slack +``` + +## Transport contract (for Stage 2) + +Package `@redrob-code/channels-core`, file `src/transport.ts`. + +```ts +export type Inbound = { + channel: string // "slack" + conversationId: string // stable id of the conversation == one session + senderId: string // what the allowlist matches + senderName?: string + text: string + replyTo?: string // platform message id to answer in-thread, if any +} + +export type TransportContext = { + config: ChannelConfig // this channel's entry from channels.json + secrets: ChannelSecrets // get(key) / require(key) / keys(), this channel only + daemon: { url: string; headers: { Authorization: string } } + log: Logger // info / warn / error + onMessage: (message: Inbound) => void +} + +export interface Transport { + readonly name: string + readonly maxChunk?: number // platform limit; renderer splits to fit (default 3900) + start(ctx: TransportContext): Promise // resolve when connected, reject on failure + stop(): Promise + send(conversationId: string, chunks: readonly string[], replyTo?: string): Promise + typing?(conversationId: string): Promise +} +``` + +Rules a transport follows: + +1. Call `ctx.onMessage` for every human message and never answer on its own. The core decides + whether the sender is allowed, which session it is, and what to reply; the transport only sees + `send`. Drop the bot's own messages and platform noise (joins, edits) before calling. +2. `conversationId` must be stable for the lifetime of the conversation and unique within the + channel. Threads are conversations: Slack uses `:`. +3. `send` delivers chunks in order and resolves when the platform accepted them. One chunk is + already under `maxChunk`; do not re-split. +4. Read secrets through `ctx.secrets.require("botToken")` at `start`, never from the + environment -- the daemon's env belongs to nobody (see `LOCAL-ENGINE-API.md`). +5. Keep a reconnect loop inside `start`/`stop` if the platform SDK does not; a rejected `start` + is recorded as `lastError` and the channel shows `connected: false`. +6. Register the factory in `packages/redrob/src/channels/registry.ts` as a lazy constructor so + the platform SDK loads only when the channel is enabled. + +What the core gives back: the allowlist, the persisted session map, `/new` and `/status`, the +renderer, the daemon client attachment and the idle-exit override. A transport should be a +few hundred lines. From 84a123e988c3d536be91b5d43bfb2eb27a6ff13f Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:28:53 +0000 Subject: [PATCH 05/23] channels: lockfile entry for channels-core, attach typing bun.lock learns the new workspace package. The manager's daemon attachment passes the SDK the {url, headers} it actually reads. --- bun.lock | 17 +++++++++++++++++ packages/channels/core/src/index.ts | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 94ec851482..a924ab932a 100644 --- a/bun.lock +++ b/bun.lock @@ -27,6 +27,20 @@ "turbo": "2.10.2", }, }, + "packages/channels/core": { + "name": "@redrob-code/channels-core", + "version": "0.0.0", + "dependencies": { + "@redrob-labs/sdk": "workspace:*", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, "packages/cli": { "name": "@redrob-code/cli", "version": "0.0.0", @@ -350,6 +364,7 @@ "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", + "@redrob-code/channels-core": "workspace:*", "@redrob-code/codemode": "workspace:*", "@redrob-code/llm": "workspace:*", "@redrob-code/plugin": "workspace:*", @@ -1278,6 +1293,8 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], + "@redrob-code/channels-core": ["@redrob-code/channels-core@workspace:packages/channels/core"], + "@redrob-code/cli": ["@redrob-code/cli@workspace:packages/cli"], "@redrob-code/client": ["@redrob-code/client@workspace:packages/client"], diff --git a/packages/channels/core/src/index.ts b/packages/channels/core/src/index.ts index d95b32a3dd..59df3d128f 100644 --- a/packages/channels/core/src/index.ts +++ b/packages/channels/core/src/index.ts @@ -204,7 +204,10 @@ export class ChannelManager { run.connected = true this.errors.delete(name) try { - run.attachment = await attachToDaemon(daemon, `channel:${name}`, { + // The SDK's attach() reads only url and headers; the record it types is not needed here + // and the daemon's secret is already inside the Authorization header. + const connection = daemon as Parameters[0] + run.attachment = await attachToDaemon(connection, `channel:${name}`, { ...(this.options.fetch ? { fetch: this.options.fetch } : {}), }) } catch (error) { From 3efa9ab0d9c82b9aa95899060390e1743c0b982a Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:28:55 +0000 Subject: [PATCH 06/23] server: app bridge registry and /v1/apps routes A desktop app registers its loopback MCP server with POST /v1/apps/register, heartbeats it, and the daemon drops it when the heartbeats stop (same 90s TTL as /api/clients). Only loopback mcp_url is accepted. Each registration holds a client attachment so the idle-exit clock does not run while an app is bridged; the serve tick sweeps apps before clients. --- packages/protocol/src/api.ts | 2 + packages/protocol/src/errors.ts | 9 ++ packages/protocol/src/groups/apps.ts | 90 +++++++++++++++ packages/redrob/src/cli/cmd/serve.ts | 3 + packages/server/src/apps.ts | 161 +++++++++++++++++++++++++++ packages/server/src/clients.ts | 4 +- packages/server/src/handlers.ts | 2 + packages/server/src/handlers/apps.ts | 63 +++++++++++ 8 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 packages/protocol/src/groups/apps.ts create mode 100644 packages/server/src/apps.ts create mode 100644 packages/server/src/handlers/apps.ts diff --git a/packages/protocol/src/api.ts b/packages/protocol/src/api.ts index 10710bafa1..1e90f3bcb0 100644 --- a/packages/protocol/src/api.ts +++ b/packages/protocol/src/api.ts @@ -15,6 +15,7 @@ import type { Definition } from "@redrob-code/schema/event" import { AgentGroup } from "./groups/agent" import { HealthGroup } from "./groups/health" import { ClientsGroup } from "./groups/clients" +import { AppsGroup } from "./groups/apps" import { PtyGroup } from "./groups/pty" import { makeBrowserRequestGroup } from "./groups/browser-request" import { makeQuestionGroup } from "./groups/question" @@ -42,6 +43,7 @@ const makeApiFromGroup = < HttpApi.make("server") .add(HealthGroup) .add(ClientsGroup) + .add(AppsGroup) .add(LocationGroup.middleware(locationMiddleware)) .add(AgentGroup.middleware(locationMiddleware)) .add(makeSessionGroup(sessionLocationMiddleware)) diff --git a/packages/protocol/src/errors.ts b/packages/protocol/src/errors.ts index d7fccf1792..eeb659d30c 100644 --- a/packages/protocol/src/errors.ts +++ b/packages/protocol/src/errors.ts @@ -135,3 +135,12 @@ export class ClientNotFoundError extends Schema.TaggedErrorClass()( + "AppNotFoundError", + { + appID: Schema.String, + message: Schema.String, + }, + { httpApiStatus: 404 }, +) {} diff --git a/packages/protocol/src/groups/apps.ts b/packages/protocol/src/groups/apps.ts new file mode 100644 index 0000000000..82c5815e6a --- /dev/null +++ b/packages/protocol/src/groups/apps.ts @@ -0,0 +1,90 @@ +import { Schema } from "effect" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { AppNotFoundError, InvalidRequestError } from "../errors" + +/** + * The app bridge: a running Redrob desktop app registers its own loopback MCP tool server so the + * agent can drive the app. The engine connects it as MCP server `app:` for every session, + * drops it when the app unregisters or stops sending heartbeats, and never writes it to the + * user's config. docs/APP-BRIDGE.md. No location: this is about the process, not a project. + */ +export const App = Schema.Struct({ + id: Schema.String, + name: Schema.String, + mcpUrl: Schema.String, + pid: Schema.Number, + version: Schema.optional(Schema.String), + tools: Schema.optional(Schema.Array(Schema.String)), + registeredAt: Schema.Number, + lastSeen: Schema.Number, +}).annotate({ identifier: "BridgedApp" }) + +export const AppRegistration = Schema.Struct({ + id: Schema.String, + /** Heartbeat deadline in ms: a registration with no heartbeat for this long is dropped. */ + ttlMs: Schema.Number, +}).annotate({ identifier: "BridgedAppRegistration" }) + +export const AppList = Schema.Struct({ + apps: Schema.Array(App), + ttlMs: Schema.Number, +}).annotate({ identifier: "BridgedAppList" }) + +export const AppRegisterRequest = Schema.Struct({ + name: Schema.String, + /** The app's MCP server; must be on this machine (127.0.0.1, localhost or [::1]). */ + mcp_url: Schema.String, + pid: Schema.Number, + version: Schema.optional(Schema.String), + tools: Schema.optional(Schema.Array(Schema.String)), +}).annotate({ identifier: "BridgedAppRegisterRequest" }) + +export const AppsGroup = HttpApiGroup.make("server.apps") + .add( + HttpApiEndpoint.get("apps.list", "/v1/apps", { success: AppList }).annotateMerge( + OpenApi.annotations({ + identifier: "v1.apps.list", + summary: "List bridged apps", + description: "The desktop apps whose MCP tool servers this engine is connected to.", + }), + ), + ) + .add( + HttpApiEndpoint.post("apps.register", "/v1/apps/register", { + payload: AppRegisterRequest, + success: AppRegistration, + error: InvalidRequestError, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v1.apps.register", + summary: "Bridge an app", + description: + "Register the app's loopback MCP server; the engine connects it as `app:` for every session. Send a heartbeat at least every ttlMs/2 while the app runs. 400 when mcp_url is not loopback.", + }), + ), + ) + .add( + HttpApiEndpoint.post("apps.heartbeat", "/v1/apps/:appID/heartbeat", { + params: { appID: Schema.String }, + success: App, + error: AppNotFoundError, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v1.apps.heartbeat", + summary: "Keep a bridge alive", + description: "404 when the registration expired or was never made; register again.", + }), + ), + ) + .add( + HttpApiEndpoint.delete("apps.unregister", "/v1/apps/:appID", { + params: { appID: Schema.String }, + success: HttpApiSchema.NoContent, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v1.apps.unregister", + summary: "Unbridge an app", + description: "The engine disconnects the app's MCP server and forgets the registration.", + }), + ), + ) diff --git a/packages/redrob/src/cli/cmd/serve.ts b/packages/redrob/src/cli/cmd/serve.ts index 8b09624335..86d7719868 100644 --- a/packages/redrob/src/cli/cmd/serve.ts +++ b/packages/redrob/src/cli/cmd/serve.ts @@ -56,6 +56,7 @@ export const ServeCommand = effectCmd({ if (Registry && secret) { const { setIdleExitAfterMs } = yield* Effect.promise(() => import("@redrob-code/server/handlers/clients")) const Clients = yield* Effect.promise(() => import("@redrob-code/server/clients")) + const Apps = yield* Effect.promise(() => import("@redrob-code/server/apps")) const id = Registry.newID() yield* Effect.promise(() => Registry.write({ @@ -87,6 +88,8 @@ export const ServeCommand = effectCmd({ const tick = setInterval(() => { void Registry.read().then((current) => { if (current && current.id !== id) return leave(0) + // Bridged apps first: dropping one detaches its client, which the client sweep then sees. + Apps.sweep() Clients.sweep() const idle = Clients.idleFor() if (idleExitMs !== undefined && idle !== undefined && idle >= idleExitMs) leave(0) diff --git a/packages/server/src/apps.ts b/packages/server/src/apps.ts new file mode 100644 index 0000000000..6a7e19affa --- /dev/null +++ b/packages/server/src/apps.ts @@ -0,0 +1,161 @@ +/** + * The app bridge: Redrob desktop apps that registered their own loopback MCP tool server with + * this engine (`POST /v1/apps/register`), so the agent can drive the app. docs/APP-BRIDGE.md. + * + * Plain module state like `clients.ts`, and for the same reason: the serve loop sweeps it from + * outside any request. The MCP service (which lives upstream of this package) subscribes to the + * registry to connect `app:` for every live instance and to drop it again; nothing here + * touches the user's config file. An app that crashes never unregisters, so an entry also expires + * when its heartbeats stop; a live app sends one every third of the TTL. + * + * Every bridged app also holds a `clients.ts` attachment, so the daemon's idle-exit clock does not + * run while an app is bridged. + */ +import { randomUUID } from "node:crypto" +import * as Clients from "./clients" + +export type App = { + readonly id: string + readonly name: string + readonly mcpUrl: string + readonly pid: number + readonly version?: string + /** Tool names the app announced; informational, the MCP client lists the real ones. */ + readonly tools?: readonly string[] + readonly registeredAt: number + readonly lastSeen: number +} + +/** Heartbeats are expected at least this often; a registration older than this is dropped. */ +export const TTL_MS = Clients.TTL_MS + +/** The MCP server name a bridged app is connected under. */ +export function mcpName(appName: string): string { + return `app:${appName}` +} + +export type Change = { readonly kind: "registered"; readonly app: App } | { readonly kind: "unregistered"; readonly app: App } +export type Listener = (change: Change) => void + +const apps = new Map() +/** The `clients.ts` attachment each registration holds. */ +const attachments = new Map() +const listeners = new Set() + +/** Hear every registration and removal. Returns the unsubscribe. */ +export function subscribe(listener: Listener): () => void { + listeners.add(listener) + return () => { + listeners.delete(listener) + } +} + +function emit(change: Change): void { + for (const listener of listeners) { + try { + listener(change) + } catch { + /* a listener's failure is its own */ + } + } +} + +/** + * Only an MCP server on this machine may be bridged: the daemon would otherwise hand every + * session's tool calls to whatever host an app named. + */ +export function isLoopback(mcpUrl: string): boolean { + let url: URL + try { + url = new URL(mcpUrl) + } catch { + return false + } + if (url.protocol !== "http:" && url.protocol !== "https:") return false + const host = url.hostname.toLowerCase() + return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1" +} + +/** A name that is safe as an MCP server key and as a tool-name prefix. */ +export function normalizeName(name: string): string { + return name + .trim() + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 64) +} + +export function register( + input: { name: string; mcpUrl: string; pid: number; version?: string; tools?: readonly string[] }, + now = Date.now(), +): App { + const name = normalizeName(input.name) || "app" + // One bridge per app name: a restarted app replaces the entry it left behind. + for (const existing of apps.values()) { + if (existing.name === name) unregister(existing.id, now) + } + const app: App = { + id: randomUUID(), + name, + mcpUrl: input.mcpUrl, + pid: input.pid, + ...(input.version !== undefined ? { version: input.version } : {}), + ...(input.tools !== undefined ? { tools: [...input.tools] } : {}), + registeredAt: now, + lastSeen: now, + } + apps.set(app.id, app) + attachments.set(app.id, Clients.attach({ app: mcpName(name), pid: input.pid }, now).id) + emit({ kind: "registered", app }) + return app +} + +export function heartbeat(id: string, now = Date.now()): App | undefined { + const app = apps.get(id) + if (!app) return undefined + const next = { ...app, lastSeen: now } + apps.set(id, next) + const attachment = attachments.get(id) + if (attachment && !Clients.heartbeat(attachment, now)) { + attachments.set(id, Clients.attach({ app: mcpName(app.name), pid: app.pid }, now).id) + } + return next +} + +export function unregister(id: string, now = Date.now()): boolean { + const app = apps.get(id) + if (!app) return false + apps.delete(id) + const attachment = attachments.get(id) + attachments.delete(id) + if (attachment) Clients.detach(attachment, now) + emit({ kind: "unregistered", app }) + return true +} + +/** Drop registrations whose heartbeats stopped; returns how many were dropped. */ +export function sweep(now = Date.now()): number { + let dropped = 0 + for (const [id, app] of apps) { + if (now - app.lastSeen > TTL_MS) { + unregister(id, now) + dropped += 1 + } + } + return dropped +} + +export function get(id: string): App | undefined { + return apps.get(id) +} + +export function list(): App[] { + return [...apps.values()].sort((a, b) => a.registeredAt - b.registeredAt) +} + +/** Test seam. */ +export function reset(): void { + for (const id of [...apps.keys()]) unregister(id) + listeners.clear() +} diff --git a/packages/server/src/clients.ts b/packages/server/src/clients.ts index 6cecc7be35..769148c55d 100644 --- a/packages/server/src/clients.ts +++ b/packages/server/src/clients.ts @@ -77,7 +77,9 @@ export function list(): Client[] { let lastRequestAt: number | undefined let inFlight = 0 -const STATUS_PATHS = ["/api/health", "/api/clients"] +// `/v1/apps` too: a bridged app holds an attachment of its own, so its heartbeats are not what +// keeps the daemon up, and the launcher polls the list. +const STATUS_PATHS = ["/api/health", "/api/clients", "/v1/apps"] /** Whether a request to this path keeps the daemon alive. */ export function countsAsActivity(path: string): boolean { diff --git a/packages/server/src/handlers.ts b/packages/server/src/handlers.ts index 5c1b7284e7..56267b0078 100644 --- a/packages/server/src/handlers.ts +++ b/packages/server/src/handlers.ts @@ -12,6 +12,7 @@ import { EventHandler } from "./handlers/event" import { AgentHandler } from "./handlers/agent" import { HealthHandler } from "./handlers/health" import { ClientsHandler } from "./handlers/clients" +import { AppsHandler } from "./handlers/apps" import { PtyHandler } from "./handlers/pty" import { BrowserRequestHandler } from "./handlers/browser-request" import { QuestionHandler } from "./handlers/question" @@ -26,6 +27,7 @@ import { ChatCompletionHandler } from "./handlers/chat-completion" export const handlers = Layer.mergeAll( HealthHandler, ClientsHandler, + AppsHandler, LocationHandler, AgentHandler, SessionHandler, diff --git a/packages/server/src/handlers/apps.ts b/packages/server/src/handlers/apps.ts new file mode 100644 index 0000000000..b54ac127a9 --- /dev/null +++ b/packages/server/src/handlers/apps.ts @@ -0,0 +1,63 @@ +import { AppNotFoundError, InvalidRequestError } from "@redrob-code/protocol/errors" +import { Effect } from "effect" +import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { Api } from "../api" +import * as Apps from "../apps" + +export const AppsHandler = HttpApiBuilder.group(Api, "server.apps", (handlers) => + handlers + .handle( + "apps.list", + Effect.fn(function* () { + Apps.sweep() + return { apps: Apps.list(), ttlMs: Apps.TTL_MS } + }), + ) + .handle( + "apps.register", + Effect.fn(function* (ctx) { + const { name, mcp_url, pid, version, tools } = ctx.payload + if (!Apps.normalizeName(name)) { + return yield* new InvalidRequestError({ message: "name must not be empty", field: "name" }) + } + if (!Apps.isLoopback(mcp_url)) { + return yield* new InvalidRequestError({ + message: "mcp_url must be an http(s) URL on 127.0.0.1, localhost or [::1]", + kind: "not_loopback", + field: "mcp_url", + }) + } + if (!Number.isInteger(pid) || pid <= 0) { + return yield* new InvalidRequestError({ message: "pid must be a positive integer", field: "pid" }) + } + const app = Apps.register({ + name, + mcpUrl: mcp_url, + pid, + ...(version !== undefined ? { version } : {}), + ...(tools !== undefined ? { tools } : {}), + }) + return { id: app.id, ttlMs: Apps.TTL_MS } + }), + ) + .handle( + "apps.heartbeat", + Effect.fn(function* (ctx) { + const app = Apps.heartbeat(ctx.params.appID) + if (!app) { + return yield* new AppNotFoundError({ + appID: ctx.params.appID, + message: "This registration expired or was never made; register again", + }) + } + return app + }), + ) + .handle( + "apps.unregister", + Effect.fn(function* (ctx) { + Apps.unregister(ctx.params.appID) + return HttpApiSchema.NoContent.make() + }), + ), +) From ccc718b9fd650e1d2356d38924b79fa434826a99 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:28:55 +0000 Subject: [PATCH 07/23] mcp: connect bridged apps as app: at runtime The MCP service seeds every new instance with the apps already registered and subscribes to the registry so a registration connects app: on every live instance and an unregistration (or TTL expiry) disconnects it and drops its tools. Adds remove(name) to the service. Runtime only: the user's config file is never written. --- packages/redrob/src/mcp/index.ts | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/packages/redrob/src/mcp/index.ts b/packages/redrob/src/mcp/index.ts index 9c63341541..57f96ecc7b 100644 --- a/packages/redrob/src/mcp/index.ts +++ b/packages/redrob/src/mcp/index.ts @@ -34,6 +34,7 @@ import { CrossSpawnSpawner } from "@redrob-code/core/cross-spawn-spawner" import { McpCatalog } from "./catalog" import { McpEvent } from "@redrob-code/schema/mcp-event" import { McpBrowser } from "./browser" +import * as Apps from "@redrob-code/server/apps" const DEFAULT_TIMEOUT = 30_000 const CLIENT_OPTIONS = { @@ -172,6 +173,8 @@ export interface Interface { clientName?: string, ) => Effect.Effect> readonly add: (name: string, mcp: ConfigMCPV1.Info) => Effect.Effect<{ status: Record | Status }> + /** Forget a server added at runtime (`add`): disconnect it and drop its tools. Not for configured servers. */ + readonly remove: (name: string) => Effect.Effect readonly connect: (name: string) => Effect.Effect readonly disconnect: (name: string) => Effect.Effect readonly getPrompt: ( @@ -489,6 +492,21 @@ const layer = Layer.effect( } } + /** + * Every instance state that is up, with a bridge that runs effects in that instance's context. + * The app bridge (docs/APP-BRIDGE.md) registers and drops servers from outside any request, so + * it has to reach every live instance by hand; the cache keyed by directory cannot be walked. + */ + const live = new Set<{ s: State; bridge: EffectBridge.Shape }>() + + /** The runtime MCP entry for a bridged app: its loopback server, no OAuth, never in the config file. */ + const bridgedConfig = (app: Apps.App): ConfigMCPV1.Info => ({ + type: "remote", + url: app.mcpUrl, + enabled: true, + oauth: false, + }) + const state = yield* InstanceState.make( Effect.fn("MCP.state")(function* () { const cfg = yield* cfgSvc.get() @@ -528,8 +546,33 @@ const layer = Layer.effect( { concurrency: "unbounded" }, ) + // Apps bridged before this instance came up get their tools here; ones bridged later + // arrive through the registry subscription below. + yield* Effect.forEach( + Apps.list(), + (app) => + Effect.gen(function* () { + const key = Apps.mcpName(app.name) + const mcp = bridgedConfig(app) + s.config[key] = mcp + const result = yield* create(key, mcp) + s.status[key] = result.status + if (result.mcpClient) { + s.clients[key] = result.mcpClient + s.defs[key] = result.defs! + if (result.instructions) s.instructions[key] = result.instructions + watch(s, key, result.mcpClient, bridge, mcp.timeout) + } + }), + { concurrency: "unbounded" }, + ) + + const entry = { s, bridge } + live.add(entry) + yield* Effect.addFinalizer(() => Effect.gen(function* () { + live.delete(entry) const clients = Object.values(s.clients) s.clients = {} s.defs = {} @@ -658,6 +701,36 @@ const layer = Layer.effect( s.status[name] = { status: "disabled" } }) + const remove = Effect.fn("MCP.remove")(function* (name: string) { + const s = yield* InstanceState.get(state) + yield* closeClient(s, name) + delete s.config[name] + delete s.status[name] + yield* events.publish(ToolsChanged, { server: name }).pipe(Effect.ignore) + }) + + // The app bridge: connect a newly registered app for every live instance, and drop it again + // when the app unregisters or its heartbeats stop. Runs outside any request, hence the + // per-instance bridges. The user's config file is never touched. + const unsubscribe = Apps.subscribe((change) => { + const key = Apps.mcpName(change.app.name) + for (const { bridge } of live) { + const update: Effect.Effect = + change.kind === "registered" + ? add(key, bridgedConfig(change.app)).pipe(Effect.andThen(events.publish(ToolsChanged, { server: key }))) + : remove(key) + bridge.fork( + update.pipe( + Effect.catchCause((cause) => + Effect.logWarning("app bridge update failed", { server: key, cause: Cause.pretty(cause) }), + ), + Effect.asVoid, + ), + ) + } + }) + yield* Effect.addFinalizer(() => Effect.sync(unsubscribe)) + function requestTimeout(s: State, name: string, configured: McpEntry | undefined, fallback?: number) { const staticTimeout = configured && isMcpConfigured(configured) ? configured.timeout : undefined return s.config[name]?.timeout ?? staticTimeout ?? fallback @@ -978,6 +1051,7 @@ const layer = Layer.effect( resources, resourceTemplates, add, + remove, connect, disconnect, getPrompt, From 7f6de9d759a34906da03c05c1c8936b2da2ffdde Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:28:55 +0000 Subject: [PATCH 08/23] sdk, engine-client, cli: app bridge clients JS SDK (@redrob-labs/sdk/daemon): apps.register/heartbeat/unregister/list and bridgeApp() that registers and heartbeats at ttl/3 until stop(). Rust (redrob-engine-client): register_app/heartbeat_app/unregister_app/apps and bridge_app() with a heartbeat thread that re-registers on 404. CLI: redrob apps list [--json] and redrob apps unregister . --- crates/engine-client/src/lib.rs | 204 ++++++++++++++++++++++++++++ packages/redrob/src/cli/cmd/apps.ts | 113 +++++++++++++++ packages/redrob/src/index.ts | 2 + packages/sdk/js/src/daemon.ts | 133 ++++++++++++++++++ 4 files changed, 452 insertions(+) create mode 100644 packages/redrob/src/cli/cmd/apps.ts diff --git a/crates/engine-client/src/lib.rs b/crates/engine-client/src/lib.rs index 210207662e..cdba2a8810 100644 --- a/crates/engine-client/src/lib.rs +++ b/crates/engine-client/src/lib.rs @@ -266,6 +266,210 @@ pub fn stop() -> Result { Ok(read_record().is_none()) } +// --------------------------------------------------------------------------------------------- +// The app bridge: hand the daemon the app's own MCP tool server. The engine's docs/APP-BRIDGE.md. +// --------------------------------------------------------------------------------------------- + +/// One app whose MCP server the daemon is connected to (`GET /v1/apps`). +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct App { + pub id: String, + pub name: String, + #[serde(rename = "mcpUrl")] + pub mcp_url: String, + pub pid: u32, + #[serde(default)] + pub version: Option, + #[serde(default)] + pub tools: Option>, + #[serde(rename = "registeredAt")] + pub registered_at: u64, + #[serde(rename = "lastSeen")] + pub last_seen: u64, +} + +/// `GET /v1/apps`. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Apps { + pub apps: Vec, + #[serde(rename = "ttlMs")] + pub ttl_ms: u64, +} + +/// `POST /v1/apps/register` answered. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppRegistration { + pub id: String, + /// Heartbeat deadline in ms; send one at least every `ttl_ms / 2`. + #[serde(rename = "ttlMs")] + pub ttl_ms: u64, +} + +/// What an app tells the daemon about itself when it bridges. +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct AppRegisterInput { + /// The engine exposes the app's tools under MCP server `app:`. + pub name: String, + /// The app's MCP server. Must be loopback (127.0.0.1, localhost or [::1]) or the engine answers 400. + pub mcp_url: String, + /// Defaults to this process. + pub pid: Option, + pub version: Option, + pub tools: Option>, +} + +impl AppRegisterInput { + pub fn new(name: impl Into, mcp_url: impl Into) -> Self { + Self { name: name.into(), mcp_url: mcp_url.into(), pid: None, version: None, tools: None } + } +} + +fn send_json Deserialize<'de>>(record: &Record, method: &str, path: &str, body: Option<&str>) -> Result, String> { + let url = format!("{}{}", record.url.trim_end_matches('/'), path); + let auth = authorization(&record.secret); + let agent = agent(); + let mut response = match (method, body) { + ("POST", Some(json)) => agent.post(&url).header("Authorization", &auth).header("Content-Type", "application/json").send(json), + ("POST", None) => agent.post(&url).header("Authorization", &auth).send_empty(), + ("DELETE", _) => agent.delete(&url).header("Authorization", &auth).call(), + _ => agent.get(&url).header("Authorization", &auth).call(), + } + .map_err(|e| e.to_string())?; + let status = response.status().as_u16(); + match status { + 200 => { + let text = response.body_mut().read_to_string().map_err(|e| e.to_string())?; + serde_json::from_str(&text).map(Some).map_err(|e| e.to_string()) + } + 204 => Ok(None), + 404 => Ok(None), + _ => { + let text = response.body_mut().read_to_string().unwrap_or_default(); + Err(format!("{method} {path} answered {status}{}", if text.is_empty() { String::new() } else { format!(": {text}") })) + } + } +} + +/// `POST /v1/apps/register`: bridge the app. The daemon connects `mcp_url` as MCP server +/// `app:` for every session until the app unregisters or stops sending heartbeats. +pub fn register_app(record: &Record, input: &AppRegisterInput) -> Result { + #[derive(Serialize)] + struct Body<'a> { + name: &'a str, + mcp_url: &'a str, + pid: u32, + #[serde(skip_serializing_if = "Option::is_none")] + version: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + tools: Option<&'a [String]>, + } + let body = serde_json::to_string(&Body { + name: &input.name, + mcp_url: &input.mcp_url, + pid: input.pid.unwrap_or_else(std::process::id), + version: input.version.as_deref(), + tools: input.tools.as_deref(), + }) + .map_err(|e| e.to_string())?; + send_json(record, "POST", "/v1/apps/register", Some(&body))?.ok_or_else(|| "the engine answered register with no body".to_string()) +} + +/// `POST /v1/apps/:id/heartbeat`. `Ok(None)` means the registration is gone (404): register again. +pub fn heartbeat_app(record: &Record, id: &str) -> Result, String> { + send_json(record, "POST", &format!("/v1/apps/{id}/heartbeat"), None) +} + +/// `DELETE /v1/apps/:id`. A registration already gone is not an error. +pub fn unregister_app(record: &Record, id: &str) -> Result<(), String> { + send_json::(record, "DELETE", &format!("/v1/apps/{id}"), None).map(|_| ()) +} + +/// `GET /v1/apps`: who is bridged. +pub fn apps(record: &Record) -> Result { + get_json(record, "/v1/apps") +} + +/// A live bridge: registered, and kept alive by a heartbeat thread until [`AppBridge::stop`] or drop. +pub struct AppBridge { + id: std::sync::Arc>, + stop: std::sync::Arc, + thread: Option>, + record: Record, +} + +impl AppBridge { + /// The current registration id; changes when the bridge had to register again. + pub fn id(&self) -> String { + self.id.lock().map(|g| g.clone()).unwrap_or_default() + } + + /// Unregister and stop the heartbeat. Dropping the bridge does the same. + pub fn stop(mut self) { + self.shutdown(); + } + + fn shutdown(&mut self) { + if self.stop.swap(true, std::sync::atomic::Ordering::SeqCst) { + return; + } + if let Some(thread) = self.thread.take() { + thread.thread().unpark(); + let _ = thread.join(); + } + let id = self.id(); + if let Err(e) = unregister_app(&self.record, &id) { + log::warn!("could not unbridge {id}: {e}"); + } + } +} + +impl Drop for AppBridge { + fn drop(&mut self) { + self.shutdown(); + } +} + +/// Register the app's MCP server and keep the registration alive: a heartbeat every third of the +/// daemon's TTL on a background thread, and a fresh registration when the daemon answers 404 (it +/// restarted, or dropped us). Call `stop()` (or drop the bridge) when the app quits; an app that +/// cannot (it crashed) is dropped after the TTL. +pub fn bridge_app(record: &Record, input: AppRegisterInput) -> Result { + let registration = register_app(record, &input)?; + let every = Duration::from_millis((registration.ttl_ms / 3).max(5_000)); + let id = std::sync::Arc::new(std::sync::Mutex::new(registration.id)); + let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let thread = { + let id = id.clone(); + let stop = stop.clone(); + let record = record.clone(); + std::thread::Builder::new() + .name(format!("redrob-app-bridge-{}", input.name)) + .spawn(move || { + loop { + std::thread::park_timeout(every); + if stop.load(std::sync::atomic::Ordering::SeqCst) { + return; + } + let current = id.lock().map(|g| g.clone()).unwrap_or_default(); + match heartbeat_app(&record, ¤t) { + Ok(Some(_)) => {} + Ok(None) => match register_app(&record, &input) { + Ok(again) => { + if let Ok(mut g) = id.lock() { + *g = again.id; + } + } + Err(e) => log::warn!("could not bridge {} again: {e}", input.name), + }, + Err(e) => log::debug!("app bridge heartbeat failed: {e}"), + } + } + }) + .map_err(|e| format!("could not start the heartbeat thread: {e}"))? + }; + Ok(AppBridge { id, stop, thread: Some(thread), record: record.clone() }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/redrob/src/cli/cmd/apps.ts b/packages/redrob/src/cli/cmd/apps.ts new file mode 100644 index 0000000000..7d851ce1bc --- /dev/null +++ b/packages/redrob/src/cli/cmd/apps.ts @@ -0,0 +1,113 @@ +/** + * `redrob apps list | unregister `: the desktop apps whose MCP tool servers the shared daemon + * is bridged to (docs/APP-BRIDGE.md). Talks to the daemon the record names; spawns nothing. + */ +import { cmd } from "./cmd" +import { UI } from "@/cli/ui" +import { Daemon } from "@/daemon/connect" + +type BridgedApp = { + id: string + name: string + mcpUrl: string + pid: number + version?: string + tools?: string[] + registeredAt: number + lastSeen: number +} + +type BridgedAppList = { apps: BridgedApp[]; ttlMs: number } + +function age(sinceMs: number, now = Date.now()): string { + const seconds = Math.max(0, Math.floor((now - sinceMs) / 1000)) + if (seconds < 60) return `${seconds}s` + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes}m` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h${minutes % 60}m` + return `${Math.floor(hours / 24)}d${hours % 24}h` +} + +function pad(text: string, width: number): string { + return text.length >= width ? text : text + " ".repeat(width - text.length) +} + +async function daemon(json: boolean): Promise { + const connection = await Daemon.current() + if (!connection) { + if (json) process.stdout.write(JSON.stringify(null) + "\n") + else UI.println(`${UI.Style.TEXT_DIM}engine not running${UI.Style.TEXT_NORMAL}`) + process.exitCode = 1 + } + return connection +} + +export const AppsCommand = cmd({ + command: "apps", + describe: "the desktop apps bridged to the shared engine (their MCP tool servers)", + builder: (yargs) => + yargs + .command( + cmd({ + command: "list", + describe: "list bridged apps", + builder: (yargs) => yargs.option("json", { type: "boolean", default: false, describe: "print the list as JSON" }), + handler: async (args) => { + const connection = await daemon(args.json) + if (!connection) return + const response = await fetch(`${connection.url.replace(/\/+$/, "")}/v1/apps`, { headers: connection.headers }) + if (!response.ok) { + UI.error(`the engine answered ${response.status} to GET /v1/apps`) + process.exitCode = 1 + return + } + const list = (await response.json()) as BridgedAppList + if (args.json) { + process.stdout.write(JSON.stringify(list) + "\n") + return + } + if (list.apps.length === 0) { + UI.println(`${UI.Style.TEXT_DIM}no apps bridged${UI.Style.TEXT_NORMAL}`) + return + } + const now = Date.now() + const rows = list.apps.map((app) => [ + app.name, + String(app.pid), + app.mcpUrl, + app.tools ? String(app.tools.length) : "-", + age(app.registeredAt, now), + app.id, + ]) + const header = ["NAME", "PID", "MCP URL", "TOOLS", "AGE", "ID"] + const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]!.length))) + UI.println(UI.Style.TEXT_DIM + header.map((h, i) => pad(h, widths[i]!)).join(" ") + UI.Style.TEXT_NORMAL) + for (const row of rows) UI.println(row.map((cell, i) => pad(cell, widths[i]!)).join(" ")) + }, + }), + ) + .command( + cmd({ + command: "unregister ", + describe: "drop a bridged app: the engine disconnects its MCP server", + builder: (yargs) => yargs.positional("id", { type: "string", demandOption: true, describe: "the registration id from `redrob apps list`" }), + handler: async (args) => { + const connection = await daemon(false) + if (!connection) return + const response = await fetch(`${connection.url.replace(/\/+$/, "")}/v1/apps/${encodeURIComponent(args.id)}`, { + method: "DELETE", + headers: connection.headers, + }) + if (!response.ok) { + UI.error(`the engine answered ${response.status} to DELETE /v1/apps/${args.id}`) + process.exitCode = 1 + return + } + UI.println("unregistered") + }, + }), + ) + .demandCommand(), + handler: () => {}, +}) diff --git a/packages/redrob/src/index.ts b/packages/redrob/src/index.ts index bc82748ac8..ffd1d5e501 100644 --- a/packages/redrob/src/index.ts +++ b/packages/redrob/src/index.ts @@ -14,6 +14,7 @@ import { InstallationVersion } from "@redrob-code/core/installation/version" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" import { ServiceCommand } from "./cli/cmd/service" +import { AppsCommand } from "./cli/cmd/apps" import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" import { McpCommand } from "./cli/cmd/mcp" @@ -93,6 +94,7 @@ const cli = yargs(args) .command(UninstallCommand) .command(ServeCommand) .command(ServiceCommand) + .command(AppsCommand) .command(ModelsCommand) .command(ModelsLocalCommand) .command(StatsCommand) diff --git a/packages/sdk/js/src/daemon.ts b/packages/sdk/js/src/daemon.ts index eb93c46c59..60cb4c440a 100644 --- a/packages/sdk/js/src/daemon.ts +++ b/packages/sdk/js/src/daemon.ts @@ -250,3 +250,136 @@ export async function attach( }, } } + +// --------------------------------------------------------------------------------------------- +// The app bridge: hand the daemon the app's own MCP tool server. docs/APP-BRIDGE.md. +// --------------------------------------------------------------------------------------------- + +export type BridgedApp = { + readonly id: string + readonly name: string + readonly mcpUrl: string + readonly pid: number + readonly version?: string + readonly tools?: readonly string[] + readonly registeredAt: number + readonly lastSeen: number +} + +export type BridgedAppList = { readonly apps: readonly BridgedApp[]; readonly ttlMs: number } + +export type AppRegistration = { readonly id: string; readonly ttlMs: number } + +export type AppRegisterInput = { + /** The app's name; the engine exposes its tools under MCP server `app:`. */ + readonly name: string + /** The app's MCP server. Must be loopback (127.0.0.1, localhost or [::1]) or the engine answers 400. */ + readonly mcpUrl: string + readonly pid?: number + readonly version?: string + readonly tools?: readonly string[] +} + +function appsBase(connection: DaemonConnection): string { + return `${connection.url.replace(/\/+$/, "")}/v1/apps` +} + +async function refuse(response: Response, what: string): Promise { + const body = await response.text().catch(() => "") + throw new Error(`The engine refused to ${what} (${response.status})${body ? `: ${body}` : ""}`) +} + +export const apps = { + /** `POST /v1/apps/register`. */ + async register( + connection: DaemonConnection, + input: AppRegisterInput, + fetchImpl: typeof fetch = fetch, + ): Promise { + const response = await fetchImpl(`${appsBase(connection)}/register`, { + method: "POST", + headers: { ...connection.headers, "content-type": "application/json" }, + body: JSON.stringify({ + name: input.name, + mcp_url: input.mcpUrl, + pid: input.pid ?? process.pid, + ...(input.version !== undefined ? { version: input.version } : {}), + ...(input.tools !== undefined ? { tools: input.tools } : {}), + }), + }) + if (!response.ok) return refuse(response, "bridge the app") + return (await response.json()) as AppRegistration + }, + + /** `POST /v1/apps/:id/heartbeat`. Resolves to undefined on 404: the registration is gone, register again. */ + async heartbeat(connection: DaemonConnection, id: string, fetchImpl: typeof fetch = fetch): Promise { + const response = await fetchImpl(`${appsBase(connection)}/${encodeURIComponent(id)}/heartbeat`, { + method: "POST", + headers: connection.headers, + }) + if (response.status === 404) return undefined + if (!response.ok) return refuse(response, "keep the bridge alive") + return (await response.json()) as BridgedApp + }, + + /** `DELETE /v1/apps/:id`. */ + async unregister(connection: DaemonConnection, id: string, fetchImpl: typeof fetch = fetch): Promise { + const response = await fetchImpl(`${appsBase(connection)}/${encodeURIComponent(id)}`, { + method: "DELETE", + headers: connection.headers, + }) + if (!response.ok && response.status !== 404) return refuse(response, "unbridge the app") + }, + + /** `GET /v1/apps`. */ + async list(connection: DaemonConnection, fetchImpl: typeof fetch = fetch): Promise { + const response = await fetchImpl(appsBase(connection), { headers: connection.headers }) + if (!response.ok) return refuse(response, "list bridged apps") + return (await response.json()) as BridgedAppList + }, +} + +export type AppBridge = { + /** The current registration id; changes when the bridge had to register again. */ + readonly id: string + /** Unregister and stop the heartbeat. Safe to call twice. */ + readonly stop: () => Promise +} + +/** + * Register the app's MCP server and keep the registration alive: a heartbeat every third of the + * daemon's TTL, and a fresh registration when the daemon answers 404 (it restarted, or dropped + * us). Call `stop()` when the app quits; an app that cannot (it crashed) is dropped after the TTL. + */ +export async function bridgeApp( + connection: DaemonConnection, + input: AppRegisterInput, + options: { readonly fetch?: typeof fetch } = {}, +): Promise { + const fetchImpl = options.fetch ?? fetch + const registration = await apps.register(connection, input, fetchImpl) + let id = registration.id + const every = Math.max(5_000, Math.floor(registration.ttlMs / 3)) + let stopped = false + const beat = async () => { + if (stopped) return + const alive = await apps.heartbeat(connection, id, fetchImpl).catch(() => null) + if (alive === undefined && !stopped) { + const again = await apps.register(connection, input, fetchImpl).catch(() => undefined) + if (again) id = again.id + } + } + const timer = setInterval(() => void beat(), every) + timer.unref?.() + return { + get id() { + return id + }, + stop: async () => { + if (stopped) return + stopped = true + clearInterval(timer) + await apps.unregister(connection, id, fetchImpl).catch(() => undefined) + }, + } +} From 13f720e8c4fb6c61fd1b96ce3890104b27e028f9 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:28:55 +0000 Subject: [PATCH 09/23] docs: the app bridge docs/APP-BRIDGE.md: endpoints and payloads, TTL and heartbeat, the loopback rule, the app: MCP naming, JS and Rust examples, the CLI. A pointer section in LOCAL-ENGINE-API.md. --- docs/APP-BRIDGE.md | 162 +++++++++++++++++++++++++++++++++++++++ docs/LOCAL-ENGINE-API.md | 11 +++ 2 files changed, 173 insertions(+) create mode 100644 docs/APP-BRIDGE.md diff --git a/docs/APP-BRIDGE.md b/docs/APP-BRIDGE.md new file mode 100644 index 0000000000..ab5b4cee7c --- /dev/null +++ b/docs/APP-BRIDGE.md @@ -0,0 +1,162 @@ +# The app bridge + +A running Redrob desktop app (Browser, Office, Canvas, Shape, ...) lets the agent drive +it by serving its own MCP tool server on loopback and registering that server with the +shared daemon. The daemon connects it as an MCP server for every session, so a prompt in +any Redrob surface can call the app's tools while the app is up, and forgets it the +moment the app goes away. Companion to [LOCAL-ENGINE-API.md](LOCAL-ENGINE-API.md), +which covers the other direction (the app calling the engine). + +## What the engine does with a registration + +- Connects `mcp_url` as MCP server **`app:`** on every live instance, and on every + instance that starts later while the registration stands. Its tools show up to the + agent under the usual MCP naming (`app:` as the server, the app's own tool names + below it). `tool_list_changed` notifications from the app are honoured. +- Keeps the registration **in memory only**. The user's `redrob.json` is never written; + a restarted daemon starts with no bridged apps, and the app registers again (the SDK + helpers do this on a 404). +- Holds a **client attachment** (`/api/clients`) for the app, so the daemon's idle-exit + clock does not run while an app is bridged. Heartbeats to `/v1/apps` are status + traffic, not activity: the attachment is what keeps the daemon up. +- **Drops** the registration when the app `DELETE`s it or when no heartbeat arrives for + `ttlMs` (90 s): the MCP client is closed, the tools disappear, the attachment is + detached. The serve loop sweeps every 10 s; `GET /v1/apps` sweeps on read. +- Accepts **loopback only**: the host of `mcp_url` must be `127.0.0.1`, `localhost` or + `[::1]`, scheme `http` or `https`. Anything else is a 400 `not_loopback`. The daemon + would otherwise forward every session's tool calls to whatever host an app named. +- One bridge per name: registering a name that is already bridged replaces the older + registration (the restarted app wins; the stale one is unregistered first). + +Names are normalised to `[a-z0-9._-]`, lower-cased, at most 64 characters. + +## Endpoints + +All under the daemon's base URL from `~/.redrob/run/server.json`, with the record's +`Authorization: Basic redrob:` header. Paths are exact. + +### `POST /v1/apps/register` + +```json +{ "name": "canvas", "mcp_url": "http://127.0.0.1:43127/mcp", "pid": 48211, "version": "0.8.1", "tools": ["select", "move"] } +``` + +`version` and `tools` are optional; `tools` is informational (the engine lists the real +ones from the server). Answer, 200: + +```json +{ "id": "7f3b...-...", "ttlMs": 90000 } +``` + +400 (`InvalidRequestError`, `kind: "not_loopback"`, `field: "mcp_url"`) when the URL is +not loopback; 400 when `name` is empty or `pid` is not a positive integer. + +### `POST /v1/apps/:id/heartbeat` + +No body. 200 with the app record (below); **404** (`AppNotFoundError`) when the +registration expired or the daemon restarted -- register again. Send one at least every +`ttlMs / 2`; the helpers send every `ttlMs / 3`. + +### `DELETE /v1/apps/:id` + +204. Idempotent: an id that is already gone is still 204. + +### `GET /v1/apps` + +```json +{ + "apps": [ + { + "id": "7f3b...", + "name": "canvas", + "mcpUrl": "http://127.0.0.1:43127/mcp", + "pid": 48211, + "version": "0.8.1", + "tools": ["select", "move"], + "registeredAt": 1760620000000, + "lastSeen": 1760620030000 + } + ], + "ttlMs": 90000 +} +``` + +## From JavaScript (`@redrob-labs/sdk/daemon`) + +```ts +import { connectOrStart, bridgeApp, apps } from "@redrob-labs/sdk/daemon" + +// 1. Serve the app's tools over MCP on a loopback port (any MCP server library works). +const mcpUrl = await startMyMcpServer({ host: "127.0.0.1" }) // e.g. http://127.0.0.1:43127/mcp + +// 2. Find (or start) the shared daemon and bridge. +const connection = await connectOrStart({ idleExitMinutes: 10 }) +const bridge = await bridgeApp(connection, { + name: "canvas", + mcpUrl, + version: app.getVersion(), + tools: ["select", "move", "export_png"], +}) +// bridge.id -- the registration; heartbeats run on their own, re-registering after a daemon restart. + +// 3. On quit. +app.on("before-quit", () => void bridge.stop()) + +// Who else is bridged (what `redrob apps list` prints): +const { apps: bridged } = await apps.list(connection) +``` + +`apps.register / apps.heartbeat / apps.unregister / apps.list` are the four calls one +at a time; `bridgeApp` is the loop built on them. + +## From Rust (`redrob-engine-client`) + +```rust +use redrob_engine_client::{connect_or_start_as, bridge_app, apps, AppRegisterInput}; + +// 1. Serve the app's tools over MCP on loopback (rmcp or any MCP server crate). +let mcp_url = my_mcp_server.start_loopback()?; // "http://127.0.0.1:43127/mcp" + +// 2. Find or start the daemon and bridge; the heartbeat runs on a background thread. +let record = connect_or_start_as("shape", 10)?; +let bridge = bridge_app( + &record, + AppRegisterInput { + version: Some(env!("CARGO_PKG_VERSION").into()), + tools: Some(vec!["select".into(), "extrude".into()]), + ..AppRegisterInput::new("shape", mcp_url) + }, +)?; +log::info!("bridged as {}", bridge.id()); + +// Who else is bridged: +for app in apps(&record)?.apps { log::info!("{} pid {} {}", app.name, app.pid, app.mcp_url); } + +// 3. On quit: unregisters and joins the heartbeat thread. Dropping the bridge does the same. +bridge.stop(); +``` + +`register_app / heartbeat_app / unregister_app / apps` are the four calls; `bridge_app` +is the loop. `heartbeat_app` answers `Ok(None)` on a 404 so the caller knows to register +again. + +## From the command line + +``` +redrob apps list # NAME PID MCP URL TOOLS AGE ID +redrob apps list --json +redrob apps unregister # the engine disconnects that app's MCP server +``` + +## Writing the app's MCP server + +- Bind to `127.0.0.1` (or `::1`) on an ephemeral port; never `0.0.0.0`. +- Streamable HTTP is tried first, then SSE, with a 30 s connect timeout. +- Tool names are the app's own; the engine prefixes them with the server name, so keep + them short and verb-first (`select`, `move_layer`, `export_png`). +- Send `notifications/tools/list_changed` when the set of tools changes (a document + opened, a mode switched); the engine re-lists. +- The server speaks only to the daemon on the same machine, which authenticates to the + app by nothing more than being local. Do not expose destructive operations the user + did not opt into; the engine's own permission prompts cover tool calls as for any MCP + server. diff --git a/docs/LOCAL-ENGINE-API.md b/docs/LOCAL-ENGINE-API.md index f38cbeb0f2..8219ee6e82 100644 --- a/docs/LOCAL-ENGINE-API.md +++ b/docs/LOCAL-ENGINE-API.md @@ -248,6 +248,17 @@ run `redrob upgrade` instead of failing with a transport error. when a field is removed or its type changes. Without that test "we keep compatibility" is a sentence, not a guarantee. +### The app bridge: the engine drives the app + +The routes above let an app call the engine. The reverse -- the agent driving a running +app -- is the app bridge: the app serves its own MCP tool server on loopback and +registers it with the daemon (`POST /v1/apps/register`, heartbeat, `DELETE`). The +daemon connects it as MCP server `app:` for every session and drops it when the +app unregisters or its heartbeats stop; a bridged app also counts as attached, so the +idle-exit clock does not run while it is up. Only loopback URLs are accepted and +nothing is written to the user's config. Full contract, payloads and SDK/Rust examples: +[docs/APP-BRIDGE.md](APP-BRIDGE.md). `redrob apps list` shows who is bridged. + ## 4. Migration | product | change | From 9e00920612c366d33755f9ab9bcf90671aa61d5e Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:28:55 +0000 Subject: [PATCH 10/23] test: MCP mocks grow the remove stub the Interface now requires --- packages/redrob/test/session/prompt.test.ts | 1 + packages/redrob/test/session/snapshot-tool-race.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/redrob/test/session/prompt.test.ts b/packages/redrob/test/session/prompt.test.ts index 2c194e633d..3d20eb2eeb 100644 --- a/packages/redrob/test/session/prompt.test.ts +++ b/packages/redrob/test/session/prompt.test.ts @@ -123,6 +123,7 @@ function makeMcp(instructions: MCP.ServerInstructions[] = []) { add: () => Effect.succeed({ status: { status: "disabled" as const } }), connect: () => Effect.void, disconnect: () => Effect.void, + remove: () => Effect.void, getPrompt: () => Effect.succeed(undefined), readResource: () => Effect.succeed(undefined), startAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"), diff --git a/packages/redrob/test/session/snapshot-tool-race.test.ts b/packages/redrob/test/session/snapshot-tool-race.test.ts index 845ac36b8e..2b65152e6d 100644 --- a/packages/redrob/test/session/snapshot-tool-race.test.ts +++ b/packages/redrob/test/session/snapshot-tool-race.test.ts @@ -45,6 +45,7 @@ const mcp = Layer.succeed( add: () => Effect.succeed({ status: { status: "disabled" as const } }), connect: () => Effect.void, disconnect: () => Effect.void, + remove: () => Effect.void, getPrompt: () => Effect.succeed(undefined), readResource: () => Effect.succeed(undefined), startAuth: () => Effect.die("unexpected MCP auth"), From 4755df0e628a424f0d47e6eec46c1e6459ce072f Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:34:54 +0000 Subject: [PATCH 11/23] feat(channels): Telegram transport over grammY long polling @redrob-code/channels-telegram: TelegramTransport + telegramTransport factory. Chat id is the conversation, user id the sender; groups answer only mentions and replies; typing indicator; 4096 max chunk. Secret telegram.botToken documented in the package README and the CHANNELS.md secrets table. --- bun.lock | 29 ++- docs/CHANNELS.md | 8 + packages/channels/telegram/README.md | 47 +++++ packages/channels/telegram/package.json | 26 +++ packages/channels/telegram/src/index.ts | 4 + packages/channels/telegram/src/transport.ts | 211 ++++++++++++++++++++ packages/channels/telegram/tsconfig.json | 8 + 7 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 packages/channels/telegram/README.md create mode 100644 packages/channels/telegram/package.json create mode 100644 packages/channels/telegram/src/index.ts create mode 100644 packages/channels/telegram/src/transport.ts create mode 100644 packages/channels/telegram/tsconfig.json diff --git a/bun.lock b/bun.lock index a924ab932a..264d4c7b2f 100644 --- a/bun.lock +++ b/bun.lock @@ -41,6 +41,21 @@ "typescript": "catalog:", }, }, + "packages/channels/telegram": { + "name": "@redrob-code/channels-telegram", + "version": "0.0.0", + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "grammy": "1.46.0", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, "packages/cli": { "name": "@redrob-code/cli", "version": "0.0.0", @@ -975,6 +990,8 @@ "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="], + "@grammyjs/types": ["@grammyjs/types@5.0.0", "", {}, "sha512-iq1Qrq1iPKkB8yAa0qSuIURMZOCuqTY5pWy5gHpCeL1oQ+GPadGhw/cDTVE8waJwuCzacUzuIjRv1sESvk7u7A=="], + "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="], @@ -1295,6 +1312,8 @@ "@redrob-code/channels-core": ["@redrob-code/channels-core@workspace:packages/channels/core"], + "@redrob-code/channels-telegram": ["@redrob-code/channels-telegram@workspace:packages/channels/telegram"], + "@redrob-code/cli": ["@redrob-code/cli@workspace:packages/cli"], "@redrob-code/client": ["@redrob-code/client@workspace:packages/client"], @@ -2077,6 +2096,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "grammy": ["grammy@1.46.0", "", { "dependencies": { "@grammyjs/types": "5.0.0", "abort-controller": "^3.0.0", "debug": "^4.4.3", "node-fetch": "^2.7.0" } }, "sha512-/8Qw+iisrUdOMk+p2mjEHouMm/BBdBEN1DHh16wiTpRUZkxDG3PxexdjCvR+wvK3LWPdrEvnQbdrwpU954sPhg=="], + "graphql": ["graphql@16.14.2", "", {}, "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA=="], "graphql-request": ["graphql-request@6.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="], @@ -2433,7 +2454,7 @@ "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], @@ -3349,8 +3370,6 @@ "crc32-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -3383,6 +3402,8 @@ "fstream/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], "gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -3737,6 +3758,8 @@ "fstream/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "gcp-metadata/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], diff --git a/docs/CHANNELS.md b/docs/CHANNELS.md index 1f106c260b..de80c80be1 100644 --- a/docs/CHANNELS.md +++ b/docs/CHANNELS.md @@ -62,6 +62,14 @@ except to list key *names*. Transports see it through the `SecretStore` interfac (`get`, `set`, `delete`, `keys`), and only their own channel's slice, so the file store can be swapped for a keychain later without touching a transport. +Secret keys per channel: + +| Channel | Key | Holds | +| --- | --- | --- | +| `slack` | `botToken` | bot user OAuth token (`xoxb-…`) | +| `slack` | `appToken` | app-level token for Socket Mode (`xapp-…`) | +| `telegram` | `botToken` | the token @BotFather hands out (`packages/channels/telegram/README.md`) | + ### `sessions.json` One conversation is one engine session. The key is `:` -- a Slack diff --git a/packages/channels/telegram/README.md b/packages/channels/telegram/README.md new file mode 100644 index 0000000000..6dae26abff --- /dev/null +++ b/packages/channels/telegram/README.md @@ -0,0 +1,47 @@ +# @redrob-code/channels-telegram + +Telegram transport for the Redrob Code chat channels (`docs/CHANNELS.md`). Built on +[grammY](https://grammy.dev) with long polling; no webhook, no public URL needed. + +## Behaviour + +- One Telegram chat is one conversation: `conversationId` is the chat id, `senderId` is the + Telegram user id (what `redrob channels allow telegram ` takes). +- Private chats: every text message is forwarded to the core. +- Groups and supergroups: only messages that mention the bot (`@YourBot`, `/new@YourBot`) or reply + to one of its messages are forwarded; the mention is stripped before the core sees the text. + Replies in groups quote the message that asked. +- Edits, joins, media without text and other bots are ignored. +- Replies are plain text, split by the core at 4096 characters (Telegram's limit), link previews + off. A typing indicator is shown while the engine works. +- Reconnects are grammY's: the long poller retries on network errors until `stop()`. + +## Secrets + +| Key | Set with | Holds | +| --- | --- | --- | +| `botToken` | `redrob channels set-secret telegram botToken` | the token from @BotFather | + +## Options (`channels.json` -> `channels.telegram.options`) + +| Key | Default | Does | +| --- | --- | --- | +| `dropPendingUpdates` | `false` | discard messages received while the daemon was down instead of answering them on start | + +## Setup + +```sh +# @BotFather: /newbot, copy the token. For groups, /setprivacy -> Disable is NOT required: +# mentions and replies to the bot are delivered with privacy mode on. +redrob channels set-secret telegram botToken # paste the token, Ctrl-D +redrob channels allow telegram 123456789 # your Telegram user id (@userinfobot tells you) +redrob channels enable telegram +``` + +## Exports + +```ts +import { TelegramTransport, telegramTransport, TELEGRAM_MAX_CHUNK, TELEGRAM_SECRETS } from "@redrob-code/channels-telegram" +``` + +`telegramTransport` is the `TransportFactory` the registry wires in. diff --git a/packages/channels/telegram/package.json b/packages/channels/telegram/package.json new file mode 100644 index 0000000000..36698db91b --- /dev/null +++ b/packages/channels/telegram/package.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@redrob-code/channels-telegram", + "version": "0.0.0", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + ".": "./src/index.ts", + "./*": "./src/*.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "grammy": "1.46.0" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/channels/telegram/src/index.ts b/packages/channels/telegram/src/index.ts new file mode 100644 index 0000000000..4e492b3925 --- /dev/null +++ b/packages/channels/telegram/src/index.ts @@ -0,0 +1,4 @@ +/** + * @redrob-code/channels-telegram: the Telegram transport for the Redrob Code chat channels. + */ +export { TelegramTransport, telegramTransport, TELEGRAM_MAX_CHUNK, TELEGRAM_SECRETS } from "./transport" diff --git a/packages/channels/telegram/src/transport.ts b/packages/channels/telegram/src/transport.ts new file mode 100644 index 0000000000..48ed02c28c --- /dev/null +++ b/packages/channels/telegram/src/transport.ts @@ -0,0 +1,211 @@ +/** + * Telegram transport for the Redrob Code channels: grammY, long polling, no webhook. + * + * One Telegram chat is one conversation (`conversationId` = chat id). Private chats are always + * served; in groups and supergroups the bot reacts only to messages that mention it or reply to + * one of its messages, so a bot sitting in a busy group stays quiet. The sender id is the + * Telegram user id, which is what the allowlist matches. + * + * Secrets (`ctx.secrets`): `botToken` -- the token @BotFather hands out. + * Options (`ctx.config.options`): `dropPendingUpdates` (boolean, default false) -- discard + * messages that arrived while the daemon was down instead of answering them on start. + */ +import type { Inbound, Transport, TransportContext, TransportFactory } from "@redrob-code/channels-core" +import { Bot, GrammyError, HttpError, type Context } from "grammy" +import type { MessageEntity, User } from "grammy/types" + +/** Telegram's hard limit on a text message. */ +export const TELEGRAM_MAX_CHUNK = 4096 + +/** The secret key names this transport reads. */ +export const TELEGRAM_SECRETS = ["botToken"] as const + +/** A `sendChatAction("typing")` lasts about five seconds; repeat it until the reply goes out. */ +const TYPING_INTERVAL_MS = 4000 +/** Stop repeating the indicator even if no reply ever comes. */ +const TYPING_MAX_MS = 2 * 60 * 1000 + +export class TelegramTransport implements Transport { + readonly name = "telegram" + readonly maxChunk = TELEGRAM_MAX_CHUNK + + private bot?: Bot + private ctx?: TransportContext + private me?: User + private polling?: Promise + private readonly typers = new Map; startedAt: number }>() + + async start(ctx: TransportContext): Promise { + if (this.bot) throw new Error("telegram transport is already started") + this.ctx = ctx + const token = await ctx.secrets.require("botToken") + const bot = new Bot(token) + + bot.on("message:text", (c) => this.inbound(c)) + bot.catch((error) => { + ctx.log.error(`update ${error.ctx.update.update_id}: ${describe(error.error)}`) + }) + + // getMe: rejects with a 401 GrammyError on a bad token, so a wrong secret fails `start`. + try { + await bot.init() + } catch (error) { + throw new Error(`Telegram rejected the bot token: ${describe(error)}`) + } + this.me = bot.botInfo + this.bot = bot + + const dropPending = ctx.config.options?.dropPendingUpdates === true + // grammY's long poller retries on network errors by itself; it only ends when stop() is + // called or the token is revoked, which `catch` below turns into a log line. + this.polling = bot + .start({ + allowed_updates: ["message"], + drop_pending_updates: dropPending, + onStart: (info) => ctx.log.info(`connected as @${info.username} (long polling)`), + }) + .catch((error: unknown) => { + ctx.log.error(`long polling ended: ${describe(error)}`) + }) + } + + async stop(): Promise { + for (const key of [...this.typers.keys()]) this.stopTyping(key) + const bot = this.bot + this.bot = undefined + if (!bot) return + if (bot.isRunning()) await bot.stop().catch(() => undefined) + await this.polling + this.polling = undefined + this.me = undefined + } + + async send(conversationId: string, chunks: readonly string[], replyTo?: string): Promise { + const bot = this.bot + if (!bot) throw new Error("telegram transport is not started") + this.stopTyping(conversationId) + const chatId = Number(conversationId) + const replyId = replyTo === undefined ? undefined : Number(replyTo) + let first = true + for (const chunk of chunks) { + // Reply to the inbound message with the first chunk only; the rest follow in order. + const reply = + first && replyId !== undefined && Number.isFinite(replyId) + ? { reply_parameters: { message_id: replyId, allow_sending_without_reply: true } } + : {} + await bot.api.sendMessage(chatId, chunk, { ...reply, link_preview_options: { is_disabled: true } }) + first = false + } + } + + async typing(conversationId: string): Promise { + const bot = this.bot + if (!bot) return + const chatId = Number(conversationId) + await bot.api.sendChatAction(chatId, "typing") + if (this.typers.has(conversationId)) return + const startedAt = Date.now() + const timer = setInterval(() => { + if (!this.bot || Date.now() - startedAt > TYPING_MAX_MS) { + this.stopTyping(conversationId) + return + } + this.bot.api.sendChatAction(chatId, "typing").catch(() => this.stopTyping(conversationId)) + }, TYPING_INTERVAL_MS) + this.typers.set(conversationId, { timer, startedAt }) + } + + private stopTyping(conversationId: string): void { + const entry = this.typers.get(conversationId) + if (!entry) return + clearInterval(entry.timer) + this.typers.delete(conversationId) + } + + private inbound(c: Context): void { + const ctx = this.ctx + const message = c.message + const me = this.me + if (!ctx || !message || !me || typeof message.text !== "string") return + const from = message.from + // No sender (channel posts, anonymous admins) or the bot itself: nothing the allowlist can judge. + if (!from || from.id === me.id || from.is_bot) return + + const group = message.chat.type === "group" || message.chat.type === "supergroup" + let text = message.text + if (group) { + const addressed = this.addressed(message.text, message.entities ?? [], message.reply_to_message?.from, me) + if (!addressed.yes) return + text = addressed.text + } + text = text.trim() + if (text.length === 0) return + + const inbound: Inbound = { + channel: this.name, + conversationId: String(message.chat.id), + senderId: String(from.id), + senderName: from.username ?? [from.first_name, from.last_name].filter(Boolean).join(" "), + text, + // Threading only matters in a group, where several people talk; in a private chat a reply + // quote on every answer is noise. + ...(group ? { replyTo: String(message.message_id) } : {}), + } + ctx.onMessage(inbound) + } + + /** + * Whether a group message is for this bot: a `@username` mention, a `text_mention` of the bot, + * a `/command@username`, or a reply to one of the bot's messages. Returns the text with the + * mentions removed so `/new@bot` reaches the core as `/new`. + */ + private addressed( + text: string, + entities: readonly MessageEntity[], + repliedTo: User | undefined, + me: User, + ): { yes: boolean; text: string } { + const username = (me.username ?? "").toLowerCase() + const cuts: Array<{ start: number; end: number }> = [] + let mentioned = repliedTo?.id === me.id + for (const entity of entities) { + // Entity offsets are UTF-16 code units, which is what JavaScript strings index by. + const start = entity.offset + const end = entity.offset + entity.length + const slice = text.slice(start, end) + if (entity.type === "mention" && username && slice.toLowerCase() === `@${username}`) { + mentioned = true + cuts.push({ start, end }) + } else if (entity.type === "text_mention" && entity.user.id === me.id) { + mentioned = true + cuts.push({ start, end }) + } else if (entity.type === "bot_command" && username) { + const at = slice.indexOf("@") + if (at > 0 && slice.slice(at + 1).toLowerCase() === username) { + mentioned = true + // Keep the command, drop only the `@username` suffix. + cuts.push({ start: start + at, end }) + } + } + } + if (!mentioned) return { yes: false, text } + let out = "" + let cursor = 0 + for (const cut of cuts.sort((a, b) => a.start - b.start)) { + if (cut.start < cursor) continue + out += text.slice(cursor, cut.start) + cursor = cut.end + } + out += text.slice(cursor) + return { yes: true, text: out.replace(/\s{2,}/g, " ") } + } +} + +export const telegramTransport: TransportFactory = () => new TelegramTransport() + +function describe(error: unknown): string { + if (error instanceof GrammyError) return `${error.error_code} ${error.description}` + if (error instanceof HttpError) return `network error: ${error.message}` + if (error instanceof Error) return error.message + return String(error) +} diff --git a/packages/channels/telegram/tsconfig.json b/packages/channels/telegram/tsconfig.json new file mode 100644 index 0000000000..00ef125468 --- /dev/null +++ b/packages/channels/telegram/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "noUncheckedIndexedAccess": false + } +} From 6600e63cf3d65966cd3d1a5ff48d17fece373dcf Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:35:34 +0000 Subject: [PATCH 12/23] feat(channels): Discord transport on discord.js New workspace package @redrob-code/channels-discord. One Discord channel (DM, guild channel or thread) is one conversation; the sender is the author's user id. DMs are always delivered, guild messages only when the bot is mentioned, with the mention stripped. maxChunk 2000, typing via sendTyping(), gateway reconnects left to discord.js, start() rejects on a bad token. Secret: discord.botToken. Documents the Message Content privileged gateway intent in the package README and docs/CHANNELS.md, and adds the secrets table there. --- bun.lock | 81 ++++++++---- docs/CHANNELS.md | 29 +++++ packages/channels/discord/README.md | 60 +++++++++ packages/channels/discord/package.json | 26 ++++ packages/channels/discord/src/index.ts | 9 ++ packages/channels/discord/src/transport.ts | 145 +++++++++++++++++++++ packages/channels/discord/tsconfig.json | 8 ++ 7 files changed, 333 insertions(+), 25 deletions(-) create mode 100644 packages/channels/discord/README.md create mode 100644 packages/channels/discord/package.json create mode 100644 packages/channels/discord/src/index.ts create mode 100644 packages/channels/discord/src/transport.ts create mode 100644 packages/channels/discord/tsconfig.json diff --git a/bun.lock b/bun.lock index a924ab932a..f1d0259140 100644 --- a/bun.lock +++ b/bun.lock @@ -41,6 +41,21 @@ "typescript": "catalog:", }, }, + "packages/channels/discord": { + "name": "@redrob-code/channels-discord", + "version": "0.0.0", + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "discord.js": "14.27.0", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, "packages/cli": { "name": "@redrob-code/cli", "version": "0.0.0", @@ -871,6 +886,18 @@ "@clack/prompts": ["@clack/prompts@1.0.0-alpha.1", "", { "dependencies": { "@clack/core": "1.0.0-alpha.1", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-07MNT0OsxjKOcyVfX8KhXBhJiyUbDP1vuIAcHc+nx5v93MJO23pX3X/k3bWz6T3rpM9dgWPq90i4Jq7gZAyMbw=="], + "@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="], + + "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], + + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], + + "@discordjs/rest": ["@discordjs/rest@2.6.3", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.50", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg=="], + + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], + + "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.83", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.83" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-cPfCfp/ghu0itbX6Dqjdr4N0rbjng5ON4sUpnLHV5JJySG8zZpWmuOZLWIrfrNKT2ctYR1BYmp1aYCgkItaJLw=="], @@ -1295,6 +1322,8 @@ "@redrob-code/channels-core": ["@redrob-code/channels-core@workspace:packages/channels/core"], + "@redrob-code/channels-discord": ["@redrob-code/channels-discord@workspace:packages/channels/discord"], + "@redrob-code/cli": ["@redrob-code/cli@workspace:packages/cli"], "@redrob-code/client": ["@redrob-code/client@workspace:packages/client"], @@ -1331,6 +1360,12 @@ "@redrob-labs/sdk": ["@redrob-labs/sdk@workspace:packages/sdk/js"], + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], + + "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], + + "@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], + "@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="], @@ -1597,6 +1632,8 @@ "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + "@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="], "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], @@ -1863,6 +1900,10 @@ "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "discord-api-types": ["discord-api-types@0.38.56", "", {}, "sha512-Hti4RZsGvF5C7SM8rF2MDHtIm0Hyfrlk84GpEA29nj91tvVVMvX+oQSCodu10t2ZcjFGofq8TX0YyC04B2E69Q=="], + + "discord.js": ["discord.js@14.27.0", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.2", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.5", "discord-api-types": "^0.38.49", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A=="], + "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], "docx": ["docx@9.8.1", "", { "dependencies": { "@types/node": "^26.6.2", "hash.js": "^1.1.7", "jszip": "^3.10.1", "nanoid": "^6.0.1", "xml": "^1.0.1", "xml-js": "^1.6.8" } }, "sha512-bv4yDxgiV9aRyh327HG+V36wG0f23wk32SDhXGQpUhnykVtIPaR1K4ISi8E+DYJ7OcRQRkR8Y70QYlSS08V1cA=="], @@ -2343,6 +2384,8 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + "lodash.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="], "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], @@ -2355,6 +2398,8 @@ "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], + "magic-bytes.js": ["magic-bytes.js@1.13.1", "", {}, "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw=="], + "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], @@ -2871,6 +2916,8 @@ "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsscmp": ["tsscmp@1.0.6", "", {}, "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="], @@ -2901,7 +2948,7 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="], + "undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -3017,8 +3064,6 @@ "@actions/github/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@actions/http-client/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.111", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-atgBW8jZPr/KuaKX5FvDIHuXBI8VCol6kVeoD4P0657+VXR73QsLogXQVN/Zt5FHtq9WzpdIZseCJiXPqkgwwA=="], "@ai-sdk/amazon-bedrock/@ai-sdk/openai": ["@ai-sdk/openai@3.0.98", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nAvp8pVOUJ3znJHRzZs54Y7CJQSikOW1Ty7LTdtQT+/pgtAwqhuKd8oaXbiW2xQaYBdH2i8o9AcEpcUdXIyF+g=="], @@ -3205,6 +3250,14 @@ "@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/ws/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "@effect/platform-node/undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="], + "@fastify/ajv-compiler/fast-uri": ["fast-uri@4.1.3", "", {}, "sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw=="], "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.5.0", "", {}, "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w=="], @@ -3407,8 +3460,6 @@ "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], @@ -3491,8 +3542,6 @@ "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -3503,20 +3552,14 @@ "@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/deepgram/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/elevenlabs/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/elevenlabs/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/fireworks/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/google-vertex/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], @@ -3775,8 +3818,6 @@ "venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "venice-ai-sdk-provider/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -3811,30 +3852,20 @@ "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "ai-gateway-provider/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "ai-gateway-provider/@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/cohere/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "ai-gateway-provider/@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/groq/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "ai-gateway-provider/@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/perplexity/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], diff --git a/docs/CHANNELS.md b/docs/CHANNELS.md index 1f106c260b..b470b1df55 100644 --- a/docs/CHANNELS.md +++ b/docs/CHANNELS.md @@ -62,6 +62,12 @@ except to list key *names*. Transports see it through the `SecretStore` interfac (`get`, `set`, `delete`, `keys`), and only their own channel's slice, so the file store can be swapped for a keychain later without touching a transport. +Secret keys by channel: + +| Channel | Key | Value | +| --- | --- | --- | +| `discord` | `botToken` | the bot token from the Developer Portal's Bot page | + ### `sessions.json` One conversation is one engine session. The key is `:` -- a Slack @@ -142,6 +148,29 @@ redrob channels allow slack U0123ABCDEF # your Slack member id redrob channels enable slack ``` +## Discord + +Package `@redrob-code/channels-discord` (discord.js). A conversation is a Discord channel id: a DM +channel, a guild text channel, or a thread (threads have their own id). The sender id is the +author's user id. DMs are always answered; in guild channels and threads the bot answers only when +it is mentioned, and the mention is removed from the prompt. Messages are capped at 2000 +characters; the bot shows a typing indicator while the engine works. + +Gateway intents: `Guilds`, `GuildMessages`, `DirectMessages`, `MessageContent`. **Message Content +is a Privileged Gateway Intent** and must be switched on in the Developer Portal (Bot page -> +Privileged Gateway Intents -> Message Content Intent). Without it Discord delivers guild messages +with empty content and the bot never sees anything to answer. Server Members and Presence are not +needed. + +```sh +# Developer Portal: New Application -> Bot -> Reset Token; enable Message Content Intent; +# OAuth2 URL Generator: scope bot, permissions View Channels, Send Messages, +# Send Messages in Threads, Read Message History; add the bot to the server. +redrob channels set-secret discord botToken # paste the token, Ctrl-D +redrob channels allow discord 123456789012345678 # your Discord user id (Developer Mode -> Copy User ID) +redrob channels enable discord +``` + ## Transport contract (for Stage 2) Package `@redrob-code/channels-core`, file `src/transport.ts`. diff --git a/packages/channels/discord/README.md b/packages/channels/discord/README.md new file mode 100644 index 0000000000..2a46c15e9d --- /dev/null +++ b/packages/channels/discord/README.md @@ -0,0 +1,60 @@ +# @redrob-code/channels-discord + +Discord transport for the Redrob Code chat channels (`@redrob-code/channels-core`). Talk to the +shared engine from a Discord DM, a guild text channel or a thread; each channel is one engine +session. + +## Exports + +- `class DiscordTransport implements Transport` -- `name = "discord"`, `maxChunk = 2000`. +- `const discordTransport: TransportFactory` -- `() => new DiscordTransport()`, what the CLI + registry points at. +- `stripMention(content, userId)` -- removes the bot's `<@id>` mention from a guild message. +- `DISCORD_MAX_CHUNK` -- `2000`, Discord's message body limit. + +## Behaviour + +| | | +| --- | --- | +| `conversationId` | the Discord channel id: a DM channel, a guild channel, or a thread (threads have their own id) | +| `senderId` | the author's user id -- put this in the allowlist | +| `replyTo` | the inbound message id; the first chunk of the answer is sent as a reply to it | +| DMs | every message is delivered | +| Guild channels and threads | only messages that mention the bot (`@Redrob …`); the mention is stripped from the prompt | +| Ignored | the bot's own messages, other bots, system messages (joins, pins, boosts) | +| Typing | `sendTyping()` while the engine works | +| Reconnects | discord.js's gateway handles resume and reconnect; `start()` rejects on a bad token | + +Gateway intents used: `Guilds`, `GuildMessages`, `DirectMessages`, `MessageContent`. + +## Secrets + +| Key | Value | +| --- | --- | +| `discord.botToken` | the bot token from the Developer Portal (Bot page, "Reset Token") | + +```sh +redrob channels set-secret discord botToken # paste the token, Ctrl-D +``` + +No non-secret `options` are read. + +## Setting up the bot + +1. [Discord Developer Portal](https://discord.com/developers/applications) -> New Application. +2. **Bot** page -> Reset Token, copy it -> `redrob channels set-secret discord botToken`. +3. **Bot** page -> *Privileged Gateway Intents* -> turn on **Message Content Intent**. + This is required: without it Discord delivers guild messages with empty `content` and the bot + sees nothing to answer. (Bots in fewer than 100 servers can enable it with a toggle; larger + bots need Discord's approval.) *Server Members* and *Presence* are not needed. +4. **OAuth2** -> URL Generator: scope `bot`, permissions *View Channels*, *Send Messages*, + *Send Messages in Threads*, *Read Message History*. Open the URL and add the bot to your server. +5. Allow yourself and switch the channel on: + +```sh +redrob channels allow discord 123456789012345678 # your Discord user id (Settings -> Advanced -> Developer Mode, then right-click -> Copy User ID) +redrob channels enable discord +``` + +DM the bot, or mention it in a channel it can read. `/new`, `/status` and `/help` work as in every +other channel. diff --git a/packages/channels/discord/package.json b/packages/channels/discord/package.json new file mode 100644 index 0000000000..8355b0bfb9 --- /dev/null +++ b/packages/channels/discord/package.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@redrob-code/channels-discord", + "version": "0.0.0", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + ".": "./src/index.ts", + "./*": "./src/*.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "discord.js": "14.27.0" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/channels/discord/src/index.ts b/packages/channels/discord/src/index.ts new file mode 100644 index 0000000000..ec8564170a --- /dev/null +++ b/packages/channels/discord/src/index.ts @@ -0,0 +1,9 @@ +/** + * @redrob-code/channels-discord: the Discord transport for `@redrob-code/channels-core`. + * + * ```ts + * import { discordTransport } from "@redrob-code/channels-discord" + * // registry.ts: discord: discordTransport + * ``` + */ +export { DiscordTransport, discordTransport, stripMention, DISCORD_MAX_CHUNK } from "./transport" diff --git a/packages/channels/discord/src/transport.ts b/packages/channels/discord/src/transport.ts new file mode 100644 index 0000000000..683934ea91 --- /dev/null +++ b/packages/channels/discord/src/transport.ts @@ -0,0 +1,145 @@ +/** + * Discord transport for the Redrob Code channels core, on discord.js. + * + * One Discord channel is one conversation: a DM channel, a guild text channel, or a thread (a + * thread is its own channel id). The sender is the author's user id, which is what the allowlist + * matches. DMs are always delivered; in guild channels only messages that mention the bot are. + * + * Secrets (set with `redrob channels set-secret discord `): + * botToken -- the bot token from the Developer Portal's Bot page. + * + * The bot needs the **Message Content** privileged gateway intent switched on in the Developer + * Portal, otherwise every guild message arrives with empty content and nothing reaches the engine. + */ +import type { Transport, TransportContext, TransportFactory } from "@redrob-code/channels-core" +import { + ChannelType, + Client, + Events, + GatewayIntentBits, + MessageType, + Partials, + type Message, + type SendableChannels, +} from "discord.js" + +/** Discord's hard limit on a message body. */ +export const DISCORD_MAX_CHUNK = 2000 + +export class DiscordTransport implements Transport { + readonly name = "discord" + readonly maxChunk = DISCORD_MAX_CHUNK + + private client?: Client + private ctx?: TransportContext + + async start(ctx: TransportContext): Promise { + if (this.client) throw new Error("discord transport is already started") + this.ctx = ctx + const token = await ctx.secrets.require("botToken") + const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.MessageContent, + ], + // DM channels are not in the cache until something happens in them; without this partial a + // DM's messageCreate is dropped by discord.js before we see it. + partials: [Partials.Channel], + }) + this.client = client + + client.on(Events.MessageCreate, (message) => this.inbound(message)) + client.on(Events.Error, (error) => ctx.log.error(`gateway error: ${error.message}`)) + client.on(Events.Warn, (text) => ctx.log.warn(text)) + client.on(Events.ShardDisconnect, (event) => ctx.log.warn(`gateway disconnected (code ${event.code}); reconnecting`)) + client.on(Events.ShardResume, () => ctx.log.info("gateway resumed")) + + // login() resolves once the token is accepted; the ready event follows when the gateway + // session is up. A bad token rejects login() itself (TokenInvalid), which rejects start(). + const ready = new Promise((resolve, reject) => { + client.once(Events.ClientReady, () => resolve()) + client.once(Events.Error, (error) => reject(error)) + }) + try { + await client.login(token) + await ready + } catch (error) { + await client.destroy().catch(() => undefined) + this.client = undefined + throw error instanceof Error ? error : new Error(String(error)) + } + ctx.log.info(`connected as ${client.user?.tag ?? "unknown user"}`) + } + + async stop(): Promise { + const client = this.client + this.client = undefined + this.ctx = undefined + if (client) await client.destroy() + } + + async send(conversationId: string, chunks: readonly string[], replyTo?: string): Promise { + const channel = await this.sendable(conversationId) + let first = true + for (const chunk of chunks) { + if (first && replyTo) { + await channel.send({ content: chunk, reply: { messageReference: replyTo, failIfNotExists: false } }) + } else { + await channel.send({ content: chunk }) + } + first = false + } + } + + async typing(conversationId: string): Promise { + const channel = await this.sendable(conversationId) + await channel.sendTyping() + } + + private async sendable(conversationId: string): Promise { + const client = this.client + if (!client) throw new Error("discord transport is not started") + const channel = await client.channels.fetch(conversationId) + if (!channel) throw new Error(`discord channel ${conversationId} was not found`) + if (!channel.isSendable()) throw new Error(`discord channel ${conversationId} cannot receive messages`) + return channel + } + + private inbound(message: Message): void { + const ctx = this.ctx + const client = this.client + if (!ctx || !client || !client.user) return + if (message.author.bot || message.author.id === client.user.id) return + // Only ordinary messages and replies carry a human's text; joins, pins, boosts and the rest + // are platform noise. + if (message.type !== MessageType.Default && message.type !== MessageType.Reply) return + + const isDm = message.channel.type === ChannelType.DM + if (!isDm) { + // In a guild the bot answers only when it is addressed; `mentions.users` ignores @everyone + // and role mentions, which `mentions.has` would count. + if (!message.mentions.users.has(client.user.id)) return + } + + const text = stripMention(message.content, client.user.id).trim() + if (text.length === 0) return + + ctx.onMessage({ + channel: this.name, + conversationId: message.channelId, + senderId: message.author.id, + senderName: message.member?.displayName ?? message.author.displayName ?? message.author.username, + text, + replyTo: message.id, + }) + } +} + +/** Remove `<@id>` / `<@!id>` mentions of the bot so the prompt is what the human typed. */ +export function stripMention(content: string, userId: string): string { + return content.replaceAll(new RegExp(`<@!?${userId}>`, "g"), " ").replace(/\s{2,}/g, " ") +} + +export const discordTransport: TransportFactory = () => new DiscordTransport() diff --git a/packages/channels/discord/tsconfig.json b/packages/channels/discord/tsconfig.json new file mode 100644 index 0000000000..00ef125468 --- /dev/null +++ b/packages/channels/discord/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "noUncheckedIndexedAccess": false + } +} From b9724b9e7d8f39f15e475c01f3621d49af137ed7 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:36:29 +0000 Subject: [PATCH 13/23] feat(app-bridge): redrob-app-bridge crate serves an app's tools to the daemon A Rust Redrob app calls AppBridge::start with its tools: a minimal MCP streamable-HTTP server (initialize, ping, tools/list, tools/call; notifications 202) comes up on 127.0.0.1 at a free port on a std thread, registers through redrob-engine-client's bridge_app (heartbeat and re-register included) and unregisters on stop or drop. No async runtime so egui apps can call it directly. README with an example; a short "Using redrob-app-bridge (Rust)" section in docs/APP-BRIDGE.md. --- crates/redrob-app-bridge/Cargo.lock | 316 +++++++++++++++++++++ crates/redrob-app-bridge/Cargo.toml | 31 +++ crates/redrob-app-bridge/README.md | 67 +++++ crates/redrob-app-bridge/rustfmt.toml | 2 + crates/redrob-app-bridge/src/lib.rs | 381 ++++++++++++++++++++++++++ docs/APP-BRIDGE.md | 33 +++ 6 files changed, 830 insertions(+) create mode 100644 crates/redrob-app-bridge/Cargo.lock create mode 100644 crates/redrob-app-bridge/Cargo.toml create mode 100644 crates/redrob-app-bridge/README.md create mode 100644 crates/redrob-app-bridge/rustfmt.toml create mode 100644 crates/redrob-app-bridge/src/lib.rs diff --git a/crates/redrob-app-bridge/Cargo.lock b/crates/redrob-app-bridge/Cargo.lock new file mode 100644 index 0000000000..494d40d164 --- /dev/null +++ b/crates/redrob-app-bridge/Cargo.lock @@ -0,0 +1,316 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror", +] + +[[package]] +name = "redrob-app-bridge" +version = "0.1.0" +dependencies = [ + "log", + "redrob-engine-client", + "serde", + "serde_json", + "tiny_http", +] + +[[package]] +name = "redrob-engine-client" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "dirs", + "log", + "serde", + "serde_json", + "ureq", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64 0.23.1", + "log", + "percent-encoding", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +dependencies = [ + "base64 0.23.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/redrob-app-bridge/Cargo.toml b/crates/redrob-app-bridge/Cargo.toml new file mode 100644 index 0000000000..0909f2d7c7 --- /dev/null +++ b/crates/redrob-app-bridge/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "redrob-app-bridge" +version = "0.1.0" +edition = "2024" +rust-version = "1.90" +license = "MIT" +repository = "https://github.com/redrob-labs/redrob-code" +description = "Expose a Rust Redrob app's tools to the shared Redrob Code daemon in one call: a loopback MCP server on a plain thread, registered and kept alive through redrob-engine-client" +# The Rust side of the app bridge (docs/APP-BRIDGE.md). Sits beside the engine client it builds on +# so that every Rust Redrob app (Shape, Canvas, Query, Recall, the launcher) bridges the same way. +# No async runtime: the MCP server is a blocking HTTP loop on its own std thread, so an egui app +# calls `AppBridge::start` from wherever it already is. Apps depend on it as a git dependency pinned +# to the engine tag they target: +# redrob-app-bridge = { git = "https://github.com/redrob-labs/redrob-code", tag = "v0.6.3" } +# No crates.io publish yet; flip `publish` when a registry token exists. +publish = false + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +dbg_macro = "warn" +todo = "warn" + +[dependencies] +redrob-engine-client = { path = "../engine-client" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +log = "0.4" +# Blocking HTTP/1.1 on a loopback socket: the daemon is the only client and it is on this machine. +tiny_http = "0.12" diff --git a/crates/redrob-app-bridge/README.md b/crates/redrob-app-bridge/README.md new file mode 100644 index 0000000000..699ae62205 --- /dev/null +++ b/crates/redrob-app-bridge/README.md @@ -0,0 +1,67 @@ +# redrob-app-bridge + +Expose a Rust Redrob app's tools to the shared Redrob Code daemon in one call. The crate +serves the tools over MCP (streamable HTTP) on `127.0.0.1` at a free port from a plain std +thread -- no async runtime, so an egui app calls it from its setup code -- and registers +that server with the daemon through [`redrob-engine-client`](../engine-client)'s +`bridge_app`, which heartbeats and registers again after a daemon restart. Stopping or +dropping the bridge unregisters and stops the server. The contract is the engine's +[`docs/APP-BRIDGE.md`](../../docs/APP-BRIDGE.md). + +```toml +[dependencies] +redrob-app-bridge = { git = "https://github.com/redrob-labs/redrob-code", tag = "v0.6.3" } +``` + +## Example + +```rust +use redrob_app_bridge::{AppBridge, AppBridgeConfig, Tool}; +use serde_json::json; + +fn bridge(doc: std::sync::Arc>) -> Result { + let select = { + let doc = doc.clone(); + Tool::new( + "select", + "Select the layer with the given name.", + json!({ "type": "object", "properties": { "layer": { "type": "string" } }, "required": ["layer"] }), + move |args| { + let layer = args["layer"].as_str().ok_or("layer is required")?; + doc.lock().map_err(|e| e.to_string())?.select(layer).map_err(|e| e.to_string())?; + Ok(format!("selected {layer}")) + }, + ) + }; + let export_png = Tool::new("export_png", "Export the document as PNG; answers the path.", json!({ "type": "object" }), move |_| { + doc.lock().map_err(|e| e.to_string())?.export_png().map_err(|e| e.to_string()) + }); + AppBridge::start(AppBridgeConfig { version: Some(env!("CARGO_PKG_VERSION").into()), ..AppBridgeConfig::new("canvas", vec![select, export_png]) }) +} +// Keep the returned `AppBridge` in the app's state; `bridge.stop()` (or dropping it) on quit. +``` + +While the bridge stands, any Redrob surface's agent sees the tools as MCP server +`app:canvas` with `select` and `export_png` under it; `redrob apps list` shows the +registration. A handler's `Err(text)` reaches the agent as a failed tool result +(`isError: true`), a panic in a handler is caught and reported the same way. + +## What the server speaks + +`POST` with a JSON-RPC 2.0 body (one message or a batch) at any path; every reply is an +`application/json` body. `initialize` (protocol `2025-03-26`, `capabilities.tools`, +`serverInfo`), `ping`, `tools/list`, `tools/call` (`{ content: [{ type: "text", text }], isError }`). +Notifications (`notifications/initialized`, ...) are answered `202` with no body. `GET` and +`DELETE` are `405`: there is no server-to-client stream and no session. Each request runs on +its own thread, so a slow tool call never holds up the daemon's `ping`. + +## API + +- `AppBridge::start(AppBridgeConfig) -> Result` -- find or start the + daemon (as the app's name), serve, register. +- `AppBridge::start_with(&Record, AppBridgeConfig)` -- the same against a daemon the app + already connected to. +- `AppBridge::{port, mcp_url, id, stop}`; `Drop` does what `stop` does. +- `AppBridgeConfig { name, version, tools, idle_exit_minutes }`, `AppBridgeConfig::new(name, tools)`. +- `Tool { name, description, input_schema, handler }`, `Tool::new(name, description, schema, handler)`; + `handler: Arc Result + Send + Sync>`. diff --git a/crates/redrob-app-bridge/rustfmt.toml b/crates/redrob-app-bridge/rustfmt.toml new file mode 100644 index 0000000000..3121924d65 --- /dev/null +++ b/crates/redrob-app-bridge/rustfmt.toml @@ -0,0 +1,2 @@ +max_width = 150 +use_small_heuristics = "Max" diff --git a/crates/redrob-app-bridge/src/lib.rs b/crates/redrob-app-bridge/src/lib.rs new file mode 100644 index 0000000000..4b10d6a242 --- /dev/null +++ b/crates/redrob-app-bridge/src/lib.rs @@ -0,0 +1,381 @@ +//! The app bridge, from a Rust app. +//! +//! A Redrob desktop app hands the shared daemon its own tools in one call: [`AppBridge::start`] +//! serves the tools over MCP (streamable HTTP, JSON-RPC over `POST`) on `127.0.0.1` at a free +//! port, registers that server through `redrob-engine-client`'s `bridge_app` (which heartbeats and +//! re-registers after a daemon restart) and unregisters when the bridge is stopped or dropped. The +//! engine's `docs/APP-BRIDGE.md` has the contract. +//! +//! There is no async runtime: the server is a blocking loop on a std thread, each request handled +//! on a short-lived thread of its own, so an egui app calls this from its own setup code and keeps +//! the handle in its state. +//! +//! What the server speaks (the 2025-03-26 streamable HTTP transport, the parts a tool server +//! needs): `initialize`, `ping`, `tools/list`, `tools/call`; notifications and client responses +//! are acknowledged with `202`; `GET` (the optional server-to-client stream) is `405`. Every reply +//! is a plain `application/json` body -- no SSE, no session ids. + +use std::io::Read; +use std::net::SocketAddr; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread::JoinHandle; + +use serde::Deserialize; +use serde_json::{Value, json}; + +pub use redrob_engine_client::{AppRegisterInput, Record}; + +/// The MCP protocol revision this server answers `initialize` with. +pub const PROTOCOL_VERSION: &str = "2025-03-26"; + +/// What a tool's handler is: the call's `arguments` object in, the text the agent reads out. +/// `Err` becomes a tool result with `isError: true` (the agent sees the message and can recover), +/// not a JSON-RPC error. +pub type Handler = Arc Result + Send + Sync>; + +/// One tool the app exposes. +#[derive(Clone)] +pub struct Tool { + /// The app's own name for it; the engine shows it as `app:` / ``. Short, verb-first. + pub name: String, + pub description: String, + /// JSON Schema for `arguments`. `Value::Null` means "an object, anything goes". + pub input_schema: Value, + pub handler: Handler, +} + +impl Tool { + pub fn new(name: impl Into, description: impl Into, input_schema: Value, handler: F) -> Self + where + F: Fn(Value) -> Result + Send + Sync + 'static, + { + Self { name: name.into(), description: description.into(), input_schema, handler: Arc::new(handler) } + } + + fn descriptor(&self) -> Value { + let schema = if self.input_schema.is_null() { json!({ "type": "object" }) } else { self.input_schema.clone() }; + json!({ "name": self.name, "description": self.description, "inputSchema": schema }) + } +} + +impl std::fmt::Debug for Tool { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tool").field("name", &self.name).field("description", &self.description).field("input_schema", &self.input_schema).finish() + } +} + +/// What [`AppBridge::start`] needs. +#[derive(Clone, Debug)] +pub struct AppBridgeConfig { + /// The engine exposes the tools under MCP server `app:`; also the `REDROB_APP` on a + /// daemon this call has to start. + pub name: String, + /// Shown by `redrob apps list`; defaults to nothing. Apps pass `env!("CARGO_PKG_VERSION")`. + pub version: Option, + pub tools: Vec, + /// How long a daemon started here stays up with nobody attached (`0` keeps it up). The + /// bridge holds an attachment while it stands, so this only matters after the app quits. + pub idle_exit_minutes: u32, +} + +impl AppBridgeConfig { + pub fn new(name: impl Into, tools: Vec) -> Self { + Self { name: name.into(), version: None, tools, idle_exit_minutes: 10 } + } +} + +/// The MCP server the daemon reaches: one `tiny_http` server on a std thread, stopped on drop. +struct McpServer { + server: Arc, + addr: SocketAddr, + stop: Arc, + thread: Option>, +} + +/// What every request handler shares. +struct Shared { + name: String, + version: String, + tools: Vec, +} + +impl McpServer { + fn start(shared: Arc) -> Result { + let server = tiny_http::Server::http("127.0.0.1:0").map_err(|e| format!("could not bind the MCP server on 127.0.0.1: {e}"))?; + let addr = server.server_addr().to_ip().ok_or_else(|| "the MCP server bound no IP address".to_string())?; + let server = Arc::new(server); + let stop = Arc::new(AtomicBool::new(false)); + let thread = { + let server = server.clone(); + let stop = stop.clone(); + std::thread::Builder::new() + .name(format!("redrob-app-bridge-mcp-{}", shared.name)) + .spawn(move || { + loop { + let request = match server.recv() { + Ok(r) => r, + // `unblock()` makes `recv` fail; a stopped server has nothing else to do. + Err(_) if stop.load(Ordering::SeqCst) => return, + Err(e) => { + log::debug!("app bridge MCP server could not accept a request: {e}"); + continue; + } + }; + if stop.load(Ordering::SeqCst) { + let _ = request.respond(tiny_http::Response::empty(503)); + return; + } + let shared = shared.clone(); + // One thread per request: a slow tool call must not hold up the daemon's ping. + let spawned = std::thread::Builder::new().name("redrob-app-bridge-request".into()).spawn(move || handle(request, &shared)); + if let Err(e) = spawned { + log::warn!("app bridge could not spawn a request thread: {e}"); + } + } + }) + .map_err(|e| format!("could not start the MCP server thread: {e}"))? + }; + Ok(Self { server, addr, stop, thread: Some(thread) }) + } + + fn shutdown(&mut self) { + if self.stop.swap(true, Ordering::SeqCst) { + return; + } + self.server.unblock(); + if let Some(thread) = self.thread.take() { + let _ = thread.join(); + } + } +} + +impl Drop for McpServer { + fn drop(&mut self) { + self.shutdown(); + } +} + +/// A live bridge: the MCP server is up and the daemon knows about it. Keep it for as long as the +/// app wants its tools reachable; [`AppBridge::stop`] or dropping it unregisters first (the daemon +/// closes its MCP client) and then stops the server. +pub struct AppBridge { + registration: Option, + server: McpServer, + mcp_url: String, +} + +impl AppBridge { + /// Find the daemon (or start one, as `name`), serve `tools` on a loopback port and register. + pub fn start(config: AppBridgeConfig) -> Result { + let record = redrob_engine_client::connect_or_start_as(&config.name, config.idle_exit_minutes)?; + Self::start_with(&record, config) + } + + /// [`AppBridge::start`] against a daemon the app already found (its `Record`). + pub fn start_with(record: &Record, config: AppBridgeConfig) -> Result { + if config.name.trim().is_empty() { + return Err("the app bridge needs a name".to_string()); + } + let tool_names: Vec = config.tools.iter().map(|t| t.name.clone()).collect(); + let shared = Arc::new(Shared { + name: config.name.clone(), + version: config.version.clone().unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()), + tools: config.tools, + }); + // The server comes up first so the registration names a URL that already answers; if the + // registration fails the server is dropped (stopped) with it. + let server = McpServer::start(shared)?; + let mcp_url = format!("http://127.0.0.1:{}/mcp", server.addr.port()); + let input = AppRegisterInput { version: config.version, tools: Some(tool_names), ..AppRegisterInput::new(config.name, mcp_url.clone()) }; + let registration = redrob_engine_client::bridge_app(record, input)?; + Ok(Self { registration: Some(registration), server, mcp_url }) + } + + /// The port the MCP server listens on (always `127.0.0.1`). + pub fn port(&self) -> u16 { + self.server.addr.port() + } + + /// The URL the daemon was given. + pub fn mcp_url(&self) -> &str { + &self.mcp_url + } + + /// The daemon's registration id; changes when the bridge had to register again. + pub fn id(&self) -> String { + self.registration.as_ref().map(|r| r.id()).unwrap_or_default() + } + + /// Unregister from the daemon and stop the server. Dropping the bridge does the same. + pub fn stop(mut self) { + self.shutdown(); + } + + fn shutdown(&mut self) { + if let Some(registration) = self.registration.take() { + registration.stop(); + } + self.server.shutdown(); + } +} + +impl Drop for AppBridge { + fn drop(&mut self) { + self.shutdown(); + } +} + +// --------------------------------------------------------------------------------------------- +// The transport: one POST, one JSON body, one JSON reply. +// --------------------------------------------------------------------------------------------- + +/// Requests larger than this are refused; a tool call carries arguments, not documents. +const MAX_BODY: usize = 4 * 1024 * 1024; + +fn json_header() -> tiny_http::Header { + tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).expect("a static header") +} + +fn respond(request: tiny_http::Request, status: u16, body: Option) { + let response = match body { + Some(value) => tiny_http::Response::from_string(value.to_string()).with_status_code(status).with_header(json_header()), + None => tiny_http::Response::from_string(String::new()).with_status_code(status), + }; + if let Err(e) = request.respond(response) { + log::debug!("app bridge could not answer a request: {e}"); + } +} + +fn handle(mut request: tiny_http::Request, shared: &Shared) { + match request.method() { + tiny_http::Method::Post => {} + tiny_http::Method::Get | tiny_http::Method::Delete => { + // No server-to-client stream and no sessions: both are optional in the transport. + return respond(request, 405, None); + } + _ => return respond(request, 405, None), + } + if request.body_length().is_some_and(|n| n > MAX_BODY) { + return respond(request, 413, Some(rpc_error(Value::Null, -32600, "request body too large"))); + } + let mut body = String::new(); + if let Err(e) = request.as_reader().take(MAX_BODY as u64 + 1).read_to_string(&mut body) { + return respond(request, 400, Some(rpc_error(Value::Null, -32700, &format!("could not read the request body: {e}")))); + } + if body.len() > MAX_BODY { + return respond(request, 413, Some(rpc_error(Value::Null, -32600, "request body too large"))); + } + let parsed: Value = match serde_json::from_str(&body) { + Ok(v) => v, + Err(e) => return respond(request, 400, Some(rpc_error(Value::Null, -32700, &format!("parse error: {e}")))), + }; + // A batch is a JSON array; each request in it gets its reply in the array answered. + let (messages, batch) = match parsed { + Value::Array(items) => (items, true), + single => (vec![single], false), + }; + let mut replies = Vec::new(); + for message in messages { + if let Some(reply) = dispatch(message, shared) { + replies.push(reply); + } + } + match (batch, replies.len()) { + // Only notifications and client responses: accepted, nothing to say back. + (_, 0) => respond(request, 202, None), + (false, _) => respond(request, 200, Some(replies.remove(0))), + (true, _) => respond(request, 200, Some(Value::Array(replies))), + } +} + +#[derive(Deserialize)] +struct Message { + #[serde(default)] + jsonrpc: Option, + #[serde(default)] + id: Option, + #[serde(default)] + method: Option, + #[serde(default)] + params: Option, + #[serde(default)] + result: Option, + #[serde(default)] + error: Option, +} + +fn rpc_result(id: Value, result: Value) -> Value { + json!({ "jsonrpc": "2.0", "id": id, "result": result }) +} + +fn rpc_error(id: Value, code: i64, message: &str) -> Value { + json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } }) +} + +/// One JSON-RPC message in; the reply out, or `None` when the message wants none (a notification, +/// or a response to a request this server never sends). +fn dispatch(message: Value, shared: &Shared) -> Option { + let message: Message = match serde_json::from_value(message) { + Ok(m) => m, + Err(e) => return Some(rpc_error(Value::Null, -32600, &format!("invalid request: {e}"))), + }; + if message.jsonrpc.as_deref() != Some("2.0") { + return Some(rpc_error(message.id.unwrap_or(Value::Null), -32600, "invalid request: jsonrpc must be \"2.0\"")); + } + let Some(method) = message.method else { + // A response from the client (result or error) to a request we never made: ignore it. + if message.result.is_some() || message.error.is_some() { + return None; + } + return Some(rpc_error(message.id.unwrap_or(Value::Null), -32600, "invalid request: no method")); + }; + let Some(id) = message.id else { + // A notification. `notifications/initialized` and `notifications/cancelled` both land here. + log::trace!("app bridge notification {method}"); + return None; + }; + let params = message.params.unwrap_or(Value::Null); + let result = match method.as_str() { + "initialize" => Ok(json!({ + "protocolVersion": PROTOCOL_VERSION, + "capabilities": { "tools": {} }, + "serverInfo": { "name": shared.name, "version": shared.version }, + })), + "ping" => Ok(json!({})), + "tools/list" => Ok(json!({ "tools": shared.tools.iter().map(Tool::descriptor).collect::>() })), + "tools/call" => call_tool(params, shared), + _ => Err((-32601, format!("method not found: {method}"))), + }; + Some(match result { + Ok(value) => rpc_result(id, value), + Err((code, text)) => rpc_error(id, code, &text), + }) +} + +fn call_tool(params: Value, shared: &Shared) -> Result { + let name = params.get("name").and_then(Value::as_str).ok_or_else(|| (-32602, "tools/call needs params.name".to_string()))?; + let tool = shared.tools.iter().find(|t| t.name == name).ok_or_else(|| (-32602, format!("unknown tool: {name}")))?; + let arguments = match params.get("arguments") { + None | Some(Value::Null) => json!({}), + Some(other) => other.clone(), + }; + let handler = tool.handler.clone(); + // A handler that panics takes down its request thread, not the app: the agent reads the + // panic as a failed call and can try something else. + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(arguments))); + let (text, is_error) = match outcome { + Ok(Ok(text)) => (text, false), + Ok(Err(text)) => (text, true), + Err(panic) => { + let text = panic + .downcast_ref::() + .cloned() + .or_else(|| panic.downcast_ref::<&str>().map(|s| s.to_string())) + .unwrap_or_else(|| "the tool panicked".to_string()); + log::warn!("app bridge tool {name} panicked: {text}"); + (format!("tool {name} panicked: {text}"), true) + } + }; + Ok(json!({ "content": [{ "type": "text", "text": text }], "isError": is_error })) +} diff --git a/docs/APP-BRIDGE.md b/docs/APP-BRIDGE.md index ab5b4cee7c..9529800aaa 100644 --- a/docs/APP-BRIDGE.md +++ b/docs/APP-BRIDGE.md @@ -140,6 +140,39 @@ bridge.stop(); is the loop. `heartbeat_app` answers `Ok(None)` on a 404 so the caller knows to register again. +## Using `redrob-app-bridge` (Rust) + +When the app has no MCP server of its own, [`crates/redrob-app-bridge`](../crates/redrob-app-bridge) +does both steps above in one call: it serves the given tools over streamable HTTP on +`127.0.0.1` at a free port from a std thread (no async runtime, so an egui app calls it +from its setup code) and registers through `bridge_app`, heartbeats included. Dropping the +handle unregisters and stops the server. + +```rust +use redrob_app_bridge::{AppBridge, AppBridgeConfig, Tool}; +use serde_json::json; + +let select = Tool::new( + "select", + "Select the layer with the given name.", + json!({ "type": "object", "properties": { "layer": { "type": "string" } }, "required": ["layer"] }), + move |args| { + let layer = args["layer"].as_str().ok_or("layer is required")?; + Ok(format!("selected {layer}")) // Err(text) reaches the agent as isError: true + }, +); +let bridge = AppBridge::start(AppBridgeConfig { + version: Some(env!("CARGO_PKG_VERSION").into()), + ..AppBridgeConfig::new("shape", vec![select]) +})?; +log::info!("bridged as {} at {}", bridge.id(), bridge.mcp_url()); +// On quit: bridge.stop(), or just drop it. +``` + +The server answers `initialize` (protocol `2025-03-26`), `ping`, `tools/list` and +`tools/call`; notifications get a `202`. `AppBridge::start_with(&record, config)` takes a +daemon the app already connected to. + ## From the command line ``` From f013a0f9462e57bef3c53b197d05345c317710f6 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:36:51 +0000 Subject: [PATCH 14/23] feat(app-bridge): @redrob-code/app-bridge, the TS one-call app bridge startAppBridge serves ToolDefs over streamable-HTTP MCP on 127.0.0.1 (port 0, one McpServer per session so tools/list_changed can be pushed), bridges the URL into the daemon via @redrob-labs/sdk/daemon connectOrStart + bridgeApp, and unregisters on stop(). zod or JSON Schema inputs. README with example and a Using section in docs/APP-BRIDGE.md. --- bun.lock | 162 +++++++++++---------- docs/APP-BRIDGE.md | 27 ++++ packages/app-bridge/README.md | 48 ++++++ packages/app-bridge/package.json | 26 ++++ packages/app-bridge/src/index.ts | 233 ++++++++++++++++++++++++++++++ packages/app-bridge/tsconfig.json | 8 + 6 files changed, 425 insertions(+), 79 deletions(-) create mode 100644 packages/app-bridge/README.md create mode 100644 packages/app-bridge/package.json create mode 100644 packages/app-bridge/src/index.ts create mode 100644 packages/app-bridge/tsconfig.json diff --git a/bun.lock b/bun.lock index a924ab932a..d0643e0b92 100644 --- a/bun.lock +++ b/bun.lock @@ -27,6 +27,22 @@ "turbo": "2.10.2", }, }, + "packages/app-bridge": { + "name": "@redrob-code/app-bridge", + "version": "0.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "1.29.0", + "@redrob-labs/sdk": "workspace:*", + "zod": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, "packages/channels/core": { "name": "@redrob-code/channels-core", "version": "0.0.0", @@ -563,8 +579,8 @@ "tree-sitter-bash", ], "patchedDependencies": { - "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", + "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", @@ -1293,6 +1309,8 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], + "@redrob-code/app-bridge": ["@redrob-code/app-bridge@workspace:packages/app-bridge"], + "@redrob-code/channels-core": ["@redrob-code/channels-core@workspace:packages/channels/core"], "@redrob-code/cli": ["@redrob-code/cli@workspace:packages/cli"], @@ -1605,7 +1623,7 @@ "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -1697,7 +1715,7 @@ "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], - "body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], @@ -1791,7 +1809,7 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -1799,7 +1817,7 @@ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -1955,7 +1973,7 @@ "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], @@ -1997,7 +2015,7 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], @@ -2021,7 +2039,7 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], @@ -2129,7 +2147,7 @@ "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], - "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -2363,9 +2381,9 @@ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -2623,7 +2641,7 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], @@ -2709,13 +2727,13 @@ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], - "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], @@ -2885,7 +2903,7 @@ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -3221,12 +3239,8 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "@modelcontextprotocol/sdk/jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], - "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -3287,6 +3301,10 @@ "@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "@slack/bolt/express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], + + "@slack/bolt/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + "@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], "@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], @@ -3301,10 +3319,6 @@ "@types/node-fetch/form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], - "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.111", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-atgBW8jZPr/KuaKX5FvDIHuXBI8VCol6kVeoD4P0657+VXR73QsLogXQVN/Zt5FHtq9WzpdIZseCJiXPqkgwwA=="], "ai-gateway-provider/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="], @@ -3339,7 +3353,7 @@ "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], @@ -3369,16 +3383,10 @@ "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], - "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], - "fast-json-stringify/fast-uri": ["fast-uri@4.1.3", "", {}, "sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw=="], "fdir/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "fstream/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], @@ -3399,8 +3407,6 @@ "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], - "minipass-fetch/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -3441,10 +3447,6 @@ "safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], "shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], @@ -3465,7 +3467,7 @@ "tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="], - "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "unzipper/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -3605,28 +3607,6 @@ "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], - - "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], - - "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], - - "@modelcontextprotocol/sdk/raw-body/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -3663,9 +3643,33 @@ "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@types/node-fetch/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@slack/bolt/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "@slack/bolt/express/body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], + + "@slack/bolt/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "@slack/bolt/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "@slack/bolt/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "@slack/bolt/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "@slack/bolt/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "@slack/bolt/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "@slack/bolt/express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + + "@slack/bolt/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "@slack/bolt/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "@slack/bolt/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "@slack/bolt/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "@types/node-fetch/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], @@ -3711,8 +3715,6 @@ "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "c12/chokidar/readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -3729,10 +3731,6 @@ "exceljs/archiver/zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="], - "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "fstream/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -3761,12 +3759,8 @@ "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "unzipper/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "unzipper/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -3791,14 +3785,6 @@ "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], - "@modelcontextprotocol/sdk/express/body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - - "@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - - "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], - "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="], @@ -3807,6 +3793,20 @@ "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], + "@slack/bolt/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "@slack/bolt/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "@slack/bolt/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "@slack/bolt/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "@slack/bolt/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "@slack/bolt/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "@slack/bolt/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@types/node-fetch/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -3869,6 +3869,10 @@ "venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@slack/bolt/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@slack/bolt/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], diff --git a/docs/APP-BRIDGE.md b/docs/APP-BRIDGE.md index ab5b4cee7c..a8e8738e44 100644 --- a/docs/APP-BRIDGE.md +++ b/docs/APP-BRIDGE.md @@ -81,6 +81,33 @@ registration expired or the daemon restarted -- register again. Send one at leas } ``` +## Using `@redrob-code/app-bridge` + +The shortest path from JavaScript: `packages/app-bridge` serves the tools and does steps 1 to +3 below in one call. It starts a streamable-HTTP MCP server on `127.0.0.1` (random free +port), finds or starts the daemon with `connectOrStart`, registers through `bridgeApp` and +unregisters on `stop()`. + +```ts +import { startAppBridge } from "@redrob-code/app-bridge" +import { z } from "zod" + +const bridge = await startAppBridge({ + name: "canvas", + version: app.getVersion(), + tools: [ + { name: "select", description: "Select a layer", input: z.object({ id: z.string() }), handler: ({ id }) => canvas.select(id) }, + { name: "export_png", description: "Export the document as PNG", handler: async () => await canvas.exportPng() }, + ], +}) +// bridge.port, bridge.url, bridge.id; bridge.setTools([...]) sends tools/list_changed. +app.on("before-quit", () => void bridge.stop()) +``` + +`input` is a zod object schema (validated before the handler runs) or a JSON Schema object; +a handler answers a string or an MCP `CallToolResult`. See the package README for the +full API. The sections below are the pieces it is built from. + ## From JavaScript (`@redrob-labs/sdk/daemon`) ```ts diff --git a/packages/app-bridge/README.md b/packages/app-bridge/README.md new file mode 100644 index 0000000000..d2ff28abf0 --- /dev/null +++ b/packages/app-bridge/README.md @@ -0,0 +1,48 @@ +# @redrob-code/app-bridge + +One call that hands the Redrob daemon agent an app's tools. It serves the tools as an MCP +server (streamable HTTP, `127.0.0.1`, random free port), finds or starts the shared daemon +through `@redrob-labs/sdk/daemon`, registers the server with `POST /v1/apps/register`, keeps the +registration alive (re-registering after a daemon restart) and unregisters on `stop()`. +Protocol details: `docs/APP-BRIDGE.md`. + +```ts +import { startAppBridge } from "@redrob-code/app-bridge" +import { z } from "zod" + +const bridge = await startAppBridge({ + name: "canvas", + version: app.getVersion(), + tools: [ + { + name: "select", + description: "Select the layer with the given id.", + input: z.object({ id: z.string() }), + handler: ({ id }) => (canvas.select(String(id)) ? `selected ${id}` : `no layer ${id}`), + }, + { + name: "export_png", + description: "Export the current document as PNG and answer the file path.", + input: { type: "object", properties: { scale: { type: "number" } } }, + handler: async ({ scale }) => await canvas.exportPng(typeof scale === "number" ? scale : 1), + }, + ], +}) +console.log(`bridged on port ${bridge.port} as ${bridge.id}`) + +bridge.setTools([...]) // when the tool set changes; clients get tools/list_changed +app.on("before-quit", () => void bridge.stop()) // unregister, close sessions, stop listening +``` + +## API + +- `startAppBridge({ name, version?, tools, connect?, daemon?, path? }) -> Promise` + - `tools: ToolDef[]` with `{ name, description, input?, handler(args) }`. `input` is a zod + object schema (v3 or v4; arguments are validated before `handler` runs) or a JSON Schema + object (passed to the engine as is). `handler` answers a string (one text block) or an MCP + `CallToolResult`. A thrown error becomes an `isError` result, not a dropped request. + - `connect`: an existing `DaemonConnection`; otherwise `connectOrStart(daemon)` is used. + - `path`: the MCP path, default `/mcp`. +- `AppBridgeHandle`: `port`, `url`, `id` (current registration id), `setTools(tools)`, `stop()`. + +The server binds loopback only, which is also the only thing the engine accepts. diff --git a/packages/app-bridge/package.json b/packages/app-bridge/package.json new file mode 100644 index 0000000000..efaa85004a --- /dev/null +++ b/packages/app-bridge/package.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@redrob-code/app-bridge", + "version": "0.0.0", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "1.29.0", + "@redrob-labs/sdk": "workspace:*", + "zod": "catalog:" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/app-bridge/src/index.ts b/packages/app-bridge/src/index.ts new file mode 100644 index 0000000000..781aab5cf6 --- /dev/null +++ b/packages/app-bridge/src/index.ts @@ -0,0 +1,233 @@ +/** + * The app bridge, in one call: serve the app's tools as an MCP server on loopback, register that + * server with the shared daemon, keep the registration alive, unregister on stop. + * + * docs/APP-BRIDGE.md. The daemon side (find or start, register, heartbeat, re-register after a + * daemon restart) is `@redrob-labs/sdk/daemon`; this package adds the MCP server in front of it + * so a desktop app does not have to assemble one from the MCP SDK itself. + * + * Streamable HTTP over `node:http`, bound to 127.0.0.1 on a random free port, one MCP session per + * connecting client (the daemon is normally the only one). Stateful so that the server can push + * `notifications/tools/list_changed` when the app swaps its tool set with `setTools`. + */ +import { randomUUID } from "node:crypto" +import http from "node:http" +import type { AddressInfo } from "node:net" +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js" +import type { AnySchema } from "@modelcontextprotocol/sdk/server/zod-compat.js" +import { parseWithCompat, toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js" +import { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult } from "@modelcontextprotocol/sdk/types.js" +import { + bridgeApp, + connectOrStart, + type DaemonConnection, + type DaemonOptions, +} from "@redrob-labs/sdk/daemon" + +/** A JSON Schema object describing the tool's arguments (`type: "object"`). */ +export type JsonSchema = { readonly type: "object"; readonly [key: string]: unknown } + +/** What a tool handler may answer: a plain string becomes one text content block. */ +export type ToolResult = string | CallToolResult + +export type ToolDef = { + /** The app's own tool name; the engine shows it under `app:`. Short and verb-first. */ + readonly name: string + readonly description: string + /** + * The arguments: a zod object schema (v3 or v4, validated before the handler runs) or a JSON + * Schema object (passed to the engine as is, not validated here). Omitted: no arguments. + */ + readonly input?: AnySchema | JsonSchema + readonly handler: (args: Record) => ToolResult | Promise +} + +export type StartAppBridgeOptions = { + /** The app's name; normalised by the engine to `[a-z0-9._-]`, at most 64 characters. */ + readonly name: string + readonly version?: string + readonly tools: readonly ToolDef[] + /** A daemon connection to use. Omitted: `connectOrStart(daemon)` from `@redrob-labs/sdk/daemon`. */ + readonly connect?: DaemonConnection + /** Options for `connectOrStart` when `connect` is not given. */ + readonly daemon?: DaemonOptions + /** URL path the MCP server answers on. Default `/mcp`. */ + readonly path?: string +} + +export type AppBridgeHandle = { + /** The loopback port the MCP server listens on. */ + readonly port: number + /** The MCP URL the daemon was given, `http://127.0.0.1:/mcp`. */ + readonly url: string + /** The current registration id with the daemon; changes after a daemon restart. */ + readonly id: string + /** Replace the tool set; every connected client is told `tools/list_changed`. */ + readonly setTools: (tools: readonly ToolDef[]) => void + /** Unregister from the daemon, close every MCP session, stop listening. Safe to call twice. */ + readonly stop: () => Promise +} + +const NO_ARGS: JsonSchema = { type: "object", properties: {} } + +function isZodSchema(input: AnySchema | JsonSchema): input is AnySchema { + return typeof input === "object" && input !== null && ("_def" in input || "_zod" in input || "~standard" in input) +} + +function inputSchema(def: ToolDef): JsonSchema { + if (!def.input) return NO_ARGS + if (isZodSchema(def.input)) return toJsonSchemaCompat(def.input, { strictUnions: true }) as JsonSchema + return def.input +} + +function toCallResult(result: ToolResult): CallToolResult { + if (typeof result === "string") return { content: [{ type: "text", text: result }] } + return result +} + +function failure(message: string): CallToolResult { + return { isError: true, content: [{ type: "text", text: message }] } +} + +type Session = { readonly server: McpServer; readonly transport: StreamableHTTPServerTransport } + +/** + * Serve `tools` over MCP on 127.0.0.1 and bridge the server into the shared daemon. Resolves once + * the daemon has accepted the registration; rejects when the daemon cannot be reached or refuses + * the app (the HTTP server is closed again in that case). + */ +export async function startAppBridge(options: StartAppBridgeOptions): Promise { + const path = options.path ?? "/mcp" + let tools = new Map(options.tools.map((t) => [t.name, t] as const)) + const sessions = new Map() + + const newServer = (): McpServer => { + const server = new McpServer( + { name: options.name, version: options.version ?? "0.0.0" }, + { capabilities: { tools: { listChanged: true } } }, + ) + server.server.setRequestHandler(ListToolsRequestSchema, () => ({ + tools: [...tools.values()].map((t) => ({ name: t.name, description: t.description, inputSchema: inputSchema(t) })), + })) + server.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const def = tools.get(request.params.name) + if (!def) return failure(`Unknown tool: ${request.params.name}`) + let args: Record = request.params.arguments ?? {} + if (def.input && isZodSchema(def.input)) { + try { + args = parseWithCompat(def.input, args) as Record + } catch (cause) { + return failure(`Invalid arguments for ${def.name}: ${cause instanceof Error ? cause.message : String(cause)}`) + } + } + try { + return toCallResult(await def.handler(args)) + } catch (cause) { + return failure(cause instanceof Error ? cause.message : String(cause)) + } + }) + return server + } + + const httpServer = http.createServer(async (req, res) => { + const url = new URL(req.url ?? "/", "http://127.0.0.1") + if (url.pathname !== path) { + res.writeHead(404).end() + return + } + try { + const sessionId = req.headers["mcp-session-id"] + if (typeof sessionId === "string") { + const session = sessions.get(sessionId) + if (!session) { + res.writeHead(404, { "content-type": "application/json" }).end( + JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message: "Session not found" }, id: null }), + ) + return + } + await session.transport.handleRequest(req, res) + return + } + if (req.method !== "POST") { + res.writeHead(405, { allow: "POST" }).end() + return + } + // No session yet: this must be `initialize`. The transport answers 400 itself when it is not, + // in which case `onsessioninitialized` never fires and the pair is dropped below. + const server = newServer() + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (id) => { + sessions.set(id, { server, transport }) + }, + onsessionclosed: (id) => { + sessions.delete(id) + }, + }) + transport.onclose = () => { + if (transport.sessionId) sessions.delete(transport.sessionId) + } + await server.connect(transport) + await transport.handleRequest(req, res) + if (!transport.sessionId) await server.close().catch(() => undefined) + } catch (cause) { + if (!res.headersSent) { + res.writeHead(500, { "content-type": "application/json" }).end( + JSON.stringify({ + jsonrpc: "2.0", + error: { code: -32603, message: cause instanceof Error ? cause.message : String(cause) }, + id: null, + }), + ) + } + } + }) + + const port = await new Promise((resolve, reject) => { + httpServer.once("error", reject) + httpServer.listen(0, "127.0.0.1", () => resolve((httpServer.address() as AddressInfo).port)) + }) + const mcpUrl = `http://127.0.0.1:${port}${path}` + + const closeAll = async () => { + const open = [...sessions.values()] + sessions.clear() + await Promise.all(open.map((s) => s.server.close().catch(() => undefined))) + await new Promise((resolve) => httpServer.close(() => resolve())) + httpServer.closeAllConnections?.() + } + + let bridge: Awaited> + try { + const connection = options.connect ?? (await connectOrStart(options.daemon)) + bridge = await bridgeApp(connection, { + name: options.name, + mcpUrl, + ...(options.version !== undefined ? { version: options.version } : {}), + tools: [...tools.keys()], + }) + } catch (cause) { + await closeAll() + throw cause + } + + let stopped = false + return { + port, + url: mcpUrl, + get id() { + return bridge.id + }, + setTools: (next) => { + tools = new Map(next.map((t) => [t.name, t] as const)) + for (const session of sessions.values()) session.server.sendToolListChanged() + }, + stop: async () => { + if (stopped) return + stopped = true + await bridge.stop() + await closeAll() + }, + } +} diff --git a/packages/app-bridge/tsconfig.json b/packages/app-bridge/tsconfig.json new file mode 100644 index 0000000000..00ef125468 --- /dev/null +++ b/packages/app-bridge/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "noUncheckedIndexedAccess": false + } +} From f528dfb6175d99cf0e6b142d0f668da2d17efa38 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:37:09 +0000 Subject: [PATCH 15/23] feat(channels): WhatsApp transport over Baileys with QR pairing New workspace package @redrob-code/channels-whatsapp. Pairs as a linked device via QR (no secret); start() resolves while unpaired and the QR is exposed through qr()/qrDataUrl() and written to channels/whatsapp/qr.png. conversationId = remoteJid, senderId = phone number; fromMe and status broadcasts dropped; maxChunk 4000; composing presence as typing(). --- bun.lock | 257 +++++++++++++-- docs/CHANNELS.md | 19 ++ packages/channels/whatsapp/README.md | 69 ++++ packages/channels/whatsapp/package.json | 27 ++ packages/channels/whatsapp/src/index.ts | 400 +++++++++++++++++++++++ packages/channels/whatsapp/tsconfig.json | 8 + 6 files changed, 754 insertions(+), 26 deletions(-) create mode 100644 packages/channels/whatsapp/README.md create mode 100644 packages/channels/whatsapp/package.json create mode 100644 packages/channels/whatsapp/src/index.ts create mode 100644 packages/channels/whatsapp/tsconfig.json diff --git a/bun.lock b/bun.lock index a924ab932a..fe8e1bfea9 100644 --- a/bun.lock +++ b/bun.lock @@ -41,6 +41,23 @@ "typescript": "catalog:", }, }, + "packages/channels/whatsapp": { + "name": "@redrob-code/channels-whatsapp", + "version": "0.0.0", + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "@whiskeysockets/baileys": "6.7.24", + "qrcode": "1.5.4", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@types/qrcode": "1.5.6", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, "packages/cli": { "name": "@redrob-code/cli", "version": "0.0.0", @@ -863,10 +880,18 @@ "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.14.0", "", {}, "sha512-C3UGsiCwSprE2NKIIFA3hCDlpXTMCAXRZuEVp88L1GY36Y41+rYL5fryE+nOFhp4p4JPQvdV8PQ4DWgHgeTE+w=="], "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.14.0", "", { "dependencies": { "@bufbuild/protobuf": "2.14.0", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-S0TvZhVtTEkZQRj2JsXBCXiib/b0+bLQtJCcn+hj9O/usgbcgxHn4D+ii22Ao3qj0/kKxVYeo7BtAI2dMk+iig=="], + "@cacheable/memory": ["@cacheable/memory@2.2.0", "", { "dependencies": { "@cacheable/utils": "^2.5.0", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" } }, "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ=="], + + "@cacheable/node-cache": ["@cacheable/node-cache@1.7.6", "", { "dependencies": { "cacheable": "^2.3.1", "hookified": "^1.14.0", "keyv": "^5.5.5" } }, "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A=="], + + "@cacheable/utils": ["@cacheable/utils@2.5.0", "", { "dependencies": { "hashery": "^1.5.1", "keyv": "^5.6.0" } }, "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA=="], + "@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="], "@clack/prompts": ["@clack/prompts@1.0.0-alpha.1", "", { "dependencies": { "@clack/core": "1.0.0-alpha.1", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-07MNT0OsxjKOcyVfX8KhXBhJiyUbDP1vuIAcHc+nx5v93MJO23pX3X/k3bWz6T3rpM9dgWPq90i4Jq7gZAyMbw=="], @@ -881,6 +906,8 @@ "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.83", "", { "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-6OaxLsWffxkh9pXYUSyj/AxjVb9URY2rG9U6atjxClWy30Jx77R9Pm3Rrc7cQ63kQurePavEw1bQbzQ/SILiQQ=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -977,6 +1004,10 @@ "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], + "@hapi/boom": ["@hapi/boom@9.1.4", "", { "dependencies": { "@hapi/hoek": "9.x.x" } }, "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw=="], + + "@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + "@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="], "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="], @@ -987,6 +1018,60 @@ "@hono/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.4" }, "os": "darwin", "cpu": "x64" }, "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "os": "freebsd" }, "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "cpu": "none" }, "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="], + "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], @@ -1013,6 +1098,10 @@ "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + "@keyv/bigmap": ["@keyv/bigmap@1.3.1", "", { "dependencies": { "hashery": "^1.4.0", "hookified": "^1.15.0" }, "peerDependencies": { "keyv": "^5.6.0" } }, "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ=="], + + "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], + "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], @@ -1295,6 +1384,8 @@ "@redrob-code/channels-core": ["@redrob-code/channels-core@workspace:packages/channels/core"], + "@redrob-code/channels-whatsapp": ["@redrob-code/channels-whatsapp@workspace:packages/channels/whatsapp"], + "@redrob-code/cli": ["@redrob-code/cli@workspace:packages/cli"], "@redrob-code/client": ["@redrob-code/client@workspace:packages/client"], @@ -1469,6 +1560,10 @@ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@tsconfig/bun": ["@tsconfig/bun@1.0.9", "", {}, "sha512-4M0/Ivfwcpz325z6CwSifOBZYji3DFOEpY6zEUt0+Xi2qRhzwvmqQN9XAHJh3OVvRJuAqVTLU2abdCplvp6mwQ=="], "@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="], @@ -1545,6 +1640,8 @@ "@types/promise.allsettled": ["@types/promise.allsettled@1.0.6", "", {}, "sha512-wA0UT0HeT2fGHzIFV9kWpYz5mdoyLxKrTgMdZQM++5h6pYAFH73HXcQhefg24nD1yivUFEn5KU+EF4b+CXJ4Wg=="], + "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], + "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], @@ -1597,6 +1694,8 @@ "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + "@whiskeysockets/baileys": ["@whiskeysockets/baileys@6.7.24", "", { "dependencies": { "@cacheable/node-cache": "^1.4.0", "@hapi/boom": "^9.1.3", "async-mutex": "^0.5.0", "axios": "^1.6.0", "libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git", "music-metadata": "^11.7.0", "pino": "^9.6", "protobufjs": "^7.2.4", "ws": "^8.13.0" }, "peerDependencies": { "audio-decode": "^2.1.3", "jimp": "^1.6.0", "link-preview-js": "^3.0.0", "sharp": "*" }, "optionalPeers": ["audio-decode", "jimp", "link-preview-js"] }, "sha512-Ljq7si+gsNIE9d5dP69E99TTQp+BxxPgitWvmZMXExGc1Ctr0SCDLFZmxRLUDC5HB0/Itw5uk+HJeBONrLP99Q=="], + "@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="], "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], @@ -1609,7 +1708,7 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], @@ -1647,6 +1746,8 @@ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], @@ -1735,12 +1836,16 @@ "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="], + "cacheable": ["cacheable@2.5.0", "", { "dependencies": { "@cacheable/memory": "^2.2.0", "@cacheable/utils": "^2.5.0", "hookified": "^1.15.0", "keyv": "^5.6.0", "qified": "^0.10.1" } }, "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g=="], + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -1817,6 +1922,8 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "curve25519-js": ["curve25519-js@0.0.4", "", {}, "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], @@ -1829,6 +1936,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], @@ -1863,6 +1972,8 @@ "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], "docx": ["docx@9.8.1", "", { "dependencies": { "@types/node": "^26.6.2", "hash.js": "^1.1.7", "jszip": "^3.10.1", "nanoid": "^6.0.1", "xml": "^1.0.1", "xml-js": "^1.6.8" } }, "sha512-bv4yDxgiV9aRyh327HG+V36wG0f23wk32SDhXGQpUhnykVtIPaR1K4ISi8E+DYJ7OcRQRkR8Y70QYlSS08V1cA=="], @@ -1995,6 +2106,8 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "file-type": ["file-type@21.3.4", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], @@ -2005,7 +2118,7 @@ "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], - "find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="], @@ -2015,7 +2128,7 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "form-data": ["form-data@2.5.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA=="], + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], @@ -2097,6 +2210,8 @@ "hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="], + "hashery": ["hashery@1.5.1", "", { "dependencies": { "hookified": "^1.15.0" } }, "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], @@ -2107,6 +2222,8 @@ "hono": ["hono@4.13.5", "", {}, "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw=="], + "hookified": ["hookified@1.15.1", "", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="], + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], @@ -2123,7 +2240,7 @@ "https": ["https@1.0.0", "", {}, "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], @@ -2295,19 +2412,23 @@ "jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="], + "keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], + "libsignal": ["libsignal@github:whiskeysockets/libsignal-node#bcea72d", { "dependencies": { "curve25519-js": "^0.0.4", "protobufjs": "^7.5.5" } }, "WhiskeySockets-libsignal-node-bcea72d", "sha512-zKDaKfYGwQ/ALHPan2W2KZq/6njngElVNcqQQmQy0rl3fZbaqjiIchbPaJyCVwkTwHx1+s3Li1U/tf+f+AbCQA=="], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], "listenercount": ["listenercount@1.0.1", "", {}, "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="], - "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], @@ -2363,7 +2484,7 @@ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "media-typer": ["media-typer@2.0.0", "", {}, "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q=="], "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], @@ -2423,6 +2544,8 @@ "multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="], + "music-metadata": ["music-metadata@11.16.1", "", { "dependencies": { "@borewit/text-codec": "^0.2.2", "@tokenizer/token": "^0.3.0", "content-type": "^2.1.0", "debug": "^4.4.3", "file-type": "^21.3.4", "media-typer": "^2.0.0", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0", "win-guid": "^0.2.1" } }, "sha512-uR/mHK6eyfl8h3jVCobF5b38Mfg1IDJMiBxIVbmm5F+G1OXEFcUpQHveO84tJBAqvY93GCRY2R4kDyx7s06Rug=="], + "nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="], "nanoid": ["nanoid@6.0.1", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw=="], @@ -2515,7 +2638,7 @@ "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "p-map": ["p-map@7.0.6", "", {}, "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg=="], @@ -2541,7 +2664,7 @@ "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], - "path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], @@ -2565,9 +2688,9 @@ "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], + "pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="], - "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], @@ -2577,6 +2700,8 @@ "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "poe-oauth": ["poe-oauth@0.0.14", "", {}, "sha512-EHteyy5PsT8cl9zVjYcCQsm6FmLgqfwBkGj6IzUA83d9aWc57bH+jj2ItwJlf6RBhK+DN5N8YVlwEc0gOLb5fw=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -2615,6 +2740,10 @@ "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], + "qified": ["qified@0.10.1", "", { "dependencies": { "hookified": "^2.1.1" } }, "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA=="], + + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], @@ -2659,8 +2788,12 @@ "remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], @@ -2717,6 +2850,8 @@ "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -2729,6 +2864,8 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "sharp": ["sharp@0.35.5", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.5", "@img/sharp-darwin-x64": "0.35.5", "@img/sharp-freebsd-wasm32": "0.35.5", "@img/sharp-libvips-darwin-arm64": "1.3.4", "@img/sharp-libvips-darwin-x64": "1.3.4", "@img/sharp-libvips-linux-arm": "1.3.4", "@img/sharp-libvips-linux-arm64": "1.3.4", "@img/sharp-libvips-linux-ppc64": "1.3.4", "@img/sharp-libvips-linux-riscv64": "1.3.4", "@img/sharp-libvips-linux-s390x": "1.3.4", "@img/sharp-libvips-linux-x64": "1.3.4", "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", "@img/sharp-libvips-linuxmusl-x64": "1.3.4", "@img/sharp-linux-arm": "0.35.5", "@img/sharp-linux-arm64": "0.35.5", "@img/sharp-linux-ppc64": "0.35.5", "@img/sharp-linux-riscv64": "0.35.5", "@img/sharp-linux-s390x": "0.35.5", "@img/sharp-linux-x64": "0.35.5", "@img/sharp-linuxmusl-arm64": "0.35.5", "@img/sharp-linuxmusl-x64": "0.35.5", "@img/sharp-webcontainers-wasm32": "0.35.5", "@img/sharp-win32-arm64": "0.35.5", "@img/sharp-win32-ia32": "0.35.5", "@img/sharp-win32-x64": "0.35.5" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -2827,6 +2964,8 @@ "strnum": ["strnum@2.4.2", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw=="], + "strtok3": ["strtok3@10.4.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-MeSp4vHXiZRXM9K0rSWqTvXMU+YzmGICc5+briMVK+RY8qQ1GRTffFYVpK6cSgV/ueImFIEh3uCJJD3xBh1TaQ=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], @@ -2839,7 +2978,7 @@ "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], - "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], + "thread-stream": ["thread-stream@3.2.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw=="], "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], @@ -2855,6 +2994,8 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + "toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], @@ -2897,6 +3038,8 @@ "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "uint8array-extras": ["uint8array-extras@1.6.0", "", {}, "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q=="], + "ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], @@ -2963,10 +3106,14 @@ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], + "win-guid": ["win-guid@0.2.1", "", {}, "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A=="], + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -3227,6 +3374,10 @@ "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "@npmcli/agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "@npmcli/agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -3299,7 +3450,11 @@ "@slack/web-api/eventemitter3": ["eventemitter3@3.1.2", "", {}, "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="], - "@types/node-fetch/form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], + "@slack/web-api/form-data": ["form-data@2.5.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA=="], + + "@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "@whiskeysockets/baileys/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -3329,10 +3484,6 @@ "archiver-utils/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "axios/form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], - - "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], @@ -3375,6 +3526,8 @@ "fast-json-stringify/fast-uri": ["fast-uri@4.1.3", "", {}, "sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw=="], + "fastify/pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], + "fdir/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -3383,6 +3536,8 @@ "fstream/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + "gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], "gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -3391,6 +3546,8 @@ "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -3405,6 +3562,8 @@ "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "music-metadata/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], @@ -3429,6 +3588,12 @@ "pdf-lib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "qified/hookified": ["hookified@2.2.0", "", {}, "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA=="], + + "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], "redrob/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="], @@ -3445,10 +3610,14 @@ "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], "shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "socks-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -3459,12 +3628,12 @@ "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], - "tinyglobby/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="], + "type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "unzipper/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -3663,7 +3832,9 @@ "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@types/node-fetch/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -3701,10 +3872,6 @@ "archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "axios/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -3731,12 +3898,20 @@ "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "fastify/pino/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], + + "fastify/pino/thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "fstream/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "gcp-metadata/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -3749,6 +3924,16 @@ "opencode-poe-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + + "qrcode/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + + "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "redrob/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="], @@ -3807,7 +3992,7 @@ "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], - "@types/node-fetch/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -3839,8 +4024,6 @@ "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "axios/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -3855,8 +4038,24 @@ "exceljs/archiver/zip-stream/compress-commons": ["compress-commons@4.1.2", "", { "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg=="], + "fastify/pino/thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], + "fstream/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "gcp-metadata/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "qrcode/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "qrcode/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "redrob/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -3885,6 +4084,12 @@ "fstream/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], + "qrcode/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "qrcode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "qrcode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "exceljs/archiver/archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], diff --git a/docs/CHANNELS.md b/docs/CHANNELS.md index 1f106c260b..2c08b60a7c 100644 --- a/docs/CHANNELS.md +++ b/docs/CHANNELS.md @@ -62,6 +62,13 @@ except to list key *names*. Transports see it through the `SecretStore` interfac (`get`, `set`, `delete`, `keys`), and only their own channel's slice, so the file store can be swapped for a keychain later without touching a transport. +Secret key names per channel: + +| Channel | Keys | Notes | +| --- | --- | --- | +| `slack` | `botToken`, `appToken` | Socket Mode app | +| `whatsapp` | *(none)* | pairs by QR code; the session lives in `~/.redrob/channels/whatsapp/` (0700) | + ### `sessions.json` One conversation is one engine session. The key is `:` -- a Slack @@ -142,6 +149,18 @@ redrob channels allow slack U0123ABCDEF # your Slack member id redrob channels enable slack ``` +Setting up WhatsApp (`@redrob-code/channels-whatsapp`, an **unofficial** client over Baileys -- +WhatsApp may ban accounts that use it; see the package README): + +```sh +redrob channels allow whatsapp 821012345678 # the sender's phone number, digits only +redrob channels enable whatsapp +``` + +No secret is set. While unpaired the channel starts anyway and writes the QR code to +`~/.redrob/channels/whatsapp/qr.png` (also `qr()` / `qrDataUrl()` on the transport); scan it from +WhatsApp > Linked devices. `conversationId` is the chat JID, `senderId` the phone number. + ## Transport contract (for Stage 2) Package `@redrob-code/channels-core`, file `src/transport.ts`. diff --git a/packages/channels/whatsapp/README.md b/packages/channels/whatsapp/README.md new file mode 100644 index 0000000000..0347af0419 --- /dev/null +++ b/packages/channels/whatsapp/README.md @@ -0,0 +1,69 @@ +# @redrob-code/channels-whatsapp + +The WhatsApp transport for Redrob Code channels (`docs/CHANNELS.md`), built on +[Baileys](https://github.com/WhiskeySockets/Baileys) `6.7.24`. + +**Baileys is an unofficial client.** It speaks the WhatsApp Web protocol directly instead of the +WhatsApp Business API, which WhatsApp's terms do not permit for automation. Accounts running +unofficial clients can be, and sometimes are, **banned by WhatsApp without notice**. Use a number +you can afford to lose, keep the allowlist short, and do not use this transport to message people +who did not message you first. + +## Pairing (no token) + +There is no bot token and nothing to put in `secrets.json`. The daemon is linked to a WhatsApp +account as a *linked device* by scanning a QR code once: + +```sh +redrob channels allow whatsapp 821012345678 # the phone number that may talk, digits only +redrob channels enable whatsapp +``` + +While the channel is not paired, `start()` still resolves and the transport exposes the QR code +three ways: + +| Where | What | +| --- | --- | +| `transport.qr()` | the raw QR string, `undefined` once paired | +| `transport.qrDataUrl()` | the same as a `data:image/png;base64,...` URL | +| `~/.redrob/channels/whatsapp/qr.png` | the PNG on disk (mode 0600), removed once connected | + +Open WhatsApp on the phone, *Linked devices* > *Link a device*, scan. WhatsApp rotates the QR +every ~20 seconds; the file and the getters follow. `transport.pairing()` is `"pairing"` while a +QR is waiting, `"paired"` once the socket is open, `"disconnected"` otherwise. + +The pairing session (`creds.json` plus signal key files, written by Baileys' +`useMultiFileAuthState`) lives in `~/.redrob/channels/whatsapp/`, created `0700`, and is reused on +every start. When the phone unlinks the device (`loggedOut`), the files are deleted and a fresh QR +is produced. `REDROB_CHANNELS_HOME` moves the folder together with the other channel files. + +## Settings + +`channels.json` -> `channels.whatsapp.options` (all optional): + +| Key | Default | Meaning | +| --- | --- | --- | +| `markOnlineOnConnect` | `false` | show the linked device as online; on, the phone stops getting notifications | +| `maxReconnectMs` | `30000` | ceiling of the exponential reconnect backoff | + +Secrets: none. The channel reads nothing from `ctx.secrets`. + +## Mapping + +- `conversationId` = the chat's `remoteJid` (`@s.whatsapp.net` for a direct chat, + `@g.us` for a group). One chat is one engine session. +- `senderId` = the sender's phone number without `@...` or device suffix (the group participant, + or the chat itself in a direct chat). This is what the allowlist matches, e.g. `821012345678`. + When WhatsApp delivers a LID (`@lid`) sender, the phone-number form it sends alongside is used. +- Dropped before reaching the core: the account's own messages (`fromMe`), status broadcasts and + broadcast lists, history sync / offline backfill, and media without a caption. +- `maxChunk` is 4000; `typing()` sends a `composing` presence update. + +## Exports + +```ts +import { WhatsAppTransport, whatsappTransport, authDirectory, qrFile } from "@redrob-code/channels-whatsapp" +``` + +`whatsappTransport` is the `TransportFactory` the registry uses; `WhatsAppTransport` is the class +when a launcher wants `qr()` / `qrDataUrl()` / `paired()` / `pairing()`. diff --git a/packages/channels/whatsapp/package.json b/packages/channels/whatsapp/package.json new file mode 100644 index 0000000000..171fe9bdd7 --- /dev/null +++ b/packages/channels/whatsapp/package.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@redrob-code/channels-whatsapp", + "version": "0.0.0", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "@whiskeysockets/baileys": "6.7.24", + "qrcode": "1.5.4" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@types/qrcode": "1.5.6", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/channels/whatsapp/src/index.ts b/packages/channels/whatsapp/src/index.ts new file mode 100644 index 0000000000..a22e8594cc --- /dev/null +++ b/packages/channels/whatsapp/src/index.ts @@ -0,0 +1,400 @@ +/** + * @redrob-code/channels-whatsapp: the WhatsApp transport, over Baileys. + * + * Baileys speaks the WhatsApp Web protocol directly, so there is no bot token: the daemon is paired + * as a *linked device* of a WhatsApp account by scanning a QR code once. The session that pairing + * produces lives in `~/.redrob/channels/whatsapp/` (mode 0700, written by Baileys' + * `useMultiFileAuthState`) and is reused on every start until the phone unlinks it. + * + * Pairing is exposed rather than printed: `start()` resolves as soon as the socket has either + * connected or produced a QR code, `qr()` returns the latest raw QR string, `qrDataUrl()` the same + * as a PNG data URL, and the PNG is also written to `~/.redrob/channels/whatsapp/qr.png` so a + * launcher can show it. `paired()` says whether the account is linked right now. The file is + * removed once the connection opens. + * + * Message mapping: `conversationId` is the chat's `remoteJid` (a user or a group), `senderId` is the + * phone number of the sender (the `participant` in a group, the chat itself in a direct chat), so + * the allowlist takes plain numbers like `821012345678`. The transport's own messages (`fromMe`) + * and status broadcasts are dropped before they reach the core. + */ +import fs from "node:fs/promises" +import path from "node:path" +import { Paths, type Inbound, type Logger, type Transport, type TransportContext, type TransportFactory } from "@redrob-code/channels-core" +import makeWASocket, { + Browsers, + DisconnectReason, + fetchLatestBaileysVersion, + isJidStatusBroadcast, + jidDecode, + useMultiFileAuthState, + type UserFacingSocketConfig, + type WAMessage, + type WASocket, + type proto, +} from "@whiskeysockets/baileys" +import QRCode from "qrcode" + +/** Baileys' pino-shaped logger interface, which the package does not re-export. */ +type BaileysLogger = NonNullable + +/** WhatsApp accepts 65536 characters in a text message; 4000 keeps replies readable on a phone. */ +export const WHATSAPP_MAX_CHUNK = 4000 + +/** Non-secret settings read from `channels.json` -> `channels.whatsapp.options`. */ +export type WhatsAppOptions = { + /** + * Show the linked device as online while the daemon runs. Off by default: an online linked device + * stops WhatsApp from pushing notifications to the phone. + */ + readonly markOnlineOnConnect?: boolean + /** Longest reconnect wait in milliseconds. Default 30000. */ + readonly maxReconnectMs?: number +} + +export type PairingState = "paired" | "pairing" | "disconnected" + +/** Where the pairing session and the QR image live: `/whatsapp`. */ +export function authDirectory(env: NodeJS.ProcessEnv = process.env): string { + return path.join(Paths.directory(env), "whatsapp") +} + +export function qrFile(env: NodeJS.ProcessEnv = process.env): string { + return path.join(authDirectory(env), "qr.png") +} + +function options(ctx: TransportContext): WhatsAppOptions { + const raw = ctx.config.options ?? {} + return { + markOnlineOnConnect: raw.markOnlineOnConnect === true, + maxReconnectMs: typeof raw.maxReconnectMs === "number" && raw.maxReconnectMs > 0 ? raw.maxReconnectMs : 30_000, + } +} + +/** The phone-number part of a JID: `821012345678:3@s.whatsapp.net` -> `821012345678`. */ +export function phoneOf(jid: string | undefined | null): string | undefined { + if (!jid) return undefined + const decoded = jidDecode(jid) + return decoded?.user || undefined +} + +/** The text a human typed, unwrapping ephemeral / view-once / edited envelopes; undefined for media without caption. */ +export function textOf(message: proto.IMessage | null | undefined): string | undefined { + let m = message ?? undefined + for (let i = 0; m && i < 4; i++) { + const inner = + m.ephemeralMessage?.message ?? + m.viewOnceMessage?.message ?? + m.viewOnceMessageV2?.message ?? + m.documentWithCaptionMessage?.message ?? + m.editedMessage?.message?.protocolMessage?.editedMessage + if (!inner) break + m = inner + } + if (!m) return undefined + const text = + m.conversation ?? + m.extendedTextMessage?.text ?? + m.imageMessage?.caption ?? + m.videoMessage?.caption ?? + m.documentMessage?.caption + return typeof text === "string" && text.trim() !== "" ? text : undefined +} + +/** Baileys wants a pino-shaped logger; route warnings and errors to the channel log and drop the rest. */ +function baileysLogger(log: Logger): BaileysLogger { + const describe = (obj: unknown, msg?: string): string => { + if (msg) return msg + if (obj instanceof Error) return obj.message + if (typeof obj === "string") return obj + try { + return JSON.stringify(obj) + } catch { + return String(obj) + } + } + const logger: BaileysLogger = { + level: "warn", + child: () => logger, + trace: () => undefined, + debug: () => undefined, + info: () => undefined, + warn: (obj: unknown, msg?: string) => log.warn(`baileys: ${describe(obj, msg)}`), + error: (obj: unknown, msg?: string) => log.error(`baileys: ${describe(obj, msg)}`), + } + return logger +} + +function statusCode(error: unknown): number | undefined { + const output = (error as { output?: { statusCode?: unknown } } | undefined)?.output + return typeof output?.statusCode === "number" ? output.statusCode : undefined +} + +export class WhatsAppTransport implements Transport { + readonly name = "whatsapp" + readonly maxChunk = WHATSAPP_MAX_CHUNK + + private readonly env: NodeJS.ProcessEnv + private ctx?: TransportContext + private sock?: WASocket + private saveCreds?: () => Promise + private stopping = false + private open = false + private latestQr?: string + private latestQrDataUrl?: string + private reconnectAttempt = 0 + private reconnectTimer?: ReturnType + + constructor(env: NodeJS.ProcessEnv = process.env) { + this.env = env + } + + /** The latest QR code as the raw string WhatsApp expects, while pairing; undefined once paired. */ + qr(): string | undefined { + return this.latestQr + } + + /** The latest QR code as a `data:image/png;base64,...` URL, while pairing. */ + qrDataUrl(): string | undefined { + return this.latestQrDataUrl + } + + /** Whether the account is linked and the socket is open. */ + paired(): boolean { + return this.open + } + + pairing(): PairingState { + if (this.open) return "paired" + if (this.latestQr) return "pairing" + return "disconnected" + } + + /** + * Resolves once the socket is open *or* a QR code is waiting to be scanned, so an unpaired channel + * still starts and the launcher can pick the QR up. Rejects when the connection closes for good + * before either happens. + */ + async start(ctx: TransportContext): Promise { + this.ctx = ctx + this.stopping = false + const dir = authDirectory(this.env) + await fs.mkdir(dir, { recursive: true, mode: 0o700 }) + await fs.chmod(dir, 0o700).catch(() => undefined) + await new Promise((resolve, reject) => { + let settled = false + const done = (error?: Error) => { + if (settled) return + settled = true + if (error) reject(error) + else resolve() + } + void this.connect(done).catch((error: unknown) => done(error instanceof Error ? error : new Error(String(error)))) + }) + } + + async stop(): Promise { + this.stopping = true + if (this.reconnectTimer) clearTimeout(this.reconnectTimer) + this.reconnectTimer = undefined + const sock = this.sock + this.sock = undefined + this.open = false + this.clearQr() + await this.removeQrFile() + if (sock) { + try { + sock.ev.removeAllListeners("connection.update") + sock.ev.removeAllListeners("messages.upsert") + sock.ev.removeAllListeners("creds.update") + sock.end(undefined) + } catch { + // already closed + } + } + } + + async send(conversationId: string, chunks: readonly string[]): Promise { + const sock = this.socket() + for (const chunk of chunks) { + await sock.sendMessage(conversationId, { text: chunk }) + } + await sock.sendPresenceUpdate("paused", conversationId).catch(() => undefined) + } + + async typing(conversationId: string): Promise { + await this.socket().sendPresenceUpdate("composing", conversationId) + } + + private socket(): WASocket { + if (!this.sock || !this.open) throw new Error("WhatsApp is not connected (not paired yet, or reconnecting)") + return this.sock + } + + private log(): Logger { + return this.ctx!.log + } + + /** Build a socket and wire events. `settle` is the pending start(), if any. */ + private async connect(settle?: (error?: Error) => void): Promise { + const ctx = this.ctx! + const opts = options(ctx) + const dir = authDirectory(this.env) + const { state, saveCreds } = await useMultiFileAuthState(dir) + this.saveCreds = saveCreds + // Baileys' auth files are written 0644 inside our 0700 folder; the folder is the guard. + const { version } = await fetchLatestBaileysVersion().catch(() => ({ version: undefined })) + const sock = makeWASocket({ + auth: state, + ...(version ? { version } : {}), + logger: baileysLogger(ctx.log), + browser: Browsers.ubuntu("Redrob Code"), + markOnlineOnConnect: opts.markOnlineOnConnect === true, + syncFullHistory: false, + generateHighQualityLinkPreview: false, + // Ignore status updates and broadcast lists at the socket; groups and chats still arrive. + shouldIgnoreJid: (jid) => isJidStatusBroadcast(jid) || jid.endsWith("@broadcast"), + }) + this.sock = sock + + sock.ev.on("creds.update", () => { + void this.saveCreds?.().catch((error: unknown) => + ctx.log.warn(`could not save credentials: ${error instanceof Error ? error.message : String(error)}`), + ) + }) + + sock.ev.on("connection.update", (update) => { + if (this.sock !== sock) return + if (update.qr) { + this.reconnectAttempt = 0 + void this.publishQr(update.qr).then(() => settle?.()) + } + if (update.connection === "open") { + this.open = true + this.reconnectAttempt = 0 + this.clearQr() + void this.removeQrFile() + const me = phoneOf(sock.user?.id) + ctx.log.info(`connected${me ? ` as +${me}` : ""}`) + settle?.() + } + if (update.connection === "close") { + this.open = false + const code = statusCode(update.lastDisconnect?.error) + const reason = update.lastDisconnect?.error?.message ?? "connection closed" + if (this.stopping) return + if (code === DisconnectReason.loggedOut || code === DisconnectReason.forbidden) { + // The phone unlinked this device (or WhatsApp refused it). The stored session is dead; + // drop it and come back with a fresh QR so the account can be re-paired. + ctx.log.warn(`logged out by WhatsApp (${code}); clearing the pairing session, a new QR code will follow`) + void this.resetSession().then(() => this.scheduleReconnect(settle, 1_000)) + return + } + if (code === DisconnectReason.connectionReplaced) { + const error = new Error("another WhatsApp Web session replaced this one; stop the other client and restart the channel") + ctx.log.error(error.message) + settle?.(error) + return + } + const wait = code === DisconnectReason.restartRequired ? 500 : this.backoff(opts.maxReconnectMs ?? 30_000) + ctx.log.warn(`disconnected (${code ?? "?"}: ${reason}); reconnecting in ${wait}ms`) + this.scheduleReconnect(settle, wait) + } + }) + + sock.ev.on("messages.upsert", ({ messages, type }) => { + if (this.sock !== sock) return + // "append" is history sync and offline backfill; only live notifications are prompts. + if (type !== "notify") return + for (const message of messages) this.inbound(message) + }) + } + + private inbound(message: WAMessage): void { + const ctx = this.ctx + if (!ctx) return + const key = message.key + const remoteJid = key.remoteJid ?? undefined + if (!remoteJid || key.fromMe || isJidStatusBroadcast(remoteJid) || remoteJid.endsWith("@broadcast")) return + const text = textOf(message.message) + if (text === undefined) return + // In a group the sender is the participant; in a direct chat it is the chat itself. Prefer the + // phone-number form when WhatsApp handed us a LID (`@lid`) with its number alongside. + const senderJid = key.participant ? (key.participantPn ?? key.participant) : (key.senderPn ?? remoteJid) + const senderId = phoneOf(senderJid) + if (!senderId) return + const inbound: Inbound = { + channel: this.name, + conversationId: remoteJid, + senderId, + ...(message.pushName ? { senderName: message.pushName } : {}), + text, + ...(key.id ? { replyTo: key.id } : {}), + } + ctx.onMessage(inbound) + } + + private async publishQr(qr: string): Promise { + this.latestQr = qr + try { + this.latestQrDataUrl = await QRCode.toDataURL(qr, { errorCorrectionLevel: "M", margin: 2, scale: 6 }) + const file = qrFile(this.env) + const temp = `${file}.${process.pid}.tmp` + await QRCode.toFile(temp, qr, { type: "png", errorCorrectionLevel: "M", margin: 2, scale: 6 }) + await fs.chmod(temp, 0o600).catch(() => undefined) + await fs.rename(temp, file) + this.log().info(`not paired: scan the QR code at ${path.basename(file)} in WhatsApp > Linked devices`) + } catch (error) { + this.log().warn(`could not render the QR code: ${error instanceof Error ? error.message : String(error)}`) + } + } + + private clearQr(): void { + this.latestQr = undefined + this.latestQrDataUrl = undefined + } + + private async removeQrFile(): Promise { + await fs.rm(qrFile(this.env), { force: true }).catch(() => undefined) + } + + /** Delete Baileys' credential and key files so the next connect starts a fresh pairing. */ + private async resetSession(): Promise { + const dir = authDirectory(this.env) + const entries = await fs.readdir(dir).catch(() => [] as string[]) + await Promise.all( + entries.filter((name) => name.endsWith(".json")).map((name) => fs.rm(path.join(dir, name), { force: true }).catch(() => undefined)), + ) + } + + private backoff(max: number): number { + const wait = Math.min(max, 1_000 * 2 ** Math.min(this.reconnectAttempt, 10)) + this.reconnectAttempt++ + return wait + } + + private scheduleReconnect(settle: ((error?: Error) => void) | undefined, wait: number): void { + if (this.stopping) return + const previous = this.sock + this.sock = undefined + try { + previous?.ev.removeAllListeners("connection.update") + previous?.ev.removeAllListeners("messages.upsert") + previous?.ev.removeAllListeners("creds.update") + } catch { + // nothing to detach + } + if (this.reconnectTimer) clearTimeout(this.reconnectTimer) + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = undefined + if (this.stopping) return + void this.connect(settle).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error) + this.log().error(`reconnect failed: ${message}`) + if (settle) settle(error instanceof Error ? error : new Error(message)) + else this.scheduleReconnect(undefined, this.backoff(options(this.ctx!).maxReconnectMs ?? 30_000)) + }) + }, wait) + } +} + +export const whatsappTransport: TransportFactory = () => new WhatsAppTransport() diff --git a/packages/channels/whatsapp/tsconfig.json b/packages/channels/whatsapp/tsconfig.json new file mode 100644 index 0000000000..00ef125468 --- /dev/null +++ b/packages/channels/whatsapp/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "noUncheckedIndexedAccess": false + } +} From df6549aa5ac5bcb5d1f4d75d3ca37596efce661d Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:36:42 +0000 Subject: [PATCH 16/23] channels: Slack transport as @redrob-code/channels-slack Move packages/slack (Bolt 3, Socket Mode) into packages/channels/slack as a Transport for channels-core. One Slack thread is one conversation (`:`), replies go in-thread, app_mention and message.im/channels are handled with bot_id/subtype dropped and mentions de-duplicated. Tokens come from the channel secret store (slack.botToken, slack.appToken), never the environment. The Slack app manifest YAML now lives in this package (manifestYaml, manifestUrl). The old standalone bot and its workspace entry are removed. The CLI's slack-manifest command keeps its own copy of the manifest for now: pointing it at this package needs a dependency in packages/redrob/package.json, which the parent wires after merge. --- bun.lock | 74 +++++--- docs/CHANNELS.md | 20 ++- package.json | 3 +- packages/channels/slack/README.md | 42 +++++ packages/channels/slack/package.json | 26 +++ packages/channels/slack/src/index.ts | 6 + packages/channels/slack/src/manifest.ts | 54 ++++++ packages/channels/slack/src/transport.ts | 178 ++++++++++++++++++++ packages/{ => channels}/slack/tsconfig.json | 0 packages/slack/.env.example | 3 - packages/slack/.gitignore | 4 - packages/slack/README.md | 27 --- packages/slack/package.json | 19 --- packages/slack/src/index.ts | 145 ---------------- packages/slack/sst-env.d.ts | 10 -- 15 files changed, 374 insertions(+), 237 deletions(-) create mode 100644 packages/channels/slack/README.md create mode 100644 packages/channels/slack/package.json create mode 100644 packages/channels/slack/src/index.ts create mode 100644 packages/channels/slack/src/manifest.ts create mode 100644 packages/channels/slack/src/transport.ts rename packages/{ => channels}/slack/tsconfig.json (100%) delete mode 100644 packages/slack/.env.example delete mode 100644 packages/slack/.gitignore delete mode 100644 packages/slack/README.md delete mode 100644 packages/slack/package.json delete mode 100644 packages/slack/src/index.ts delete mode 100644 packages/slack/sst-env.d.ts diff --git a/bun.lock b/bun.lock index a924ab932a..aac8efe4d4 100644 --- a/bun.lock +++ b/bun.lock @@ -41,6 +41,21 @@ "typescript": "catalog:", }, }, + "packages/channels/slack": { + "name": "@redrob-code/channels-slack", + "version": "0.0.0", + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "@slack/bolt": "3.22.0", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, "packages/cli": { "name": "@redrob-code/cli", "version": "0.0.0", @@ -515,19 +530,6 @@ "@typescript/native-preview": "catalog:", }, }, - "packages/slack": { - "name": "@redrob-code/slack", - "version": "0.0.0", - "dependencies": { - "@redrob-labs/sdk": "workspace:*", - "@slack/bolt": "^3.17.1", - }, - "devDependencies": { - "@types/node": "catalog:", - "@typescript/native-preview": "catalog:", - "typescript": "catalog:", - }, - }, "packages/tui": { "name": "@redrob-code/tui", "version": "0.0.0", @@ -1295,6 +1297,8 @@ "@redrob-code/channels-core": ["@redrob-code/channels-core@workspace:packages/channels/core"], + "@redrob-code/channels-slack": ["@redrob-code/channels-slack@workspace:packages/channels/slack"], + "@redrob-code/cli": ["@redrob-code/cli@workspace:packages/cli"], "@redrob-code/client": ["@redrob-code/client@workspace:packages/client"], @@ -1325,8 +1329,6 @@ "@redrob-code/server": ["@redrob-code/server@workspace:packages/server"], - "@redrob-code/slack": ["@redrob-code/slack@workspace:packages/slack"], - "@redrob-code/tui": ["@redrob-code/tui@workspace:packages/tui"], "@redrob-labs/sdk": ["@redrob-labs/sdk@workspace:packages/sdk/js"], @@ -1609,7 +1611,7 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], @@ -2123,7 +2125,7 @@ "https": ["https@1.0.0", "", {}, "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], @@ -2427,7 +2429,7 @@ "nanoid": ["nanoid@6.0.1", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw=="], - "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], @@ -3227,6 +3229,10 @@ "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "@npmcli/agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "@npmcli/agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -3301,9 +3307,9 @@ "@types/node-fetch/form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], - "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.111", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-atgBW8jZPr/KuaKX5FvDIHuXBI8VCol6kVeoD4P0657+VXR73QsLogXQVN/Zt5FHtq9WzpdIZseCJiXPqkgwwA=="], @@ -3331,8 +3337,6 @@ "axios/form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], - "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], @@ -3383,6 +3387,8 @@ "fstream/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + "gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], "gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -3391,6 +3397,8 @@ "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -3399,14 +3407,14 @@ "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], + "make-fetch-happen/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + "minipass-fetch/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -3449,6 +3457,8 @@ "shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "socks-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -3665,6 +3675,8 @@ "@types/node-fetch/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], @@ -3703,8 +3715,6 @@ "axios/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -3737,6 +3747,10 @@ "fstream/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "gcp-metadata/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -3745,6 +3759,8 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "make-fetch-happen/negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "opencode-poe-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], @@ -3791,6 +3807,8 @@ "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + "@modelcontextprotocol/sdk/express/body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], @@ -3857,6 +3875,8 @@ "fstream/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "gcp-metadata/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "redrob/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -3869,6 +3889,8 @@ "venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], diff --git a/docs/CHANNELS.md b/docs/CHANNELS.md index 1f106c260b..d315205071 100644 --- a/docs/CHANNELS.md +++ b/docs/CHANNELS.md @@ -62,6 +62,13 @@ except to list key *names*. Transports see it through the `SecretStore` interfac (`get`, `set`, `delete`, `keys`), and only their own channel's slice, so the file store can be swapped for a keychain later without touching a transport. +Secret key names, per channel: + +| Channel | Key | Value | +| --- | --- | --- | +| `slack` | `botToken` | Bot user OAuth token (`xoxb-…`) | +| `slack` | `appToken` | App-level token with `connections:write` (`xapp-…`), for Socket Mode | + ### `sessions.json` One conversation is one engine session. The key is `:` -- a Slack @@ -132,7 +139,7 @@ mpim:history users:read`, bot events `app_mention message.im message.channels`. prints `https://api.slack.com/apps?new_app=1&manifest_yaml=`, which opens the "create app" page pre-filled. -Setting up Slack, once the transport exists: +Setting up Slack: ```sh redrob channels slack-manifest --url # open it, create the app, install to the workspace @@ -142,6 +149,17 @@ redrob channels allow slack U0123ABCDEF # your Slack member id redrob channels enable slack ``` +## Transports + +### Slack (`@redrob-code/channels-slack`) + +Bolt 3 in Socket Mode. Handles `app_mention`, `message.im` and `message.channels` (channels the +bot was invited to); drops messages with a `bot_id` or `subtype` and the bot's own posts, and +delivers a mention once even though Slack sends it as two events. `conversationId` is +`:` -- one thread is one session -- and replies go into that thread. +`senderId` is the Slack user id. `maxChunk` 3900, no typing indicator. The manifest YAML lives +in this package (`manifestYaml`, `manifestUrl()`). + ## Transport contract (for Stage 2) Package `@redrob-code/channels-core`, file `src/transport.ts`. diff --git a/package.json b/package.json index 5029ab96f2..121f0598a4 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,7 @@ "packages": [ "packages/*", "packages/channels/*", - "packages/sdk/js", - "packages/slack" + "packages/sdk/js" ], "catalog": { "@effect/opentelemetry": "4.0.0-beta.83", diff --git a/packages/channels/slack/README.md b/packages/channels/slack/README.md new file mode 100644 index 0000000000..a0f6506372 --- /dev/null +++ b/packages/channels/slack/README.md @@ -0,0 +1,42 @@ +# @redrob-code/channels-slack + +Slack transport for `@redrob-code/channels-core`: Bolt 3 in Socket Mode, so the daemon needs no +public URL. One Slack thread is one engine session. + +## Secrets + +Read at `start` from the channel secret store, never from the environment: + +| Key | Value | +| --- | --- | +| `slack.botToken` | Bot user OAuth token, `xoxb-…` | +| `slack.appToken` | App-level token with `connections:write`, `xapp-…` | + +```sh +redrob channels slack-manifest --url # create the app from the manifest, install it +redrob channels set-secret slack botToken +redrob channels set-secret slack appToken +redrob channels allow slack U0123ABCDEF # Slack member id +redrob channels enable slack +``` + +No non-secret `options` are read yet. + +## Behaviour + +- Events: `app_mention`, `message.im`, `message.channels`. Messages with a `bot_id` or a + `subtype` (edits, joins, bot posts) and the bot's own messages are dropped. A mention in a + channel arrives as both `app_mention` and `message.channels`; it is delivered once. +- `conversationId` is `:`; replies go into that thread. The + `<@bot>` token is stripped from the text before it reaches the core. +- `senderId` is the Slack user id (what the allowlist matches); `senderName` is filled from + `users.info` when `users:read` is granted. +- `maxChunk` is 3900. No typing indicator: Slack has none for classic bots. +- `start` rejects on a bad bot token (`auth.test`) or a bad app token (socket open). Reconnects + are Bolt's own. + +## Exports + +`SlackTransport`, `slackTransport` (factory), `SLACK_MAX_CHUNK`, `conversationId`, +`splitConversationId`, `stripMention`, and the manifest: `manifestYaml`, `manifestUrl()`, +`SLACK_SCOPES`, `SLACK_EVENTS`. diff --git a/packages/channels/slack/package.json b/packages/channels/slack/package.json new file mode 100644 index 0000000000..3caa00b574 --- /dev/null +++ b/packages/channels/slack/package.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@redrob-code/channels-slack", + "version": "0.0.0", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + ".": "./src/index.ts", + "./*": "./src/*.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "@slack/bolt": "3.22.0" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/channels/slack/src/index.ts b/packages/channels/slack/src/index.ts new file mode 100644 index 0000000000..d636012895 --- /dev/null +++ b/packages/channels/slack/src/index.ts @@ -0,0 +1,6 @@ +/** + * @redrob-code/channels-slack: the Slack transport for the channels core, plus the app manifest + * that describes the Slack app it expects. + */ +export { SlackTransport, slackTransport, SLACK_MAX_CHUNK, conversationId, splitConversationId, stripMention } from "./transport" +export { manifestYaml, manifestUrl, SLACK_SCOPES, SLACK_EVENTS } from "./manifest" diff --git a/packages/channels/slack/src/manifest.ts b/packages/channels/slack/src/manifest.ts new file mode 100644 index 0000000000..0ab414dbd0 --- /dev/null +++ b/packages/channels/slack/src/manifest.ts @@ -0,0 +1,54 @@ +/** + * The Slack app manifest for the Redrob bot: the one place the scopes and events the transport + * needs are written down. `redrob channels slack-manifest` prints it; a user pastes it into + * api.slack.com to create the app, or opens `manifestUrl()` which pre-fills the create page. + * + * Keep `SLACK_SCOPES` and `SLACK_EVENTS` in step with what `transport.ts` actually uses: + * `app_mentions:read` + `app_mention` for mentions, `im:*` + `message.im` for direct messages, + * `channels:history` + `message.channels` for channels the bot is a member of, `chat:write` to + * reply, `users:read` to resolve sender names. + */ + +export const SLACK_SCOPES = [ + "app_mentions:read", + "chat:write", + "im:history", + "im:read", + "im:write", + "channels:history", + "groups:history", + "mpim:history", + "users:read", +] as const + +export const SLACK_EVENTS = ["app_mention", "message.im", "message.channels"] as const + +export const manifestYaml: string = [ + "display_information:", + " name: Redrob", + " description: Talk to your Redrob Code engine from Slack.", + " background_color: '#1a1a1a'", + "features:", + " bot_user:", + " display_name: Redrob", + " always_online: true", + "oauth_config:", + " scopes:", + " bot:", + ...SLACK_SCOPES.map((scope) => ` - ${scope}`), + "settings:", + " event_subscriptions:", + " bot_events:", + ...SLACK_EVENTS.map((event) => ` - ${event}`), + " interactivity:", + " is_enabled: false", + " org_deploy_enabled: false", + " socket_mode_enabled: true", + " token_rotation_enabled: false", + "", +].join("\n") + +/** The api.slack.com "create an app" page with the manifest pre-filled. */ +export function manifestUrl(): string { + return `https://api.slack.com/apps?new_app=1&manifest_yaml=${encodeURIComponent(manifestYaml)}` +} diff --git a/packages/channels/slack/src/transport.ts b/packages/channels/slack/src/transport.ts new file mode 100644 index 0000000000..3b0bfcb4e7 --- /dev/null +++ b/packages/channels/slack/src/transport.ts @@ -0,0 +1,178 @@ +/** + * Slack transport: Bolt in Socket Mode, so nothing has to be reachable from the internet. + * + * One Slack thread is one conversation: `conversationId` is `:`, where + * `thread_ts` is the root message's `ts` (a top-level message starts the thread named after + * itself). Replies always go into that thread. Mentions (`app_mention`), direct messages + * (`message.im`) and channel messages (`message.channels`, in channels the bot was invited to) + * are all handed to the core; the bot's own posts, other bots and message subtypes (edits, + * joins, file shares without text) are dropped here. + * + * Secrets, read at `start` from `ctx.secrets`: `botToken` (xoxb-…) and `appToken` (xapp-…, the + * app-level token with `connections:write`). Nothing is read from the environment. + */ +import { App, LogLevel } from "@slack/bolt" +import type { Inbound, Transport, TransportContext, TransportFactory } from "@redrob-code/channels-core" + +/** Slack rejects `chat.postMessage` text past ~4000 characters; keep headroom for formatting. */ +export const SLACK_MAX_CHUNK = 3900 + +/** How many recent event keys to remember for de-duplication (a mention arrives twice). */ +const SEEN_LIMIT = 1000 + +/** The subset of a Slack message event the transport reads; Bolt's unions are wider than needed. */ +type MessageLike = { + readonly type?: string + readonly subtype?: string + readonly bot_id?: string + readonly user?: string + readonly text?: string + readonly ts?: string + readonly thread_ts?: string + readonly channel?: string + readonly channel_type?: string +} + +export function conversationId(channel: string, ts: string, threadTs?: string): string { + return `${channel}:${threadTs ?? ts}` +} + +/** Split `:` back into its halves; a bare channel id has no thread. */ +export function splitConversationId(id: string): { channel: string; threadTs?: string } { + const at = id.indexOf(":") + if (at < 0) return { channel: id } + const threadTs = id.slice(at + 1) + return { channel: id.slice(0, at), ...(threadTs ? { threadTs } : {}) } +} + +/** Drop the bot's own mention token(s) from a message so the engine sees the request alone. */ +export function stripMention(text: string, botUserId: string | undefined): string { + if (!botUserId) return text.trim() + return text.replaceAll(new RegExp(`<@${botUserId}(?:\\|[^>]*)?>`, "g"), "").replace(/\s{2,}/g, " ").trim() +} + +export class SlackTransport implements Transport { + readonly name = "slack" + readonly maxChunk = SLACK_MAX_CHUNK + + private app?: App + private ctx?: TransportContext + private botUserId?: string + private readonly seen = new Set() + private readonly names = new Map() + + async start(ctx: TransportContext): Promise { + if (this.app) throw new Error("slack transport is already started") + this.ctx = ctx + const token = await ctx.secrets.require("botToken") + const appToken = await ctx.secrets.require("appToken") + + const app = new App({ + token, + appToken, + socketMode: true, + logLevel: LogLevel.WARN, + // Socket Mode needs no signing secret; Bolt insists on one unless a receiver is given, and + // the socket-mode receiver is selected by `socketMode`, so any non-empty value satisfies it. + signingSecret: "unused-in-socket-mode", + }) + + // A bad bot token fails here, before the socket opens, with Slack's own reason (invalid_auth). + const auth = await app.client.auth.test({ token }) + this.botUserId = typeof auth.user_id === "string" ? auth.user_id : undefined + + app.event("app_mention", async ({ event }) => { + this.deliver(event as MessageLike) + }) + app.message(async ({ message }) => { + this.deliver(message as MessageLike) + }) + app.error(async (error) => { + ctx.log.warn(`bolt: ${error.message}`) + }) + + // A bad app token (or one without connections:write) rejects here; Socket Mode reconnects on + // its own after that, so no loop of ours is needed. + await app.start() + this.app = app + ctx.log.info(`connected as ${this.botUserId ?? "unknown bot user"} (socket mode)`) + } + + async stop(): Promise { + const app = this.app + this.app = undefined + this.ctx = undefined + this.seen.clear() + if (app) await app.stop() + } + + async send(id: string, chunks: readonly string[], replyTo?: string): Promise { + const app = this.app + if (!app) throw new Error("slack transport is not started") + const { channel, threadTs } = splitConversationId(id) + const thread_ts = replyTo ?? threadTs + for (const text of chunks) { + if (!text) continue + await app.client.chat.postMessage({ channel, text, ...(thread_ts ? { thread_ts } : {}) }) + } + } + + // Slack has no typing indicator for classic bots (assistant.threads.setStatus needs the + // Agents & Assistants feature), so `typing` is intentionally not implemented. + + private deliver(event: MessageLike): void { + const ctx = this.ctx + if (!ctx) return + if (event.bot_id || event.subtype) return + if (!event.user || !event.channel || !event.ts) return + if (this.botUserId && event.user === this.botUserId) return + const text = stripMention(event.text ?? "", this.botUserId) + if (!text) return + + // A mention in a channel arrives as both app_mention and message.channels; take the first. + const key = `${event.channel}:${event.ts}` + if (this.seen.has(key)) return + this.remember(key) + + const threadTs = event.thread_ts ?? event.ts + const message: Inbound = { + channel: this.name, + conversationId: conversationId(event.channel, event.ts, event.thread_ts), + senderId: event.user, + text, + replyTo: threadTs, + } + const cached = this.names.get(event.user) + if (cached) { + ctx.onMessage({ ...message, senderName: cached }) + return + } + void this.lookupName(event.user).then((name) => { + ctx.onMessage(name ? { ...message, senderName: name } : message) + }) + } + + private remember(key: string): void { + this.seen.add(key) + if (this.seen.size > SEEN_LIMIT) { + const oldest = this.seen.values().next().value + if (oldest !== undefined) this.seen.delete(oldest) + } + } + + private async lookupName(user: string): Promise { + const app = this.app + if (!app) return undefined + try { + const result = await app.client.users.info({ user }) + const name = result.user?.profile?.display_name || result.user?.real_name || result.user?.name + if (name) this.names.set(user, name) + return name || undefined + } catch { + // users:read may be missing; the sender id is what the allowlist needs anyway. + return undefined + } + } +} + +export const slackTransport: TransportFactory = () => new SlackTransport() diff --git a/packages/slack/tsconfig.json b/packages/channels/slack/tsconfig.json similarity index 100% rename from packages/slack/tsconfig.json rename to packages/channels/slack/tsconfig.json diff --git a/packages/slack/.env.example b/packages/slack/.env.example deleted file mode 100644 index ff72adaf9f..0000000000 --- a/packages/slack/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -SLACK_BOT_TOKEN=xoxb-your-bot-token -SLACK_SIGNING_SECRET=your-signing-secret -SLACK_APP_TOKEN=xapp-your-app-token \ No newline at end of file diff --git a/packages/slack/.gitignore b/packages/slack/.gitignore deleted file mode 100644 index 1ef9d19ba0..0000000000 --- a/packages/slack/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -dist -.env -.DS_Store \ No newline at end of file diff --git a/packages/slack/README.md b/packages/slack/README.md deleted file mode 100644 index 1170ba435f..0000000000 --- a/packages/slack/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# @redrob-code/slack - -Slack bot integration for Redrob Code that creates threaded conversations. - -## Setup - -1. Create a Slack app at https://api.slack.com/apps -2. Enable Socket Mode -3. Add the following OAuth scopes: - - `chat:write` - - `app_mentions:read` - - `channels:history` - - `groups:history` -4. Install the app to your workspace -5. Set environment variables in `.env`: - - `SLACK_BOT_TOKEN` - Bot User OAuth Token - - `SLACK_SIGNING_SECRET` - Signing Secret from Basic Information - - `SLACK_APP_TOKEN` - App-Level Token from Basic Information - -## Usage - -```bash -# Edit .env with your Slack app credentials -bun dev -``` - -The bot will respond to messages in channels where it's added, creating separate opencode sessions for each thread. diff --git a/packages/slack/package.json b/packages/slack/package.json deleted file mode 100644 index f3cdb0bd75..0000000000 --- a/packages/slack/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@redrob-code/slack", - "version": "0.0.0", - "type": "module", - "license": "MIT", - "scripts": { - "dev": "bun run src/index.ts", - "typecheck": "tsgo --noEmit" - }, - "dependencies": { - "@redrob-labs/sdk": "workspace:*", - "@slack/bolt": "^3.17.1" - }, - "devDependencies": { - "@types/node": "catalog:", - "typescript": "catalog:", - "@typescript/native-preview": "catalog:" - } -} diff --git a/packages/slack/src/index.ts b/packages/slack/src/index.ts deleted file mode 100644 index 217f0654d4..0000000000 --- a/packages/slack/src/index.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { App } from "@slack/bolt" -import { createRedrob, type ToolPart } from "@redrob-labs/sdk" - -const app = new App({ - token: process.env.SLACK_BOT_TOKEN, - signingSecret: process.env.SLACK_SIGNING_SECRET, - socketMode: true, - appToken: process.env.SLACK_APP_TOKEN, -}) - -console.log("🔧 Bot configuration:") -console.log("- Bot token present:", !!process.env.SLACK_BOT_TOKEN) -console.log("- Signing secret present:", !!process.env.SLACK_SIGNING_SECRET) -console.log("- App token present:", !!process.env.SLACK_APP_TOKEN) - -console.log("🚀 Starting redrob server...") -const redrob = await createRedrob({ - port: 0, -}) -console.log("✅ redrob server ready") - -const sessions = new Map() -void (async () => { - const events = await redrob.client.event.subscribe() - for await (const event of events.stream) { - if (event.type === "message.part.updated") { - const part = event.properties.part - if (part.type === "tool") { - // Find the session for this tool update - for (const [_sessionKey, session] of sessions.entries()) { - if (session.sessionId === part.sessionID) { - void handleToolUpdate(part, session.channel, session.thread) - break - } - } - } - } - } -})() - -async function handleToolUpdate(part: ToolPart, channel: string, thread: string) { - if (part.state.status !== "completed") return - const toolMessage = `*${part.tool}* - ${part.state.title}` - await app.client.chat - .postMessage({ - channel, - thread_ts: thread, - text: toolMessage, - }) - .catch(() => {}) -} - -app.use(async ({ next, context }) => { - console.log("📡 Raw Slack event:", JSON.stringify(context, null, 2)) - await next() -}) - -app.message(async ({ message, say }) => { - console.log("📨 Received message event:", JSON.stringify(message, null, 2)) - - if (message.subtype || !("text" in message) || !message.text) { - console.log("⏭️ Skipping message - no text or has subtype") - return - } - - console.log("✅ Processing message:", message.text) - - const channel = message.channel - const thread = (message as any).thread_ts || message.ts - const sessionKey = `${channel}-${thread}` - - let session = sessions.get(sessionKey) - - if (!session) { - console.log("🆕 Creating new redrob session...") - const { client, server } = redrob - - const createResult = await client.session.create({ - body: { title: `Slack thread ${thread}` }, - }) - - if (createResult.error) { - console.error("❌ Failed to create session:", createResult.error) - await say({ - text: "Sorry, I had trouble creating a session. Please try again.", - thread_ts: thread, - }) - return - } - - console.log("✅ Created redrob session:", createResult.data.id) - - session = { client, server, sessionId: createResult.data.id, channel, thread } - sessions.set(sessionKey, session) - - const shareResult = await client.session.share({ path: { id: createResult.data.id } }) - if (!shareResult.error && shareResult.data) { - const sessionUrl = shareResult.data.share?.url - console.log("🔗 Session shared:", sessionUrl) - await app.client.chat.postMessage({ channel, thread_ts: thread, text: sessionUrl }) - } - } - - console.log("📝 Sending to redrob:", message.text) - const result = await session.client.session.prompt({ - path: { id: session.sessionId }, - body: { parts: [{ type: "text", text: message.text }] }, - }) - - console.log("📤 redrob response:", JSON.stringify(result, null, 2)) - - if (result.error) { - console.error("❌ Failed to send message:", result.error) - await say({ - text: "Sorry, I had trouble processing your message. Please try again.", - thread_ts: thread, - }) - return - } - - const response = result.data - - // Build response text - const responseText = - response.info?.content || - response.parts - ?.filter((p: any) => p.type === "text") - .map((p: any) => p.text) - .join("\n") || - "I received your message but didn't have a response." - - console.log("💬 Sending response:", responseText) - - // Send main response (tool updates will come via live events) - await say({ text: responseText, thread_ts: thread }) -}) - -app.command("/test", async ({ command, ack, say }) => { - await ack() - console.log("🧪 Test command received:", JSON.stringify(command, null, 2)) - await say("🤖 Bot is working! I can hear you loud and clear.") -}) - -await app.start() -console.log("⚡️ Slack bot is running!") diff --git a/packages/slack/sst-env.d.ts b/packages/slack/sst-env.d.ts deleted file mode 100644 index 64441936d7..0000000000 --- a/packages/slack/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file From 30a81d4a2c0f7d5a044449bbd522d9910697d223 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:48:34 +0000 Subject: [PATCH 17/23] docs(channels): tidy WhatsApp section after merge --- docs/CHANNELS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANNELS.md b/docs/CHANNELS.md index 874bcaf701..f334ff96fa 100644 --- a/docs/CHANNELS.md +++ b/docs/CHANNELS.md @@ -189,7 +189,7 @@ redrob channels enable discord ### WhatsApp (`@redrob-code/channels-whatsapp`) An **unofficial** client over Baileys -- -WhatsApp may ban accounts that use it; see the package README): +WhatsApp may ban accounts that use it; see the package README. ```sh redrob channels allow whatsapp 821012345678 # the sender's phone number, digits only From df77fd92f34f69999841a44f58a393996c54d63d Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:50:05 +0000 Subject: [PATCH 18/23] channels: register Slack, Telegram, Discord and WhatsApp transports The daemon now wires all four stage-2 transports into channelFactories and depends on their workspace packages; bun.lock regenerated after the merges. --- bun.lock | 389 +++++++++++++++++++---- packages/redrob/package.json | 4 + packages/redrob/src/channels/registry.ts | 22 +- 3 files changed, 347 insertions(+), 68 deletions(-) diff --git a/bun.lock b/bun.lock index d0643e0b92..3d50b99980 100644 --- a/bun.lock +++ b/bun.lock @@ -57,6 +57,68 @@ "typescript": "catalog:", }, }, + "packages/channels/discord": { + "name": "@redrob-code/channels-discord", + "version": "0.0.0", + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "discord.js": "14.27.0", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/channels/slack": { + "name": "@redrob-code/channels-slack", + "version": "0.0.0", + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "@slack/bolt": "3.22.0", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/channels/telegram": { + "name": "@redrob-code/channels-telegram", + "version": "0.0.0", + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "grammy": "1.46.0", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/channels/whatsapp": { + "name": "@redrob-code/channels-whatsapp", + "version": "0.0.0", + "dependencies": { + "@redrob-code/channels-core": "workspace:*", + "@whiskeysockets/baileys": "6.7.24", + "qrcode": "1.5.4", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@types/qrcode": "1.5.6", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, "packages/cli": { "name": "@redrob-code/cli", "version": "0.0.0", @@ -381,6 +443,10 @@ "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@redrob-code/channels-core": "workspace:*", + "@redrob-code/channels-discord": "workspace:*", + "@redrob-code/channels-slack": "workspace:*", + "@redrob-code/channels-telegram": "workspace:*", + "@redrob-code/channels-whatsapp": "workspace:*", "@redrob-code/codemode": "workspace:*", "@redrob-code/llm": "workspace:*", "@redrob-code/plugin": "workspace:*", @@ -531,19 +597,6 @@ "@typescript/native-preview": "catalog:", }, }, - "packages/slack": { - "name": "@redrob-code/slack", - "version": "0.0.0", - "dependencies": { - "@redrob-labs/sdk": "workspace:*", - "@slack/bolt": "^3.17.1", - }, - "devDependencies": { - "@types/node": "catalog:", - "@typescript/native-preview": "catalog:", - "typescript": "catalog:", - }, - }, "packages/tui": { "name": "@redrob-code/tui", "version": "0.0.0", @@ -879,14 +932,34 @@ "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.14.0", "", {}, "sha512-C3UGsiCwSprE2NKIIFA3hCDlpXTMCAXRZuEVp88L1GY36Y41+rYL5fryE+nOFhp4p4JPQvdV8PQ4DWgHgeTE+w=="], "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.14.0", "", { "dependencies": { "@bufbuild/protobuf": "2.14.0", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-S0TvZhVtTEkZQRj2JsXBCXiib/b0+bLQtJCcn+hj9O/usgbcgxHn4D+ii22Ao3qj0/kKxVYeo7BtAI2dMk+iig=="], + "@cacheable/memory": ["@cacheable/memory@2.2.0", "", { "dependencies": { "@cacheable/utils": "^2.5.0", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" } }, "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ=="], + + "@cacheable/node-cache": ["@cacheable/node-cache@1.7.6", "", { "dependencies": { "cacheable": "^2.3.1", "hookified": "^1.14.0", "keyv": "^5.5.5" } }, "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A=="], + + "@cacheable/utils": ["@cacheable/utils@2.5.0", "", { "dependencies": { "hashery": "^1.5.1", "keyv": "^5.6.0" } }, "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA=="], + "@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="], "@clack/prompts": ["@clack/prompts@1.0.0-alpha.1", "", { "dependencies": { "@clack/core": "1.0.0-alpha.1", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-07MNT0OsxjKOcyVfX8KhXBhJiyUbDP1vuIAcHc+nx5v93MJO23pX3X/k3bWz6T3rpM9dgWPq90i4Jq7gZAyMbw=="], + "@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="], + + "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], + + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], + + "@discordjs/rest": ["@discordjs/rest@2.6.3", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.50", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg=="], + + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], + + "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.83", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.83" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-cPfCfp/ghu0itbX6Dqjdr4N0rbjng5ON4sUpnLHV5JJySG8zZpWmuOZLWIrfrNKT2ctYR1BYmp1aYCgkItaJLw=="], @@ -897,6 +970,8 @@ "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.83", "", { "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-6OaxLsWffxkh9pXYUSyj/AxjVb9URY2rG9U6atjxClWy30Jx77R9Pm3Rrc7cQ63kQurePavEw1bQbzQ/SILiQQ=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -991,8 +1066,14 @@ "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="], + "@grammyjs/types": ["@grammyjs/types@5.0.0", "", {}, "sha512-iq1Qrq1iPKkB8yAa0qSuIURMZOCuqTY5pWy5gHpCeL1oQ+GPadGhw/cDTVE8waJwuCzacUzuIjRv1sESvk7u7A=="], + "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], + "@hapi/boom": ["@hapi/boom@9.1.4", "", { "dependencies": { "@hapi/hoek": "9.x.x" } }, "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw=="], + + "@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + "@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="], "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="], @@ -1003,6 +1084,60 @@ "@hono/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.4" }, "os": "darwin", "cpu": "x64" }, "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "os": "freebsd" }, "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "cpu": "none" }, "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="], + "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], @@ -1029,6 +1164,10 @@ "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + "@keyv/bigmap": ["@keyv/bigmap@1.3.1", "", { "dependencies": { "hashery": "^1.4.0", "hookified": "^1.15.0" }, "peerDependencies": { "keyv": "^5.6.0" } }, "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ=="], + + "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], + "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], @@ -1313,6 +1452,14 @@ "@redrob-code/channels-core": ["@redrob-code/channels-core@workspace:packages/channels/core"], + "@redrob-code/channels-discord": ["@redrob-code/channels-discord@workspace:packages/channels/discord"], + + "@redrob-code/channels-slack": ["@redrob-code/channels-slack@workspace:packages/channels/slack"], + + "@redrob-code/channels-telegram": ["@redrob-code/channels-telegram@workspace:packages/channels/telegram"], + + "@redrob-code/channels-whatsapp": ["@redrob-code/channels-whatsapp@workspace:packages/channels/whatsapp"], + "@redrob-code/cli": ["@redrob-code/cli@workspace:packages/cli"], "@redrob-code/client": ["@redrob-code/client@workspace:packages/client"], @@ -1343,12 +1490,16 @@ "@redrob-code/server": ["@redrob-code/server@workspace:packages/server"], - "@redrob-code/slack": ["@redrob-code/slack@workspace:packages/slack"], - "@redrob-code/tui": ["@redrob-code/tui@workspace:packages/tui"], "@redrob-labs/sdk": ["@redrob-labs/sdk@workspace:packages/sdk/js"], + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], + + "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], + + "@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], + "@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="], @@ -1487,6 +1638,10 @@ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@tsconfig/bun": ["@tsconfig/bun@1.0.9", "", {}, "sha512-4M0/Ivfwcpz325z6CwSifOBZYji3DFOEpY6zEUt0+Xi2qRhzwvmqQN9XAHJh3OVvRJuAqVTLU2abdCplvp6mwQ=="], "@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="], @@ -1563,6 +1718,8 @@ "@types/promise.allsettled": ["@types/promise.allsettled@1.0.6", "", {}, "sha512-wA0UT0HeT2fGHzIFV9kWpYz5mdoyLxKrTgMdZQM++5h6pYAFH73HXcQhefg24nD1yivUFEn5KU+EF4b+CXJ4Wg=="], + "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], + "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], @@ -1615,6 +1772,10 @@ "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + + "@whiskeysockets/baileys": ["@whiskeysockets/baileys@6.7.24", "", { "dependencies": { "@cacheable/node-cache": "^1.4.0", "@hapi/boom": "^9.1.3", "async-mutex": "^0.5.0", "axios": "^1.6.0", "libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git", "music-metadata": "^11.7.0", "pino": "^9.6", "protobufjs": "^7.2.4", "ws": "^8.13.0" }, "peerDependencies": { "audio-decode": "^2.1.3", "jimp": "^1.6.0", "link-preview-js": "^3.0.0", "sharp": "*" }, "optionalPeers": ["audio-decode", "jimp", "link-preview-js"] }, "sha512-Ljq7si+gsNIE9d5dP69E99TTQp+BxxPgitWvmZMXExGc1Ctr0SCDLFZmxRLUDC5HB0/Itw5uk+HJeBONrLP99Q=="], + "@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="], "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], @@ -1627,7 +1788,7 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], @@ -1665,6 +1826,8 @@ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], @@ -1753,12 +1916,16 @@ "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="], + "cacheable": ["cacheable@2.5.0", "", { "dependencies": { "@cacheable/memory": "^2.2.0", "@cacheable/utils": "^2.5.0", "hookified": "^1.15.0", "keyv": "^5.6.0", "qified": "^0.10.1" } }, "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g=="], + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -1835,6 +2002,8 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "curve25519-js": ["curve25519-js@0.0.4", "", {}, "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], @@ -1847,6 +2016,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], @@ -1881,6 +2052,12 @@ "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + + "discord-api-types": ["discord-api-types@0.38.56", "", {}, "sha512-Hti4RZsGvF5C7SM8rF2MDHtIm0Hyfrlk84GpEA29nj91tvVVMvX+oQSCodu10t2ZcjFGofq8TX0YyC04B2E69Q=="], + + "discord.js": ["discord.js@14.27.0", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.2", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.5", "discord-api-types": "^0.38.49", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A=="], + "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], "docx": ["docx@9.8.1", "", { "dependencies": { "@types/node": "^26.6.2", "hash.js": "^1.1.7", "jszip": "^3.10.1", "nanoid": "^6.0.1", "xml": "^1.0.1", "xml-js": "^1.6.8" } }, "sha512-bv4yDxgiV9aRyh327HG+V36wG0f23wk32SDhXGQpUhnykVtIPaR1K4ISi8E+DYJ7OcRQRkR8Y70QYlSS08V1cA=="], @@ -2013,6 +2190,8 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "file-type": ["file-type@21.3.4", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -2023,7 +2202,7 @@ "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], - "find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="], @@ -2095,6 +2274,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "grammy": ["grammy@1.46.0", "", { "dependencies": { "@grammyjs/types": "5.0.0", "abort-controller": "^3.0.0", "debug": "^4.4.3", "node-fetch": "^2.7.0" } }, "sha512-/8Qw+iisrUdOMk+p2mjEHouMm/BBdBEN1DHh16wiTpRUZkxDG3PxexdjCvR+wvK3LWPdrEvnQbdrwpU954sPhg=="], + "graphql": ["graphql@16.14.2", "", {}, "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA=="], "graphql-request": ["graphql-request@6.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="], @@ -2115,6 +2296,8 @@ "hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="], + "hashery": ["hashery@1.5.1", "", { "dependencies": { "hookified": "^1.15.0" } }, "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], @@ -2125,6 +2308,8 @@ "hono": ["hono@4.13.5", "", {}, "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw=="], + "hookified": ["hookified@1.15.1", "", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="], + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], @@ -2141,7 +2326,7 @@ "https": ["https@1.0.0", "", {}, "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], @@ -2313,19 +2498,23 @@ "jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="], + "keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], + "libsignal": ["libsignal@github:whiskeysockets/libsignal-node#bcea72d", { "dependencies": { "curve25519-js": "^0.0.4", "protobufjs": "^7.5.5" } }, "WhiskeySockets-libsignal-node-bcea72d", "sha512-zKDaKfYGwQ/ALHPan2W2KZq/6njngElVNcqQQmQy0rl3fZbaqjiIchbPaJyCVwkTwHx1+s3Li1U/tf+f+AbCQA=="], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], "listenercount": ["listenercount@1.0.1", "", {}, "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="], - "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], @@ -2361,6 +2550,8 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + "lodash.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="], "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], @@ -2373,6 +2564,8 @@ "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], + "magic-bytes.js": ["magic-bytes.js@1.13.1", "", {}, "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw=="], + "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], @@ -2381,7 +2574,7 @@ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], + "media-typer": ["media-typer@2.0.0", "", {}, "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], @@ -2441,6 +2634,8 @@ "multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="], + "music-metadata": ["music-metadata@11.16.1", "", { "dependencies": { "@borewit/text-codec": "^0.2.2", "@tokenizer/token": "^0.3.0", "content-type": "^2.1.0", "debug": "^4.4.3", "file-type": "^21.3.4", "media-typer": "^2.0.0", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0", "win-guid": "^0.2.1" } }, "sha512-uR/mHK6eyfl8h3jVCobF5b38Mfg1IDJMiBxIVbmm5F+G1OXEFcUpQHveO84tJBAqvY93GCRY2R4kDyx7s06Rug=="], + "nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="], "nanoid": ["nanoid@6.0.1", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw=="], @@ -2451,7 +2646,7 @@ "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], @@ -2533,7 +2728,7 @@ "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "p-map": ["p-map@7.0.6", "", {}, "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg=="], @@ -2559,7 +2754,7 @@ "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], - "path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], @@ -2583,9 +2778,9 @@ "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], + "pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="], - "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], @@ -2595,6 +2790,8 @@ "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "poe-oauth": ["poe-oauth@0.0.14", "", {}, "sha512-EHteyy5PsT8cl9zVjYcCQsm6FmLgqfwBkGj6IzUA83d9aWc57bH+jj2ItwJlf6RBhK+DN5N8YVlwEc0gOLb5fw=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -2633,6 +2830,10 @@ "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], + "qified": ["qified@0.10.1", "", { "dependencies": { "hookified": "^2.1.1" } }, "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA=="], + + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], @@ -2677,8 +2878,12 @@ "remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], @@ -2735,6 +2940,8 @@ "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -2747,6 +2954,8 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "sharp": ["sharp@0.35.5", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.5", "@img/sharp-darwin-x64": "0.35.5", "@img/sharp-freebsd-wasm32": "0.35.5", "@img/sharp-libvips-darwin-arm64": "1.3.4", "@img/sharp-libvips-darwin-x64": "1.3.4", "@img/sharp-libvips-linux-arm": "1.3.4", "@img/sharp-libvips-linux-arm64": "1.3.4", "@img/sharp-libvips-linux-ppc64": "1.3.4", "@img/sharp-libvips-linux-riscv64": "1.3.4", "@img/sharp-libvips-linux-s390x": "1.3.4", "@img/sharp-libvips-linux-x64": "1.3.4", "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", "@img/sharp-libvips-linuxmusl-x64": "1.3.4", "@img/sharp-linux-arm": "0.35.5", "@img/sharp-linux-arm64": "0.35.5", "@img/sharp-linux-ppc64": "0.35.5", "@img/sharp-linux-riscv64": "0.35.5", "@img/sharp-linux-s390x": "0.35.5", "@img/sharp-linux-x64": "0.35.5", "@img/sharp-linuxmusl-arm64": "0.35.5", "@img/sharp-linuxmusl-x64": "0.35.5", "@img/sharp-webcontainers-wasm32": "0.35.5", "@img/sharp-win32-arm64": "0.35.5", "@img/sharp-win32-ia32": "0.35.5", "@img/sharp-win32-x64": "0.35.5" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -2845,6 +3054,8 @@ "strnum": ["strnum@2.4.2", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw=="], + "strtok3": ["strtok3@10.4.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-MeSp4vHXiZRXM9K0rSWqTvXMU+YzmGICc5+briMVK+RY8qQ1GRTffFYVpK6cSgV/ueImFIEh3uCJJD3xBh1TaQ=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], @@ -2857,7 +3068,7 @@ "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], - "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], + "thread-stream": ["thread-stream@3.2.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw=="], "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], @@ -2873,6 +3084,8 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + "toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], @@ -2889,6 +3102,8 @@ "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsscmp": ["tsscmp@1.0.6", "", {}, "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="], @@ -2915,11 +3130,13 @@ "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "uint8array-extras": ["uint8array-extras@1.6.0", "", {}, "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q=="], + "ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="], + "undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -2981,10 +3198,14 @@ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], + "win-guid": ["win-guid@0.2.1", "", {}, "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A=="], + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -3035,8 +3256,6 @@ "@actions/github/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@actions/http-client/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.111", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-atgBW8jZPr/KuaKX5FvDIHuXBI8VCol6kVeoD4P0657+VXR73QsLogXQVN/Zt5FHtq9WzpdIZseCJiXPqkgwwA=="], "@ai-sdk/amazon-bedrock/@ai-sdk/openai": ["@ai-sdk/openai@3.0.98", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nAvp8pVOUJ3znJHRzZs54Y7CJQSikOW1Ty7LTdtQT+/pgtAwqhuKd8oaXbiW2xQaYBdH2i8o9AcEpcUdXIyF+g=="], @@ -3223,6 +3442,14 @@ "@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/ws/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "@effect/platform-node/undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="], + "@fastify/ajv-compiler/fast-uri": ["fast-uri@4.1.3", "", {}, "sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw=="], "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.5.0", "", {}, "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w=="], @@ -3241,6 +3468,10 @@ "@modelcontextprotocol/sdk/jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], + "@npmcli/agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "@npmcli/agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -3319,6 +3550,10 @@ "@types/node-fetch/form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], + "@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "@whiskeysockets/baileys/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.111", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-atgBW8jZPr/KuaKX5FvDIHuXBI8VCol6kVeoD4P0657+VXR73QsLogXQVN/Zt5FHtq9WzpdIZseCJiXPqkgwwA=="], "ai-gateway-provider/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="], @@ -3345,8 +3580,6 @@ "axios/form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], - "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], @@ -3363,8 +3596,6 @@ "crc32-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -3385,12 +3616,18 @@ "fast-json-stringify/fast-uri": ["fast-uri@4.1.3", "", {}, "sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw=="], + "fastify/pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], + "fdir/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "fstream/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + "gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], "gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -3399,6 +3636,8 @@ "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -3411,9 +3650,9 @@ "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "music-metadata/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], + "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -3435,6 +3674,12 @@ "pdf-lib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "qified/hookified": ["hookified@2.2.0", "", {}, "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA=="], + + "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], "redrob/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="], @@ -3447,10 +3692,14 @@ "safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], "shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "socks-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -3461,14 +3710,14 @@ "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], - "tinyglobby/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="], "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "type-is/media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], + "unzipper/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-glcEJC2mBXJKj7joFI0fRhcbdDYKTBgXMPcT6Vcnlym67tTzuNG9pFx3zblxVv8TdOxhojJja5zGG19yeGJxuA=="], @@ -3493,8 +3742,6 @@ "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -3505,20 +3752,14 @@ "@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/deepgram/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/elevenlabs/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/elevenlabs/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/fireworks/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/google-vertex/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], @@ -3671,6 +3912,8 @@ "@types/node-fetch/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.46", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tEtld97plCFiYevsJuOkGkeuhQndeMWFBVrJS4AjnbD5AqrNSXRCe0p+BZ3Cju/sxDeeZ9ym3q9YUV8fASA7aQ=="], @@ -3707,8 +3950,6 @@ "axios/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -3731,10 +3972,20 @@ "exceljs/archiver/zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="], + "fastify/pino/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], + + "fastify/pino/thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "fstream/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "gcp-metadata/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "gcp-metadata/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -3747,6 +3998,16 @@ "opencode-poe-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + + "qrcode/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + + "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "redrob/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="], @@ -3769,8 +4030,6 @@ "venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "venice-ai-sdk-provider/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -3811,30 +4070,20 @@ "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "ai-gateway-provider/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "ai-gateway-provider/@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/cohere/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "ai-gateway-provider/@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/groq/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "ai-gateway-provider/@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/perplexity/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -3855,8 +4104,24 @@ "exceljs/archiver/zip-stream/compress-commons": ["compress-commons@4.1.2", "", { "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg=="], + "fastify/pino/thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], + "fstream/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "gcp-metadata/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "qrcode/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "qrcode/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "redrob/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -3889,6 +4154,12 @@ "fstream/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], + "qrcode/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "qrcode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "qrcode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "exceljs/archiver/archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], diff --git a/packages/redrob/package.json b/packages/redrob/package.json index 9aba7ca00a..c173b88abc 100644 --- a/packages/redrob/package.json +++ b/packages/redrob/package.json @@ -88,6 +88,10 @@ "@openauthjs/openauth": "catalog:", "@redrob-code/codemode": "workspace:*", "@redrob-code/channels-core": "workspace:*", + "@redrob-code/channels-discord": "workspace:*", + "@redrob-code/channels-slack": "workspace:*", + "@redrob-code/channels-telegram": "workspace:*", + "@redrob-code/channels-whatsapp": "workspace:*", "@redrob-code/llm": "workspace:*", "@redrob-code/plugin": "workspace:*", "@redrob-code/protocol": "workspace:*", diff --git a/packages/redrob/src/channels/registry.ts b/packages/redrob/src/channels/registry.ts index a1de3f48a5..69a25ba24a 100644 --- a/packages/redrob/src/channels/registry.ts +++ b/packages/redrob/src/channels/registry.ts @@ -1,15 +1,19 @@ /** * The transports this build of the CLI can run, by channel name. * - * Stage 1 ships the core and no transports, so the map is empty: an enabled channel in - * `channels.json` shows up in `redrob channels status` as "no transport is available". Stage 2 - * adds one entry per platform, each a lazy factory so a platform SDK is only loaded when its - * channel is enabled: - * - * ```ts - * slack: () => new SlackTransport(), - * ``` + * One entry per platform. Each value is a factory, so a transport (and its platform client) is + * only instantiated when that channel is enabled in `channels.json`; a channel with no entry + * here shows up in `redrob channels status` as "no transport is available". */ import type { ChannelName, TransportFactory } from "@redrob-code/channels-core" +import { discordTransport } from "@redrob-code/channels-discord" +import { slackTransport } from "@redrob-code/channels-slack" +import { telegramTransport } from "@redrob-code/channels-telegram" +import { whatsappTransport } from "@redrob-code/channels-whatsapp" -export const channelFactories: Partial> = {} +export const channelFactories: Partial> = { + slack: slackTransport, + telegram: telegramTransport, + discord: discordTransport, + whatsapp: whatsappTransport, +} From d18345b9c83cdadd1a7c1c18b25621a456f2a996 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:58:08 +0000 Subject: [PATCH 19/23] service: start the engine at login while a channel is enabled daemon/autostart.ts registers a systemd user unit, a launchd agent or a Task Scheduler ONLOGON task running `redrob serve --register --idle-exit 0` with the installing binary. `redrob channels enable` installs it on the first enabled channel and `disable` removes it when none is left; `redrob service install|uninstall|status` are the by-hand versions. --- docs/CHANNELS.md | 30 ++- packages/redrob/src/cli/cmd/channels.ts | 24 ++- packages/redrob/src/cli/cmd/service.ts | 49 ++++- packages/redrob/src/daemon/autostart.ts | 276 ++++++++++++++++++++++++ 4 files changed, 369 insertions(+), 10 deletions(-) create mode 100644 packages/redrob/src/daemon/autostart.ts diff --git a/docs/CHANNELS.md b/docs/CHANNELS.md index f334ff96fa..67fb1f1c69 100644 --- a/docs/CHANNELS.md +++ b/docs/CHANNELS.md @@ -103,9 +103,33 @@ attached to the daemon as a client named `channel:` (so `GET /api/clients` When a reload turns the last channel off, the `--idle-exit` policy is restored. `SIGTERM`/`SIGINT` stop the transports before the record is removed. -A channel enabled in the file but with no transport in the build (Stage 1: all of them) is -reported in `status` as `available: false` with `lastError: "no transport is available for this -channel in this build"`; it does not keep the daemon up. +A channel enabled in the file but with no transport in the build is reported in `status` as +`available: false` with `lastError: "no transport is available for this channel in this build"`; +it does not keep the daemon up. + +## Start at login + +A bot that is on must survive a reboot, so the daemon registers itself to start at login while at +least one channel is enabled (`packages/redrob/src/daemon/autostart.ts`): + +| Platform | Registration | Where | +| --- | --- | --- | +| Linux | systemd user unit, `enable --now` | `~/.config/systemd/user/redrob-engine.service` | +| macOS | launchd agent, `launchctl load -w` | `~/Library/LaunchAgents/ai.redrob.engine.plist` | +| Windows | Task Scheduler task, trigger `ONLOGON`, run level limited | task `Redrob Engine` | + +The unit runs ` serve --register --idle-exit 0`, so the resident daemon is the +same build as the CLI that installed it. `redrob channels enable` installs the registration the +first time a channel turns on and `redrob channels disable` removes it when no channel is left +enabled -- a machine that never enables a channel never gets a resident process. A failure to +register (no `systemctl` on PATH, for instance) is printed as a warning and does not undo the +channel change. By hand: + +``` +redrob service install # register and start now +redrob service uninstall # remove the registration (Linux/Windows leave a running daemon up; launchd stops it) +redrob service status # also says whether login start is registered +``` ## HTTP diff --git a/packages/redrob/src/cli/cmd/channels.ts b/packages/redrob/src/cli/cmd/channels.ts index 492776d208..58cba587ce 100644 --- a/packages/redrob/src/cli/cmd/channels.ts +++ b/packages/redrob/src/cli/cmd/channels.ts @@ -8,6 +8,7 @@ import { cmd } from "./cmd" import { UI } from "@/cli/ui" import { Daemon } from "@/daemon/connect" +import * as Autostart from "@/daemon/autostart" import { Config, FileSecretStore, type ChannelName } from "@redrob-code/channels-core" const NAMES = Config.CHANNEL_NAMES @@ -57,6 +58,25 @@ const after = (reloaded: boolean) => : `${UI.Style.TEXT_DIM}no running daemon; takes effect on its next start${UI.Style.TEXT_NORMAL}`, ) +/** + * Login start follows the config: the first enabled channel registers it, the last disabled one + * removes it. A failure here (no systemctl, no launchctl) is reported, not fatal: the channel + * change itself already took. + */ +async function syncAutostart(config: Config.ChannelsConfig): Promise { + const result = await Autostart.sync(Config.enabledChannels(config).length) + if (result.error) { + UI.println(`${UI.Style.TEXT_WARNING}login start not updated: ${result.error} (redrob service install)${UI.Style.TEXT_NORMAL}`) + return + } + if (!result.changed) return + UI.println( + result.state.installed + ? `${UI.Style.TEXT_DIM}engine will start at login (${result.state.location})${UI.Style.TEXT_NORMAL}` + : `${UI.Style.TEXT_DIM}no channel left on; engine no longer starts at login${UI.Style.TEXT_NORMAL}`, + ) +} + export const SLACK_SCOPES = [ "app_mentions:read", "chat:write", @@ -177,6 +197,7 @@ export const ChannelsCommand = cmd({ UI.println(`${UI.Style.TEXT_WARNING}allowlist is empty: nobody can talk to it yet (redrob channels allow ${name} )${UI.Style.TEXT_NORMAL}`) } after(await reload()) + await syncAutostart(config) }, }), ) @@ -187,8 +208,9 @@ export const ChannelsCommand = cmd({ builder: (yargs) => yargs.positional("name", { type: "string", demandOption: true, choices: NAMES }), handler: async (args) => { const name = channelName(args.name) - await Config.update((c) => Config.setChannel(c, name, { enabled: false })) + const config = await Config.update((c) => Config.setChannel(c, name, { enabled: false })) after(await reload()) + await syncAutostart(config) }, }), ) diff --git a/packages/redrob/src/cli/cmd/service.ts b/packages/redrob/src/cli/cmd/service.ts index 872de23f59..285a045e53 100644 --- a/packages/redrob/src/cli/cmd/service.ts +++ b/packages/redrob/src/cli/cmd/service.ts @@ -1,14 +1,17 @@ /** - * `redrob service start | status | stop`: the user's shared daemon, by hand. + * `redrob service start | status | stop | restart | install | uninstall`: the user's shared daemon, by hand. * * Apps do the same through the SDK's `connectOrStart`; this is the command line and what the * launcher calls. `start --json` prints the record so a caller written in another language (the - * launcher is Rust) gets the url and secret without parsing prose. + * launcher is Rust) gets the url and secret without parsing prose. `install` / `uninstall` + * register or remove login start (see daemon/autostart.ts); `channels enable|disable` do that + * automatically. */ import { cmd } from "./cmd" import { UI } from "@/cli/ui" import { Daemon } from "@/daemon/connect" import * as Registry from "@/daemon/registry" +import * as Autostart from "@/daemon/autostart" import { DEFAULT_IDLE_EXIT_MINUTES } from "./serve" const print = (record: Registry.Record, json: boolean) => { @@ -49,13 +52,23 @@ export const ServiceCommand = cmd({ builder: (yargs) => yargs.option("json", { type: "boolean", default: false }), handler: async (args) => { const connection = await Daemon.current() + if (args.json) { + process.stdout.write(JSON.stringify(connection?.record ?? null) + "\n") + if (!connection) process.exitCode = 1 + return + } + const autostart = await Autostart.status() if (!connection) { - if (args.json) process.stdout.write(JSON.stringify(null) + "\n") - else UI.println(`${UI.Style.TEXT_DIM}not running${UI.Style.TEXT_NORMAL}`) + UI.println(`${UI.Style.TEXT_DIM}not running${UI.Style.TEXT_NORMAL}`) process.exitCode = 1 - return + } else { + print(connection.record, false) } - print(connection.record, args.json) + UI.println( + autostart.installed + ? `${UI.Style.TEXT_DIM}starts at login (${autostart.location})${UI.Style.TEXT_NORMAL}` + : `${UI.Style.TEXT_DIM}not registered to start at login${UI.Style.TEXT_NORMAL}`, + ) }, }), ) @@ -69,6 +82,30 @@ export const ServiceCommand = cmd({ }, }), ) + .command( + cmd({ + command: "install", + describe: "start the shared engine at login (systemd user / launchd / Task Scheduler)", + builder: (yargs) => yargs.option("json", { type: "boolean", default: false }), + handler: async (args) => { + const state = await Autostart.install() + if (args.json) process.stdout.write(JSON.stringify(state) + "\n") + else UI.println(`${UI.Style.TEXT_SUCCESS_BOLD}installed${UI.Style.TEXT_NORMAL} ${state.location}`) + }, + }), + ) + .command( + cmd({ + command: "uninstall", + describe: "stop starting the shared engine at login", + builder: (yargs) => yargs.option("json", { type: "boolean", default: false }), + handler: async (args) => { + const state = await Autostart.uninstall() + if (args.json) process.stdout.write(JSON.stringify(state) + "\n") + else UI.println(state.platform === "unsupported" ? `${UI.Style.TEXT_DIM}not supported on ${process.platform}${UI.Style.TEXT_NORMAL}` : "uninstalled") + }, + }), + ) .command( cmd({ command: "restart", diff --git a/packages/redrob/src/daemon/autostart.ts b/packages/redrob/src/daemon/autostart.ts new file mode 100644 index 0000000000..f2c6c14418 --- /dev/null +++ b/packages/redrob/src/daemon/autostart.ts @@ -0,0 +1,276 @@ +/** + * Start the shared daemon at login, so a chat channel keeps answering after a reboot. + * + * One registration per platform, all user-level (no root, no admin): + * + * Linux systemd user unit ~/.config/systemd/user/redrob-engine.service + * macOS launchd agent ~/Library/LaunchAgents/ai.redrob.engine.plist + * Windows Task Scheduler task "Redrob Engine", trigger ONLOGON, via schtasks.exe + * + * The unit runs `redrob serve --register --idle-exit 0` with the binary that installed it, so the + * daemon is the same build as the CLI. `redrob channels enable` installs it the first time a + * channel is turned on and `redrob channels disable` removes it once no channel is left enabled, + * so a machine that never uses a channel never gets a resident process. `redrob service + * install|uninstall|status` are the by-hand versions. + */ +import { spawn } from "node:child_process" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { selfCommand } from "./connect" + +export type Platform = "linux" | "darwin" | "win32" + +export type State = { + platform: Platform | "unsupported" + installed: boolean + /** The unit file (Linux/macOS) or the task name (Windows). */ + location: string +} + +const SERVICE_NAME = "redrob-engine" +const LAUNCHD_LABEL = "ai.redrob.engine" +const TASK_NAME = "Redrob Engine" + +function platform(): Platform | "unsupported" { + const p = process.platform + return p === "linux" || p === "darwin" || p === "win32" ? p : "unsupported" +} + +export function location(env: NodeJS.ProcessEnv = process.env): string { + switch (platform()) { + case "linux": + return path.join(env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config"), "systemd", "user", `${SERVICE_NAME}.service`) + case "darwin": + return path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) + case "win32": + return TASK_NAME + default: + return "" + } +} + +/** The daemon's command line, as the unit will run it. */ +export function daemonCommand(): { command: string; args: string[] } { + const self = selfCommand() + return { command: self.command, args: [...self.args, "serve", "--register", "--idle-exit", "0"] } +} + +function run(command: string, args: string[], input?: string): Promise<{ code: number; stdout: string; stderr: string }> { + return new Promise((resolve) => { + const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }) + let stdout = "" + let stderr = "" + child.stdout.on("data", (d) => (stdout += d.toString())) + child.stderr.on("data", (d) => (stderr += d.toString())) + child.on("error", (error) => resolve({ code: 127, stdout, stderr: stderr + String(error) })) + child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr })) + if (input !== undefined) child.stdin.write(input) + child.stdin.end() + }) +} + +const exists = (p: string) => + fs + .stat(p) + .then(() => true) + .catch(() => false) + +// ---- Linux: systemd --user ----------------------------------------------------------------- + +function systemdQuote(value: string): string { + // systemd ExecStart: double-quote, escape backslash and double quote. + return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` +} + +function systemdUnit(): string { + const { command, args } = daemonCommand() + const exec = [command, ...args].map(systemdQuote).join(" ") + return [ + "[Unit]", + "Description=Redrob Code engine (shared daemon for chat channels and apps)", + "After=network-online.target", + "Wants=network-online.target", + "", + "[Service]", + "Type=simple", + `ExecStart=${exec}`, + "Restart=on-failure", + "RestartSec=5", + "Environment=REDROB_SERVICE=systemd", + "", + "[Install]", + "WantedBy=default.target", + "", + ].join("\n") +} + +async function installLinux(env: NodeJS.ProcessEnv): Promise { + const file = location(env) + await fs.mkdir(path.dirname(file), { recursive: true }) + await fs.writeFile(file, systemdUnit(), { mode: 0o644 }) + const reload = await run("systemctl", ["--user", "daemon-reload"]) + if (reload.code !== 0) throw new Error(`systemctl --user daemon-reload failed: ${reload.stderr.trim() || reload.code}`) + const enable = await run("systemctl", ["--user", "enable", "--now", `${SERVICE_NAME}.service`]) + if (enable.code !== 0) throw new Error(`systemctl --user enable failed: ${enable.stderr.trim() || enable.code}`) +} + +async function uninstallLinux(env: NodeJS.ProcessEnv): Promise { + const file = location(env) + // Disable without --now: the running daemon stays up for the apps attached to it and exits on + // its own idle policy once they go; only the login start goes away. + await run("systemctl", ["--user", "disable", `${SERVICE_NAME}.service`]) + await fs.rm(file, { force: true }) + await run("systemctl", ["--user", "daemon-reload"]) +} + +// ---- macOS: launchd agent ------------------------------------------------------------------- + +function xml(value: string): string { + return value.replace(/&/g, "&").replace(//g, ">") +} + +function launchdPlist(): string { + const { command, args } = daemonCommand() + const argv = [command, ...args].map((a) => ` ${xml(a)}`).join("\n") + return [ + '', + '', + '', + "", + " Label", + ` ${LAUNCHD_LABEL}`, + " ProgramArguments", + " ", + argv, + " ", + " RunAtLoad", + " ", + " KeepAlive", + " ", + " SuccessfulExit", + " ", + " ", + " ProcessType", + " Background", + " EnvironmentVariables", + " ", + " REDROB_SERVICE", + " launchd", + " ", + "", + "", + "", + ].join("\n") +} + +async function installDarwin(env: NodeJS.ProcessEnv): Promise { + const file = location(env) + await fs.mkdir(path.dirname(file), { recursive: true }) + // Unload an older copy first; launchctl refuses to load a label twice. + if (await exists(file)) await run("launchctl", ["unload", file]) + await fs.writeFile(file, launchdPlist(), { mode: 0o644 }) + const load = await run("launchctl", ["load", "-w", file]) + if (load.code !== 0) throw new Error(`launchctl load failed: ${load.stderr.trim() || load.code}`) +} + +async function uninstallDarwin(env: NodeJS.ProcessEnv): Promise { + const file = location(env) + if (await exists(file)) await run("launchctl", ["unload", "-w", file]) + await fs.rm(file, { force: true }) +} + +// ---- Windows: Task Scheduler ---------------------------------------------------------------- + +function cmdQuote(value: string): string { + return /[\s"]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value +} + +async function installWin32(): Promise { + const { command, args } = daemonCommand() + const tr = [command, ...args].map(cmdQuote).join(" ") + // /F overwrites an existing task of the same name; /RL LIMITED = no elevation. + const result = await run("schtasks.exe", ["/Create", "/F", "/SC", "ONLOGON", "/TN", TASK_NAME, "/TR", tr, "/RL", "LIMITED"]) + if (result.code !== 0) throw new Error(`schtasks /Create failed: ${result.stderr.trim() || result.stdout.trim() || result.code}`) + // Start it now too, so the first enable does not wait for the next login. + await run("schtasks.exe", ["/Run", "/TN", TASK_NAME]) +} + +async function uninstallWin32(): Promise { + // /Delete alone: the daemon the task started keeps running for attached apps. + await run("schtasks.exe", ["/Delete", "/F", "/TN", TASK_NAME]) +} + +async function installedWin32(): Promise { + const result = await run("schtasks.exe", ["/Query", "/TN", TASK_NAME]) + return result.code === 0 +} + +// ---- public --------------------------------------------------------------------------------- + +export async function status(env: NodeJS.ProcessEnv = process.env): Promise { + const p = platform() + const loc = location(env) + switch (p) { + case "linux": + case "darwin": + return { platform: p, installed: await exists(loc), location: loc } + case "win32": + return { platform: p, installed: await installedWin32(), location: loc } + default: + return { platform: "unsupported", installed: false, location: "" } + } +} + +/** Register login start and start the daemon now. Idempotent: rewrites the unit each time. */ +export async function install(env: NodeJS.ProcessEnv = process.env): Promise { + switch (platform()) { + case "linux": + await installLinux(env) + break + case "darwin": + await installDarwin(env) + break + case "win32": + await installWin32() + break + default: + throw new Error(`Login start is not supported on ${process.platform}`) + } + return status(env) +} + +/** Remove the login registration. Does nothing when none is installed. */ +export async function uninstall(env: NodeJS.ProcessEnv = process.env): Promise { + switch (platform()) { + case "linux": + await uninstallLinux(env) + break + case "darwin": + await uninstallDarwin(env) + break + case "win32": + await uninstallWin32() + break + default: + return { platform: "unsupported", installed: false, location: "" } + } + return status(env) +} + +/** + * Keep the registration in step with the channels config: installed while at least one channel + * is enabled, removed when none is. Called by `redrob channels enable|disable`. Errors are + * returned, not thrown, so a missing systemctl never blocks the config change itself. + */ +export async function sync(enabledCount: number, env: NodeJS.ProcessEnv = process.env): Promise<{ state: State; changed: boolean; error?: string }> { + const before = await status(env) + if (before.platform === "unsupported") return { state: before, changed: false } + try { + if (enabledCount > 0 && !before.installed) return { state: await install(env), changed: true } + if (enabledCount === 0 && before.installed) return { state: await uninstall(env), changed: true } + return { state: before, changed: false } + } catch (error) { + return { state: before, changed: false, error: error instanceof Error ? error.message : String(error) } + } +} From bfd2dd91d82ca63b67624b83f82021245fff04dd Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:01:51 +0000 Subject: [PATCH 20/23] app-bridge: publish as @redrob-labs/app-bridge Apps outside this repo install the bridge from npm next to @redrob-labs/sdk, so the package takes the published scope and drops private. --- bun.lock | 156 +++++++++++++++++-------------- docs/APP-BRIDGE.md | 4 +- packages/app-bridge/README.md | 4 +- packages/app-bridge/package.json | 6 +- 4 files changed, 95 insertions(+), 75 deletions(-) diff --git a/bun.lock b/bun.lock index 3d50b99980..f421d45a14 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,7 @@ }, }, "packages/app-bridge": { - "name": "@redrob-code/app-bridge", + "name": "@redrob-labs/app-bridge", "version": "0.0.0", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -632,8 +632,8 @@ "tree-sitter-bash", ], "patchedDependencies": { - "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", + "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", @@ -1448,8 +1448,6 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], - "@redrob-code/app-bridge": ["@redrob-code/app-bridge@workspace:packages/app-bridge"], - "@redrob-code/channels-core": ["@redrob-code/channels-core@workspace:packages/channels/core"], "@redrob-code/channels-discord": ["@redrob-code/channels-discord@workspace:packages/channels/discord"], @@ -1492,6 +1490,8 @@ "@redrob-code/tui": ["@redrob-code/tui@workspace:packages/tui"], + "@redrob-labs/app-bridge": ["@redrob-labs/app-bridge@workspace:packages/app-bridge"], + "@redrob-labs/sdk": ["@redrob-labs/sdk@workspace:packages/sdk/js"], "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], @@ -1784,7 +1784,7 @@ "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -1878,7 +1878,7 @@ "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], - "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + "body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], @@ -1976,7 +1976,7 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -1984,7 +1984,7 @@ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -2150,7 +2150,7 @@ "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], @@ -2194,7 +2194,7 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], @@ -2218,7 +2218,7 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], @@ -2332,7 +2332,7 @@ "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], - "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -2576,7 +2576,7 @@ "media-typer": ["media-typer@2.0.0", "", {}, "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q=="], - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -2640,7 +2640,7 @@ "nanoid": ["nanoid@6.0.1", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw=="], - "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], @@ -2842,7 +2842,7 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], @@ -2932,13 +2932,13 @@ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], @@ -3118,7 +3118,7 @@ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -3466,8 +3466,12 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "@modelcontextprotocol/sdk/jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], + "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "@npmcli/agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "@npmcli/agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -3532,10 +3536,6 @@ "@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@slack/bolt/express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], - - "@slack/bolt/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - "@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], "@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], @@ -3554,6 +3554,8 @@ "@whiskeysockets/baileys/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.111", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-atgBW8jZPr/KuaKX5FvDIHuXBI8VCol6kVeoD4P0657+VXR73QsLogXQVN/Zt5FHtq9WzpdIZseCJiXPqkgwwA=="], "ai-gateway-provider/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="], @@ -3586,7 +3588,7 @@ "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], @@ -3614,12 +3616,18 @@ "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + "fast-json-stringify/fast-uri": ["fast-uri@4.1.3", "", {}, "sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw=="], "fastify/pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], "fdir/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "fstream/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], @@ -3646,14 +3654,16 @@ "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], + "make-fetch-happen/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + + "minipass-fetch/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "music-metadata/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], @@ -3692,6 +3702,10 @@ "safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], @@ -3714,9 +3728,9 @@ "tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="], - "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - "type-is/media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], + "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "unzipper/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -3848,6 +3862,28 @@ "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + + "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + + "@modelcontextprotocol/sdk/raw-body/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -3884,36 +3920,12 @@ "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@slack/bolt/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "@slack/bolt/express/body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], - - "@slack/bolt/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - - "@slack/bolt/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], - - "@slack/bolt/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "@slack/bolt/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], - - "@slack/bolt/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - - "@slack/bolt/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], - - "@slack/bolt/express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], - - "@slack/bolt/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - - "@slack/bolt/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], - - "@slack/bolt/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - - "@slack/bolt/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "@types/node-fetch/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.46", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tEtld97plCFiYevsJuOkGkeuhQndeMWFBVrJS4AjnbD5AqrNSXRCe0p+BZ3Cju/sxDeeZ9ym3q9YUV8fASA7aQ=="], @@ -3956,6 +3968,8 @@ "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "c12/chokidar/readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -3972,10 +3986,14 @@ "exceljs/archiver/zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="], + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "fastify/pino/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], "fastify/pino/thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "fstream/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -3994,6 +4012,8 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "make-fetch-happen/negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "opencode-poe-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], @@ -4020,8 +4040,12 @@ "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "unzipper/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "unzipper/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -4044,27 +4068,23 @@ "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], - "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], - - "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="], - - "@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@3.0.0", "", {}, "sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], + "@modelcontextprotocol/sdk/express/body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "@slack/bolt/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - "@slack/bolt/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "@slack/bolt/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], - "@slack/bolt/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], - "@slack/bolt/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="], - "@slack/bolt/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@3.0.0", "", {}, "sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw=="], - "@slack/bolt/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], "@types/node-fetch/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -4134,9 +4154,7 @@ "venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@slack/bolt/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "@slack/bolt/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], diff --git a/docs/APP-BRIDGE.md b/docs/APP-BRIDGE.md index 0fb6c7e55b..0899428c32 100644 --- a/docs/APP-BRIDGE.md +++ b/docs/APP-BRIDGE.md @@ -81,7 +81,7 @@ registration expired or the daemon restarted -- register again. Send one at leas } ``` -## Using `@redrob-code/app-bridge` +## Using `@redrob-labs/app-bridge` The shortest path from JavaScript: `packages/app-bridge` serves the tools and does steps 1 to 3 below in one call. It starts a streamable-HTTP MCP server on `127.0.0.1` (random free @@ -89,7 +89,7 @@ port), finds or starts the daemon with `connectOrStart`, registers through `brid unregisters on `stop()`. ```ts -import { startAppBridge } from "@redrob-code/app-bridge" +import { startAppBridge } from "@redrob-labs/app-bridge" import { z } from "zod" const bridge = await startAppBridge({ diff --git a/packages/app-bridge/README.md b/packages/app-bridge/README.md index d2ff28abf0..9bd4d31241 100644 --- a/packages/app-bridge/README.md +++ b/packages/app-bridge/README.md @@ -1,4 +1,4 @@ -# @redrob-code/app-bridge +# @redrob-labs/app-bridge One call that hands the Redrob daemon agent an app's tools. It serves the tools as an MCP server (streamable HTTP, `127.0.0.1`, random free port), finds or starts the shared daemon @@ -7,7 +7,7 @@ registration alive (re-registering after a daemon restart) and unregisters on `s Protocol details: `docs/APP-BRIDGE.md`. ```ts -import { startAppBridge } from "@redrob-code/app-bridge" +import { startAppBridge } from "@redrob-labs/app-bridge" import { z } from "zod" const bridge = await startAppBridge({ diff --git a/packages/app-bridge/package.json b/packages/app-bridge/package.json index efaa85004a..5a63af43f6 100644 --- a/packages/app-bridge/package.json +++ b/packages/app-bridge/package.json @@ -1,8 +1,10 @@ { "$schema": "https://json.schemastore.org/package.json", - "name": "@redrob-code/app-bridge", + "name": "@redrob-labs/app-bridge", "version": "0.0.0", - "private": true, + "private": false, + "publishConfig": { "access": "public" }, + "repository": { "type": "git", "url": "https://github.com/redrob-labs/redrob-code.git", "directory": "packages/app-bridge" }, "type": "module", "license": "MIT", "exports": { From c5bea324fe0a454d23431083ea14a2bd1caf0146 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+its-janghoon@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:04:36 +0000 Subject: [PATCH 21/23] Revert "app-bridge: publish as @redrob-labs/app-bridge" This reverts commit bfd2dd91d82ca63b67624b83f82021245fff04dd. --- bun.lock | 156 ++++++++++++++----------------- docs/APP-BRIDGE.md | 4 +- packages/app-bridge/README.md | 4 +- packages/app-bridge/package.json | 6 +- 4 files changed, 75 insertions(+), 95 deletions(-) diff --git a/bun.lock b/bun.lock index f421d45a14..3d50b99980 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,7 @@ }, }, "packages/app-bridge": { - "name": "@redrob-labs/app-bridge", + "name": "@redrob-code/app-bridge", "version": "0.0.0", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -632,8 +632,8 @@ "tree-sitter-bash", ], "patchedDependencies": { - "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", + "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", @@ -1448,6 +1448,8 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], + "@redrob-code/app-bridge": ["@redrob-code/app-bridge@workspace:packages/app-bridge"], + "@redrob-code/channels-core": ["@redrob-code/channels-core@workspace:packages/channels/core"], "@redrob-code/channels-discord": ["@redrob-code/channels-discord@workspace:packages/channels/discord"], @@ -1490,8 +1492,6 @@ "@redrob-code/tui": ["@redrob-code/tui@workspace:packages/tui"], - "@redrob-labs/app-bridge": ["@redrob-labs/app-bridge@workspace:packages/app-bridge"], - "@redrob-labs/sdk": ["@redrob-labs/sdk@workspace:packages/sdk/js"], "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], @@ -1784,7 +1784,7 @@ "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -1878,7 +1878,7 @@ "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], - "body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], @@ -1976,7 +1976,7 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -1984,7 +1984,7 @@ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -2150,7 +2150,7 @@ "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], @@ -2194,7 +2194,7 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], @@ -2218,7 +2218,7 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], @@ -2332,7 +2332,7 @@ "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], - "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -2576,7 +2576,7 @@ "media-typer": ["media-typer@2.0.0", "", {}, "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q=="], - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -2640,7 +2640,7 @@ "nanoid": ["nanoid@6.0.1", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw=="], - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], @@ -2842,7 +2842,7 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], @@ -2932,13 +2932,13 @@ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], - "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], @@ -3118,7 +3118,7 @@ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -3466,12 +3466,8 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "@modelcontextprotocol/sdk/jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], - "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "@npmcli/agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "@npmcli/agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -3536,6 +3532,10 @@ "@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "@slack/bolt/express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], + + "@slack/bolt/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + "@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], "@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], @@ -3554,8 +3554,6 @@ "@whiskeysockets/baileys/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], - "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.111", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-atgBW8jZPr/KuaKX5FvDIHuXBI8VCol6kVeoD4P0657+VXR73QsLogXQVN/Zt5FHtq9WzpdIZseCJiXPqkgwwA=="], "ai-gateway-provider/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="], @@ -3588,7 +3586,7 @@ "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], @@ -3616,18 +3614,12 @@ "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], - "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], - "fast-json-stringify/fast-uri": ["fast-uri@4.1.3", "", {}, "sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw=="], "fastify/pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], "fdir/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "fstream/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], @@ -3654,16 +3646,14 @@ "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], - "make-fetch-happen/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - - "minipass-fetch/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "music-metadata/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], @@ -3702,10 +3692,6 @@ "safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], @@ -3728,9 +3714,9 @@ "tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="], - "type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "type-is/media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], "unzipper/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -3862,28 +3848,6 @@ "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], - - "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], - - "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], - - "@modelcontextprotocol/sdk/raw-body/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -3920,12 +3884,36 @@ "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@slack/bolt/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "@slack/bolt/express/body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], + + "@slack/bolt/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "@slack/bolt/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "@slack/bolt/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "@slack/bolt/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "@slack/bolt/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "@slack/bolt/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "@slack/bolt/express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + + "@slack/bolt/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "@slack/bolt/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "@slack/bolt/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "@slack/bolt/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "@types/node-fetch/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.46", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tEtld97plCFiYevsJuOkGkeuhQndeMWFBVrJS4AjnbD5AqrNSXRCe0p+BZ3Cju/sxDeeZ9ym3q9YUV8fASA7aQ=="], @@ -3968,8 +3956,6 @@ "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "c12/chokidar/readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -3986,14 +3972,10 @@ "exceljs/archiver/zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="], - "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "fastify/pino/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], "fastify/pino/thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "fstream/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -4012,8 +3994,6 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "make-fetch-happen/negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "opencode-poe-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], @@ -4040,12 +4020,8 @@ "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "unzipper/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "unzipper/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -4068,23 +4044,27 @@ "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], - "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], + + "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="], + + "@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@3.0.0", "", {}, "sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw=="], - "@modelcontextprotocol/sdk/express/body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], - "@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + "@slack/bolt/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "@slack/bolt/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], + "@slack/bolt/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], + "@slack/bolt/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="], + "@slack/bolt/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@3.0.0", "", {}, "sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw=="], + "@slack/bolt/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], + "@slack/bolt/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "@types/node-fetch/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -4154,7 +4134,9 @@ "venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@modelcontextprotocol/sdk/express/accepts/negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "@slack/bolt/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@slack/bolt/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], diff --git a/docs/APP-BRIDGE.md b/docs/APP-BRIDGE.md index 0899428c32..0fb6c7e55b 100644 --- a/docs/APP-BRIDGE.md +++ b/docs/APP-BRIDGE.md @@ -81,7 +81,7 @@ registration expired or the daemon restarted -- register again. Send one at leas } ``` -## Using `@redrob-labs/app-bridge` +## Using `@redrob-code/app-bridge` The shortest path from JavaScript: `packages/app-bridge` serves the tools and does steps 1 to 3 below in one call. It starts a streamable-HTTP MCP server on `127.0.0.1` (random free @@ -89,7 +89,7 @@ port), finds or starts the daemon with `connectOrStart`, registers through `brid unregisters on `stop()`. ```ts -import { startAppBridge } from "@redrob-labs/app-bridge" +import { startAppBridge } from "@redrob-code/app-bridge" import { z } from "zod" const bridge = await startAppBridge({ diff --git a/packages/app-bridge/README.md b/packages/app-bridge/README.md index 9bd4d31241..d2ff28abf0 100644 --- a/packages/app-bridge/README.md +++ b/packages/app-bridge/README.md @@ -1,4 +1,4 @@ -# @redrob-labs/app-bridge +# @redrob-code/app-bridge One call that hands the Redrob daemon agent an app's tools. It serves the tools as an MCP server (streamable HTTP, `127.0.0.1`, random free port), finds or starts the shared daemon @@ -7,7 +7,7 @@ registration alive (re-registering after a daemon restart) and unregisters on `s Protocol details: `docs/APP-BRIDGE.md`. ```ts -import { startAppBridge } from "@redrob-labs/app-bridge" +import { startAppBridge } from "@redrob-code/app-bridge" import { z } from "zod" const bridge = await startAppBridge({ diff --git a/packages/app-bridge/package.json b/packages/app-bridge/package.json index 5a63af43f6..efaa85004a 100644 --- a/packages/app-bridge/package.json +++ b/packages/app-bridge/package.json @@ -1,10 +1,8 @@ { "$schema": "https://json.schemastore.org/package.json", - "name": "@redrob-labs/app-bridge", + "name": "@redrob-code/app-bridge", "version": "0.0.0", - "private": false, - "publishConfig": { "access": "public" }, - "repository": { "type": "git", "url": "https://github.com/redrob-labs/redrob-code.git", "directory": "packages/app-bridge" }, + "private": true, "type": "module", "license": "MIT", "exports": { From 16853d6b658037a7a9ddf5aa5c652d490325fb44 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Sat, 17 Oct 2026 06:18:30 +0000 Subject: [PATCH 22/23] ci: run checks on pull requests only; app-bridge is a private reference Drop push triggers from test, typecheck, engine-client and gitflow and the weekly schedule from upstream-sync. A merge is a pull request that already passed. Document that packages/app-bridge is vendored by apps, not published. --- .github/workflows/engine-client.yml | 5 ----- .github/workflows/gitflow.yml | 2 -- .github/workflows/test.yml | 17 ++++------------- .github/workflows/typecheck.yml | 2 -- .github/workflows/upstream-sync.yml | 4 ---- docs/APP-BRIDGE.md | 8 ++++++-- 6 files changed, 10 insertions(+), 28 deletions(-) diff --git a/.github/workflows/engine-client.yml b/.github/workflows/engine-client.yml index 7613fd1c99..7b956fe41e 100644 --- a/.github/workflows/engine-client.yml +++ b/.github/workflows/engine-client.yml @@ -7,11 +7,6 @@ on: paths: - "crates/engine-client/**" - ".github/workflows/engine-client.yml" - push: - branches: [develop, main] - paths: - - "crates/engine-client/**" - - ".github/workflows/engine-client.yml" permissions: contents: read diff --git a/.github/workflows/gitflow.yml b/.github/workflows/gitflow.yml index 8c75c9c488..0016556c69 100644 --- a/.github/workflows/gitflow.yml +++ b/.github/workflows/gitflow.yml @@ -8,8 +8,6 @@ name: gitflow on: pull_request: types: [opened, reopened, edited, synchronize] - push: - branches: [main] workflow_dispatch: permissions: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5e4e828a8..3966ae5f1a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,22 +2,13 @@ name: test on: workflow_dispatch: - # `develop` is the integration branch, so it has to be here for two reasons. It is the - # branch releases are cut from, and — less obviously — GitHub scopes the Actions cache - # to a branch and its base. A pull request can only restore a cache written by its base - # branch, so a workflow that never runs on `develop` writes no cache a pull request can - # read, and every run starts cold. - push: - branches: [develop, main] + # Pull requests only. A merge to `develop` or `main` is a pull request that already + # passed, so running again on push would repeat the same checks. pull_request: concurrency: - # Keep every run on the long-lived branches so cancelled checks do not pollute their - # commit history. PRs and other branches share a group and cancel stale runs. - group: >- - ${{ (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main') - && format('{0}-{1}', github.workflow, github.run_id) - || format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref) }} + # One run per pull request; a new push cancels the stale run. + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 7ee84ed92a..813b3068e5 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -2,8 +2,6 @@ name: typecheck on: workflow_dispatch: - push: - branches: [develop, main] pull_request: jobs: diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml index 873568b015..9140c29a05 100644 --- a/.github/workflows/upstream-sync.yml +++ b/.github/workflows/upstream-sync.yml @@ -19,10 +19,6 @@ on: description: "Upstream tag to merge, e.g. v1.18.32. Leave empty for upstream's latest release." required: false type: string - schedule: - # Weekly, so a sync that nobody remembers to run still surfaces as a pull request - # rather than as six months of drift discovered during an incident. - - cron: "17 6 * * 1" permissions: # Pushing the sync branch is all this needs. It deliberately does not open the pull diff --git a/docs/APP-BRIDGE.md b/docs/APP-BRIDGE.md index 0fb6c7e55b..4229ea5369 100644 --- a/docs/APP-BRIDGE.md +++ b/docs/APP-BRIDGE.md @@ -83,8 +83,12 @@ registration expired or the daemon restarted -- register again. Send one at leas ## Using `@redrob-code/app-bridge` -The shortest path from JavaScript: `packages/app-bridge` serves the tools and does steps 1 to -3 below in one call. It starts a streamable-HTTP MCP server on `127.0.0.1` (random free +`packages/app-bridge` is the reference implementation. It is a private workspace package and +is not published to npm: a JavaScript app copies `src/index.ts` (folded with the app-bridge +section of `packages/sdk/js/src/daemon.ts`) into its own tree as `app-bridge.ts`, marked +"VENDORED COPY" with this location in its header. Change it here first, then recopy. + +It serves the tools and does steps 1 to 3 below in one call. It starts a streamable-HTTP MCP server on `127.0.0.1` (random free port), finds or starts the daemon with `connectOrStart`, registers through `bridgeApp` and unregisters on `stop()`. From 43306b51f59130f8cee57fd64f468e70908a477d Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Sat, 17 Oct 2026 06:29:04 +0000 Subject: [PATCH 23/23] client: regenerate for channels and apps endpoints; exercise the six new routes The generated client lagged the protocol (channels.status, channels.reload, apps.list/register/heartbeat/unregister), which failed check:generated, and the HttpApi exerciser flagged the same six routes as missing scenarios. --- .../client/src/generated-effect/client.ts | 901 +++++++++--------- packages/client/src/generated/client.ts | 68 ++ packages/client/src/generated/types.ts | 87 ++ .../test/server/httpapi-exercise/index.ts | 37 + 4 files changed, 668 insertions(+), 425 deletions(-) diff --git a/packages/client/src/generated-effect/client.ts b/packages/client/src/generated-effect/client.ts index bfff7296bd..c578ca1a51 100644 --- a/packages/client/src/generated-effect/client.ts +++ b/packages/client/src/generated-effect/client.ts @@ -46,32 +46,78 @@ const adaptGroup1 = (raw: RawClient["server.clients"]) => ({ detach: Endpoint1_3(raw), }) -type Endpoint2_0Request = Parameters[0] -type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } -const Endpoint2_0 = (raw: RawClient["server.location"]) => (input?: Endpoint2_0Input) => +const Endpoint2_0 = (raw: RawClient["server.channels"]) => () => + raw["channels.status"]({}).pipe(Effect.mapError(mapClientError)) + +const Endpoint2_1 = (raw: RawClient["server.channels"]) => () => + raw["channels.reload"]({}).pipe(Effect.mapError(mapClientError)) + +const adaptGroup2 = (raw: RawClient["server.channels"]) => ({ status: Endpoint2_0(raw), reload: Endpoint2_1(raw) }) + +const Endpoint3_0 = (raw: RawClient["server.apps"]) => () => raw["apps.list"]({}).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_1Request = Parameters[0] +type Endpoint3_1Input = { + readonly name: Endpoint3_1Request["payload"]["name"] + readonly mcp_url: Endpoint3_1Request["payload"]["mcp_url"] + readonly pid: Endpoint3_1Request["payload"]["pid"] + readonly version?: Endpoint3_1Request["payload"]["version"] + readonly tools?: Endpoint3_1Request["payload"]["tools"] +} +const Endpoint3_1 = (raw: RawClient["server.apps"]) => (input: Endpoint3_1Input) => + raw["apps.register"]({ + payload: { + name: input["name"], + mcp_url: input["mcp_url"], + pid: input["pid"], + version: input["version"], + tools: input["tools"], + }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_2Request = Parameters[0] +type Endpoint3_2Input = { readonly appID: Endpoint3_2Request["params"]["appID"] } +const Endpoint3_2 = (raw: RawClient["server.apps"]) => (input: Endpoint3_2Input) => + raw["apps.heartbeat"]({ params: { appID: input["appID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_3Request = Parameters[0] +type Endpoint3_3Input = { readonly appID: Endpoint3_3Request["params"]["appID"] } +const Endpoint3_3 = (raw: RawClient["server.apps"]) => (input: Endpoint3_3Input) => + raw["apps.unregister"]({ params: { appID: input["appID"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup3 = (raw: RawClient["server.apps"]) => ({ + list: Endpoint3_0(raw), + register: Endpoint3_1(raw), + heartbeat: Endpoint3_2(raw), + unregister: Endpoint3_3(raw), +}) + +type Endpoint4_0Request = Parameters[0] +type Endpoint4_0Input = { readonly location?: Endpoint4_0Request["query"]["location"] } +const Endpoint4_0 = (raw: RawClient["server.location"]) => (input?: Endpoint4_0Input) => raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -const adaptGroup2 = (raw: RawClient["server.location"]) => ({ get: Endpoint2_0(raw) }) +const adaptGroup4 = (raw: RawClient["server.location"]) => ({ get: Endpoint4_0(raw) }) -type Endpoint3_0Request = Parameters[0] -type Endpoint3_0Input = { readonly location?: Endpoint3_0Request["query"]["location"] } -const Endpoint3_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint3_0Input) => +type Endpoint5_0Request = Parameters[0] +type Endpoint5_0Input = { readonly location?: Endpoint5_0Request["query"]["location"] } +const Endpoint5_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint5_0Input) => raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -const adaptGroup3 = (raw: RawClient["server.agent"]) => ({ list: Endpoint3_0(raw) }) +const adaptGroup5 = (raw: RawClient["server.agent"]) => ({ list: Endpoint5_0(raw) }) -type Endpoint4_0Request = Parameters[0] -type Endpoint4_0Input = { - readonly workspace?: Endpoint4_0Request["query"]["workspace"] - readonly limit?: Endpoint4_0Request["query"]["limit"] - readonly order?: Endpoint4_0Request["query"]["order"] - readonly search?: Endpoint4_0Request["query"]["search"] - readonly directory?: Endpoint4_0Request["query"]["directory"] - readonly project?: Endpoint4_0Request["query"]["project"] - readonly subpath?: Endpoint4_0Request["query"]["subpath"] - readonly cursor?: Endpoint4_0Request["query"]["cursor"] +type Endpoint6_0Request = Parameters[0] +type Endpoint6_0Input = { + readonly workspace?: Endpoint6_0Request["query"]["workspace"] + readonly limit?: Endpoint6_0Request["query"]["limit"] + readonly order?: Endpoint6_0Request["query"]["order"] + readonly search?: Endpoint6_0Request["query"]["search"] + readonly directory?: Endpoint6_0Request["query"]["directory"] + readonly project?: Endpoint6_0Request["query"]["project"] + readonly subpath?: Endpoint6_0Request["query"]["subpath"] + readonly cursor?: Endpoint6_0Request["query"]["cursor"] } -const Endpoint4_0 = (raw: RawClient["server.session"]) => (input?: Endpoint4_0Input) => +const Endpoint6_0 = (raw: RawClient["server.session"]) => (input?: Endpoint6_0Input) => raw["session.list"]({ query: { workspace: input?.["workspace"], @@ -85,14 +131,14 @@ const Endpoint4_0 = (raw: RawClient["server.session"]) => (input?: Endpoint4_0In }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint4_1Request = Parameters[0] -type Endpoint4_1Input = { - readonly id?: Endpoint4_1Request["payload"]["id"] - readonly agent?: Endpoint4_1Request["payload"]["agent"] - readonly model?: Endpoint4_1Request["payload"]["model"] - readonly location?: Endpoint4_1Request["payload"]["location"] +type Endpoint6_1Request = Parameters[0] +type Endpoint6_1Input = { + readonly id?: Endpoint6_1Request["payload"]["id"] + readonly agent?: Endpoint6_1Request["payload"]["agent"] + readonly model?: Endpoint6_1Request["payload"]["model"] + readonly location?: Endpoint6_1Request["payload"]["location"] } -const Endpoint4_1 = (raw: RawClient["server.session"]) => (input?: Endpoint4_1Input) => +const Endpoint6_1 = (raw: RawClient["server.session"]) => (input?: Endpoint6_1Input) => raw["session.create"]({ payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, }).pipe( @@ -100,49 +146,49 @@ const Endpoint4_1 = (raw: RawClient["server.session"]) => (input?: Endpoint4_1In Effect.map((value) => value.data), ) -const Endpoint4_2 = (raw: RawClient["server.session"]) => () => +const Endpoint6_2 = (raw: RawClient["server.session"]) => () => raw["session.active"]({}).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint4_3Request = Parameters[0] -type Endpoint4_3Input = { readonly sessionID: Endpoint4_3Request["params"]["sessionID"] } -const Endpoint4_3 = (raw: RawClient["server.session"]) => (input: Endpoint4_3Input) => +type Endpoint6_3Request = Parameters[0] +type Endpoint6_3Input = { readonly sessionID: Endpoint6_3Request["params"]["sessionID"] } +const Endpoint6_3 = (raw: RawClient["server.session"]) => (input: Endpoint6_3Input) => raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint4_4Request = Parameters[0] -type Endpoint4_4Input = { - readonly sessionID: Endpoint4_4Request["params"]["sessionID"] - readonly agent: Endpoint4_4Request["payload"]["agent"] +type Endpoint6_4Request = Parameters[0] +type Endpoint6_4Input = { + readonly sessionID: Endpoint6_4Request["params"]["sessionID"] + readonly agent: Endpoint6_4Request["payload"]["agent"] } -const Endpoint4_4 = (raw: RawClient["server.session"]) => (input: Endpoint4_4Input) => +const Endpoint6_4 = (raw: RawClient["server.session"]) => (input: Endpoint6_4Input) => raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint4_5Request = Parameters[0] -type Endpoint4_5Input = { - readonly sessionID: Endpoint4_5Request["params"]["sessionID"] - readonly model: Endpoint4_5Request["payload"]["model"] +type Endpoint6_5Request = Parameters[0] +type Endpoint6_5Input = { + readonly sessionID: Endpoint6_5Request["params"]["sessionID"] + readonly model: Endpoint6_5Request["payload"]["model"] } -const Endpoint4_5 = (raw: RawClient["server.session"]) => (input: Endpoint4_5Input) => +const Endpoint6_5 = (raw: RawClient["server.session"]) => (input: Endpoint6_5Input) => raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint4_6Request = Parameters[0] -type Endpoint4_6Input = { - readonly sessionID: Endpoint4_6Request["params"]["sessionID"] - readonly id?: Endpoint4_6Request["payload"]["id"] - readonly prompt: Endpoint4_6Request["payload"]["prompt"] - readonly delivery?: Endpoint4_6Request["payload"]["delivery"] - readonly resume?: Endpoint4_6Request["payload"]["resume"] +type Endpoint6_6Request = Parameters[0] +type Endpoint6_6Input = { + readonly sessionID: Endpoint6_6Request["params"]["sessionID"] + readonly id?: Endpoint6_6Request["payload"]["id"] + readonly prompt: Endpoint6_6Request["payload"]["prompt"] + readonly delivery?: Endpoint6_6Request["payload"]["delivery"] + readonly resume?: Endpoint6_6Request["payload"]["resume"] } -const Endpoint4_6 = (raw: RawClient["server.session"]) => (input: Endpoint4_6Input) => +const Endpoint6_6 = (raw: RawClient["server.session"]) => (input: Endpoint6_6Input) => raw["session.prompt"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, @@ -151,23 +197,23 @@ const Endpoint4_6 = (raw: RawClient["server.session"]) => (input: Endpoint4_6Inp Effect.map((value) => value.data), ) -type Endpoint4_7Request = Parameters[0] -type Endpoint4_7Input = { readonly sessionID: Endpoint4_7Request["params"]["sessionID"] } -const Endpoint4_7 = (raw: RawClient["server.session"]) => (input: Endpoint4_7Input) => +type Endpoint6_7Request = Parameters[0] +type Endpoint6_7Input = { readonly sessionID: Endpoint6_7Request["params"]["sessionID"] } +const Endpoint6_7 = (raw: RawClient["server.session"]) => (input: Endpoint6_7Input) => raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint4_8Request = Parameters[0] -type Endpoint4_8Input = { readonly sessionID: Endpoint4_8Request["params"]["sessionID"] } -const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Input) => +type Endpoint6_8Request = Parameters[0] +type Endpoint6_8Input = { readonly sessionID: Endpoint6_8Request["params"]["sessionID"] } +const Endpoint6_8 = (raw: RawClient["server.session"]) => (input: Endpoint6_8Input) => raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint4_9Request = Parameters[0] -type Endpoint4_9Input = { - readonly sessionID: Endpoint4_9Request["params"]["sessionID"] - readonly messageID: Endpoint4_9Request["payload"]["messageID"] - readonly files?: Endpoint4_9Request["payload"]["files"] +type Endpoint6_9Request = Parameters[0] +type Endpoint6_9Input = { + readonly sessionID: Endpoint6_9Request["params"]["sessionID"] + readonly messageID: Endpoint6_9Request["payload"]["messageID"] + readonly files?: Endpoint6_9Request["payload"]["files"] } -const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) => +const Endpoint6_9 = (raw: RawClient["server.session"]) => (input: Endpoint6_9Input) => raw["session.revert.stage"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"], files: input["files"] }, @@ -176,42 +222,42 @@ const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Inp Effect.map((value) => value.data), ) -type Endpoint4_10Request = Parameters[0] -type Endpoint4_10Input = { readonly sessionID: Endpoint4_10Request["params"]["sessionID"] } -const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) => +type Endpoint6_10Request = Parameters[0] +type Endpoint6_10Input = { readonly sessionID: Endpoint6_10Request["params"]["sessionID"] } +const Endpoint6_10 = (raw: RawClient["server.session"]) => (input: Endpoint6_10Input) => raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint4_11Request = Parameters[0] -type Endpoint4_11Input = { readonly sessionID: Endpoint4_11Request["params"]["sessionID"] } -const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) => +type Endpoint6_11Request = Parameters[0] +type Endpoint6_11Input = { readonly sessionID: Endpoint6_11Request["params"]["sessionID"] } +const Endpoint6_11 = (raw: RawClient["server.session"]) => (input: Endpoint6_11Input) => raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint4_12Request = Parameters[0] -type Endpoint4_12Input = { readonly sessionID: Endpoint4_12Request["params"]["sessionID"] } -const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) => +type Endpoint6_12Request = Parameters[0] +type Endpoint6_12Input = { readonly sessionID: Endpoint6_12Request["params"]["sessionID"] } +const Endpoint6_12 = (raw: RawClient["server.session"]) => (input: Endpoint6_12Input) => raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint4_13Request = Parameters[0] -type Endpoint4_13Input = { - readonly sessionID: Endpoint4_13Request["params"]["sessionID"] - readonly limit?: Endpoint4_13Request["query"]["limit"] - readonly after?: Endpoint4_13Request["query"]["after"] +type Endpoint6_13Request = Parameters[0] +type Endpoint6_13Input = { + readonly sessionID: Endpoint6_13Request["params"]["sessionID"] + readonly limit?: Endpoint6_13Request["query"]["limit"] + readonly after?: Endpoint6_13Request["query"]["after"] } -const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) => +const Endpoint6_13 = (raw: RawClient["server.session"]) => (input: Endpoint6_13Input) => raw["session.history"]({ params: { sessionID: input["sessionID"] }, query: { limit: input["limit"], after: input["after"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint4_14Request = Parameters[0] -type Endpoint4_14Input = { - readonly sessionID: Endpoint4_14Request["params"]["sessionID"] - readonly after?: Endpoint4_14Request["query"]["after"] +type Endpoint6_14Request = Parameters[0] +type Endpoint6_14Input = { + readonly sessionID: Endpoint6_14Request["params"]["sessionID"] + readonly after?: Endpoint6_14Request["query"]["after"] } -const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) => +const Endpoint6_14 = (raw: RawClient["server.session"]) => (input: Endpoint6_14Input) => Stream.unwrap( raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe( Effect.mapError(mapClientError), @@ -219,270 +265,273 @@ const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14I ), ) -type Endpoint4_15Request = Parameters[0] -type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] } -const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) => +type Endpoint6_15Request = Parameters[0] +type Endpoint6_15Input = { readonly sessionID: Endpoint6_15Request["params"]["sessionID"] } +const Endpoint6_15 = (raw: RawClient["server.session"]) => (input: Endpoint6_15Input) => raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint4_16Request = Parameters[0] -type Endpoint4_16Input = { - readonly sessionID: Endpoint4_16Request["params"]["sessionID"] - readonly messageID: Endpoint4_16Request["params"]["messageID"] +type Endpoint6_16Request = Parameters[0] +type Endpoint6_16Input = { + readonly sessionID: Endpoint6_16Request["params"]["sessionID"] + readonly messageID: Endpoint6_16Request["params"]["messageID"] } -const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) => +const Endpoint6_16 = (raw: RawClient["server.session"]) => (input: Endpoint6_16Input) => raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -const adaptGroup4 = (raw: RawClient["server.session"]) => ({ - list: Endpoint4_0(raw), - create: Endpoint4_1(raw), - active: Endpoint4_2(raw), - get: Endpoint4_3(raw), - switchAgent: Endpoint4_4(raw), - switchModel: Endpoint4_5(raw), - prompt: Endpoint4_6(raw), - compact: Endpoint4_7(raw), - wait: Endpoint4_8(raw), - stage: Endpoint4_9(raw), - clear: Endpoint4_10(raw), - commit: Endpoint4_11(raw), - context: Endpoint4_12(raw), - history: Endpoint4_13(raw), - events: Endpoint4_14(raw), - interrupt: Endpoint4_15(raw), - message: Endpoint4_16(raw), +const adaptGroup6 = (raw: RawClient["server.session"]) => ({ + list: Endpoint6_0(raw), + create: Endpoint6_1(raw), + active: Endpoint6_2(raw), + get: Endpoint6_3(raw), + switchAgent: Endpoint6_4(raw), + switchModel: Endpoint6_5(raw), + prompt: Endpoint6_6(raw), + compact: Endpoint6_7(raw), + wait: Endpoint6_8(raw), + stage: Endpoint6_9(raw), + clear: Endpoint6_10(raw), + commit: Endpoint6_11(raw), + context: Endpoint6_12(raw), + history: Endpoint6_13(raw), + events: Endpoint6_14(raw), + interrupt: Endpoint6_15(raw), + message: Endpoint6_16(raw), }) -type Endpoint5_0Request = Parameters[0] -type Endpoint5_0Input = { - readonly sessionID: Endpoint5_0Request["params"]["sessionID"] - readonly limit?: Endpoint5_0Request["query"]["limit"] - readonly order?: Endpoint5_0Request["query"]["order"] - readonly cursor?: Endpoint5_0Request["query"]["cursor"] +type Endpoint7_0Request = Parameters[0] +type Endpoint7_0Input = { + readonly sessionID: Endpoint7_0Request["params"]["sessionID"] + readonly limit?: Endpoint7_0Request["query"]["limit"] + readonly order?: Endpoint7_0Request["query"]["order"] + readonly cursor?: Endpoint7_0Request["query"]["cursor"] } -const Endpoint5_0 = (raw: RawClient["server.message"]) => (input: Endpoint5_0Input) => +const Endpoint7_0 = (raw: RawClient["server.message"]) => (input: Endpoint7_0Input) => raw["session.messages"]({ params: { sessionID: input["sessionID"] }, query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup5 = (raw: RawClient["server.message"]) => ({ list: Endpoint5_0(raw) }) +const adaptGroup7 = (raw: RawClient["server.message"]) => ({ list: Endpoint7_0(raw) }) -type Endpoint6_0Request = Parameters[0] -type Endpoint6_0Input = { readonly location?: Endpoint6_0Request["query"]["location"] } -const Endpoint6_0 = (raw: RawClient["server.model"]) => (input?: Endpoint6_0Input) => +type Endpoint8_0Request = Parameters[0] +type Endpoint8_0Input = { readonly location?: Endpoint8_0Request["query"]["location"] } +const Endpoint8_0 = (raw: RawClient["server.model"]) => (input?: Endpoint8_0Input) => raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -const adaptGroup6 = (raw: RawClient["server.model"]) => ({ list: Endpoint6_0(raw) }) +const adaptGroup8 = (raw: RawClient["server.model"]) => ({ list: Endpoint8_0(raw) }) -type Endpoint7_0Request = Parameters[0] -type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } -const Endpoint7_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint7_0Input) => +type Endpoint9_0Request = Parameters[0] +type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } +const Endpoint9_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint9_0Input) => raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint7_1Request = Parameters[0] -type Endpoint7_1Input = { - readonly providerID: Endpoint7_1Request["params"]["providerID"] - readonly location?: Endpoint7_1Request["query"]["location"] +type Endpoint9_1Request = Parameters[0] +type Endpoint9_1Input = { + readonly providerID: Endpoint9_1Request["params"]["providerID"] + readonly location?: Endpoint9_1Request["query"]["location"] } -const Endpoint7_1 = (raw: RawClient["server.provider"]) => (input: Endpoint7_1Input) => +const Endpoint9_1 = (raw: RawClient["server.provider"]) => (input: Endpoint9_1Input) => raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ) -const adaptGroup7 = (raw: RawClient["server.provider"]) => ({ list: Endpoint7_0(raw), get: Endpoint7_1(raw) }) +const adaptGroup9 = (raw: RawClient["server.provider"]) => ({ list: Endpoint9_0(raw), get: Endpoint9_1(raw) }) -type Endpoint8_0Request = Parameters[0] -type Endpoint8_0Input = { - readonly text: Endpoint8_0Request["payload"]["text"] - readonly models: Endpoint8_0Request["payload"]["models"] - readonly requestId?: Endpoint8_0Request["payload"]["requestId"] +type Endpoint10_0Request = Parameters[0] +type Endpoint10_0Input = { + readonly text: Endpoint10_0Request["payload"]["text"] + readonly models: Endpoint10_0Request["payload"]["models"] + readonly requestId?: Endpoint10_0Request["payload"]["requestId"] } -const Endpoint8_0 = (raw: RawClient["server.variant"]) => (input: Endpoint8_0Input) => +const Endpoint10_0 = (raw: RawClient["server.variant"]) => (input: Endpoint10_0Input) => raw["variant.paraphrase"]({ payload: { text: input["text"], models: input["models"], requestId: input["requestId"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint8_1Request = Parameters[0] -type Endpoint8_1Input = { - readonly messages: Endpoint8_1Request["payload"]["messages"] - readonly models: Endpoint8_1Request["payload"]["models"] - readonly requestId?: Endpoint8_1Request["payload"]["requestId"] +type Endpoint10_1Request = Parameters[0] +type Endpoint10_1Input = { + readonly messages: Endpoint10_1Request["payload"]["messages"] + readonly models: Endpoint10_1Request["payload"]["models"] + readonly requestId?: Endpoint10_1Request["payload"]["requestId"] } -const Endpoint8_1 = (raw: RawClient["server.variant"]) => (input: Endpoint8_1Input) => +const Endpoint10_1 = (raw: RawClient["server.variant"]) => (input: Endpoint10_1Input) => raw["variant.compare"]({ payload: { messages: input["messages"], models: input["models"], requestId: input["requestId"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup8 = (raw: RawClient["server.variant"]) => ({ paraphrase: Endpoint8_0(raw), compare: Endpoint8_1(raw) }) +const adaptGroup10 = (raw: RawClient["server.variant"]) => ({ + paraphrase: Endpoint10_0(raw), + compare: Endpoint10_1(raw), +}) -type Endpoint9_0Request = Parameters[0] -type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } -const Endpoint9_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint9_0Input) => +type Endpoint11_0Request = Parameters[0] +type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } +const Endpoint11_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint11_0Input) => raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint9_1Request = Parameters[0] -type Endpoint9_1Input = { - readonly integrationID: Endpoint9_1Request["params"]["integrationID"] - readonly location?: Endpoint9_1Request["query"]["location"] +type Endpoint11_1Request = Parameters[0] +type Endpoint11_1Input = { + readonly integrationID: Endpoint11_1Request["params"]["integrationID"] + readonly location?: Endpoint11_1Request["query"]["location"] } -const Endpoint9_1 = (raw: RawClient["server.integration"]) => (input: Endpoint9_1Input) => +const Endpoint11_1 = (raw: RawClient["server.integration"]) => (input: Endpoint11_1Input) => raw["integration.get"]({ params: { integrationID: input["integrationID"] }, query: { location: input["location"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint9_2Request = Parameters[0] -type Endpoint9_2Input = { - readonly integrationID: Endpoint9_2Request["params"]["integrationID"] - readonly location?: Endpoint9_2Request["query"]["location"] - readonly key: Endpoint9_2Request["payload"]["key"] - readonly label?: Endpoint9_2Request["payload"]["label"] +type Endpoint11_2Request = Parameters[0] +type Endpoint11_2Input = { + readonly integrationID: Endpoint11_2Request["params"]["integrationID"] + readonly location?: Endpoint11_2Request["query"]["location"] + readonly key: Endpoint11_2Request["payload"]["key"] + readonly label?: Endpoint11_2Request["payload"]["label"] } -const Endpoint9_2 = (raw: RawClient["server.integration"]) => (input: Endpoint9_2Input) => +const Endpoint11_2 = (raw: RawClient["server.integration"]) => (input: Endpoint11_2Input) => raw["integration.connect.key"]({ params: { integrationID: input["integrationID"] }, query: { location: input["location"] }, payload: { key: input["key"], label: input["label"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint9_3Request = Parameters[0] -type Endpoint9_3Input = { - readonly integrationID: Endpoint9_3Request["params"]["integrationID"] - readonly location?: Endpoint9_3Request["query"]["location"] - readonly methodID: Endpoint9_3Request["payload"]["methodID"] - readonly inputs: Endpoint9_3Request["payload"]["inputs"] - readonly label?: Endpoint9_3Request["payload"]["label"] +type Endpoint11_3Request = Parameters[0] +type Endpoint11_3Input = { + readonly integrationID: Endpoint11_3Request["params"]["integrationID"] + readonly location?: Endpoint11_3Request["query"]["location"] + readonly methodID: Endpoint11_3Request["payload"]["methodID"] + readonly inputs: Endpoint11_3Request["payload"]["inputs"] + readonly label?: Endpoint11_3Request["payload"]["label"] } -const Endpoint9_3 = (raw: RawClient["server.integration"]) => (input: Endpoint9_3Input) => +const Endpoint11_3 = (raw: RawClient["server.integration"]) => (input: Endpoint11_3Input) => raw["integration.connect.oauth"]({ params: { integrationID: input["integrationID"] }, query: { location: input["location"] }, payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint9_4Request = Parameters[0] -type Endpoint9_4Input = { - readonly attemptID: Endpoint9_4Request["params"]["attemptID"] - readonly location?: Endpoint9_4Request["query"]["location"] +type Endpoint11_4Request = Parameters[0] +type Endpoint11_4Input = { + readonly attemptID: Endpoint11_4Request["params"]["attemptID"] + readonly location?: Endpoint11_4Request["query"]["location"] } -const Endpoint9_4 = (raw: RawClient["server.integration"]) => (input: Endpoint9_4Input) => +const Endpoint11_4 = (raw: RawClient["server.integration"]) => (input: Endpoint11_4Input) => raw["integration.attempt.status"]({ params: { attemptID: input["attemptID"] }, query: { location: input["location"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint9_5Request = Parameters[0] -type Endpoint9_5Input = { - readonly attemptID: Endpoint9_5Request["params"]["attemptID"] - readonly location?: Endpoint9_5Request["query"]["location"] - readonly code?: Endpoint9_5Request["payload"]["code"] +type Endpoint11_5Request = Parameters[0] +type Endpoint11_5Input = { + readonly attemptID: Endpoint11_5Request["params"]["attemptID"] + readonly location?: Endpoint11_5Request["query"]["location"] + readonly code?: Endpoint11_5Request["payload"]["code"] } -const Endpoint9_5 = (raw: RawClient["server.integration"]) => (input: Endpoint9_5Input) => +const Endpoint11_5 = (raw: RawClient["server.integration"]) => (input: Endpoint11_5Input) => raw["integration.attempt.complete"]({ params: { attemptID: input["attemptID"] }, query: { location: input["location"] }, payload: { code: input["code"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint9_6Request = Parameters[0] -type Endpoint9_6Input = { - readonly attemptID: Endpoint9_6Request["params"]["attemptID"] - readonly location?: Endpoint9_6Request["query"]["location"] +type Endpoint11_6Request = Parameters[0] +type Endpoint11_6Input = { + readonly attemptID: Endpoint11_6Request["params"]["attemptID"] + readonly location?: Endpoint11_6Request["query"]["location"] } -const Endpoint9_6 = (raw: RawClient["server.integration"]) => (input: Endpoint9_6Input) => +const Endpoint11_6 = (raw: RawClient["server.integration"]) => (input: Endpoint11_6Input) => raw["integration.attempt.cancel"]({ params: { attemptID: input["attemptID"] }, query: { location: input["location"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup9 = (raw: RawClient["server.integration"]) => ({ - list: Endpoint9_0(raw), - get: Endpoint9_1(raw), - connectKey: Endpoint9_2(raw), - connectOauth: Endpoint9_3(raw), - attemptStatus: Endpoint9_4(raw), - attemptComplete: Endpoint9_5(raw), - attemptCancel: Endpoint9_6(raw), +const adaptGroup11 = (raw: RawClient["server.integration"]) => ({ + list: Endpoint11_0(raw), + get: Endpoint11_1(raw), + connectKey: Endpoint11_2(raw), + connectOauth: Endpoint11_3(raw), + attemptStatus: Endpoint11_4(raw), + attemptComplete: Endpoint11_5(raw), + attemptCancel: Endpoint11_6(raw), }) -type Endpoint10_0Request = Parameters[0] -type Endpoint10_0Input = { - readonly credentialID: Endpoint10_0Request["params"]["credentialID"] - readonly location?: Endpoint10_0Request["query"]["location"] - readonly label: Endpoint10_0Request["payload"]["label"] +type Endpoint12_0Request = Parameters[0] +type Endpoint12_0Input = { + readonly credentialID: Endpoint12_0Request["params"]["credentialID"] + readonly location?: Endpoint12_0Request["query"]["location"] + readonly label: Endpoint12_0Request["payload"]["label"] } -const Endpoint10_0 = (raw: RawClient["server.credential"]) => (input: Endpoint10_0Input) => +const Endpoint12_0 = (raw: RawClient["server.credential"]) => (input: Endpoint12_0Input) => raw["credential.update"]({ params: { credentialID: input["credentialID"] }, query: { location: input["location"] }, payload: { label: input["label"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint10_1Request = Parameters[0] -type Endpoint10_1Input = { - readonly credentialID: Endpoint10_1Request["params"]["credentialID"] - readonly location?: Endpoint10_1Request["query"]["location"] +type Endpoint12_1Request = Parameters[0] +type Endpoint12_1Input = { + readonly credentialID: Endpoint12_1Request["params"]["credentialID"] + readonly location?: Endpoint12_1Request["query"]["location"] } -const Endpoint10_1 = (raw: RawClient["server.credential"]) => (input: Endpoint10_1Input) => +const Endpoint12_1 = (raw: RawClient["server.credential"]) => (input: Endpoint12_1Input) => raw["credential.remove"]({ params: { credentialID: input["credentialID"] }, query: { location: input["location"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup10 = (raw: RawClient["server.credential"]) => ({ update: Endpoint10_0(raw), remove: Endpoint10_1(raw) }) +const adaptGroup12 = (raw: RawClient["server.credential"]) => ({ update: Endpoint12_0(raw), remove: Endpoint12_1(raw) }) -type Endpoint11_0Request = Parameters[0] -type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } -const Endpoint11_0 = (raw: RawClient["server.privacy"]) => (input?: Endpoint11_0Input) => +type Endpoint13_0Request = Parameters[0] +type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] } +const Endpoint13_0 = (raw: RawClient["server.privacy"]) => (input?: Endpoint13_0Input) => raw["privacy.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint11_1Request = Parameters[0] -type Endpoint11_1Input = { - readonly location?: Endpoint11_1Request["query"]["location"] - readonly mode: Endpoint11_1Request["payload"]["mode"] - readonly model?: Endpoint11_1Request["payload"]["model"] +type Endpoint13_1Request = Parameters[0] +type Endpoint13_1Input = { + readonly location?: Endpoint13_1Request["query"]["location"] + readonly mode: Endpoint13_1Request["payload"]["mode"] + readonly model?: Endpoint13_1Request["payload"]["model"] } -const Endpoint11_1 = (raw: RawClient["server.privacy"]) => (input: Endpoint11_1Input) => +const Endpoint13_1 = (raw: RawClient["server.privacy"]) => (input: Endpoint13_1Input) => raw["privacy.set"]({ query: { location: input["location"] }, payload: { mode: input["mode"], model: input["model"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup11 = (raw: RawClient["server.privacy"]) => ({ get: Endpoint11_0(raw), set: Endpoint11_1(raw) }) +const adaptGroup13 = (raw: RawClient["server.privacy"]) => ({ get: Endpoint13_0(raw), set: Endpoint13_1(raw) }) -type Endpoint12_0Request = Parameters[0] -type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] } -const Endpoint12_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint12_0Input) => +type Endpoint14_0Request = Parameters[0] +type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } +const Endpoint14_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint14_0Input) => raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint12_1Request = Parameters[0] -type Endpoint12_1Input = { readonly projectID?: Endpoint12_1Request["query"]["projectID"] } -const Endpoint12_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint12_1Input) => +type Endpoint14_1Request = Parameters[0] +type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["query"]["projectID"] } +const Endpoint14_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint14_1Input) => raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint12_2Request = Parameters[0] -type Endpoint12_2Input = { readonly id: Endpoint12_2Request["params"]["id"] } -const Endpoint12_2 = (raw: RawClient["server.permission"]) => (input: Endpoint12_2Input) => +type Endpoint14_2Request = Parameters[0] +type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] } +const Endpoint14_2 = (raw: RawClient["server.permission"]) => (input: Endpoint14_2Input) => raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint12_3Request = Parameters[0] -type Endpoint12_3Input = { - readonly sessionID: Endpoint12_3Request["params"]["sessionID"] - readonly id?: Endpoint12_3Request["payload"]["id"] - readonly action: Endpoint12_3Request["payload"]["action"] - readonly resources: Endpoint12_3Request["payload"]["resources"] - readonly save?: Endpoint12_3Request["payload"]["save"] - readonly metadata?: Endpoint12_3Request["payload"]["metadata"] - readonly source?: Endpoint12_3Request["payload"]["source"] - readonly agent?: Endpoint12_3Request["payload"]["agent"] -} -const Endpoint12_3 = (raw: RawClient["server.permission"]) => (input: Endpoint12_3Input) => +type Endpoint14_3Request = Parameters[0] +type Endpoint14_3Input = { + readonly sessionID: Endpoint14_3Request["params"]["sessionID"] + readonly id?: Endpoint14_3Request["payload"]["id"] + readonly action: Endpoint14_3Request["payload"]["action"] + readonly resources: Endpoint14_3Request["payload"]["resources"] + readonly save?: Endpoint14_3Request["payload"]["save"] + readonly metadata?: Endpoint14_3Request["payload"]["metadata"] + readonly source?: Endpoint14_3Request["payload"]["source"] + readonly agent?: Endpoint14_3Request["payload"]["agent"] +} +const Endpoint14_3 = (raw: RawClient["server.permission"]) => (input: Endpoint14_3Input) => raw["session.permission.create"]({ params: { sessionID: input["sessionID"] }, payload: { @@ -499,87 +548,87 @@ const Endpoint12_3 = (raw: RawClient["server.permission"]) => (input: Endpoint12 Effect.map((value) => value.data), ) -type Endpoint12_4Request = Parameters[0] -type Endpoint12_4Input = { readonly sessionID: Endpoint12_4Request["params"]["sessionID"] } -const Endpoint12_4 = (raw: RawClient["server.permission"]) => (input: Endpoint12_4Input) => +type Endpoint14_4Request = Parameters[0] +type Endpoint14_4Input = { readonly sessionID: Endpoint14_4Request["params"]["sessionID"] } +const Endpoint14_4 = (raw: RawClient["server.permission"]) => (input: Endpoint14_4Input) => raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint12_5Request = Parameters[0] -type Endpoint12_5Input = { - readonly sessionID: Endpoint12_5Request["params"]["sessionID"] - readonly requestID: Endpoint12_5Request["params"]["requestID"] +type Endpoint14_5Request = Parameters[0] +type Endpoint14_5Input = { + readonly sessionID: Endpoint14_5Request["params"]["sessionID"] + readonly requestID: Endpoint14_5Request["params"]["requestID"] } -const Endpoint12_5 = (raw: RawClient["server.permission"]) => (input: Endpoint12_5Input) => +const Endpoint14_5 = (raw: RawClient["server.permission"]) => (input: Endpoint14_5Input) => raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint12_6Request = Parameters[0] -type Endpoint12_6Input = { - readonly sessionID: Endpoint12_6Request["params"]["sessionID"] - readonly requestID: Endpoint12_6Request["params"]["requestID"] - readonly reply: Endpoint12_6Request["payload"]["reply"] - readonly message?: Endpoint12_6Request["payload"]["message"] +type Endpoint14_6Request = Parameters[0] +type Endpoint14_6Input = { + readonly sessionID: Endpoint14_6Request["params"]["sessionID"] + readonly requestID: Endpoint14_6Request["params"]["requestID"] + readonly reply: Endpoint14_6Request["payload"]["reply"] + readonly message?: Endpoint14_6Request["payload"]["message"] } -const Endpoint12_6 = (raw: RawClient["server.permission"]) => (input: Endpoint12_6Input) => +const Endpoint14_6 = (raw: RawClient["server.permission"]) => (input: Endpoint14_6Input) => raw["session.permission.reply"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] }, payload: { reply: input["reply"], message: input["message"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup12 = (raw: RawClient["server.permission"]) => ({ - listRequests: Endpoint12_0(raw), - listSaved: Endpoint12_1(raw), - removeSaved: Endpoint12_2(raw), - create: Endpoint12_3(raw), - list: Endpoint12_4(raw), - get: Endpoint12_5(raw), - reply: Endpoint12_6(raw), +const adaptGroup14 = (raw: RawClient["server.permission"]) => ({ + listRequests: Endpoint14_0(raw), + listSaved: Endpoint14_1(raw), + removeSaved: Endpoint14_2(raw), + create: Endpoint14_3(raw), + list: Endpoint14_4(raw), + get: Endpoint14_5(raw), + reply: Endpoint14_6(raw), }) -type Endpoint13_0Request = Parameters[0] -type Endpoint13_0Input = { - readonly location?: Endpoint13_0Request["query"]["location"] - readonly path?: Endpoint13_0Request["query"]["path"] +type Endpoint15_0Request = Parameters[0] +type Endpoint15_0Input = { + readonly location?: Endpoint15_0Request["query"]["location"] + readonly path?: Endpoint15_0Request["query"]["path"] } -const Endpoint13_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint13_0Input) => +const Endpoint15_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint15_0Input) => raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint13_1Request = Parameters[0] -type Endpoint13_1Input = { - readonly location?: Endpoint13_1Request["query"]["location"] - readonly query: Endpoint13_1Request["query"]["query"] - readonly type?: Endpoint13_1Request["query"]["type"] - readonly limit?: Endpoint13_1Request["query"]["limit"] +type Endpoint15_1Request = Parameters[0] +type Endpoint15_1Input = { + readonly location?: Endpoint15_1Request["query"]["location"] + readonly query: Endpoint15_1Request["query"]["query"] + readonly type?: Endpoint15_1Request["query"]["type"] + readonly limit?: Endpoint15_1Request["query"]["limit"] } -const Endpoint13_1 = (raw: RawClient["server.fs"]) => (input: Endpoint13_1Input) => +const Endpoint15_1 = (raw: RawClient["server.fs"]) => (input: Endpoint15_1Input) => raw["fs.find"]({ query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup13 = (raw: RawClient["server.fs"]) => ({ list: Endpoint13_0(raw), find: Endpoint13_1(raw) }) +const adaptGroup15 = (raw: RawClient["server.fs"]) => ({ list: Endpoint15_0(raw), find: Endpoint15_1(raw) }) -type Endpoint14_0Request = Parameters[0] -type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } -const Endpoint14_0 = (raw: RawClient["server.command"]) => (input?: Endpoint14_0Input) => +type Endpoint16_0Request = Parameters[0] +type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] } +const Endpoint16_0 = (raw: RawClient["server.command"]) => (input?: Endpoint16_0Input) => raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -const adaptGroup14 = (raw: RawClient["server.command"]) => ({ list: Endpoint14_0(raw) }) +const adaptGroup16 = (raw: RawClient["server.command"]) => ({ list: Endpoint16_0(raw) }) -type Endpoint15_0Request = Parameters[0] -type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } -const Endpoint15_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint15_0Input) => +type Endpoint17_0Request = Parameters[0] +type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } +const Endpoint17_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint17_0Input) => raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -const adaptGroup15 = (raw: RawClient["server.skill"]) => ({ list: Endpoint15_0(raw) }) +const adaptGroup17 = (raw: RawClient["server.skill"]) => ({ list: Endpoint17_0(raw) }) -const Endpoint16_0 = (raw: RawClient["server.event"]) => () => +const Endpoint18_0 = (raw: RawClient["server.event"]) => () => Stream.unwrap( raw["event.subscribe"]({}).pipe( Effect.mapError(mapClientError), @@ -587,23 +636,23 @@ const Endpoint16_0 = (raw: RawClient["server.event"]) => () => ), ) -const adaptGroup16 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint16_0(raw) }) +const adaptGroup18 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint18_0(raw) }) -type Endpoint17_0Request = Parameters[0] -type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } -const Endpoint17_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint17_0Input) => +type Endpoint19_0Request = Parameters[0] +type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] } +const Endpoint19_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint19_0Input) => raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint17_1Request = Parameters[0] -type Endpoint17_1Input = { - readonly location?: Endpoint17_1Request["query"]["location"] - readonly command?: Endpoint17_1Request["payload"]["command"] - readonly args?: Endpoint17_1Request["payload"]["args"] - readonly cwd?: Endpoint17_1Request["payload"]["cwd"] - readonly title?: Endpoint17_1Request["payload"]["title"] - readonly env?: Endpoint17_1Request["payload"]["env"] +type Endpoint19_1Request = Parameters[0] +type Endpoint19_1Input = { + readonly location?: Endpoint19_1Request["query"]["location"] + readonly command?: Endpoint19_1Request["payload"]["command"] + readonly args?: Endpoint19_1Request["payload"]["args"] + readonly cwd?: Endpoint19_1Request["payload"]["cwd"] + readonly title?: Endpoint19_1Request["payload"]["title"] + readonly env?: Endpoint19_1Request["payload"]["env"] } -const Endpoint17_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint17_1Input) => +const Endpoint19_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint19_1Input) => raw["pty.create"]({ query: { location: input?.["location"] }, payload: { @@ -615,206 +664,206 @@ const Endpoint17_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint17_1Inpu }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint17_2Request = Parameters[0] -type Endpoint17_2Input = { - readonly ptyID: Endpoint17_2Request["params"]["ptyID"] - readonly location?: Endpoint17_2Request["query"]["location"] +type Endpoint19_2Request = Parameters[0] +type Endpoint19_2Input = { + readonly ptyID: Endpoint19_2Request["params"]["ptyID"] + readonly location?: Endpoint19_2Request["query"]["location"] } -const Endpoint17_2 = (raw: RawClient["server.pty"]) => (input: Endpoint17_2Input) => +const Endpoint19_2 = (raw: RawClient["server.pty"]) => (input: Endpoint19_2Input) => raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint17_3Request = Parameters[0] -type Endpoint17_3Input = { - readonly ptyID: Endpoint17_3Request["params"]["ptyID"] - readonly location?: Endpoint17_3Request["query"]["location"] - readonly title?: Endpoint17_3Request["payload"]["title"] - readonly size?: Endpoint17_3Request["payload"]["size"] +type Endpoint19_3Request = Parameters[0] +type Endpoint19_3Input = { + readonly ptyID: Endpoint19_3Request["params"]["ptyID"] + readonly location?: Endpoint19_3Request["query"]["location"] + readonly title?: Endpoint19_3Request["payload"]["title"] + readonly size?: Endpoint19_3Request["payload"]["size"] } -const Endpoint17_3 = (raw: RawClient["server.pty"]) => (input: Endpoint17_3Input) => +const Endpoint19_3 = (raw: RawClient["server.pty"]) => (input: Endpoint19_3Input) => raw["pty.update"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] }, payload: { title: input["title"], size: input["size"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint17_4Request = Parameters[0] -type Endpoint17_4Input = { - readonly ptyID: Endpoint17_4Request["params"]["ptyID"] - readonly location?: Endpoint17_4Request["query"]["location"] +type Endpoint19_4Request = Parameters[0] +type Endpoint19_4Input = { + readonly ptyID: Endpoint19_4Request["params"]["ptyID"] + readonly location?: Endpoint19_4Request["query"]["location"] } -const Endpoint17_4 = (raw: RawClient["server.pty"]) => (input: Endpoint17_4Input) => +const Endpoint19_4 = (raw: RawClient["server.pty"]) => (input: Endpoint19_4Input) => raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ) -const adaptGroup17 = (raw: RawClient["server.pty"]) => ({ - list: Endpoint17_0(raw), - create: Endpoint17_1(raw), - get: Endpoint17_2(raw), - update: Endpoint17_3(raw), - remove: Endpoint17_4(raw), +const adaptGroup19 = (raw: RawClient["server.pty"]) => ({ + list: Endpoint19_0(raw), + create: Endpoint19_1(raw), + get: Endpoint19_2(raw), + update: Endpoint19_3(raw), + remove: Endpoint19_4(raw), }) -type Endpoint18_0Request = Parameters[0] -type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } -const Endpoint18_0 = (raw: RawClient["server.question"]) => (input?: Endpoint18_0Input) => +type Endpoint20_0Request = Parameters[0] +type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } +const Endpoint20_0 = (raw: RawClient["server.question"]) => (input?: Endpoint20_0Input) => raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint18_1Request = Parameters[0] -type Endpoint18_1Input = { readonly sessionID: Endpoint18_1Request["params"]["sessionID"] } -const Endpoint18_1 = (raw: RawClient["server.question"]) => (input: Endpoint18_1Input) => +type Endpoint20_1Request = Parameters[0] +type Endpoint20_1Input = { readonly sessionID: Endpoint20_1Request["params"]["sessionID"] } +const Endpoint20_1 = (raw: RawClient["server.question"]) => (input: Endpoint20_1Input) => raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint18_2Request = Parameters[0] -type Endpoint18_2Input = { - readonly sessionID: Endpoint18_2Request["params"]["sessionID"] - readonly requestID: Endpoint18_2Request["params"]["requestID"] - readonly answers: Endpoint18_2Request["payload"]["answers"] +type Endpoint20_2Request = Parameters[0] +type Endpoint20_2Input = { + readonly sessionID: Endpoint20_2Request["params"]["sessionID"] + readonly requestID: Endpoint20_2Request["params"]["requestID"] + readonly answers: Endpoint20_2Request["payload"]["answers"] } -const Endpoint18_2 = (raw: RawClient["server.question"]) => (input: Endpoint18_2Input) => +const Endpoint20_2 = (raw: RawClient["server.question"]) => (input: Endpoint20_2Input) => raw["session.question.reply"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] }, payload: { answers: input["answers"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint18_3Request = Parameters[0] -type Endpoint18_3Input = { - readonly sessionID: Endpoint18_3Request["params"]["sessionID"] - readonly requestID: Endpoint18_3Request["params"]["requestID"] +type Endpoint20_3Request = Parameters[0] +type Endpoint20_3Input = { + readonly sessionID: Endpoint20_3Request["params"]["sessionID"] + readonly requestID: Endpoint20_3Request["params"]["requestID"] } -const Endpoint18_3 = (raw: RawClient["server.question"]) => (input: Endpoint18_3Input) => +const Endpoint20_3 = (raw: RawClient["server.question"]) => (input: Endpoint20_3Input) => raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( Effect.mapError(mapClientError), ) -const adaptGroup18 = (raw: RawClient["server.question"]) => ({ - listRequests: Endpoint18_0(raw), - list: Endpoint18_1(raw), - reply: Endpoint18_2(raw), - reject: Endpoint18_3(raw), +const adaptGroup20 = (raw: RawClient["server.question"]) => ({ + listRequests: Endpoint20_0(raw), + list: Endpoint20_1(raw), + reply: Endpoint20_2(raw), + reject: Endpoint20_3(raw), }) -type Endpoint19_0Request = Parameters[0] -type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] } -const Endpoint19_0 = (raw: RawClient["server.browser"]) => (input?: Endpoint19_0Input) => +type Endpoint21_0Request = Parameters[0] +type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] } +const Endpoint21_0 = (raw: RawClient["server.browser"]) => (input?: Endpoint21_0Input) => raw["browser.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint19_1Request = Parameters[0] -type Endpoint19_1Input = { readonly sessionID: Endpoint19_1Request["params"]["sessionID"] } -const Endpoint19_1 = (raw: RawClient["server.browser"]) => (input: Endpoint19_1Input) => +type Endpoint21_1Request = Parameters[0] +type Endpoint21_1Input = { readonly sessionID: Endpoint21_1Request["params"]["sessionID"] } +const Endpoint21_1 = (raw: RawClient["server.browser"]) => (input: Endpoint21_1Input) => raw["session.browser.list"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint19_2Request = Parameters[0] -type Endpoint19_2Input = { - readonly sessionID: Endpoint19_2Request["params"]["sessionID"] - readonly requestID: Endpoint19_2Request["params"]["requestID"] - readonly value: Endpoint19_2Request["payload"]["value"] +type Endpoint21_2Request = Parameters[0] +type Endpoint21_2Input = { + readonly sessionID: Endpoint21_2Request["params"]["sessionID"] + readonly requestID: Endpoint21_2Request["params"]["requestID"] + readonly value: Endpoint21_2Request["payload"]["value"] } -const Endpoint19_2 = (raw: RawClient["server.browser"]) => (input: Endpoint19_2Input) => +const Endpoint21_2 = (raw: RawClient["server.browser"]) => (input: Endpoint21_2Input) => raw["session.browser.reply"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] }, payload: { value: input["value"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint19_3Request = Parameters[0] -type Endpoint19_3Input = { - readonly sessionID: Endpoint19_3Request["params"]["sessionID"] - readonly requestID: Endpoint19_3Request["params"]["requestID"] - readonly reason: Endpoint19_3Request["payload"]["reason"] +type Endpoint21_3Request = Parameters[0] +type Endpoint21_3Input = { + readonly sessionID: Endpoint21_3Request["params"]["sessionID"] + readonly requestID: Endpoint21_3Request["params"]["requestID"] + readonly reason: Endpoint21_3Request["payload"]["reason"] } -const Endpoint19_3 = (raw: RawClient["server.browser"]) => (input: Endpoint19_3Input) => +const Endpoint21_3 = (raw: RawClient["server.browser"]) => (input: Endpoint21_3Input) => raw["session.browser.refuse"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] }, payload: { reason: input["reason"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup19 = (raw: RawClient["server.browser"]) => ({ - listRequests: Endpoint19_0(raw), - list: Endpoint19_1(raw), - reply: Endpoint19_2(raw), - refuse: Endpoint19_3(raw), +const adaptGroup21 = (raw: RawClient["server.browser"]) => ({ + listRequests: Endpoint21_0(raw), + list: Endpoint21_1(raw), + reply: Endpoint21_2(raw), + refuse: Endpoint21_3(raw), }) -type Endpoint20_0Request = Parameters[0] -type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } -const Endpoint20_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint20_0Input) => +type Endpoint22_0Request = Parameters[0] +type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] } +const Endpoint22_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint22_0Input) => raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -const adaptGroup20 = (raw: RawClient["server.reference"]) => ({ list: Endpoint20_0(raw) }) +const adaptGroup22 = (raw: RawClient["server.reference"]) => ({ list: Endpoint22_0(raw) }) -type Endpoint21_0Request = Parameters[0] -type Endpoint21_0Input = { - readonly projectID: Endpoint21_0Request["params"]["projectID"] - readonly location?: Endpoint21_0Request["query"]["location"] - readonly strategy: Endpoint21_0Request["payload"]["strategy"] - readonly directory: Endpoint21_0Request["payload"]["directory"] - readonly name?: Endpoint21_0Request["payload"]["name"] +type Endpoint23_0Request = Parameters[0] +type Endpoint23_0Input = { + readonly projectID: Endpoint23_0Request["params"]["projectID"] + readonly location?: Endpoint23_0Request["query"]["location"] + readonly strategy: Endpoint23_0Request["payload"]["strategy"] + readonly directory: Endpoint23_0Request["payload"]["directory"] + readonly name?: Endpoint23_0Request["payload"]["name"] } -const Endpoint21_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint21_0Input) => +const Endpoint23_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint23_0Input) => raw["projectCopy.create"]({ params: { projectID: input["projectID"] }, query: { location: input["location"] }, payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint21_1Request = Parameters[0] -type Endpoint21_1Input = { - readonly projectID: Endpoint21_1Request["params"]["projectID"] - readonly location?: Endpoint21_1Request["query"]["location"] - readonly directory: Endpoint21_1Request["payload"]["directory"] - readonly force: Endpoint21_1Request["payload"]["force"] +type Endpoint23_1Request = Parameters[0] +type Endpoint23_1Input = { + readonly projectID: Endpoint23_1Request["params"]["projectID"] + readonly location?: Endpoint23_1Request["query"]["location"] + readonly directory: Endpoint23_1Request["payload"]["directory"] + readonly force: Endpoint23_1Request["payload"]["force"] } -const Endpoint21_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint21_1Input) => +const Endpoint23_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint23_1Input) => raw["projectCopy.remove"]({ params: { projectID: input["projectID"] }, query: { location: input["location"] }, payload: { directory: input["directory"], force: input["force"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint21_2Request = Parameters[0] -type Endpoint21_2Input = { - readonly projectID: Endpoint21_2Request["params"]["projectID"] - readonly location?: Endpoint21_2Request["query"]["location"] +type Endpoint23_2Request = Parameters[0] +type Endpoint23_2Input = { + readonly projectID: Endpoint23_2Request["params"]["projectID"] + readonly location?: Endpoint23_2Request["query"]["location"] } -const Endpoint21_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint21_2Input) => +const Endpoint23_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint23_2Input) => raw["projectCopy.refresh"]({ params: { projectID: input["projectID"] }, query: { location: input["location"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup21 = (raw: RawClient["server.projectCopy"]) => ({ - create: Endpoint21_0(raw), - remove: Endpoint21_1(raw), - refresh: Endpoint21_2(raw), +const adaptGroup23 = (raw: RawClient["server.projectCopy"]) => ({ + create: Endpoint23_0(raw), + remove: Endpoint23_1(raw), + refresh: Endpoint23_2(raw), }) -type Endpoint22_0Request = Parameters[0] -type Endpoint22_0Input = { - readonly model: Endpoint22_0Request["payload"]["model"] - readonly messages: Endpoint22_0Request["payload"]["messages"] - readonly tools?: Endpoint22_0Request["payload"]["tools"] - readonly tool_choice?: Endpoint22_0Request["payload"]["tool_choice"] - readonly stream?: Endpoint22_0Request["payload"]["stream"] - readonly max_tokens?: Endpoint22_0Request["payload"]["max_tokens"] - readonly max_completion_tokens?: Endpoint22_0Request["payload"]["max_completion_tokens"] - readonly temperature?: Endpoint22_0Request["payload"]["temperature"] - readonly top_p?: Endpoint22_0Request["payload"]["top_p"] - readonly stop?: Endpoint22_0Request["payload"]["stop"] - readonly seed?: Endpoint22_0Request["payload"]["seed"] - readonly frequency_penalty?: Endpoint22_0Request["payload"]["frequency_penalty"] - readonly presence_penalty?: Endpoint22_0Request["payload"]["presence_penalty"] - readonly reasoning_effort?: Endpoint22_0Request["payload"]["reasoning_effort"] - readonly user?: Endpoint22_0Request["payload"]["user"] -} -const Endpoint22_0 = (raw: RawClient["server.chat"]) => (input: Endpoint22_0Input) => +type Endpoint24_0Request = Parameters[0] +type Endpoint24_0Input = { + readonly model: Endpoint24_0Request["payload"]["model"] + readonly messages: Endpoint24_0Request["payload"]["messages"] + readonly tools?: Endpoint24_0Request["payload"]["tools"] + readonly tool_choice?: Endpoint24_0Request["payload"]["tool_choice"] + readonly stream?: Endpoint24_0Request["payload"]["stream"] + readonly max_tokens?: Endpoint24_0Request["payload"]["max_tokens"] + readonly max_completion_tokens?: Endpoint24_0Request["payload"]["max_completion_tokens"] + readonly temperature?: Endpoint24_0Request["payload"]["temperature"] + readonly top_p?: Endpoint24_0Request["payload"]["top_p"] + readonly stop?: Endpoint24_0Request["payload"]["stop"] + readonly seed?: Endpoint24_0Request["payload"]["seed"] + readonly frequency_penalty?: Endpoint24_0Request["payload"]["frequency_penalty"] + readonly presence_penalty?: Endpoint24_0Request["payload"]["presence_penalty"] + readonly reasoning_effort?: Endpoint24_0Request["payload"]["reasoning_effort"] + readonly user?: Endpoint24_0Request["payload"]["user"] +} +const Endpoint24_0 = (raw: RawClient["server.chat"]) => (input: Endpoint24_0Input) => raw["chat.completions"]({ payload: { model: input["model"], @@ -835,32 +884,34 @@ const Endpoint22_0 = (raw: RawClient["server.chat"]) => (input: Endpoint22_0Inpu }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup22 = (raw: RawClient["server.chat"]) => ({ completions: Endpoint22_0(raw) }) +const adaptGroup24 = (raw: RawClient["server.chat"]) => ({ completions: Endpoint24_0(raw) }) const adaptClient = (raw: RawClient) => ({ health: adaptGroup0(raw["server.health"]), "server.clients": adaptGroup1(raw["server.clients"]), - location: adaptGroup2(raw["server.location"]), - agents: adaptGroup3(raw["server.agent"]), - sessions: adaptGroup4(raw["server.session"]), - messages: adaptGroup5(raw["server.message"]), - models: adaptGroup6(raw["server.model"]), - providers: adaptGroup7(raw["server.provider"]), - "server.variant": adaptGroup8(raw["server.variant"]), - integrations: adaptGroup9(raw["server.integration"]), - credentials: adaptGroup10(raw["server.credential"]), - privacy: adaptGroup11(raw["server.privacy"]), - permissions: adaptGroup12(raw["server.permission"]), - files: adaptGroup13(raw["server.fs"]), - commands: adaptGroup14(raw["server.command"]), - skills: adaptGroup15(raw["server.skill"]), - events: adaptGroup16(raw["server.event"]), - ptys: adaptGroup17(raw["server.pty"]), - questions: adaptGroup18(raw["server.question"]), - browserRequests: adaptGroup19(raw["server.browser"]), - references: adaptGroup20(raw["server.reference"]), - projectCopies: adaptGroup21(raw["server.projectCopy"]), - "server.chat": adaptGroup22(raw["server.chat"]), + "server.channels": adaptGroup2(raw["server.channels"]), + "server.apps": adaptGroup3(raw["server.apps"]), + location: adaptGroup4(raw["server.location"]), + agents: adaptGroup5(raw["server.agent"]), + sessions: adaptGroup6(raw["server.session"]), + messages: adaptGroup7(raw["server.message"]), + models: adaptGroup8(raw["server.model"]), + providers: adaptGroup9(raw["server.provider"]), + "server.variant": adaptGroup10(raw["server.variant"]), + integrations: adaptGroup11(raw["server.integration"]), + credentials: adaptGroup12(raw["server.credential"]), + privacy: adaptGroup13(raw["server.privacy"]), + permissions: adaptGroup14(raw["server.permission"]), + files: adaptGroup15(raw["server.fs"]), + commands: adaptGroup16(raw["server.command"]), + skills: adaptGroup17(raw["server.skill"]), + events: adaptGroup18(raw["server.event"]), + ptys: adaptGroup19(raw["server.pty"]), + questions: adaptGroup20(raw["server.question"]), + browserRequests: adaptGroup21(raw["server.browser"]), + references: adaptGroup22(raw["server.reference"]), + projectCopies: adaptGroup23(raw["server.projectCopy"]), + "server.chat": adaptGroup24(raw["server.chat"]), }) export const make = (options?: { readonly baseUrl?: URL | string }) => diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts index 3ab8629ebd..538c7c726a 100644 --- a/packages/client/src/generated/client.ts +++ b/packages/client/src/generated/client.ts @@ -7,6 +7,15 @@ import type { ServerClientsHeartbeatOutput, ServerClientsDetachInput, ServerClientsDetachOutput, + ServerChannelsStatusOutput, + ServerChannelsReloadOutput, + ServerAppsListOutput, + ServerAppsRegisterInput, + ServerAppsRegisterOutput, + ServerAppsHeartbeatInput, + ServerAppsHeartbeatOutput, + ServerAppsUnregisterInput, + ServerAppsUnregisterOutput, LocationGetInput, LocationGetOutput, AgentsListInput, @@ -320,6 +329,65 @@ export function make(options: ClientOptions) { requestOptions, ), }, + "server.channels": { + status: (requestOptions?: RequestOptions) => + request( + { method: "GET", path: `/channels/status`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ), + reload: (requestOptions?: RequestOptions) => + request( + { method: "POST", path: `/channels/reload`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ), + }, + "server.apps": { + list: (requestOptions?: RequestOptions) => + request( + { method: "GET", path: `/v1/apps`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ), + register: (input: ServerAppsRegisterInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/v1/apps/register`, + body: { + name: input["name"], + mcp_url: input["mcp_url"], + pid: input["pid"], + version: input["version"], + tools: input["tools"], + }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + heartbeat: (input: ServerAppsHeartbeatInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/v1/apps/${encodeURIComponent(input.appID)}/heartbeat`, + successStatus: 200, + declaredStatuses: [404, 401, 400], + empty: false, + }, + requestOptions, + ), + unregister: (input: ServerAppsUnregisterInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/v1/apps/${encodeURIComponent(input.appID)}`, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + }, location: { get: (input?: LocationGetInput, requestOptions?: RequestOptions) => request( diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index 508ffa3dc0..c9a21957d7 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -29,6 +29,10 @@ export type ClientNotFoundError = { export const isClientNotFoundError = (value: unknown): value is ClientNotFoundError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ClientNotFoundError" +export type AppNotFoundError = { readonly _tag: "AppNotFoundError"; readonly appID: string; readonly message: string } +export const isAppNotFoundError = (value: unknown): value is AppNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "AppNotFoundError" + export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly message: string } export const isInvalidCursorError = (value: unknown): value is InvalidCursorError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError" @@ -159,6 +163,89 @@ export type ServerClientsDetachInput = { readonly clientID: { readonly clientID: export type ServerClientsDetachOutput = void +export type ServerChannelsStatusOutput = { + readonly channels: ReadonlyArray<{ + readonly name: string + readonly enabled: boolean + readonly connected: boolean + readonly available: boolean + readonly lastError?: string | undefined + }> +} + +export type ServerChannelsReloadOutput = { readonly running: ReadonlyArray } + +export type ServerAppsListOutput = { + readonly apps: ReadonlyArray<{ + readonly id: string + readonly name: string + readonly mcpUrl: string + readonly pid: number + readonly version?: string | undefined + readonly tools?: ReadonlyArray | undefined + readonly registeredAt: number + readonly lastSeen: number + }> + readonly ttlMs: number +} + +export type ServerAppsRegisterInput = { + readonly name: { + readonly name: string + readonly mcp_url: string + readonly pid: number + readonly version?: string | undefined + readonly tools?: ReadonlyArray | undefined + }["name"] + readonly mcp_url: { + readonly name: string + readonly mcp_url: string + readonly pid: number + readonly version?: string | undefined + readonly tools?: ReadonlyArray | undefined + }["mcp_url"] + readonly pid: { + readonly name: string + readonly mcp_url: string + readonly pid: number + readonly version?: string | undefined + readonly tools?: ReadonlyArray | undefined + }["pid"] + readonly version?: { + readonly name: string + readonly mcp_url: string + readonly pid: number + readonly version?: string | undefined + readonly tools?: ReadonlyArray | undefined + }["version"] + readonly tools?: { + readonly name: string + readonly mcp_url: string + readonly pid: number + readonly version?: string | undefined + readonly tools?: ReadonlyArray | undefined + }["tools"] +} + +export type ServerAppsRegisterOutput = { readonly id: string; readonly ttlMs: number } + +export type ServerAppsHeartbeatInput = { readonly appID: { readonly appID: string }["appID"] } + +export type ServerAppsHeartbeatOutput = { + readonly id: string + readonly name: string + readonly mcpUrl: string + readonly pid: number + readonly version?: string | undefined + readonly tools?: ReadonlyArray | undefined + readonly registeredAt: number + readonly lastSeen: number +} + +export type ServerAppsUnregisterInput = { readonly appID: { readonly appID: string }["appID"] } + +export type ServerAppsUnregisterOutput = void + export type LocationGetInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined diff --git a/packages/redrob/test/server/httpapi-exercise/index.ts b/packages/redrob/test/server/httpapi-exercise/index.ts index 3e22c0f895..ddd158dc2b 100644 --- a/packages/redrob/test/server/httpapi-exercise/index.ts +++ b/packages/redrob/test/server/httpapi-exercise/index.ts @@ -796,6 +796,43 @@ const scenarios: Scenario[] = [ .seeded(() => Effect.sync(() => EngineClients.attach({ app: "httpapi-detach" }))) .at((ctx) => ({ path: route("/api/clients/{clientID}", { clientID: ctx.state.id }), headers: ctx.headers() })) .status(204, undefined, "status"), + // App bridge. Registering a real loopback MCP server would make the engine connect to it, so + // the write path is exercised through its 400 (non-loopback URL); the id paths use a missing id. + http.protected.get("/v1/apps", "v1.apps.list").json(200, (body) => { + object(body) + check(Array.isArray(body.apps), "apps should be a list") + check(typeof body.ttlMs === "number", "the heartbeat deadline should be reported") + }), + http.protected + .post("/v1/apps/register", "v1.apps.register.invalid") + .at((ctx) => ({ + path: "/v1/apps/register", + headers: ctx.headers(), + body: { name: "httpapi", mcp_url: "http://example.com:1234/mcp", pid: 4242 }, + })) + .json(400, object, "status"), + http.protected + .post("/v1/apps/{appID}/heartbeat", "v1.apps.heartbeat.missing") + .at((ctx) => ({ path: route("/v1/apps/{appID}/heartbeat", { appID: "httpapi-missing" }), headers: ctx.headers() })) + .json(404, object, "status"), + http.protected + .delete("/v1/apps/{appID}", "v1.apps.unregister.missing") + .mutating() + .at((ctx) => ({ path: route("/v1/apps/{appID}", { appID: "httpapi-missing" }), headers: ctx.headers() })) + .status(204, undefined, "status"), + // Chat channels. No channels.json in the exerciser environment, so nothing is enabled and a + // reload leaves nothing running. + http.protected.get("/channels/status", "v2.channels.status").json(200, (body) => { + object(body) + check(Array.isArray(body.channels), "channels should be a list") + }), + http.protected + .post("/channels/reload", "v2.channels.reload") + .mutating() + .json(200, (body) => { + object(body) + check(Array.isArray(body.running), "running channels should be a list") + }), http.protected.get("/api/privacy", "v2.privacy.get").json(200, (body) => { object(body) check(body.mode === "off", "masking should be off until someone turns it on")