agent-messenger 2.11.1 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +11 -0
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/channeltalk/credential-manager.js +2 -2
  5. package/dist/src/platforms/channeltalk/credential-manager.js.map +1 -1
  6. package/dist/src/platforms/channeltalkbot/credential-manager.js +2 -2
  7. package/dist/src/platforms/channeltalkbot/credential-manager.js.map +1 -1
  8. package/dist/src/platforms/discord/credential-manager.d.ts.map +1 -1
  9. package/dist/src/platforms/discord/credential-manager.js +2 -2
  10. package/dist/src/platforms/discord/credential-manager.js.map +1 -1
  11. package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
  12. package/dist/src/platforms/discordbot/client.js +3 -2
  13. package/dist/src/platforms/discordbot/client.js.map +1 -1
  14. package/dist/src/platforms/discordbot/credential-manager.js +2 -2
  15. package/dist/src/platforms/discordbot/credential-manager.js.map +1 -1
  16. package/dist/src/platforms/instagram/credential-manager.js +2 -2
  17. package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
  18. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  19. package/dist/src/platforms/kakaotalk/client.js +3 -4
  20. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  21. package/dist/src/platforms/kakaotalk/credential-manager.js +2 -2
  22. package/dist/src/platforms/kakaotalk/credential-manager.js.map +1 -1
  23. package/dist/src/platforms/line/client.js +2 -2
  24. package/dist/src/platforms/line/client.js.map +1 -1
  25. package/dist/src/platforms/line/credential-manager.js +2 -2
  26. package/dist/src/platforms/line/credential-manager.js.map +1 -1
  27. package/dist/src/platforms/slack/credential-manager.js +2 -2
  28. package/dist/src/platforms/slack/credential-manager.js.map +1 -1
  29. package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -1
  30. package/dist/src/platforms/slackbot/credential-manager.js +20 -14
  31. package/dist/src/platforms/slackbot/credential-manager.js.map +1 -1
  32. package/dist/src/platforms/teams/credential-manager.js +2 -2
  33. package/dist/src/platforms/teams/credential-manager.js.map +1 -1
  34. package/dist/src/platforms/telegram/credential-manager.js +2 -2
  35. package/dist/src/platforms/telegram/credential-manager.js.map +1 -1
  36. package/dist/src/platforms/webex/credential-manager.js +2 -2
  37. package/dist/src/platforms/webex/credential-manager.js.map +1 -1
  38. package/dist/src/platforms/wechatbot/credential-manager.js +2 -2
  39. package/dist/src/platforms/wechatbot/credential-manager.js.map +1 -1
  40. package/dist/src/platforms/whatsapp/credential-manager.js +2 -2
  41. package/dist/src/platforms/whatsapp/credential-manager.js.map +1 -1
  42. package/dist/src/platforms/whatsappbot/credential-manager.js +2 -2
  43. package/dist/src/platforms/whatsappbot/credential-manager.js.map +1 -1
  44. package/dist/src/shared/utils/config-dir.d.ts +14 -0
  45. package/dist/src/shared/utils/config-dir.d.ts.map +1 -0
  46. package/dist/src/shared/utils/config-dir.js +22 -0
  47. package/dist/src/shared/utils/config-dir.js.map +1 -0
  48. package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
  49. package/dist/src/shared/utils/derived-key-cache.js +2 -2
  50. package/dist/src/shared/utils/derived-key-cache.js.map +1 -1
  51. package/package.json +1 -1
  52. package/skills/agent-channeltalk/SKILL.md +1 -1
  53. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  54. package/skills/agent-discord/SKILL.md +1 -1
  55. package/skills/agent-discordbot/SKILL.md +1 -1
  56. package/skills/agent-instagram/SKILL.md +1 -1
  57. package/skills/agent-kakaotalk/SKILL.md +1 -1
  58. package/skills/agent-line/SKILL.md +1 -1
  59. package/skills/agent-slack/SKILL.md +1 -1
  60. package/skills/agent-slackbot/SKILL.md +1 -1
  61. package/skills/agent-teams/SKILL.md +1 -1
  62. package/skills/agent-telegram/SKILL.md +1 -1
  63. package/skills/agent-webex/SKILL.md +1 -1
  64. package/skills/agent-wechatbot/SKILL.md +1 -1
  65. package/skills/agent-whatsapp/SKILL.md +1 -1
  66. package/skills/agent-whatsappbot/SKILL.md +1 -1
  67. package/src/platforms/channeltalk/credential-manager.ts +2 -2
  68. package/src/platforms/channeltalkbot/credential-manager.ts +2 -2
  69. package/src/platforms/discord/credential-manager.ts +3 -2
  70. package/src/platforms/discordbot/client.test.ts +19 -0
  71. package/src/platforms/discordbot/client.ts +3 -2
  72. package/src/platforms/discordbot/credential-manager.ts +2 -2
  73. package/src/platforms/instagram/credential-manager.ts +2 -2
  74. package/src/platforms/kakaotalk/client.ts +3 -5
  75. package/src/platforms/kakaotalk/credential-manager.ts +2 -2
  76. package/src/platforms/line/client.ts +2 -2
  77. package/src/platforms/line/credential-manager.ts +2 -2
  78. package/src/platforms/slack/credential-manager.ts +2 -2
  79. package/src/platforms/slackbot/credential-manager.ts +18 -14
  80. package/src/platforms/teams/credential-manager.ts +2 -2
  81. package/src/platforms/telegram/commands/whoami.test.ts +1 -0
  82. package/src/platforms/telegram/credential-manager.ts +2 -2
  83. package/src/platforms/webex/credential-manager.ts +2 -2
  84. package/src/platforms/wechatbot/credential-manager.ts +2 -2
  85. package/src/platforms/whatsapp/credential-manager.ts +2 -2
  86. package/src/platforms/whatsappbot/credential-manager.ts +2 -2
  87. package/src/shared/utils/config-dir.test.ts +41 -0
  88. package/src/shared/utils/config-dir.ts +23 -0
  89. package/src/shared/utils/derived-key-cache.ts +3 -2
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-line
3
3
  description: Interact with LINE - send messages, read chats, manage conversations
