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.
This commit is contained in:
Gagancreates 2026-06-13 01:23:03 +05:30
parent 3e2ffa9eb0
commit 2554a9b8da
10 changed files with 742 additions and 108 deletions

View file

@ -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)`);

View file

@ -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",

View file

@ -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<string, unknown>;
@ -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<ISlackConfigRepo>('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<string, unknown>;
@ -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<string, unknown>;
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<string, unknown>;
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),
});
}
}

View file

@ -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();

View file

@ -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<typeof BuiltinToolsSchema> = {
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<typeof BuiltinToolsSchema> = {
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<typeof BuiltinToolsSchema> = {
}
// 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,

View file

@ -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<unknown> {
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<SlackMessage[]> {

View file

@ -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'] });
});
});

View file

@ -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>`. */
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<AgentSlackResult> {
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<AgentSlackCliStatus> {
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] ?? ''}` };
}

View file

@ -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({

204
apps/x/pnpm-lock.yaml generated
View file

@ -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: {}