metheus-governance-mcp-cli 0.2.52 → 0.2.53

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 (4) hide show
  1. package/README.md +31 -12
  2. package/cli.mjs +291 -79
  3. package/package.json +1 -1
  4. package/postinstall.mjs +25 -19
package/README.md CHANGED
@@ -20,23 +20,35 @@ Compatibility note: legacy command alias `metheus-governance-mcp` is still suppo
20
20
  npm install -g metheus-governance-mcp-cli@latest
21
21
  ```
22
22
 
23
- Install creates a local Telegram settings template here:
23
+ Install creates local provider settings templates here:
24
24
 
25
25
  - `~/.metheus/telegram.env`
26
+ - `~/.metheus/slack.env`
27
+ - `~/.metheus/kakaotalk.env`
26
28
 
27
- This file is for the local Telegram bot secret only.
29
+ These files are for local provider bot secrets only.
28
30
 
29
31
  - Store locally:
30
32
  - `TELEGRAM_BOT_TOKEN`
31
- - Server-side Metheus stores project Telegram destination metadata separately:
32
- - `chat_id`
33
+ - `SLACK_BOT_TOKEN`
34
+ - `KAKAOTALK_BOT_TOKEN`
35
+ - Server-side Metheus stores project chat destination metadata separately:
36
+ - provider
37
+ - `chat_id` / channel / room identifier
33
38
  - label / active state
34
- - Do not put project `chat_id` in the local Telegram env file.
39
+ - Do not put project chat destination identifiers in local env files.
35
40
 
36
- Example template:
41
+ Example templates:
37
42
 
38
43
  ```env
44
+ # ~/.metheus/telegram.env
39
45
  TELEGRAM_BOT_TOKEN=
46
+
47
+ # ~/.metheus/slack.env
48
+ SLACK_BOT_TOKEN=
49
+
50
+ # ~/.metheus/kakaotalk.env
51
+ KAKAOTALK_BOT_TOKEN=
40
52
  ```
41
53
 
42
54
  ## One command bootstrap (recommended)
@@ -69,11 +81,13 @@ Recommended for Codex/Claude/Gemini/Antigravity/Cursor multi-workspace sessions:
69
81
  metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com --workspace-dir auto
70
82
  ```
71
83
 
72
- `setup` also ensures the local Telegram template exists:
84
+ `setup` also ensures local provider templates exist:
73
85
 
74
86
  - `~/.metheus/telegram.env`
87
+ - `~/.metheus/slack.env`
88
+ - `~/.metheus/kakaotalk.env`
75
89
 
76
- Fill only the bot token locally. Project Telegram `chat_id` destinations should be managed on the Metheus server as project Telegram destinations, not as local env values and not inside Chat Hooks.
90
+ Fill only provider bot tokens locally. Project chat destination identifiers should be managed on the Metheus server as project chat destinations, not as local env values and not inside legacy Chat Hooks/webhooks.
77
91
 
78
92
  Gemini CLI note:
79
93
  - `gemini mcp` commands require Gemini auth to be configured first (`GEMINI_API_KEY` or `~/.gemini/settings.json` auth).
@@ -105,7 +119,8 @@ metheus-governance-mcp-cli doctor --project-id <project_uuid> --base-url https:/
105
119
 
106
120
  Checks:
107
121
  - auth token status (+ auto refresh attempt)
108
- - local Telegram env token presence
122
+ - local provider env template presence
123
+ - local provider token presence for active project destinations
109
124
  - codex/claude/gemini/antigravity/cursor registration state
110
125
  - gateway `tools/list` reachability
111
126
  - `project.summary` access
@@ -113,10 +128,14 @@ Checks:
113
128
  - smoke calls: `workitem.list`, `evidence.list`, `decision.list`
114
129
 
115
130
  Direct bot posting:
116
- - `me.send-bot-message` uses the local `TELEGRAM_BOT_TOKEN` from `~/.metheus/telegram.env`
131
+ - `me.send-bot-message` uses local provider tokens from `~/.metheus/<provider>.env`
117
132
  - it does not use a server-stored bot token
118
- - the destination `chat_id` is resolved from the current project's saved Chat Destinations on the Metheus server
119
- - if multiple active Telegram destinations exist for the project, pass `destination_id` or `destination_label`
133
+ - the destination identifier is resolved from the current project's saved Chat Destinations on the Metheus server
134
+ - if multiple active destinations exist for the same provider, pass `destination_id` or `destination_label`
135
+ - direct local delivery is implemented today for:
136
+ - Telegram
137
+ - Slack
138
+ - KakaoTalk profiles and destinations can be stored now, but direct local delivery is not implemented yet
120
139
 
121
140
  ## Use in MCP