4
- version: 2.11.1
4
+ version: 2.12.0
5
5
  allowed-tools: Bash(agent-line:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slack
3
3
  description: Interact with Slack workspaces - send messages, read channels, manage reactions
4
- version: 2.11.1
4
+ version: 2.12.0
5
5
  allowed-tools: Bash(agent-slack:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slackbot
3
3
  description: Interact with Slack workspaces using bot tokens - send messages, read channels, manage reactions
4
- version: 2.11.1
4
+ version: 2.12.0
5
5
  allowed-tools: Bash(agent-slackbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-teams
3
3
  description: Interact with Microsoft Teams - send messages, read channels, manage reactions
4
- version: 2.11.1
4
+ version: 2.12.0
5
5
  allowed-tools: Bash(agent-teams:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegram
3
3
  description: Interact with Telegram through TDLib - authenticate, inspect chats, and send messages
4
- version: 2.11.1
4
+ version: 2.12.0
5
5
  allowed-tools: Bash(agent-telegram:*)
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webex
3
3
  description: Interact with Cisco Webex - send messages, read spaces, manage memberships
4
- version: 2.11.1
4
+ version: 2.12.0
5
5
  allowed-tools: Bash(agent-webex:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-wechatbot
3
3
  description: Interact with WeChat Official Account using API credentials - send messages, manage templates, list followers
4
- version: 2.11.1
4
+ version: 2.12.0
5
5
  allowed-tools: Bash(agent-wechatbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsapp
3
3
  description: Interact with WhatsApp - send messages, read chats, manage conversations
4
- version: 2.11.1
4
+ version: 2.12.0
5
5
  allowed-tools: Bash(agent-whatsapp:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsappbot
3
3
  description: Interact with WhatsApp using Cloud API credentials - send messages, manage templates
4
- version: 2.11.1
4
+ version: 2.12.0
5
5
  allowed-tools: Bash(agent-whatsappbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import type { ChannelConfig, ChannelCredentials, ChannelWorkspaceEntry } from './types'
7
7
  import { ChannelConfigSchema } from './types'
8
8
 
@@ -11,7 +11,7 @@ export class ChannelCredentialManager {
11
11
  private credentialsPath: string
12
12
 
13
13
  constructor(configDir?: string) {
14
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
14
+ this.configDir = configDir ?? getConfigDir()
15
15
  this.credentialsPath = join(this.configDir, 'channel-credentials.json')
16
16
  }
17
17
 
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import type { ChannelBotConfig, ChannelBotCredentials, ChannelBotWorkspaceEntry } from './types'
7
7
  import { ChannelBotConfigSchema } from './types'
8
8
 
@@ -17,7 +17,7 @@ export class ChannelBotCredentialManager {
17
17
  protected renameFile: typeof rename = rename
18
18
 
19
19
  constructor(configDir?: string) {
20
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
20
+ this.configDir = configDir ?? getConfigDir()
21
21
  this.credentialsPath = join(this.configDir, CREDENTIALS_FILENAME)
22
22
  this.legacyPath = join(this.configDir, LEGACY_FILENAME)
23
23
  }
@@ -1,8 +1,9 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
+
6
7
  export interface DiscordConfig {
7
8
  token: string | null
8
9
  current_server: string | null
@@ -14,7 +15,7 @@ export class DiscordCredentialManager {
14
15
  private credentialsPath: string
15
16
 
16
17
  constructor(configDir?: string) {
17
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
18
+ this.configDir = configDir ?? getConfigDir()
18
19
  this.credentialsPath = join(this.configDir, 'discord-credentials.json')
19
20
  }
20
21
 
@@ -290,6 +290,25 @@ describe('DiscordBotClient', () => {
290
290
  const headers = fetchCalls[0].options?.headers as Record<string, string>
291
291
  expect(headers.Authorization).toBe('Bot bot-token')
292
292
  })
293
+
294
+ it('throws when response has no attachments', async () => {
295
+ const { tmpdir } = await import('node:os')
296
+ const { join } = await import('node:path')
297
+ const tempFile = join(tmpdir(), 'test-discordbot-upload-empty.txt')
298
+ await Bun.write(tempFile, 'test content')
299
+
300
+ mockResponse({
301
+ id: 'msg1',
302
+ channel_id: 'ch1',
303
+ author: { id: '123', username: 'bot' },
304
+ content: '',
305
+ timestamp: '2024-01-01T00:00:00.000Z',
306
+ attachments: [],
307
+ })
308
+
309
+ const client = await new DiscordBotClient().login({ token: 'bot-token' })
310
+ await expect(client.uploadFile('ch1', tempFile)).rejects.toThrow('Upload succeeded but no attachments returned')
311
+ })
293
312
  })
294
313
 
295
314
  describe('listFiles', () => {
@@ -307,11 +307,12 @@ export class DiscordBotClient {
307
307
  }
308
308
  const message = await this.requestFormData<MessageWithAttachments>(`/channels/${channelId}/messages`, formData)
309
309
 
310
- if (!message.attachments || message.attachments.length === 0) {
310
+ const first = message.attachments?.[0]
311
+ if (!first) {
311
312
  throw new DiscordBotError('Upload succeeded but no attachments returned', 'no_attachments')
312
313
  }
313
314
 
314
- return message.attachments[0]
315
+ return first
315
316
  }
316
317
 
317
318
  async listFiles(channelId: string): Promise<DiscordFile[]> {
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import type { DiscordBotConfig, DiscordBotCredentials } from './types'
7
7
  import { DiscordBotConfigSchema } from './types'
8
8
 
@@ -11,7 +11,7 @@ export class DiscordBotCredentialManager {
11
11
  private credentialsPath: string
12
12
 
13
13
  constructor(configDir?: string) {
14
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
14
+ this.configDir = configDir ?? getConfigDir()
15
15
  this.credentialsPath = join(this.configDir, 'discordbot-credentials.json')
16
16
  }
17
17
 
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import { createAccountId, type InstagramAccount, type InstagramAccountPaths, type InstagramConfig } from './types'
7
7
 
8
8
  export class InstagramCredentialManager {
@@ -11,7 +11,7 @@ export class InstagramCredentialManager {
11
11
  private instagramRootDir: string
12
12
 
13
13
  constructor(configDir?: string) {
14
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
14
+ this.configDir = configDir ?? getConfigDir()
15
15
  this.credentialsPath = join(this.configDir, 'instagram-credentials.json')
16
16
  this.instagramRootDir = join(this.configDir, 'instagram')
17
17
  }
@@ -1,10 +1,10 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
6
5
  import { Long } from 'bson'
7
6
 
7
+ import { getConfigDir } from '@/shared/utils/config-dir'
8
8
  import { warn } from '@/shared/utils/stderr'
9
9
 
10
10
  import { LANG, PC_OS_NAME, getLocoDeviceConfig } from './protocol/config'
@@ -105,10 +105,8 @@ function wrapError(error: unknown, code: string): KakaoTalkError {
105
105
 
106
106
  const MAX_PAGES = 50
107
107
 
108
- const CONFIG_DIR = join(homedir(), '.config', 'agent-messenger')
109
-
110
108
  function syncStatePath(deviceUuid: string): string {
111
- return join(CONFIG_DIR, `kakaotalk-sync-state-${deviceUuid}.json`)
109
+ return join(getConfigDir(), `kakaotalk-sync-state-${deviceUuid}.json`)
112
110
  }
113
111
 
114
112
  async function loadSyncState(deviceUuid: string): Promise<SyncState | undefined> {
@@ -133,7 +131,7 @@ async function loadSyncState(deviceUuid: string): Promise<SyncState | undefined>
133
131
  }
134
132
 
135
133
  async function saveSyncState(deviceUuid: string, state: SyncState): Promise<void> {
136
- await mkdir(CONFIG_DIR, { recursive: true })
134
+ await mkdir(getConfigDir(), { recursive: true })
137
135
  const path = syncStatePath(deviceUuid)
138
136
  await writeFile(path, JSON.stringify(state, null, 2))
139
137
  await chmod(path, 0o600)
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import type { KakaoAccountCredentials, KakaoConfig, KakaoDeviceType } from './types'
7
7
 
8
8
  export interface PendingLoginState {
@@ -18,7 +18,7 @@ export class KakaoCredentialManager {
18
18
  private pendingLoginPath: string
19
19
 
20
20
  constructor(configDir?: string) {
21
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
21
+ this.configDir = configDir ?? getConfigDir()
22
22
  this.credentialsPath = join(this.configDir, 'kakaotalk-credentials.json')
23
23
  this.pendingLoginPath = join(this.configDir, 'kakaotalk-pending-login.json')
24
24
  }
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync } from 'node:fs'
2
- import { homedir } from 'node:os'
3
2
  import { join } from 'node:path'
4
3
 
4
+ import { getConfigDir } from '@/shared/utils/config-dir'
5
5
  import { FileStorage } from '@/vendor/linejs/base/storage/mod.js'
6
6
  import {
7
7
  loginWithQR as linejsLoginWithQR,
@@ -41,7 +41,7 @@ function getDefaultDevice(): LineDevice {
41
41
  }
42
42
 
43
43
  function createStorage(accountId?: string): FileStorage {
44
- const dir = join(homedir(), '.config', 'agent-messenger', 'line-storage')
44
+ const dir = join(getConfigDir(), 'line-storage')
45
45
  mkdirSync(dir, { recursive: true })
46
46
  return new FileStorage(join(dir, `${accountId ?? 'default'}.json`))
47
47
  }
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, rm, writeFile, readFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import type { LineAccountCredentials, LineConfig } from './types'
7
7
 
8
8
  export class LineCredentialManager {
@@ -10,7 +10,7 @@ export class LineCredentialManager {
10
10
  private credentialsPath: string
11
11
 
12
12
  constructor(configDir?: string) {
13
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
13
+ this.configDir = configDir ?? getConfigDir()
14
14
  this.credentialsPath = join(this.configDir, 'line-credentials.json')
15
15
  }
16
16
 
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import type { Config, WorkspaceCredentials } from './types'
7
7
 
8
8
  export class SlackCredentialManager {
@@ -10,7 +10,7 @@ export class SlackCredentialManager {
10
10
  private credentialsPath: string
11
11
 
12
12
  constructor(configDir?: string) {
13
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
13
+ this.configDir = configDir ?? getConfigDir()
14
14
  this.credentialsPath = join(this.configDir, 'slack-credentials.json')
15
15
  }
16
16
 
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import type { SlackBotConfig, SlackBotCredentials, SlackBotWorkspace } from './types'
7
7
  import { SlackBotConfigSchema } from './types'
8
8
 
@@ -11,7 +11,7 @@ export class SlackBotCredentialManager {
11
11
  private credentialsPath: string
12
12
 
13
13
  constructor(configDir?: string) {
14
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
14
+ this.configDir = configDir ?? getConfigDir()
15
15
  this.credentialsPath = join(this.configDir, 'slackbot-credentials.json')
16
16
  }
17
17
 
@@ -103,6 +103,7 @@ export class SlackBotCredentialManager {
103
103
  // Try "workspace_id/bot_id" format first
104
104
  if (botId.includes('/')) {
105
105
  const [workspaceId, id] = botId.split('/')
106
+ if (!workspaceId || !id) return null
106
107
  const workspace = config.workspaces[workspaceId]
107
108
  if (!workspace) return null
108
109
  const bot = workspace.bots[id]
@@ -131,22 +132,24 @@ export class SlackBotCredentialManager {
131
132
  }
132
133
  }
133
134
 
134
- if (matches.length === 1) return matches[0]
135
+ const [onlyMatch, ...rest] = matches
136
+ if (onlyMatch && rest.length === 0) return onlyMatch
135
137
  return null
136
138
  }
137
139
 
138
140
  async setCredentials(creds: SlackBotCredentials): Promise<void> {
139
141
  const config = await this.load()
140
142
 
141
- if (!config.workspaces[creds.workspace_id]) {
142
- config.workspaces[creds.workspace_id] = {
143
- workspace_id: creds.workspace_id,
144
- workspace_name: creds.workspace_name,
145
- bots: {},
146
- }
143
+ const existing = config.workspaces[creds.workspace_id]
144
+ const workspace: SlackBotWorkspace = existing ?? {
145
+ workspace_id: creds.workspace_id,
146
+ workspace_name: creds.workspace_name,
147
+ bots: {},
148
+ }
149
+ if (!existing) {
150
+ config.workspaces[creds.workspace_id] = workspace
147
151
  }
148
152
 
149
- const workspace = config.workspaces[creds.workspace_id]
150
153
  workspace.workspace_name = creds.workspace_name
151
154
  workspace.bots[creds.bot_id] = {
152
155
  bot_id: creds.bot_id,
@@ -167,6 +170,7 @@ export class SlackBotCredentialManager {
167
170
 
168
171
  if (botId.includes('/')) {
169
172
  const [workspaceId, id] = botId.split('/')
173
+ if (!workspaceId || !id) return false
170
174
  const workspace = config.workspaces[workspaceId]
171
175
  if (!workspace || !workspace.bots[id]) return false
172
176
 
@@ -183,16 +187,16 @@ export class SlackBotCredentialManager {
183
187
  return true
184
188
  }
185
189
 
186
- const matches: { workspace: SlackBotWorkspace }[] = []
190
+ const matches: SlackBotWorkspace[] = []
187
191
  for (const workspace of Object.values(config.workspaces)) {
188
192
  if (workspace.bots[botId]) {
189
- matches.push({ workspace })
193
+ matches.push(workspace)
190
194
  }
191
195
  }
192
196
 
193
- if (matches.length !== 1) return false
197
+ const [workspace, ...rest] = matches
198
+ if (!workspace || rest.length > 0) return false
194
199
 
195
- const { workspace } = matches[0]
196
200
  delete workspace.bots[botId]
197
201
  if (Object.keys(workspace.bots).length === 0) {
198
202
  delete config.workspaces[workspace.workspace_id]
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import type { TeamsAccount, TeamsAccountType, TeamsConfig, TeamsConfigLegacy, TeamsRegion } from './types'
7
7
 
8
8
  export class TeamsCredentialManager {
@@ -12,7 +12,7 @@ export class TeamsCredentialManager {
12
12
  private credentialsPath: string
13
13
 
14
14
  constructor(configDir?: string) {
15
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
15
+ this.configDir = configDir ?? getConfigDir()
16
16
  this.credentialsPath = join(this.configDir, 'teams-credentials.json')
17
17
  }
18
18
 
@@ -33,6 +33,7 @@ describe('whoami command', () => {
33
33
  return fn(fakeClient, {} as TelegramAccount, new TelegramCredentialManager())
34
34
  })
35
35
  consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
36
+ consoleLogSpy.mockClear()
36
37
  })
37
38
 
38
39
  afterEach(() => {
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import {
7
7
  createAccountId,
8
8
  type TelegramAccount,
@@ -21,7 +21,7 @@ export class TelegramCredentialManager {
21
21
  private tdlibRootDir: string
22
22
 
23
23
  constructor(configDir?: string) {
24
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
24
+ this.configDir = configDir ?? getConfigDir()
25
25
  this.credentialsPath = join(this.configDir, 'telegram-credentials.json')
26
26
  this.provisioningStatePath = join(this.configDir, 'telegram-provisioning-state.json')
27
27
  this.tdlibRootDir = join(this.configDir, 'telegram')
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import { getWebexAppCredentials } from './app-config'
7
7
  import type { WebexConfig } from './types'
8
8
  import { WebexConfigSchema } from './types'
@@ -19,7 +19,7 @@ export class WebexCredentialManager {
19
19
  private credentialsPath: string
20
20
 
21
21
  constructor(configDir?: string) {
22
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
22
+ this.configDir = configDir ?? getConfigDir()
23
23
  this.credentialsPath = join(this.configDir, 'webex-credentials.json')
24
24
  }
25
25
 
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import type { WeChatBotAccountEntry, WeChatBotConfig, WeChatBotCredentials } from './types'
7
7
  import { WeChatBotConfigSchema } from './types'
8
8
 
@@ -11,7 +11,7 @@ export class WeChatBotCredentialManager {
11
11
  private credentialsPath: string
12
12
 
13
13
  constructor(configDir?: string) {
14
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
14
+ this.configDir = configDir ?? getConfigDir()
15
15
  this.credentialsPath = join(this.configDir, 'wechatbot-credentials.json')
16
16
  }
17
17
 
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import { createAccountId, type WhatsAppAccount, type WhatsAppAccountPaths, type WhatsAppConfig } from './types'
7
7
 
8
8
  export class WhatsAppCredentialManager {
@@ -11,7 +11,7 @@ export class WhatsAppCredentialManager {
11
11
  private baileysRootDir: string
12
12
 
13
13
  constructor(configDir?: string) {
14
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
14
+ this.configDir = configDir ?? getConfigDir()
15
15
  this.credentialsPath = join(this.configDir, 'whatsapp-credentials.json')
16
16
  this.baileysRootDir = join(this.configDir, 'whatsapp')
17
17
  }
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from '../../shared/utils/config-dir'
6
6
  import type { WhatsAppBotAccountEntry, WhatsAppBotConfig, WhatsAppBotCredentials } from './types'
7
7
  import { WhatsAppBotConfigSchema } from './types'
8
8
 
@@ -11,7 +11,7 @@ export class WhatsAppBotCredentialManager {
11
11
  private credentialsPath: string
12
12
 
13
13
  constructor(configDir?: string) {
14
- this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
14
+ this.configDir = configDir ?? getConfigDir()
15
15
  this.credentialsPath = join(this.configDir, 'whatsappbot-credentials.json')
16
16
  }
17
17
 
@@ -0,0 +1,41 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ import { CONFIG_DIR_ENV_VAR, getConfigDir } from './config-dir'
6
+
7
+ describe('getConfigDir', () => {
8
+ let original: string | undefined
9
+
10
+ beforeEach(() => {
11
+ original = process.env[CONFIG_DIR_ENV_VAR]
12
+ delete process.env[CONFIG_DIR_ENV_VAR]
13
+ })
14
+
15
+ afterEach(() => {
16
+ if (original === undefined) {
17
+ delete process.env[CONFIG_DIR_ENV_VAR]
18
+ } else {
19
+ process.env[CONFIG_DIR_ENV_VAR] = original
20
+ }
21
+ })
22
+
23
+ it('returns the default path when the env var is unset', () => {
24
+ expect(getConfigDir()).toBe(join(homedir(), '.config', 'agent-messenger'))
25
+ })
26
+
27
+ it('returns the env var value when set', () => {
28
+ process.env[CONFIG_DIR_ENV_VAR] = '/tmp/custom-config-dir'
29
+ expect(getConfigDir()).toBe('/tmp/custom-config-dir')
30
+ })
31
+
32
+ it('falls back to the default when the env var is empty', () => {
33
+ process.env[CONFIG_DIR_ENV_VAR] = ''
34
+ expect(getConfigDir()).toBe(join(homedir(), '.config', 'agent-messenger'))
35
+ })
36
+
37
+ it('does not append agent-messenger to the override path', () => {
38
+ process.env[CONFIG_DIR_ENV_VAR] = '/var/lib/messenger'
39
+ expect(getConfigDir()).toBe('/var/lib/messenger')
40
+ })
41
+ })
@@ -0,0 +1,23 @@
1
+ import { homedir } from 'node:os'
2
+ import { join } from 'node:path'
3
+
4
+ export const CONFIG_DIR_ENV_VAR = 'AGENT_MESSENGER_CONFIG_DIR'
5
+
6
+ /**
7
+ * Resolves the directory used to persist agent-messenger configuration and
8
+ * credentials.
9
+ *
10
+ * Resolution order:
11
+ * 1. `AGENT_MESSENGER_CONFIG_DIR` environment variable (if set and non-empty)
12
+ * 2. Default: `~/.config/agent-messenger`
13
+ *
14
+ * Used by every platform credential manager so that a single env var override
15
+ * relocates all stored credentials, sync state, and derived-key caches.
16
+ */
17
+ export function getConfigDir(): string {
18
+ const override = process.env[CONFIG_DIR_ENV_VAR]
19
+ if (override && override.length > 0) {
20
+ return override
21
+ }
22
+ return join(homedir(), '.config', 'agent-messenger')
23
+ }
@@ -1,8 +1,9 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
5
+ import { getConfigDir } from './config-dir'
6
+
6
7
  export type Platform = 'slack' | 'discord' | 'teams' | 'instagram' | 'channeltalk'
7
8
 
8
9
  /**
@@ -18,7 +19,7 @@ export class DerivedKeyCache {
18
19
  private cacheDir: string
19
20
 
20
21
  constructor(cacheDir?: string) {
21
- this.cacheDir = cacheDir ?? join(homedir(), '.config', 'agent-messenger', '.derived-keys')
22
+ this.cacheDir = cacheDir ?? join(getConfigDir(), '.derived-keys')
22
23
  }
23
24
 
24
25
  private getKeyPath(platform: Platform): string {