metheus-governance-mcp-cli 0.2.51 → 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 +294 -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) {
@@ -3967,6 +4166,7 @@ function appendProjectHintToInitialize(responseObj, args, options = {}) {
3967
4166
  const ctxpackEnsureTool = displayToolNameForClient("ctxpack.ensure", useSafeToolAliases);
3968
4167
  const ctxpackMergeBriefTool = displayToolNameForClient("ctxpack.merge.brief", useSafeToolAliases);
3969
4168
  const ctxpackMergeExecuteTool = displayToolNameForClient("ctxpack.merge.execute", useSafeToolAliases);
4169
+ const projectListDestinationsTool = displayToolNameForClient("project.list-telegram-destinations", useSafeToolAliases);
3970
4170
  const meListBotsTool = displayToolNameForClient("me.list-bots", useSafeToolAliases);
3971
4171
  const meSendBotMessageTool = displayToolNameForClient("me.send-bot-message", useSafeToolAliases);
3972
4172
  const hintLines = [
@@ -3980,6 +4180,8 @@ function appendProjectHintToInitialize(responseObj, args, options = {}) {
3980
4180
  `- If user enters a Project ID in text (e.g., "Project ID <uuid>"), pass that exact UUID as \`project_id\` argument when calling \`${projectSummaryTool}\`.`,
3981
4181
  `- \`${projectDescribeTool}\` and \`${projectGetTool}\` are aliases of \`${projectSummaryTool}\`.`,
3982
4182
  `- Never handle a bare Project ID by local file/transcript search before \`${projectSummaryTool}\` succeeds.`,
4183
+ `- For project room/provider checks, call \`${projectListDestinationsTool}\` first. Treat those chat destinations as the normal routing source.`,
4184
+ "- Do not surface webhook target URLs for normal room routing unless the user explicitly asks about advanced relays or webhook debugging.",
3983
4185
  `- Run \`${ctxpackEnsureTool}\` only after \`${projectSummaryTool}\`, and only when additional ctxpack refresh/export context is needed.`,
3984
4186
  "- After project summary, use workitem/evidence/decision tools as follow-up.",
3985
4187
  `- For direct bot posting, call \`${meListBotsTool}\` first, then use \`${meSendBotMessageTool}\`. The proxy delivers with your local provider token config and the current project's saved chat destination.`,
@@ -4083,24 +4285,26 @@ function normalizeChatDestination(record) {
4083
4285
  };
4084
4286
  }
4085
4287
 
4086
- function selectProjectChatDestination(destinations, selectors = {}) {
4288
+ function selectProjectChatDestination(destinations, selectors = {}, provider = "telegram") {
4289
+ const normalizedProvider = normalizeBotProvider(provider);
4290
+ const providerLabel = providerEnvConfig(normalizedProvider).label;
4087
4291
  const list = ensureArray(destinations)
4088
4292
  .map(normalizeChatDestination)
4089
- .filter((item) => item.provider === "telegram" && item.chatID && item.isActive);
4293
+ .filter((item) => item.provider === normalizedProvider && item.chatID && item.isActive);
4090
4294
  const destinationID = String(selectors.destinationID || "").trim();
4091
4295
  const destinationLabel = String(selectors.destinationLabel || "").trim().toLowerCase();
4092
4296
 
4093
4297
  if (destinationID) {
4094
4298
  const match = list.find((item) => item.id === destinationID);
4095
4299
  if (!match) {
4096
- 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`);
4097
4301
  }
4098
4302
  return match;
4099
4303
  }
4100
4304
  if (destinationLabel) {
4101
4305
  const match = list.find((item) => item.label.toLowerCase() === destinationLabel);
4102
4306
  if (!match) {
4103
- 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`);
4104
4308
  }
4105
4309
  return match;
4106
4310
  }
@@ -4108,11 +4312,11 @@ function selectProjectChatDestination(destinations, selectors = {}) {
4108
4312
  return list[0];
4109
4313
  }
4110
4314
  if (list.length === 0) {
4111
- 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`);
4112
4316
  }
4113
4317
  const labels = list.map((item) => item.label || item.id).filter(Boolean);
4114
4318
  throw new Error(
4115
- `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(", ")})`,
4116
4320
  );
4117
4321
  }
4118
4322
 
@@ -4145,11 +4349,6 @@ async function handleLocalBotMessageToolCall({
4145
4349
  throw new Error("project_id is required for local bot delivery; set --project-id during setup or include project_id");
4146
4350
  }
4147
4351
 
4148
- const telegramEnv = loadTelegramEnvConfig();
4149
- if (!telegramEnv.ok) {
4150
- throw new Error(`local telegram env is not ready (${telegramEnv.error})`);
4151
- }
4152
-
4153
4352
  const siteBaseURL = normalizeSiteBaseURL(args.baseURL);
4154
4353
  const encodedBotID = encodeURIComponent(botID);
4155
4354
  const encodedProjectID = encodeURIComponent(projectID);
@@ -4163,12 +4362,13 @@ async function handleLocalBotMessageToolCall({
4163
4362
  if (bot.is_active === false || bot.isActive === false) {
4164
4363
  throw new Error("bot is inactive");
4165
4364
  }
4365
+ const provider = normalizeBotProvider(bot.provider);
4166
4366
 
4167
4367
  const destinations = ensureArray(await getJSONWithAuth(destinationURL, args.timeoutSeconds, token));
4168
4368
  const destination = selectProjectChatDestination(destinations, {
4169
4369
  destinationID,
4170
4370
  destinationLabel,
4171
- });
4371
+ }, provider);
4172
4372
 
4173
4373
  const disableWebPagePreview = boolFromRaw(
4174
4374
  Object.prototype.hasOwnProperty.call(toolArgs, "disable_web_page_preview")
@@ -4185,28 +4385,31 @@ async function handleLocalBotMessageToolCall({
4185
4385
  throw new Error("reply_to_message_id must be a positive integer");
4186
4386
  }
4187
4387
 
4188
- const telegramURL = `https://api.telegram.org/bot${telegramEnv.token}/sendMessage`;
4189
- const telegramPayload = {
4190
- chat_id: destination.chatID,
4191
- text,
4192
- disable_web_page_preview: disableWebPagePreview,
4193
- };
4194
- if (replyToMessageID > 0) {
4195
- 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})`);
4196
4391
  }
4197
4392
 
4198
- const response = await postJSONWithoutAuth(telegramURL, args.timeoutSeconds, telegramPayload);
4199
- const responseJSON = parseJSONText(response.bodyText);
4200
- const deliveryOK = response.statusCode >= 200 && response.statusCode < 300 && Boolean(responseJSON?.ok ?? true);
4201
- 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);
4202
4404
  const errorDetail =
4203
- String(responseJSON?.description || response.bodyText || "").trim() || `telegram api status ${response.statusCode}`;
4204
- 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)) {
4205
4408
  throw new Error(
4206
- `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}`,
4207
4410
  );
4208
4411
  }
4209
- throw new Error(`local telegram delivery failed (${errorDetail})`);
4412
+ throw new Error(`local ${provider} delivery failed (${errorDetail})`);
4210
4413
  }
4211
4414
 
4212
4415
  return jsonRpcResult(
@@ -4214,7 +4417,7 @@ async function handleLocalBotMessageToolCall({
4214
4417
  buildLocalToolRPCResult("me.send-bot-message", {
4215
4418
  ok: true,
4216
4419
  local_delivery: true,
4217
- provider: "telegram",
4420
+ provider,
4218
4421
  bot_id: String(bot.id || botID).trim(),
4219
4422
  bot_name: String(bot.name || "").trim(),
4220
4423
  bot_role: String(bot.bot_role || bot.botRole || "").trim(),
@@ -4222,9 +4425,10 @@ async function handleLocalBotMessageToolCall({
4222
4425
  destination_id: destination.id,
4223
4426
  destination_label: destination.label,
4224
4427
  chat_id: destination.chatID,
4225
- status: response.statusCode,
4226
- url: sanitizeTelegramAPIURL(telegramURL),
4227
- body: responseJSON || response.bodyText,
4428
+ status: delivery.statusCode,
4429
+ url: delivery.url,
4430
+ body: delivery.body,
4431
+ reply_supported: delivery.replySupported,
4228
4432
  }),
4229
4433
  );
4230
4434
  }
@@ -5701,7 +5905,7 @@ function runSetupInternal(flags, options = {}) {
5701
5905
  const context = resolveSetupContext(flags);
5702
5906
  const clients = [...MCP_CLIENTS];
5703
5907
  const results = [];
5704
- const telegramEnv = ensureTelegramEnvTemplate();
5908
+ const providerTemplates = ensureAllProviderEnvTemplates();
5705
5909
 
5706
5910
  for (const cliBin of clients) {
5707
5911
  if (!commandExists(cliBin)) continue;
@@ -5751,12 +5955,16 @@ function runSetupInternal(flags, options = {}) {
5751
5955
  process.stdout.write(`Fallback: ${context.workspaceFallbackDir} (METHEUS_WORKSPACE_DIR)\n`);
5752
5956
  }
5753
5957
  process.stdout.write(`Project: ${context.projectID || "auto-detect from .metheus_ctxpack_sync.json"}\n`);
5754
- if (telegramEnv.error) {
5755
- process.stdout.write(`Telegram: template unavailable (${telegramEnv.error})\n`);
5756
- } else {
5757
- process.stdout.write(
5758
- `Telegram: ${telegramEnv.created ? "template created" : "template ready"} (${telegramEnv.filePath})\n`,
5759
- );
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
+ }
5760
5968
  }
5761
5969
  if (context.ctxpackKey) {
5762
5970
  process.stdout.write(`Ctxpack: ${context.ctxpackKey}\n`);
@@ -5779,12 +5987,17 @@ function runSelftest(flags = {}) {
5779
5987
  const checks = [];
5780
5988
  const push = (name, ok, detail = "") => checks.push({ name, ok: Boolean(ok), detail: String(detail || "") });
5781
5989
 
5782
- 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");
5783
5991
  push(
5784
5992
  "telegram_env_parse_token",
5785
5993
  String(parsedEnv.TELEGRAM_BOT_TOKEN || "") === "abc:123",
5786
5994
  `token=${String(parsedEnv.TELEGRAM_BOT_TOKEN || "(missing)")}`,
5787
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
+ );
5788
6001
 
5789
6002
  try {
5790
6003
  const selected = selectProjectChatDestination(
@@ -5793,6 +6006,7 @@ function runSelftest(flags = {}) {
5793
6006
  { id: "dest-2", provider: "slack", label: "Slack", chat_id: "C123", is_active: true },
5794
6007
  ],
5795
6008
  {},
6009
+ "telegram",
5796
6010
  );
5797
6011
  push(
5798
6012
  "telegram_destination_select_single_active",
@@ -5810,6 +6024,7 @@ function runSelftest(flags = {}) {
5810
6024
  { id: "dest-2", provider: "telegram", label: "Room B", chat_id: "-1002", is_active: true },
5811
6025
  ],
5812
6026
  {},
6027
+ "telegram",
5813
6028
  );
5814
6029
  push("telegram_destination_multi_requires_selector", false, "selection unexpectedly succeeded");
5815
6030
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.51",
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