122
141
 
package/cli.mjs CHANGED
@@ -15,7 +15,27 @@ const DEFAULT_SITE_URL = "https://metheus.gesiaplatform.com";
15
15
  const DEFAULT_BASE_URL = `${DEFAULT_SITE_URL}/governance/mcp`;
16
16
  const DEFAULT_SERVER_NAME = "metheus-governance-mcp";
17
17
  const AUTH_STORE_RELATIVE_PATH = path.join(".metheus", "governance-mcp-auth.json");
18
- const TELEGRAM_ENV_RELATIVE_PATH = path.join(".metheus", "telegram.env");
18
+ const PROVIDER_ENV_CONFIG = {
19
+ telegram: {
20
+ relativePath: path.join(".metheus", "telegram.env"),
21
+ tokenKey: "TELEGRAM_BOT_TOKEN",
22
+ label: "Telegram",
23
+ verifyURL: "https://api.telegram.org",
24
+ },
25
+ slack: {
26
+ relativePath: path.join(".metheus", "slack.env"),
27
+ tokenKey: "SLACK_BOT_TOKEN",
28
+ label: "Slack",
29
+ verifyURL: "https://slack.com/api/auth.test",
30
+ },
31
+ kakaotalk: {
32
+ relativePath: path.join(".metheus", "kakaotalk.env"),
33
+ tokenKey: "KAKAOTALK_BOT_TOKEN",
34
+ label: "KakaoTalk",
35
+ verifyURL: "",
36
+ },
37
+ };
38
+ const PROVIDER_ENV_ORDER = Object.keys(PROVIDER_ENV_CONFIG);
19
39
  const SELF_UPDATE_STATE_RELATIVE_PATH = path.join(".metheus", "governance-mcp-cli-update.json");
20
40
  const CTXPACK_CACHE_RELATIVE_DIR = path.join(".metheus", "ctxpack-cache");
21
41
  const CTXPACK_META_FILENAME = ".metheus_ctxpack_sync.json";
@@ -58,8 +78,8 @@ function printUsage() {
58
78
  ` ${ALLOW_HOME_WORKSPACE_ENV_KEY}=1 to allow using home directory as workspace root (disabled by default).`,
59
79
  " If env is missing, stored token file is used:",
60
80
  ` ${AUTH_STORE_RELATIVE_PATH}`,
61
- " Local Telegram bot token template is stored at:",
62
- ` ${TELEGRAM_ENV_RELATIVE_PATH}`,
81
+ " Local provider bot token templates are stored under:",
82
+ ...PROVIDER_ENV_ORDER.map((provider) => ` ${PROVIDER_ENV_CONFIG[provider].relativePath}`),
63
83
  "",
64
84
  ].join("\n"),
65
85
  );
@@ -479,36 +499,57 @@ function authStoreFilePath() {
479
499
  return resolveHomeFilePath(AUTH_STORE_RELATIVE_PATH);
480
500
  }
481
501
 
