From 2554a9b8da497dbf272cc11175524a2c92a895b3 Mon Sep 17 00:00:00 2001 From: Gagancreates Date: Sat, 13 Jun 2026 01:23:03 +0530 Subject: [PATCH] feat: bundle agent-slack CLI and route all calls through shared executor Pins agent-slack@0.9.3, bundles it next to main.cjs (replaces the startup npm install -g), adds a structured-result executor with bundled/global/PATH resolution, a slack:cliStatus IPC probe, and a PATH shim so the Copilot skill keeps working. --- apps/x/apps/main/bundle.mjs | 30 +- apps/x/apps/main/package.json | 1 + apps/x/apps/main/src/ipc.ts | 148 +++++----- apps/x/apps/main/src/main.ts | 25 +- .../core/src/application/lib/builtin-tools.ts | 7 +- .../core/src/knowledge/sources/sync_slack.ts | 21 +- .../core/src/slack/agent-slack-exec.test.ts | 139 +++++++++ .../core/src/slack/agent-slack-exec.ts | 267 ++++++++++++++++++ apps/x/packages/shared/src/ipc.ts | 8 + apps/x/pnpm-lock.yaml | 204 +++++++++++++ 10 files changed, 742 insertions(+), 108 deletions(-) create mode 100644 apps/x/packages/core/src/slack/agent-slack-exec.test.ts create mode 100644 apps/x/packages/core/src/slack/agent-slack-exec.ts diff --git a/apps/x/apps/main/bundle.mjs b/apps/x/apps/main/bundle.mjs index 976e8db3..2adf3938 100644 --- a/apps/x/apps/main/bundle.mjs +++ b/apps/x/apps/main/bundle.mjs @@ -42,4 +42,32 @@ await esbuild.build({ }, }); -console.log('✅ Main process bundled to .package/dist-bundle/main.js'); +// Bundle the vendored agent-slack CLI into a single self-contained script next +// to main.cjs. It runs as a child process (process.execPath with +// ELECTRON_RUN_AS_NODE=1), so it must exist as a real file on disk — it can't +// be inlined into main.cjs. Bundling here means the packaged app needs neither +// node_modules nor a global npm install. +const agentSlackPkg = JSON.parse( + await readFile(new URL('./node_modules/agent-slack/package.json', import.meta.url), 'utf8'), +); +await esbuild.build({ + entryPoints: ['./node_modules/agent-slack/dist/index.js'], + bundle: true, + platform: 'node', + target: 'node22', + outfile: './.package/dist/agent-slack.cjs', + format: 'cjs', + banner: { js: cjsBanner }, + define: { + 'import.meta.url': '__import_meta_url', + // Without this constant the CLI's --version walks up the directory tree + // for a package.json and would find Rowboat's instead of agent-slack's. + 'AGENT_SLACK_BUILD_VERSION': JSON.stringify(agentSlackPkg.version), + }, + // The CLI probes bun:sqlite via dynamic import inside a try/catch and falls + // back to node:sqlite; keep it external so the probe fails at runtime the + // same way it does under plain node. + external: ['bun:sqlite'], +}); + +console.log(`✅ Main process bundled to .package/dist/main.cjs (+ agent-slack ${agentSlackPkg.version} CLI)`); diff --git a/apps/x/apps/main/package.json b/apps/x/apps/main/package.json index b6edd064..cbee1d3e 100644 --- a/apps/x/apps/main/package.json +++ b/apps/x/apps/main/package.json @@ -17,6 +17,7 @@ "@agentclientprotocol/codex-acp": "^0.0.44", "@x/core": "workspace:*", "@x/shared": "workspace:*", + "agent-slack": "0.9.3", "chokidar": "^4.0.3", "electron-squirrel-startup": "^1.0.1", "html-to-docx": "^1.8.0", diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index 511df3a7..fb688ae4 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -16,12 +16,8 @@ import { bus } from '@x/core/dist/runs/bus.js'; import { serviceBus } from '@x/core/dist/services/service_bus.js'; import type { FSWatcher } from 'chokidar'; import fs from 'node:fs/promises'; -import { exec, execFile } from 'node:child_process'; -import { promisify } from 'node:util'; import z from 'zod'; -const execAsync = promisify(exec); -const execFileAsync = promisify(execFile); import { RunEvent } from '@x/shared/dist/runs.js'; import { ServiceEvent } from '@x/shared/dist/service-events.js'; import container from '@x/core/dist/di/container.js'; @@ -38,6 +34,7 @@ import { checkCodeModeAgentStatus } from '@x/core/dist/code-mode/status.js'; import { invalidateCopilotInstructionsCache } from '@x/core/dist/application/assistant/instructions.js'; import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js'; import { ISlackConfigRepo } from '@x/core/dist/slack/repo.js'; +import { runAgentSlack, getAgentSlackCliStatus } from '@x/core/dist/slack/agent-slack-exec.js'; import { knowledgeSourcesRepo } from '@x/core/dist/knowledge/sources/repo.js'; import { rankSlackHomeMessages } from '@x/core/dist/knowledge/sources/rank_slack_home.js'; import { syncSlackKnowledgeSources, triggerSync as triggerSlackKnowledgeSync } from '@x/core/dist/knowledge/sources/sync_slack.js'; @@ -100,8 +97,7 @@ type SlackHomeMessage = { url?: string; }; -function parseJsonArrayPayload(stdout: string): unknown[] { - const parsed = JSON.parse(stdout || '[]'); +function extractArrayPayload(parsed: unknown): unknown[] { if (Array.isArray(parsed)) return parsed; if (parsed && typeof parsed === 'object') { const obj = parsed as Record; @@ -168,16 +164,15 @@ async function resolveSlackUserName( args.push('--workspace', workspaceUrl); } - try { - const { stdout } = await execFileAsync('agent-slack', args, { timeout: 10000, maxBuffer: 512 * 1024 }); - const parsed = JSON.parse(stdout || '{}'); - const name = extractSlackUserName(parsed); + const result = await runAgentSlack(args, { timeoutMs: 10000, maxBuffer: 512 * 1024 }); + if (result.ok) { + const name = extractSlackUserName(result.data ?? {}); if (name) { cache.set(key, name); return name; } - } catch (error) { - console.warn(`[Slack] Failed to resolve user ${userId}:`, error); + } else { + console.warn(`[Slack] Failed to resolve user ${userId}: ${result.message}`); } cache.set(key, userId); @@ -844,43 +839,41 @@ export function setupIpcHandlers() { await repo.setConfig({ enabled: args.enabled, workspaces: args.workspaces }); return { success: true }; }, + 'slack:cliStatus': async () => { + return await getAgentSlackCliStatus(); + }, 'slack:listWorkspaces': async () => { - try { - const { stdout } = await execAsync('agent-slack auth whoami', { timeout: 10000 }); - const parsed = JSON.parse(stdout); - const workspaces = (parsed.workspaces || []).map((w: { workspace_url?: string; workspace_name?: string }) => ({ - url: w.workspace_url || '', - name: w.workspace_name || '', - })); - return { workspaces }; - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to list Slack workspaces'; - return { workspaces: [], error: message }; + const result = await runAgentSlack(['auth', 'whoami'], { timeoutMs: 10000 }); + if (!result.ok) { + return { workspaces: [], error: result.message }; } + const parsed = (result.data ?? {}) as { workspaces?: Array<{ workspace_url?: string; workspace_name?: string }> }; + const workspaces = (parsed.workspaces || []).map((w) => ({ + url: w.workspace_url || '', + name: w.workspace_name || '', + })); + return { workspaces }; }, 'slack:listChannels': async (_event, args) => { - try { - const { stdout } = await execFileAsync('agent-slack', ['channel', 'list', '--all', '--workspace', args.workspaceUrl, '--limit', '200'], { timeout: 15000 }); - const parsed = JSON.parse(stdout); - const rawChannels = Array.isArray(parsed) ? parsed : (parsed.channels || parsed.items || parsed.results || []); - const channels = rawChannels.map((ch: { - id?: string; - name?: string; - is_private?: boolean; - isPrivate?: boolean; - is_member?: boolean; - isMember?: boolean; - }) => ({ - id: ch.id || ch.name || '', - name: ch.name || ch.id || '', - isPrivate: ch.is_private ?? ch.isPrivate, - isMember: ch.is_member ?? ch.isMember, - })).filter((ch: { id: string; name: string }) => ch.id && ch.name); - return { channels }; - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to list Slack channels'; - return { channels: [], error: message }; + const result = await runAgentSlack(['channel', 'list', '--all', '--workspace', args.workspaceUrl, '--limit', '200'], { timeoutMs: 15000 }); + if (!result.ok) { + return { channels: [], error: result.message }; } + const rawChannels = extractArrayPayload(result.data) as Array<{ + id?: string; + name?: string; + is_private?: boolean; + isPrivate?: boolean; + is_member?: boolean; + isMember?: boolean; + }>; + const channels = rawChannels.map((ch) => ({ + id: ch.id || ch.name || '', + name: ch.name || ch.id || '', + isPrivate: ch.is_private ?? ch.isPrivate, + isMember: ch.is_member ?? ch.isMember, + })).filter((ch) => ch.id && ch.name); + return { channels }; }, 'slack:getRecentMessages': async (_event, args) => { const repo = container.resolve('slackConfigRepo'); @@ -907,8 +900,11 @@ export function setupIpcHandlers() { if (channels.length === 0) { for (const workspace of config.workspaces) { - const { stdout } = await execFileAsync('agent-slack', ['channel', 'list', '--workspace', workspace.url, '--limit', '12'], { timeout: 15000 }); - const rawChannels = parseJsonArrayPayload(stdout); + const channelList = await runAgentSlack(['channel', 'list', '--workspace', workspace.url, '--limit', '12'], { timeoutMs: 15000 }); + if (!channelList.ok) { + throw new Error(channelList.message); + } + const rawChannels = extractArrayPayload(channelList.data); for (const raw of rawChannels) { if (!raw || typeof raw !== 'object') continue; const channel = raw as Record; @@ -928,36 +924,36 @@ export function setupIpcHandlers() { if (channel.workspaceUrl) { commandArgs.push('--workspace', channel.workspaceUrl); } - try { - const { stdout } = await execFileAsync('agent-slack', commandArgs, { timeout: 15000, maxBuffer: 1024 * 1024 }); - const rawMessages = parseJsonArrayPayload(stdout); - for (const raw of rawMessages) { - if (!raw || typeof raw !== 'object') continue; - const message = raw as Record; - const ts = typeof message.ts === 'string' ? message.ts : undefined; - const text = slackMessageText(message); - if (!ts || !text) continue; - const channelId = typeof message.channel_id === 'string' - ? message.channel_id - : typeof message.channel === 'string' - ? message.channel - : channel.id; - const resolvedAuthor = await resolveSlackAuthor(slackMessageAuthor(message), channel.workspaceUrl, userNameCache); - const resolvedText = await resolveSlackMessageText(text, channel.workspaceUrl, userNameCache); - messages.push({ - id: `${channel.workspaceUrl ?? 'workspace'}:${channelId}:${ts}`, - workspaceName: channel.workspaceName, - workspaceUrl: channel.workspaceUrl, - channelId, - channelName: channel.name, - author: resolvedAuthor, - text: resolvedText, - ts, - url: slackMessageUrl(message, channel.workspaceUrl, channelId, ts), - }); - } - } catch (error) { - console.warn(`[Slack] Failed to load messages for ${channel.name}:`, error); + const messageList = await runAgentSlack(commandArgs, { timeoutMs: 15000, maxBuffer: 1024 * 1024 }); + if (!messageList.ok) { + console.warn(`[Slack] Failed to load messages for ${channel.name}: ${messageList.message}`); + continue; + } + const rawMessages = extractArrayPayload(messageList.data); + for (const raw of rawMessages) { + if (!raw || typeof raw !== 'object') continue; + const message = raw as Record; + const ts = typeof message.ts === 'string' ? message.ts : undefined; + const text = slackMessageText(message); + if (!ts || !text) continue; + const channelId = typeof message.channel_id === 'string' + ? message.channel_id + : typeof message.channel === 'string' + ? message.channel + : channel.id; + const resolvedAuthor = await resolveSlackAuthor(slackMessageAuthor(message), channel.workspaceUrl, userNameCache); + const resolvedText = await resolveSlackMessageText(text, channel.workspaceUrl, userNameCache); + messages.push({ + id: `${channel.workspaceUrl ?? 'workspace'}:${channelId}:${ts}`, + workspaceName: channel.workspaceName, + workspaceUrl: channel.workspaceUrl, + channelId, + channelName: channel.name, + author: resolvedAuthor, + text: resolvedText, + ts, + url: slackMessageUrl(message, channel.workspaceUrl, channelId, ts), + }); } } diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index 40e49e35..1dc72ec0 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -35,10 +35,10 @@ import { shutdown as shutdownAnalytics } from "@x/core/dist/analytics/posthog.js import { identifyIfSignedIn } from "@x/core/dist/analytics/identify.js"; import { initConfigs } from "@x/core/dist/config/initConfigs.js"; +import { getAgentSlackCliStatus } from "@x/core/dist/slack/agent-slack-exec.js"; import { resolveWorkspacePath } from "@x/core/dist/workspace/workspace.js"; import started from "electron-squirrel-startup"; -import { execSync, exec, execFileSync } from "node:child_process"; -import { promisify } from "node:util"; +import { execFileSync } from "node:child_process"; import { init as initChromeSync } from "@x/core/dist/knowledge/chrome-extension/server/server.js"; import container, { registerBrowserControlService, registerNotificationService } from "@x/core/dist/di/container.js"; import type { CodeModeManager } from "@x/core/dist/code-mode/acp/manager.js"; @@ -54,8 +54,6 @@ import { } from "./deeplink.js"; import { disconnectGoogleIfScopesStale } from "./oauth-handler.js"; -const execAsync = promisify(exec); - const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -311,18 +309,13 @@ app.whenReady().then(async () => { }); } - // Ensure agent-slack CLI is available - try { - execSync('agent-slack --version', { stdio: 'ignore', timeout: 5000 }); - } catch { - try { - console.log('agent-slack not found, installing...'); - await execAsync('npm install -g agent-slack', { timeout: 60000 }); - console.log('agent-slack installed successfully'); - } catch (e) { - console.error('Failed to install agent-slack:', e); - } - } + // The agent-slack CLI ships bundled with the app (.package/dist/agent-slack.cjs) + // and is resolved per call by the shared executor in @x/core. Availability is + // exposed to the UI via the slack:cliStatus IPC channel; this startup log is + // diagnostics only. + getAgentSlackCliStatus().then((status) => { + console.log('[Slack] agent-slack CLI status:', status); + }).catch(() => { /* probe failures already surface through slack:cliStatus */ }); // Initialize all config files before UI can access them await initConfigs(); diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index 08e8334f..1d2f3cc5 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -2,6 +2,7 @@ import { z, ZodType } from "zod"; import * as path from "path"; import * as fs from "fs/promises"; import { executeCommand, executeCommandAbortable } from "./command-executor.js"; +import { agentSlackShimEnv } from "../../slack/agent-slack-exec.js"; import { resolveSkill, availableSkills } from "../assistant/skills/index.js"; import { executeTool, listServers, listTools } from "../../mcp/mcp.js"; import container from "../../di/container.js"; @@ -740,6 +741,9 @@ export const BuiltinTools: z.infer = { try { const rootDir = path.resolve(WorkDir); const workingDir = cwd ? path.resolve(rootDir, cwd) : rootDir; + // Make `agent-slack` resolvable for skill-authored shell + // commands; the shim forwards to the bundled CLI. + const env = agentSlackShimEnv(path.join(rootDir, 'bin')); // TODO: Re-enable this check // const rootPrefix = rootDir.endsWith(path.sep) @@ -758,6 +762,7 @@ export const BuiltinTools: z.infer = { if (ctx?.signal) { const { promise, process: proc } = executeCommandAbortable(command, { cwd: workingDir, + env, signal: ctx.signal, onData: (chunk: string) => { ctx.publish({ @@ -788,7 +793,7 @@ export const BuiltinTools: z.infer = { } // Fallback to original for backward compatibility - const result = await executeCommand(command, { cwd: workingDir }); + const result = await executeCommand(command, { cwd: workingDir, env }); return { success: result.exitCode === 0, diff --git a/apps/x/packages/core/src/knowledge/sources/sync_slack.ts b/apps/x/packages/core/src/knowledge/sources/sync_slack.ts index 9299a3f5..77286b00 100644 --- a/apps/x/packages/core/src/knowledge/sources/sync_slack.ts +++ b/apps/x/packages/core/src/knowledge/sources/sync_slack.ts @@ -1,15 +1,13 @@ import fs from 'fs'; import path from 'path'; -import { promisify } from 'util'; -import { execFile } from 'child_process'; import { WorkDir } from '../../config/config.js'; +import { runAgentSlack as execAgentSlack } from '../../slack/agent-slack-exec.js'; import { serviceLogger } from '../../services/service_logger.js'; import { limitEventItems } from '../limit_event_items.js'; import { createEvent } from '../../events/producer.js'; import { knowledgeSourcesRepo } from './repo.js'; import type { KnowledgeArtifact, KnowledgeSourceConfig, KnowledgeSourceScope } from './types.js'; -const execFileAsync = promisify(execFile); const DEFAULT_LIMIT = 100; const DEFAULT_SYNC_INTERVAL_MS = 5 * 60 * 1000; const DEFAULT_RECENT_BACKFILL_SECONDS = 6 * 60 * 60; @@ -97,12 +95,6 @@ function compareSlackTs(a: string | undefined, b: string | undefined): number { return an - bn; } -function parseJsonOutput(stdout: string): unknown { - const trimmed = stdout.trim(); - if (!trimmed) return []; - return JSON.parse(trimmed); -} - function extractMessages(raw: unknown): SlackMessage[] { if (Array.isArray(raw)) return raw as SlackMessage[]; if (raw && typeof raw === 'object') { @@ -124,11 +116,12 @@ function getMessageAuthor(message: SlackMessage): string { } async function runAgentSlack(args: string[]): Promise { - const { stdout } = await execFileAsync('agent-slack', args, { - timeout: 30_000, - maxBuffer: 2 * 1024 * 1024, - }); - return parseJsonOutput(stdout); + const result = await execAgentSlack(args, { timeoutMs: 30_000, maxBuffer: 2 * 1024 * 1024 }); + if (!result.ok) { + // Sync error handling stays throw-based for now; callers log per run. + throw new Error(`agent-slack ${result.kind}: ${result.message}`); + } + return result.data ?? []; } async function listMessages(source: KnowledgeSourceConfig, scope: KnowledgeSourceScope, oldest?: string): Promise { diff --git a/apps/x/packages/core/src/slack/agent-slack-exec.test.ts b/apps/x/packages/core/src/slack/agent-slack-exec.test.ts new file mode 100644 index 00000000..392e83cd --- /dev/null +++ b/apps/x/packages/core/src/slack/agent-slack-exec.test.ts @@ -0,0 +1,139 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { exec } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import { agentSlackShimEnv, resolveAgentSlackCli, runAgentSlack } from './agent-slack-exec.js'; + +const execAsync = promisify(exec); + +// Fixture CLI scripts spawned via process.execPath (real node under vitest), +// exercising the same spawn path the app uses. +let fixtureDir: string; +let jsonCli: string; +let garbageCli: string; +let sleepCli: string; +let failingCli: string; + +function writeFixture(name: string, code: string): string { + const file = path.join(fixtureDir, name); + fs.writeFileSync(file, code, 'utf-8'); + return file; +} + +beforeAll(() => { + fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-slack-exec-test-')); + jsonCli = writeFixture('json.cjs', `process.stdout.write(JSON.stringify({ args: process.argv.slice(2) }));`); + garbageCli = writeFixture('garbage.cjs', `process.stdout.write('definitely: not json');`); + sleepCli = writeFixture('sleep.cjs', `setTimeout(() => {}, 60_000);`); + failingCli = writeFixture('fail.cjs', `process.stderr.write('boom'); process.exit(2);`); +}); + +afterAll(() => { + fs.rmSync(fixtureDir, { recursive: true, force: true }); +}); + +const missing = path.join('/nonexistent', 'agent-slack.cjs'); + +describe('resolveAgentSlackCli', () => { + it('prefers the bundled bin over global and PATH', () => { + const resolved = resolveAgentSlackCli({ + bundledCandidates: [jsonCli], + globalCandidates: [garbageCli], + pathProbe: () => garbageCli, + }); + expect(resolved).toEqual({ entry: jsonCli, source: 'bundled' }); + }); + + it('falls back to a global install when the bundled bin is missing', () => { + const resolved = resolveAgentSlackCli({ + bundledCandidates: [missing], + globalCandidates: [jsonCli], + pathProbe: () => garbageCli, + }); + expect(resolved).toEqual({ entry: jsonCli, source: 'global' }); + }); + + it('falls back to PATH last', () => { + const resolved = resolveAgentSlackCli({ + bundledCandidates: [missing], + globalCandidates: [missing], + pathProbe: () => jsonCli, + }); + expect(resolved).toEqual({ entry: jsonCli, source: 'path' }); + }); + + it('returns null when nothing is found', () => { + const resolved = resolveAgentSlackCli({ + bundledCandidates: [missing], + globalCandidates: [missing], + pathProbe: () => null, + }); + expect(resolved).toBeNull(); + }); +}); + +describe('runAgentSlack', () => { + const via = (entry: string) => ({ + bundledCandidates: [entry], + globalCandidates: [], + pathProbe: () => null, + }); + + it('returns parsed JSON stdout and forwards args', async () => { + const result = await runAgentSlack(['auth', 'whoami'], { resolve: via(jsonCli) }); + expect(result).toMatchObject({ ok: true, data: { args: ['auth', 'whoami'] } }); + }); + + it('returns raw stdout when parseJson is false', async () => { + const result = await runAgentSlack([], { resolve: via(garbageCli), parseJson: false }); + expect(result.ok).toBe(true); + if (result.ok) expect(result.stdout).toBe('definitely: not json'); + }); + + it('reports not_installed when no binary resolves', async () => { + const result = await runAgentSlack(['--version'], { + resolve: { bundledCandidates: [missing], globalCandidates: [missing], pathProbe: () => null }, + }); + expect(result).toMatchObject({ ok: false, kind: 'not_installed' }); + }); + + it('reports parse_error on malformed JSON stdout', async () => { + const result = await runAgentSlack([], { resolve: via(garbageCli) }); + expect(result).toMatchObject({ ok: false, kind: 'parse_error' }); + }); + + it('kills a hung CLI and reports timeout', async () => { + const result = await runAgentSlack([], { resolve: via(sleepCli), timeoutMs: 300 }); + expect(result).toMatchObject({ ok: false, kind: 'timeout' }); + }, 10_000); + + it('reports exec_error with stderr on non-zero exit', async () => { + const result = await runAgentSlack([], { resolve: via(failingCli) }); + expect(result).toMatchObject({ ok: false, kind: 'exec_error', stderr: 'boom' }); + }); +}); + +describe('agentSlackShimEnv', () => { + it('returns the base env unchanged when no CLI resolves', () => { + const base = { PATH: '/usr/bin' }; + const env = agentSlackShimEnv(path.join(fixtureDir, 'bin'), base, { + bundledCandidates: [missing], globalCandidates: [missing], pathProbe: () => null, + }); + expect(env).toBe(base); + }); + + it('makes `agent-slack` runnable by name through a shell', async () => { + const shimDir = path.join(fixtureDir, 'bin'); + const env = agentSlackShimEnv(shimDir, process.env, { + bundledCandidates: [jsonCli], globalCandidates: [], pathProbe: () => null, + }); + const pathKey = Object.keys(env).find(key => key.toUpperCase() === 'PATH') ?? 'PATH'; + expect(env[pathKey]!.startsWith(`${shimDir}${path.delimiter}`)).toBe(true); + + // Same spawn shape as executeCommand: command string through a shell. + const { stdout } = await execAsync('agent-slack hello world', { env }); + expect(JSON.parse(stdout)).toEqual({ args: ['hello', 'world'] }); + }); +}); diff --git a/apps/x/packages/core/src/slack/agent-slack-exec.ts b/apps/x/packages/core/src/slack/agent-slack-exec.ts new file mode 100644 index 00000000..ca68117d --- /dev/null +++ b/apps/x/packages/core/src/slack/agent-slack-exec.ts @@ -0,0 +1,267 @@ +import { execFile, execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; + +const execFileAsync = promisify(execFile); + +/** + * Single shared executor for the agent-slack CLI. + * + * Every agent-slack invocation in the app must go through runAgentSlack() — + * never execFile('agent-slack', ...) directly. Spawning the bare command + * requires it on PATH (we no longer auto-install it) and on Windows hits the + * .cmd-shim EINVAL bug. Instead we resolve a JS entry file and spawn it with + * process.execPath, which works without Node/npm on the user's machine. + */ + +export type AgentSlackSource = 'bundled' | 'global' | 'path'; + +export interface ResolvedAgentSlack { + /** Absolute path to a JS entry file runnable via `node `. */ + entry: string; + source: AgentSlackSource; +} + +export type AgentSlackErrorKind = 'not_installed' | 'timeout' | 'parse_error' | 'exec_error'; + +export type AgentSlackResult = + | { ok: true; stdout: string; data: unknown } + | { ok: false; kind: AgentSlackErrorKind; message: string; stderr: string }; + +export interface ResolveOptions { + /** Re-probe even if a previous resolution succeeded. */ + refresh?: boolean; + /** Test hooks — override the default probe locations. */ + bundledCandidates?: string[]; + globalCandidates?: string[]; + pathProbe?: () => string | null; +} + +export interface RunAgentSlackOptions { + timeoutMs?: number; + maxBuffer?: number; + /** Set false for commands with non-JSON output (e.g. --version). */ + parseJson?: boolean; + /** Test hook — bypass the default resolver. */ + resolve?: ResolveOptions; +} + +const DEFAULT_TIMEOUT_MS = 30_000; +const DEFAULT_MAX_BUFFER = 2 * 1024 * 1024; + +// The CLI is bundled by apps/main/bundle.mjs to agent-slack.cjs next to +// main.cjs. At runtime import.meta.url is rewritten by esbuild to point at +// main.cjs, so the sibling lookup works in dev and packaged builds alike. +// (Under vitest/tsc output the sibling doesn't exist and we fall through.) +function defaultBundledCandidates(): string[] { + return [path.join(path.dirname(fileURLToPath(import.meta.url)), 'agent-slack.cjs')]; +} + +const GLOBAL_BIN_REL = path.join('node_modules', 'agent-slack', 'bin', 'agent-slack.js'); + +function defaultGlobalCandidates(): string[] { + if (process.platform === 'win32') { + const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming'); + return [path.join(appData, 'npm', GLOBAL_BIN_REL)]; + } + return [ + path.join('/usr/local/lib', GLOBAL_BIN_REL), + path.join('/opt/homebrew/lib', GLOBAL_BIN_REL), + ]; +} + +/** Map a PATH hit (symlink, npm .cmd/.ps1/sh shim) to the underlying JS bin. */ +function jsEntryFromPathHit(hit: string): string | null { + try { + const real = fs.realpathSync(hit); + if (/\.(c|m)?js$/.test(real)) return real; + // npm shims live next to the global node_modules tree. + const sibling = path.join(path.dirname(real), GLOBAL_BIN_REL); + if (fs.existsSync(sibling)) return sibling; + } catch { + // Broken symlink or unreadable shim — treat as no hit. + } + return null; +} + +function defaultPathProbe(): string | null { + const lookup = process.platform === 'win32' ? 'where.exe' : 'which'; + let output: string; + try { + output = execFileSync(lookup, ['agent-slack'], { + timeout: 5_000, + encoding: 'utf-8', + windowsHide: true, + }); + } catch { + return null; + } + for (const line of output.split(/\r?\n/)) { + const hit = line.trim(); + if (!hit) continue; + const entry = jsEntryFromPathHit(hit); + if (entry) return entry; + } + return null; +} + +let cachedResolution: ResolvedAgentSlack | null = null; + +export function resolveAgentSlackCli(opts: ResolveOptions = {}): ResolvedAgentSlack | null { + if (cachedResolution && !opts.refresh + && !opts.bundledCandidates && !opts.globalCandidates && !opts.pathProbe) { + return cachedResolution; + } + + let resolved: ResolvedAgentSlack | null = null; + for (const candidate of opts.bundledCandidates ?? defaultBundledCandidates()) { + if (fs.existsSync(candidate)) { + resolved = { entry: candidate, source: 'bundled' }; + break; + } + } + if (!resolved) { + for (const candidate of opts.globalCandidates ?? defaultGlobalCandidates()) { + if (fs.existsSync(candidate)) { + resolved = { entry: candidate, source: 'global' }; + break; + } + } + } + if (!resolved) { + const entry = (opts.pathProbe ?? defaultPathProbe)(); + if (entry) resolved = { entry, source: 'path' }; + } + + // Only cache the default probe — test overrides must not leak, and a + // failed probe should retry next call (the user may install meanwhile). + if (resolved && !opts.bundledCandidates && !opts.globalCandidates && !opts.pathProbe) { + cachedResolution = resolved; + } + return resolved; +} + +export async function runAgentSlack(args: string[], opts: RunAgentSlackOptions = {}): Promise { + const resolved = resolveAgentSlackCli(opts.resolve ?? {}); + if (!resolved) { + return { + ok: false, + kind: 'not_installed', + message: 'agent-slack CLI not found (bundled copy missing and no global install)', + stderr: '', + }; + } + + const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; + let stdout: string; + try { + // process.execPath inside Electron's main process is the Electron + // binary, not node — ELECTRON_RUN_AS_NODE makes it behave as plain + // node (and is ignored when we already run under real node). + const result = await execFileAsync(process.execPath, [resolved.entry, ...args], { + timeout, + maxBuffer: opts.maxBuffer ?? DEFAULT_MAX_BUFFER, + encoding: 'utf-8', + windowsHide: true, + env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, + }); + stdout = result.stdout; + } catch (error) { + const err = error as NodeJS.ErrnoException & { killed?: boolean; signal?: string; stderr?: string }; + const stderr = typeof err.stderr === 'string' ? err.stderr : ''; + if (err.code === 'ENOENT') { + return { ok: false, kind: 'not_installed', message: `agent-slack entry vanished: ${resolved.entry}`, stderr }; + } + if (err.killed || err.signal === 'SIGTERM') { + return { ok: false, kind: 'timeout', message: `agent-slack timed out after ${timeout}ms`, stderr }; + } + return { ok: false, kind: 'exec_error', message: err.message ?? 'agent-slack failed', stderr }; + } + + if (opts.parseJson === false) { + return { ok: true, stdout, data: undefined }; + } + const trimmed = stdout.trim(); + try { + return { ok: true, stdout, data: trimmed ? JSON.parse(trimmed) : undefined }; + } catch { + return { + ok: false, + kind: 'parse_error', + message: `agent-slack returned non-JSON output: ${trimmed.slice(0, 200)}`, + stderr: '', + }; + } +} + +export type AgentSlackCliStatus = + | { available: true; version: string; source: AgentSlackSource } + | { available: false }; + +/** Availability probe backing the slack:cliStatus IPC channel. */ +export async function getAgentSlackCliStatus(): Promise { + const resolved = resolveAgentSlackCli({ refresh: true }); + if (!resolved) return { available: false }; + const result = await runAgentSlack(['--version'], { timeoutMs: 10_000, parseJson: false }); + if (!result.ok) return { available: false }; + return { available: true, version: result.stdout.trim(), source: resolved.source }; +} + +// --- PATH shim for shell consumers (Copilot skill via executeCommand) ------- +// +// The Copilot Slack skill runs literal `agent-slack ...` shell commands. Those +// used to rely on the startup `npm install -g` that this module replaced, so +// without help they'd only work on machines with a manual global install. +// We generate a tiny launcher script that forwards to the resolved CLI entry +// and prepend its directory to PATH for executeCommand children. + +let shimmedFor: string | null = null; + +function ensureAgentSlackShim(shimDir: string, entry: string): void { + const cacheKey = `${process.execPath}${entry}${shimDir}`; + if (shimmedFor === cacheKey) return; + fs.mkdirSync(shimDir, { recursive: true }); + if (process.platform === 'win32') { + const cmd = `@echo off\r\nset ELECTRON_RUN_AS_NODE=1\r\n"${process.execPath}" "${entry}" %*\r\n`; + const cmdPath = path.join(shimDir, 'agent-slack.cmd'); + if (!fs.existsSync(cmdPath) || fs.readFileSync(cmdPath, 'utf-8') !== cmd) { + fs.writeFileSync(cmdPath, cmd, 'utf-8'); + } + } else { + const sh = `#!/bin/sh\nELECTRON_RUN_AS_NODE=1 exec "${process.execPath}" "${entry}" "$@"\n`; + const shPath = path.join(shimDir, 'agent-slack'); + if (!fs.existsSync(shPath) || fs.readFileSync(shPath, 'utf-8') !== sh) { + fs.writeFileSync(shPath, sh, { encoding: 'utf-8', mode: 0o755 }); + } + fs.chmodSync(shPath, 0o755); + } + shimmedFor = cacheKey; +} + +/** + * Environment for shell commands that may invoke `agent-slack` by name. + * Prepends a shim directory to PATH so the resolved CLI (bundled first) wins + * over — or substitutes for — a global npm install. Returns the base env + * unchanged when no CLI can be resolved. + */ +export function agentSlackShimEnv( + shimDir: string, + base: NodeJS.ProcessEnv = process.env, + resolve?: ResolveOptions, +): NodeJS.ProcessEnv { + const resolved = resolveAgentSlackCli(resolve ?? {}); + if (!resolved) return base; + try { + ensureAgentSlackShim(shimDir, resolved.entry); + } catch (error) { + console.warn('[Slack] Failed to write agent-slack PATH shim:', error); + return base; + } + // Windows env vars are case-insensitive; reuse the existing key ('Path') + // rather than introducing a duplicate 'PATH'. + const pathKey = Object.keys(base).find(key => key.toUpperCase() === 'PATH') ?? 'PATH'; + return { ...base, [pathKey]: `${shimDir}${path.delimiter}${base[pathKey] ?? ''}` }; +} diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 0bcb8e50..4a74dc81 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -519,6 +519,14 @@ const ipcSchemas = { success: z.literal(true), }), }, + 'slack:cliStatus': { + req: z.null(), + res: z.object({ + available: z.boolean(), + version: z.string().optional(), + source: z.enum(['bundled', 'global', 'path']).optional(), + }), + }, 'slack:listWorkspaces': { req: z.null(), res: z.object({ diff --git a/apps/x/pnpm-lock.yaml b/apps/x/pnpm-lock.yaml index 55ec19f2..055d8d09 100644 --- a/apps/x/pnpm-lock.yaml +++ b/apps/x/pnpm-lock.yaml @@ -64,6 +64,9 @@ importers: '@x/shared': specifier: workspace:* version: link:../../packages/shared + agent-slack: + specifier: 0.9.3 + version: 0.9.3 chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -1641,6 +1644,9 @@ packages: '@mermaid-js/parser@1.1.0': resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@modelcontextprotocol/sdk@1.25.1': resolution: {integrity: sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==} engines: {node: '>=18'} @@ -2984,6 +2990,18 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@slack/logger@4.0.1': + resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@slack/types@2.21.1': + resolution: {integrity: sha512-I8vmSjNYWsaxuWPx6dz4yeh0h7vRBWbgAMK14LEmblbZ404BtrPbXs6jDPx4cYgGf8msDGF4A9opLZBu21FViQ==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + + '@slack/web-api@7.17.0': + resolution: {integrity: sha512-jejr34a8B4L5AS713wOAx1LAqNkW16HVMDEa6sYBvFDc/llUBl8hXaiI4BwF+Al+Sug19Vn2O7iokTVIhVvZ1Q==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + '@smithy/abort-controller@4.2.8': resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==} engines: {node: '>=18.0.0'} @@ -3746,6 +3764,9 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -3983,6 +4004,11 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agent-slack@0.9.3: + resolution: {integrity: sha512-A9ts5J7RVUf3Oyja/sPxyr4oCxvJy66s0p9c1YeYmlKTqBsUoHRGcAM+198rH6DiYTLOOTIJbT/mL8Lo0bRlHg==} + engines: {node: '>=22.5'} + hasBin: true + agentkeepalive@4.6.0: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} @@ -4106,6 +4132,9 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axios@1.17.0: + resolution: {integrity: sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -4266,6 +4295,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -4391,6 +4424,10 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -4863,6 +4900,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + encode-utf8@1.0.3: resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} @@ -5043,6 +5083,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -5191,6 +5234,15 @@ packages: debug: optional: true + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + fontkit@2.0.4: resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} @@ -5582,6 +5634,9 @@ packages: hyphen@1.14.1: resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==} + hysnappy@1.1.1: + resolution: {integrity: sha512-/V9XcN2NtRyWjR4LYMfvnvasVVF8jbT/ej0eofBQjZel91E3D813FQ3mQC6gDSMMTCq/FJh28XHeyqr3I/oBRw==} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -5715,6 +5770,9 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -6536,6 +6594,10 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -6699,6 +6761,18 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + p-try@1.0.0: resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} engines: {node: '>=4'} @@ -6973,6 +7047,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -7253,6 +7331,10 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -7443,6 +7525,10 @@ packages: simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -7745,6 +7831,13 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turndown-plugin-gfm@1.0.2: + resolution: {integrity: sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==} + + turndown@7.2.4: + resolution: {integrity: sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==} + engines: {node: '>=18', npm: '>=9'} + tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} @@ -7807,6 +7900,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + unicode-properties@1.4.1: resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} @@ -8240,6 +8337,9 @@ packages: zod@4.2.1: resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -9911,6 +10011,8 @@ snapshots: dependencies: langium: 4.2.2 + '@mixmark-io/domino@2.2.0': {} + '@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1)': dependencies: '@hono/node-server': 1.19.7(hono@4.11.3) @@ -11280,6 +11382,30 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@slack/logger@4.0.1': + dependencies: + '@types/node': 25.0.3 + + '@slack/types@2.21.1': {} + + '@slack/web-api@7.17.0': + dependencies: + '@slack/logger': 4.0.1 + '@slack/types': 2.21.1 + '@types/node': 25.0.3 + '@types/retry': 0.12.0 + axios: 1.17.0 + eventemitter3: 5.0.1 + form-data: 4.0.5 + is-electron: 2.2.2 + is-stream: 2.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + retry: 0.13.1 + transitivePeerDependencies: + - debug + - supports-color + '@smithy/abort-controller@4.2.8': dependencies: '@smithy/types': 4.12.0 @@ -12211,6 +12337,8 @@ snapshots: dependencies: '@types/node': 25.0.3 + '@types/retry@0.12.0': {} + '@types/send@1.2.1': dependencies: '@types/node': 25.0.3 @@ -12510,6 +12638,19 @@ snapshots: agent-base@7.1.4: {} + agent-slack@0.9.3: + dependencies: + '@slack/web-api': 7.17.0 + commander: 14.0.3 + hysnappy: 1.1.1 + node-emoji: 2.2.0 + turndown: 7.2.4 + turndown-plugin-gfm: 1.0.2 + zod: 4.4.3 + transitivePeerDependencies: + - debug + - supports-color + agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 @@ -12637,6 +12778,16 @@ snapshots: transitivePeerDependencies: - debug + axios@1.17.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -12833,6 +12984,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -12950,6 +13103,8 @@ snapshots: commander@11.1.0: {} + commander@14.0.3: {} + commander@2.20.3: {} commander@5.1.0: {} @@ -13471,6 +13626,8 @@ snapshots: emoji-regex@9.2.2: {} + emojilib@2.4.0: {} + encode-utf8@1.0.3: optional: true @@ -13712,6 +13869,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} events@3.3.0: {} @@ -13891,6 +14050,8 @@ snapshots: follow-redirects@1.15.11: {} + follow-redirects@1.16.0: {} + fontkit@2.0.4: dependencies: '@swc/helpers': 0.5.18 @@ -14489,6 +14650,8 @@ snapshots: hyphen@1.14.1: {} + hysnappy@1.1.1: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -14586,6 +14749,8 @@ snapshots: is-docker@3.0.0: {} + is-electron@2.2.2: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -15635,6 +15800,13 @@ snapshots: node-domexception@1.0.0: {} + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -15798,6 +15970,20 @@ snapshots: dependencies: aggregate-error: 3.1.0 + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + p-try@1.0.0: {} package-json-from-dist@1.0.1: {} @@ -16096,6 +16282,8 @@ snapshots: proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} + pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -16477,6 +16665,8 @@ snapshots: retry@0.12.0: {} + retry@0.13.1: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -16725,6 +16915,10 @@ snapshots: dependencies: is-arrayish: 0.3.4 + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.3 @@ -17045,6 +17239,12 @@ snapshots: tslib@2.8.1: {} + turndown-plugin-gfm@1.0.2: {} + + turndown@7.2.4: + dependencies: + '@mixmark-io/domino': 2.2.0 + tw-animate-css@1.4.0: {} tweetnacl@1.0.3: {} @@ -17097,6 +17297,8 @@ snapshots: undici-types@7.16.0: {} + unicode-emoji-modifier-base@1.0.0: {} + unicode-properties@1.4.1: dependencies: base64-js: 1.5.1 @@ -17562,4 +17764,6 @@ snapshots: zod@4.2.1: {} + zod@4.4.3: {} + zwitch@2.0.4: {}