From 4a97f5e0ed39798d7d4650201007bc4f8497a78d Mon Sep 17 00:00:00 2001 From: Mitsuki Ogasahara Date: Fri, 10 Apr 2026 11:03:51 +0900 Subject: [PATCH 1/2] fix: pass channel_ids as comma-separated string in users invite SDK type definition expects [string, ...string[]] but the actual Slack API expects a comma-separated string. The SDK's JSON serialization of arrays produces invalid channel IDs, causing failed_to_send_invite errors. Also adds Slack API error detail display and updates CLAUDE.md type safety rules. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 3 ++- src/commands/users/invite.ts | 5 +++-- src/index.ts | 30 ++++++++++++++++++++++-------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index de925d1..6241c67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,8 @@ skills/ # Agent Skill 定義 ### 型安全性ルール -- **`as` キャスト禁止**: 実装コードで `as` を使わない。`satisfies` または設計の見直しで対応 +- **`as` キャスト原則禁止**: 実装コードで `as` を使わない。`satisfies` または設計の見直しで対応 +- **SDK 型バグの回避**: SDK の型定義が実際の API と異なる場合、`apiCall()` より `as` での回避を優先する - **discriminated union**: `client.apiCall(methodName, params)` で `Record` として渡す(void メソッド) - **データ返却メソッド**: ブランチパターンで型安全に(`if (opts.teamId) { ... } else if (opts.enterpriseId) { ... }`) diff --git a/src/commands/users/invite.ts b/src/commands/users/invite.ts index 0373eba..39d7f8b 100644 --- a/src/commands/users/invite.ts +++ b/src/commands/users/invite.ts @@ -3,7 +3,7 @@ import type { WebClient } from "@slack/web-api"; interface UsersInviteOptions { teamId: string; email: string; - channelIds: [string, ...string[]]; + channelIds: string; customMessage?: string; realName?: string; isRestricted?: boolean; @@ -13,10 +13,11 @@ interface UsersInviteOptions { } export async function executeUsersInvite(client: WebClient, opts: UsersInviteOptions) { + // SDK型定義は channel_ids: [string, ...string[]] だが、実際のAPIはカンマ区切り文字列を期待する return await client.admin.users.invite({ team_id: opts.teamId, email: opts.email, - channel_ids: opts.channelIds, + channel_ids: opts.channelIds as unknown as [string, ...string[]], ...(opts.customMessage !== undefined ? { custom_message: opts.customMessage } : {}), ...(opts.realName !== undefined ? { real_name: opts.realName } : {}), ...(opts.isRestricted !== undefined ? { is_restricted: opts.isRestricted } : {}), diff --git a/src/index.ts b/src/index.ts index b80f269..bde1b11 100755 --- a/src/index.ts +++ b/src/index.ts @@ -871,16 +871,10 @@ switch (config.cmd) { } case "users-invite": { const client = await createSlackClient(store, profileFlag); - const inviteChannelParts = config.channelIds.split(","); - const inviteFirstChannel = inviteChannelParts[0]; - if (inviteFirstChannel === undefined) { - throw new Error("--channel-ids must not be empty"); - } - const inviteChannelIds: [string, ...string[]] = [inviteFirstChannel, ...inviteChannelParts.slice(1)]; await executeUsersInvite(client, { teamId: config.teamId, email: config.email, - channelIds: inviteChannelIds, + channelIds: config.channelIds, customMessage: config.customMessage, realName: config.realName, isRestricted: config.isRestricted, @@ -1506,4 +1500,24 @@ switch (config.cmd) { } } -main(); +main().catch((err) => { + if (err.code === "slack_webapi_platform_error" && err.data) { + console.error(`Slack API error: ${err.data.error}`); + if (err.data.needed) { + console.error(` needed scope: ${err.data.needed}`); + } + if (err.data.provided) { + console.error(` provided scopes: ${err.data.provided}`); + } + if (err.data.response_metadata) { + const meta = err.data.response_metadata; + if (meta.messages) { + for (const msg of meta.messages) { + console.error(` ${msg}`); + } + } + } + process.exit(1); + } + throw err; +}); From 1f31acdb819809e8c4e10fb7954b15c5b61a563c Mon Sep 17 00:00:00 2001 From: Mitsuki Ogasahara Date: Fri, 10 Apr 2026 11:05:09 +0900 Subject: [PATCH 2/2] feat: add --verbose flag, suppress stack traces by default All errors now show a clean one-line message by default. Use --verbose to see full stack traces for debugging. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/index.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index bde1b11..c92aff4 100755 --- a/src/index.ts +++ b/src/index.ts @@ -129,17 +129,19 @@ const discoverabilityValueParser: ValueParser<"sync", TeamDiscoverability> = { // Global flags (parsed manually from process.argv) // --------------------------------------------------------------------------- -const GLOBAL_FLAGS = new Set(["--json", "--plain"]); +const GLOBAL_FLAGS = new Set(["--json", "--plain", "--verbose"]); const GLOBAL_FLAGS_WITH_VALUE = new Set(["--profile"]); function parseGlobalFlags(argv: string[]): { json: boolean; plain: boolean; + verbose: boolean; profile: string | undefined; rest: string[]; } { let json = false; let plain = false; + let verbose = false; let profile: string | undefined; const rest: string[] = []; @@ -149,6 +151,8 @@ function parseGlobalFlags(argv: string[]): { json = true; } else if (arg === "--plain") { plain = true; + } else if (arg === "--verbose") { + verbose = true; } else if (arg === "--profile") { profile = argv[i + 1]; i++; @@ -157,12 +161,13 @@ function parseGlobalFlags(argv: string[]): { } } - return { json, plain, profile, rest }; + return { json, plain, verbose, profile, rest }; } const globalFlags = parseGlobalFlags(process.argv.slice(2)); const jsonFlag = globalFlags.json; const plainFlag = globalFlags.plain; +const verboseFlag = globalFlags.verbose; const profileFlag = globalFlags.profile; const outputFormat: OutputFormat = jsonFlag ? "json" : plainFlag ? "plain" : "table"; @@ -1501,6 +1506,9 @@ switch (config.cmd) { } main().catch((err) => { + if (verboseFlag) { + throw err; + } if (err.code === "slack_webapi_platform_error" && err.data) { console.error(`Slack API error: ${err.data.error}`); if (err.data.needed) { @@ -1509,15 +1517,13 @@ main().catch((err) => { if (err.data.provided) { console.error(` provided scopes: ${err.data.provided}`); } - if (err.data.response_metadata) { - const meta = err.data.response_metadata; - if (meta.messages) { - for (const msg of meta.messages) { - console.error(` ${msg}`); - } + if (err.data.response_metadata?.messages) { + for (const msg of err.data.response_metadata.messages) { + console.error(` ${msg}`); } } - process.exit(1); + } else { + console.error(`Error: ${err.message ?? err}`); } - throw err; + process.exit(1); });