482
- function telegramEnvFilePath() {
483
- return resolveHomeFilePath(TELEGRAM_ENV_RELATIVE_PATH);
502
+ function normalizeBotProvider(rawValue) {
503
+ const value = String(rawValue || "").trim().toLowerCase();
504
+ if (Object.prototype.hasOwnProperty.call(PROVIDER_ENV_CONFIG, value)) {
505
+ return value;
506
+ }
507
+ return "telegram";
484
508
  }
485
509
 
486
- function telegramEnvTemplate() {
510
+ function providerEnvConfig(provider) {
511
+ return PROVIDER_ENV_CONFIG[normalizeBotProvider(provider)];
512
+ }
513
+
514
+ function providerEnvFilePath(provider) {
515
+ return resolveHomeFilePath(providerEnvConfig(provider).relativePath);
516
+ }
517
+
518
+ function providerEnvTemplate(provider) {
519
+ const config = providerEnvConfig(provider);
487
520
  return [
488
- "# Metheus local Telegram bot settings",
521
+ `# Metheus local ${config.label} bot settings`,
489
522
  "# Keep this file on your machine only. Do not commit it.",
490
- "# Store only the Telegram bot token locally.",
491
- "# Project chat_id must be managed on the Metheus server as a project Telegram destination.",
523
+ `# Store only the ${config.label} bot token locally.`,
524
+ "# Project chat destinations must be managed on the Metheus server.",
492
525
  "",
493
- "TELEGRAM_BOT_TOKEN=",
526
+ `${config.tokenKey}=`,
494
527
  "",
495
528
  ].join("\n");
496
529
  }
497
530
 
498
- function ensureTelegramEnvTemplate() {
499
- const filePath = telegramEnvFilePath();
531
+ function ensureProviderEnvTemplate(provider) {
532
+ const filePath = providerEnvFilePath(provider);
500
533
  try {
501
534
  if (fs.existsSync(filePath)) {
502
535
  return { filePath, created: false, existed: true };
503
536
  }
504
537
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
505
- fs.writeFileSync(filePath, telegramEnvTemplate(), "utf8");
538
+ fs.writeFileSync(filePath, providerEnvTemplate(provider), "utf8");
506
539
  return { filePath, created: true, existed: false };
507
540
  } catch (err) {
508
541
  return { filePath, created: false, existed: false, error: String(err?.message || err) };
509
542
  }
510
543
  }
511
544
 
545
+ function ensureAllProviderEnvTemplates() {
546
+ const out = {};
547
+ for (const provider of PROVIDER_ENV_ORDER) {
548
+ out[provider] = ensureProviderEnvTemplate(provider);
549
+ }
550
+ return out;
551
+ }
552
+
512
553
  function parseSimpleEnvText(rawText) {
513
554
  const out = {};
514
555
  const lines = String(rawText || "").split(/\r?\n/);
@@ -531,12 +572,16 @@ function parseSimpleEnvText(rawText) {
531
572
  return out;
532
573
  }
533
574
 
534
- function loadTelegramEnvConfig() {
535
- const ensured = ensureTelegramEnvTemplate();
575
+ function loadProviderEnvConfig(provider) {
576
+ const normalizedProvider = normalizeBotProvider(provider);
577
+ const config = providerEnvConfig(normalizedProvider);
578
+ const ensured = ensureProviderEnvTemplate(normalizedProvider);
536
579
  const filePath = ensured.filePath;
537
580
  if (ensured.error) {
538
581
  return {
539
582
  ok: false,
583
+ provider: normalizedProvider,
584
+ providerLabel: config.label,
540
585
  filePath,
541
586
  error: ensured.error,
542
587
  token: "",
@@ -545,23 +590,29 @@ function loadTelegramEnvConfig() {
545
590
  try {
546
591
  const raw = fs.readFileSync(filePath, "utf8");
547
592
  const parsed = parseSimpleEnvText(raw);
548
- const token = String(parsed.TELEGRAM_BOT_TOKEN || "").trim();
593
+ const token = String(parsed[config.tokenKey] || "").trim();
549
594
  if (!token) {
550
595
  return {
551
596
  ok: false,
597
+ provider: normalizedProvider,
598
+ providerLabel: config.label,
552
599
  filePath,
553
- error: `TELEGRAM_BOT_TOKEN is missing in ${filePath}`,
600
+ error: `${config.tokenKey} is missing in ${filePath}`,
554
601
  token: "",
555
602
  };
556
603
  }
557
604
  return {
558
605
  ok: true,
606
+ provider: normalizedProvider,
607
+ providerLabel: config.label,
559
608
  filePath,
560
609
  token,
561
610
  };
562
611
  } catch (err) {
563
612
  return {
564
613
  ok: false,
614
+ provider: normalizedProvider,
615
+ providerLabel: config.label,
565
616
  filePath,
566
617
  error: String(err?.message || err),
567
618
  token: "",
@@ -569,6 +620,132 @@ function loadTelegramEnvConfig() {
569
620
  }
570
621
  }
571
622
 
623
+ function telegramEnvFilePath() {
624
+ return providerEnvFilePath("telegram");
625
+ }
626
+
627
+ function telegramEnvTemplate() {
628
+ return providerEnvTemplate("telegram");
629
+ }
630
+
631
+ function ensureTelegramEnvTemplate() {
632
+ return ensureProviderEnvTemplate("telegram");
633
+ }
634
+
635
+ function loadTelegramEnvConfig() {
636
+ return loadProviderEnvConfig("telegram");
637
+ }
638
+
639
+ async function verifyLocalProviderToken(provider, envConfig, timeoutSeconds) {
640
+ const normalizedProvider = normalizeBotProvider(provider);
641
+ if (!envConfig?.ok) {
642
+ return {
643
+ ok: false,
644
+ detail: envConfig?.error || "local provider env is not ready",
645
+ };
646
+ }
647
+ if (normalizedProvider === "telegram") {
648
+ const me = safeObject(
649
+ await getJSONWithoutAuth(`https://api.telegram.org/bot${envConfig.token}/getMe`, timeoutSeconds),
650
+ );
651
+ if (Boolean(me.ok) && safeObject(me.result).id) {
652
+ return {
653
+ ok: true,
654
+ detail: `reachable as @${String(safeObject(me.result).username || "").trim() || "-"}`,
655
+ };
656
+ }
657
+ return {
658
+ ok: false,
659
+ detail: "getMe returned an unexpected payload",
660
+ };
661
+ }
662
+ if (normalizedProvider === "slack") {
663
+ const response = await postJSONWithoutAuth(
664
+ "https://slack.com/api/auth.test",
665
+ timeoutSeconds,
666
+ {},
667
+ {
668
+ authorization: `Bearer ${envConfig.token}`,
669
+ },
670
+ );
671
+ const responseJSON = parseJSONText(response.bodyText);
672
+ if (response.statusCode >= 200 && response.statusCode < 300 && responseJSON?.ok) {
673
+ const team = String(responseJSON.team || "").trim() || "-";
674
+ const user = String(responseJSON.user || "").trim() || "-";
675
+ return {
676
+ ok: true,
677
+ detail: `reachable as ${user} on ${team}`,
678
+ };
679
+ }
680
+ return {
681
+ ok: false,
682
+ detail: String(responseJSON?.error || response.bodyText || "auth.test failed").trim(),
683
+ };
684
+ }
685
+ return {
686
+ ok: false,
687
+ detail: `${providerEnvConfig(normalizedProvider).label} local verification is not implemented yet`,
688
+ };
689
+ }
690
+
691
+ async function deliverLocalProviderMessage({
692
+ provider,
693
+ token,
694
+ destination,
695
+ text,
696
+ disableWebPagePreview,
697
+ replyToMessageID,
698
+ timeoutSeconds,
699
+ }) {
700
+ const normalizedProvider = normalizeBotProvider(provider);
701
+ if (normalizedProvider === "telegram") {
702
+ const requestURL = `https://api.telegram.org/bot${token}/sendMessage`;
703
+ const payload = {
704
+ chat_id: destination.chatID,
705
+ text,
706
+ disable_web_page_preview: disableWebPagePreview,
707
+ };
708
+ if (replyToMessageID > 0) {
709
+ payload.reply_to_message_id = replyToMessageID;
710
+ }
711
+ const response = await postJSONWithoutAuth(requestURL, timeoutSeconds, payload);
712
+ const responseJSON = parseJSONText(response.bodyText);
713
+ return {
714
+ statusCode: response.statusCode,
715
+ body: responseJSON || response.bodyText,
716
+ ok: response.statusCode >= 200 && response.statusCode < 300 && Boolean(responseJSON?.ok ?? true),
717
+ url: sanitizeTelegramAPIURL(requestURL),
718
+ replySupported: true,
719
+ };
720
+ }
721
+ if (normalizedProvider === "slack") {
722
+ const requestURL = "https://slack.com/api/chat.postMessage";
723
+ const payload = {
724
+ channel: destination.chatID,
725
+ text,
726
+ unfurl_links: disableWebPagePreview === false,
727
+ unfurl_media: disableWebPagePreview === false,
728
+ };
729
+ const response = await postJSONWithoutAuth(
730
+ requestURL,
731
+ timeoutSeconds,
732
+ payload,
733
+ {
734
+ authorization: `Bearer ${token}`,
735
+ },
736
+ );
737
+ const responseJSON = parseJSONText(response.bodyText);
738
+ return {
739
+ statusCode: response.statusCode,
740
+ body: responseJSON || response.bodyText,
741
+ ok: response.statusCode >= 200 && response.statusCode < 300 && Boolean(responseJSON?.ok ?? false),
742
+ url: requestURL,
743
+ replySupported: false,
744
+ };
745
+ }
746
+ throw new Error(`${providerEnvConfig(normalizedProvider).label} local delivery is not implemented yet`);
747
+ }
748
+
572
749
  function resolveWorkspaceDir(rawPath) {
573
750
  const input = String(rawPath || "").trim();
574
751
  if (input) {
@@ -2199,28 +2376,14 @@ async function runDoctor(flags) {
2199
2376
  includeDrafts: true,
2200
2377
  });
2201
2378
  const rows = [];
2202
-
2203
- const telegramEnv = loadTelegramEnvConfig();
2204
- if (!telegramEnv.ok) {
2205
- addDoctorCheck(rows, "warn", "local telegram env", `${telegramEnv.error} (${telegramEnv.filePath})`);
2206
- } else {
2207
- addDoctorCheck(rows, "ok", "local telegram env", `token configured (${telegramEnv.filePath})`);
2208
- try {
2209
- const me = safeObject(
2210
- await getJSONWithoutAuth(`https://api.telegram.org/bot${telegramEnv.token}/getMe`, timeoutSeconds),
2211
- );
2212
- if (Boolean(me.ok) && safeObject(me.result).id) {
2213
- addDoctorCheck(
2214
- rows,
2215
- "ok",
2216
- "local telegram bot",
2217
- `reachable as @${String(safeObject(me.result).username || "").trim() || "-"}`,
2218
- );
2219
- } else {
2220
- addDoctorCheck(rows, "warn", "local telegram bot", "getMe returned an unexpected payload");
2221
- }
2222
- } catch (err) {
2223
- addDoctorCheck(rows, "warn", "local telegram bot", String(err?.message || err));
2379
+ const providerTemplates = ensureAllProviderEnvTemplates();
2380
+ for (const provider of PROVIDER_ENV_ORDER) {
2381
+ const ensured = providerTemplates[provider];
2382
+ const label = providerEnvConfig(provider).label;
2383
+ if (ensured.error) {
2384
+ addDoctorCheck(rows, "warn", `local ${provider} env`, `${ensured.error} (${ensured.filePath})`);
2385
+ } else {
2386
+ addDoctorCheck(rows, "ok", `local ${provider} env`, `template ready (${ensured.filePath})`);
2224
2387
  }
2225
2388
  }
2226
2389
 
@@ -2240,6 +2403,42 @@ async function runDoctor(flags) {
2240
2403
  "auth token",
2241
2404
  `configured (${resolved.source || "unknown"}${tokenExpiryIso(token) ? `, expires ${tokenExpiryIso(token)}` : ""})`,
2242
2405
  );
2406
+
2407
+ if (context.projectID && isUUID(context.projectID)) {
2408
+ try {
2409
+ const destinationURL = `${normalizeSiteBaseURL(context.baseURL)}/api/v1/projects/${encodeURIComponent(context.projectID)}/chat-destinations`;
2410
+ const destinations = ensureArray(await getJSONWithAuth(destinationURL, timeoutSeconds, token))
2411
+ .map(normalizeChatDestination)
2412
+ .filter((item) => item.isActive && item.chatID);
2413
+ const activeProviders = Array.from(new Set(destinations.map((item) => normalizeBotProvider(item.provider))));
2414
+ if (activeProviders.length === 0) {
2415
+ addDoctorCheck(rows, "warn", "project chat destinations", "no active project chat destinations configured");
2416
+ } else {
2417
+ addDoctorCheck(rows, "ok", "project chat destinations", activeProviders.join(", "));
2418
+ }
2419
+ for (const provider of activeProviders) {
2420
+ const envConfig = loadProviderEnvConfig(provider);
2421
+ if (!envConfig.ok) {
2422
+ addDoctorCheck(rows, "warn", `local ${provider} token`, `${envConfig.error} (${envConfig.filePath})`);
2423
+ continue;
2424
+ }
2425
+ addDoctorCheck(rows, "ok", `local ${provider} token`, `configured (${envConfig.filePath})`);
2426
+ try {
2427
+ const verification = await verifyLocalProviderToken(provider, envConfig, timeoutSeconds);
2428
+ addDoctorCheck(
2429
+ rows,
2430
+ verification.ok ? "ok" : "warn",
2431
+ `local ${provider} bot`,
2432
+ verification.detail,
2433
+ );
2434
+ } catch (err) {
2435
+ addDoctorCheck(rows, "warn", `local ${provider} bot`, String(err?.message || err));
2436
+ }
2437
+ }
2438
+ } catch (err) {
2439
+ addDoctorCheck(rows, "warn", "project chat destinations", String(err?.message || err));
2440
+ }
2441
+ }
2243
2442
  }
2244
2443
 
2245
2444
  for (const cliBin of MCP_CLIENTS) {
@@ -4086,24 +4285,26 @@ function normalizeChatDestination(record) {
4086
4285
  };
4087
4286
  }
4088
4287
 
4089
- function selectProjectChatDestination(destinations, selectors = {}) {
4288
+ function selectProjectChatDestination(destinations, selectors = {}, provider = "telegram") {
4289
+ const normalizedProvider = normalizeBotProvider(provider);
4290
+ const providerLabel = providerEnvConfig(normalizedProvider).label;
4090
4291
  const list = ensureArray(destinations)
4091
4292
  .map(normalizeChatDestination)
4092
- .filter((item) => item.provider === "telegram" && item.chatID && item.isActive);
4293
+ .filter((item) => item.provider === normalizedProvider && item.chatID && item.isActive);
4093
4294
  const destinationID = String(selectors.destinationID || "").trim();
4094
4295
  const destinationLabel = String(selectors.destinationLabel || "").trim().toLowerCase();
4095
4296
 
4096
4297
  if (destinationID) {
4097
4298
  const match = list.find((item) => item.id === destinationID);
4098
4299
  if (!match) {
4099
- throw new Error(`project chat destination ${destinationID} was not found or is inactive`);
4300
+ throw new Error(`${providerLabel} project chat destination ${destinationID} was not found or is inactive`);
4100
4301
  }
4101
4302
  return match;
4102
4303
  }
4103
4304
  if (destinationLabel) {
4104
4305
  const match = list.find((item) => item.label.toLowerCase() === destinationLabel);
4105
4306
  if (!match) {
4106
- throw new Error(`project chat destination label "${selectors.destinationLabel}" was not found or is inactive`);
4307
+ throw new Error(`${providerLabel} project chat destination label "${selectors.destinationLabel}" was not found or is inactive`);
4107
4308
  }
4108
4309
  return match;
4109
4310
  }
@@ -4111,11 +4312,11 @@ function selectProjectChatDestination(destinations, selectors = {}) {
4111
4312
  return list[0];
4112
4313
  }
4113
4314
  if (list.length === 0) {
4114
- throw new Error("no active Telegram chat destination is configured for this project");
4315
+ throw new Error(`no active ${providerLabel} chat destination is configured for this project`);
4115
4316
  }
4116
4317
  const labels = list.map((item) => item.label || item.id).filter(Boolean);
4117
4318
  throw new Error(
4118
- `multiple active Telegram chat destinations exist for this project; pass destination_id or destination_label (${labels.join(", ")})`,
4319
+ `multiple active ${providerLabel} chat destinations exist for this project; pass destination_id or destination_label (${labels.join(", ")})`,
4119
4320
  );
4120
4321
  }
4121
4322
 
@@ -4148,11 +4349,6 @@ async function handleLocalBotMessageToolCall({
4148
4349
  throw new Error("project_id is required for local bot delivery; set --project-id during setup or include project_id");
4149
4350
  }
4150
4351
 
4151
- const telegramEnv = loadTelegramEnvConfig();
4152
- if (!telegramEnv.ok) {
4153
- throw new Error(`local telegram env is not ready (${telegramEnv.error})`);
4154
- }
4155
-
4156
4352
  const siteBaseURL = normalizeSiteBaseURL(args.baseURL);
4157
4353
  const encodedBotID = encodeURIComponent(botID);
4158
4354
  const encodedProjectID = encodeURIComponent(projectID);
@@ -4166,12 +4362,13 @@ async function handleLocalBotMessageToolCall({
4166
4362
  if (bot.is_active === false || bot.isActive === false) {
4167
4363
  throw new Error("bot is inactive");
4168
4364
  }
4365
+ const provider = normalizeBotProvider(bot.provider);
4169
4366
 
4170
4367
  const destinations = ensureArray(await getJSONWithAuth(destinationURL, args.timeoutSeconds, token));
4171
4368
  const destination = selectProjectChatDestination(destinations, {
4172
4369
  destinationID,
4173
4370
  destinationLabel,
4174
- });
4371
+ }, provider);
4175
4372
 
4176
4373
  const disableWebPagePreview = boolFromRaw(
4177
4374
  Object.prototype.hasOwnProperty.call(toolArgs, "disable_web_page_preview")
@@ -4188,28 +4385,31 @@ async function handleLocalBotMessageToolCall({
4188
4385
  throw new Error("reply_to_message_id must be a positive integer");
4189
4386
  }
4190
4387
 
4191
- const telegramURL = `https://api.telegram.org/bot${telegramEnv.token}/sendMessage`;
4192
- const telegramPayload = {
4193
- chat_id: destination.chatID,
4194
- text,
4195
- disable_web_page_preview: disableWebPagePreview,
4196
- };
4197
- if (replyToMessageID > 0) {
4198
- telegramPayload.reply_to_message_id = replyToMessageID;
4388
+ const providerEnv = loadProviderEnvConfig(provider);
4389
+ if (!providerEnv.ok) {
4390
+ throw new Error(`local ${provider} env is not ready (${providerEnv.error})`);
4199
4391
  }
4200
4392
 
4201
- const response = await postJSONWithoutAuth(telegramURL, args.timeoutSeconds, telegramPayload);
4202
- const responseJSON = parseJSONText(response.bodyText);
4203
- const deliveryOK = response.statusCode >= 200 && response.statusCode < 300 && Boolean(responseJSON?.ok ?? true);
4204
- if (!deliveryOK) {
4393
+ const delivery = await deliverLocalProviderMessage({
4394
+ provider,
4395
+ token: providerEnv.token,
4396
+ destination,
4397
+ text,
4398
+ disableWebPagePreview,
4399
+ replyToMessageID,
4400
+ timeoutSeconds: args.timeoutSeconds,
4401
+ });
4402
+ if (!delivery.ok) {
4403
+ const responseJSON = safeObject(delivery.body);
4205
4404
  const errorDetail =
4206
- String(responseJSON?.description || response.bodyText || "").trim() || `telegram api status ${response.statusCode}`;
4207
- if (response.statusCode === 401 || /unauthorized/i.test(errorDetail)) {
4405
+ String(responseJSON.description || responseJSON.error || JSON.stringify(delivery.body || "")).trim()
4406
+ || `${provider} api status ${delivery.statusCode}`;
4407
+ if (delivery.statusCode === 401 || /unauthorized|invalid_auth/i.test(errorDetail)) {
4208
4408
  throw new Error(
4209
- `local telegram delivery failed (Unauthorized). Check TELEGRAM_BOT_TOKEN in ${telegramEnv.filePath}`,
4409
+ `local ${provider} delivery failed (Unauthorized). Check ${providerEnvConfig(provider).tokenKey} in ${providerEnv.filePath}`,
4210
4410
  );
4211
4411
  }
4212
- throw new Error(`local telegram delivery failed (${errorDetail})`);
4412
+ throw new Error(`local ${provider} delivery failed (${errorDetail})`);
4213
4413
  }
4214
4414
 
4215
4415
  return jsonRpcResult(
@@ -4217,7 +4417,7 @@ async function handleLocalBotMessageToolCall({
4217
4417
  buildLocalToolRPCResult("me.send-bot-message", {
4218
4418
  ok: true,
4219
4419
  local_delivery: true,
4220
- provider: "telegram",
4420
+ provider,
4221
4421
  bot_id: String(bot.id || botID).trim(),
4222
4422
  bot_name: String(bot.name || "").trim(),
4223
4423
  bot_role: String(bot.bot_role || bot.botRole || "").trim(),
@@ -4225,9 +4425,10 @@ async function handleLocalBotMessageToolCall({
4225
4425
  destination_id: destination.id,
4226
4426
  destination_label: destination.label,
4227
4427
  chat_id: destination.chatID,
4228
- status: response.statusCode,
4229
- url: sanitizeTelegramAPIURL(telegramURL),
4230
- body: responseJSON || response.bodyText,
4428
+ status: delivery.statusCode,
4429
+ url: delivery.url,
4430
+ body: delivery.body,
4431
+ reply_supported: delivery.replySupported,
4231
4432
  }),
4232
4433
  );
4233
4434
  }
@@ -5704,7 +5905,7 @@ function runSetupInternal(flags, options = {}) {
5704
5905
  const context = resolveSetupContext(flags);
5705
5906
  const clients = [...MCP_CLIENTS];
5706
5907
  const results = [];
5707
- const telegramEnv = ensureTelegramEnvTemplate();
5908
+ const providerTemplates = ensureAllProviderEnvTemplates();
5708
5909
 
5709
5910
  for (const cliBin of clients) {
5710
5911
  if (!commandExists(cliBin)) continue;
@@ -5754,12 +5955,16 @@ function runSetupInternal(flags, options = {}) {
5754
5955
  process.stdout.write(`Fallback: ${context.workspaceFallbackDir} (METHEUS_WORKSPACE_DIR)\n`);
5755
5956
  }
5756
5957
  process.stdout.write(`Project: ${context.projectID || "auto-detect from .metheus_ctxpack_sync.json"}\n`);
5757
- if (telegramEnv.error) {
5758
- process.stdout.write(`Telegram: template unavailable (${telegramEnv.error})\n`);
5759
- } else {
5760
- process.stdout.write(
5761
- `Telegram: ${telegramEnv.created ? "template created" : "template ready"} (${telegramEnv.filePath})\n`,
5762
- );
5958
+ for (const provider of PROVIDER_ENV_ORDER) {
5959
+ const ensured = providerTemplates[provider];
5960
+ const label = providerEnvConfig(provider).label;
5961
+ if (ensured.error) {
5962
+ process.stdout.write(`${label}: template unavailable (${ensured.error})\n`);
5963
+ } else {
5964
+ process.stdout.write(
5965
+ `${label}: ${ensured.created ? "template created" : "template ready"} (${ensured.filePath})\n`,
5966
+ );
5967
+ }
5763
5968
  }
5764
5969
  if (context.ctxpackKey) {
5765
5970
  process.stdout.write(`Ctxpack: ${context.ctxpackKey}\n`);
@@ -5782,12 +5987,17 @@ function runSelftest(flags = {}) {
5782
5987
  const checks = [];
5783
5988
  const push = (name, ok, detail = "") => checks.push({ name, ok: Boolean(ok), detail: String(detail || "") });
5784
5989
 
5785
- const parsedEnv = parseSimpleEnvText("\n# comment\nTELEGRAM_BOT_TOKEN=\"abc:123\"\nEMPTY=\n");
5990
+ const parsedEnv = parseSimpleEnvText("\n# comment\nTELEGRAM_BOT_TOKEN=\"abc:123\"\nSLACK_BOT_TOKEN=xoxb-test\nEMPTY=\n");
5786
5991
  push(
5787
5992
  "telegram_env_parse_token",
5788
5993
  String(parsedEnv.TELEGRAM_BOT_TOKEN || "") === "abc:123",
5789
5994
  `token=${String(parsedEnv.TELEGRAM_BOT_TOKEN || "(missing)")}`,
5790
5995
  );
5996
+ push(
5997
+ "slack_env_parse_token",
5998
+ String(parsedEnv.SLACK_BOT_TOKEN || "") === "xoxb-test",
5999
+ `token=${String(parsedEnv.SLACK_BOT_TOKEN || "(missing)")}`,
6000
+ );
5791
6001
 
5792
6002
  try {
5793
6003
  const selected = selectProjectChatDestination(
@@ -5796,6 +6006,7 @@ function runSelftest(flags = {}) {
5796
6006
  { id: "dest-2", provider: "slack", label: "Slack", chat_id: "C123", is_active: true },
5797
6007
  ],
5798
6008
  {},
6009
+ "telegram",
5799
6010
  );
5800
6011
  push(
5801
6012
  "telegram_destination_select_single_active",
@@ -5813,6 +6024,7 @@ function runSelftest(flags = {}) {
5813
6024
  { id: "dest-2", provider: "telegram", label: "Room B", chat_id: "-1002", is_active: true },
5814
6025
  ],
5815
6026
  {},
6027
+ "telegram",
5816
6028
  );
5817
6029
  push("telegram_destination_multi_requires_selector", false, "selection unexpectedly succeeded");
5818
6030
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.52",
3
+ "version": "0.2.53",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [
package/postinstall.mjs CHANGED
@@ -4,38 +4,44 @@ import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
 
7
- const RELATIVE_PATH = path.join(".metheus", "telegram.env");
7
+ const PROVIDERS = [
8
+ ["telegram", "TELEGRAM_BOT_TOKEN", "Telegram"],
9
+ ["slack", "SLACK_BOT_TOKEN", "Slack"],
10
+ ["kakaotalk", "KAKAOTALK_BOT_TOKEN", "KakaoTalk"],
11
+ ];
8
12
 
9
- function resolveTargetPath() {
13
+ function resolveTargetPath(provider) {
10
14
  const home = String(process.env.USERPROFILE || process.env.HOME || os.homedir() || "").trim();
11
15
  if (!home) return "";
12
- return path.join(home, RELATIVE_PATH);
16
+ return path.join(home, ".metheus", `${provider}.env`);
13
17
  }
14
18
 
15
- function template() {
19
+ function template(label, tokenKey) {
16
20
  return [
17
- "# Metheus local Telegram bot settings",
21
+ `# Metheus local ${label} bot settings`,
18
22
  "# Keep this file on your machine only. Do not commit it.",
19
- "# Store only the Telegram bot token locally.",
20
- "# Project chat_id must be managed on the Metheus server as a project Telegram destination.",
23
+ `# Store only the ${label} bot token locally.`,
24
+ "# Project chat destinations must be managed on the Metheus server.",
21
25
  "",
22
- "TELEGRAM_BOT_TOKEN=",
26
+ `${tokenKey}=`,
23
27
  "",
24
28
  ].join("\n");
25
29
  }
26
30
 
27
31
  function main() {
28
- const filePath = resolveTargetPath();
29
- if (!filePath) return;
30
- try {
31
- if (fs.existsSync(filePath)) return;
32
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
33
- fs.writeFileSync(filePath, template(), "utf8");
34
- process.stdout.write(`[metheus-governance-mcp-cli] created ${filePath}\n`);
35
- } catch (err) {
36
- process.stderr.write(
37
- `[metheus-governance-mcp-cli] could not create ${filePath}: ${String(err?.message || err)}\n`,
38
- );
32
+ for (const [provider, tokenKey, label] of PROVIDERS) {
33
+ const filePath = resolveTargetPath(provider);
34
+ if (!filePath) continue;
35
+ try {
36
+ if (fs.existsSync(filePath)) continue;
37
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
38
+ fs.writeFileSync(filePath, template(label, tokenKey), "utf8");
39
+ process.stdout.write(`[metheus-governance-mcp-cli] created ${filePath}\n`);
40
+ } catch (err) {
41
+ process.stderr.write(
42
+ `[metheus-governance-mcp-cli] could not create ${filePath}: ${String(err?.message || err)}\n`,
43
+ );
44
+ }
39
45
  }
40
46
  }
41
47