@rubytech/create-maxy-code 0.1.550 → 0.1.551

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 (48) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/agent-dispatch-rule/dist/index.d.ts +22 -8
  3. package/payload/platform/lib/agent-dispatch-rule/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/agent-dispatch-rule/dist/index.js +47 -13
  5. package/payload/platform/lib/agent-dispatch-rule/dist/index.js.map +1 -1
  6. package/payload/platform/lib/agent-dispatch-rule/src/__tests__/rule.test.ts +68 -9
  7. package/payload/platform/lib/agent-dispatch-rule/src/index.ts +60 -16
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -3
  9. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +4 -0
  10. package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
  11. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.js +2 -2
  12. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/agent-dispatch.test.js.map +1 -1
  13. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-sync-provision.test.js +2 -2
  14. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-sync-provision.test.js.map +1 -1
  15. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-sync-provision.d.ts.map +1 -1
  16. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-sync-provision.js +21 -8
  17. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-sync-provision.js.map +1 -1
  18. package/payload/platform/plugins/telegram/.claude-plugin/plugin.json +1 -1
  19. package/payload/platform/plugins/telegram/PLUGIN.md +6 -4
  20. package/payload/platform/plugins/telegram/mcp/dist/index.js +62 -38
  21. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
  22. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.d.ts +18 -5
  23. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.d.ts.map +1 -1
  24. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.js +34 -6
  25. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.js.map +1 -1
  26. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts +25 -8
  27. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts.map +1 -1
  28. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js +28 -16
  29. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js.map +1 -1
  30. package/payload/platform/plugins/telegram/mcp/dist/lib/agent-active.d.ts +12 -0
  31. package/payload/platform/plugins/telegram/mcp/dist/lib/agent-active.d.ts.map +1 -0
  32. package/payload/platform/plugins/telegram/mcp/dist/lib/agent-active.js +26 -0
  33. package/payload/platform/plugins/telegram/mcp/dist/lib/agent-active.js.map +1 -0
  34. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.d.ts +16 -13
  35. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.d.ts.map +1 -1
  36. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.js +19 -23
  37. package/payload/platform/plugins/telegram/mcp/dist/lib/secret-path.js.map +1 -1
  38. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.d.ts +9 -7
  39. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.d.ts.map +1 -1
  40. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.js +11 -8
  41. package/payload/platform/plugins/telegram/mcp/dist/lib/webhook-url.js.map +1 -1
  42. package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts +4 -0
  43. package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts.map +1 -1
  44. package/payload/platform/plugins/telegram/mcp/dist/tools/message.js +13 -3
  45. package/payload/platform/plugins/telegram/mcp/dist/tools/message.js.map +1 -1
  46. package/payload/platform/plugins/telegram/references/setup-guide.md +16 -8
  47. package/payload/platform/plugins/telegram/skills/configure/SKILL.md +30 -32
  48. package/payload/server/server.js +510 -240
@@ -4,11 +4,28 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
4
  * with platform/ui/server/public-host-gate.ts PUBLIC_ALLOWED_PREFIXES — a unit
5
5
  * test asserts they agree. The official plugin's /channels/telegram is NOT this. */
6
6
  export const CANONICAL_TELEGRAM_PREFIX = "/api/telegram/";
7
- /** Persist the bot token + access policy into account.json's telegram block.
7
+ /** Task 2368 duplicated from platform/ui/app/lib/telegram/bot-registry.ts.
8
+ * The plugin cannot import from platform/ui/app, the same boundary that makes
9
+ * configDirName and the secret path duplicates; telegram-account-write.test.ts
10
+ * asserts the two derivations agree. */
11
+ export function botIdFromToken(token) {
12
+ const i = token.indexOf(":");
13
+ if (i <= 0)
14
+ return null;
15
+ const id = token.slice(0, i);
16
+ return /^\d+$/.test(id) ? id : null;
17
+ }
18
+ /** Persist one bot entry into account.json's `telegram.bots` array, replacing
19
+ * any entry with the same id and leaving every other entry byte-identical.
20
+ *
21
+ * Task 2368 — registering, re-pointing or removing one bot cannot disturb
22
+ * another, which the two-slot shape could not promise: a second admin bot
23
+ * overwrote the first's token AND its adminUsers in one write.
24
+ *
8
25
  * Brand-isolated: account.json lives under the install's data/accounts. Returns
9
- * the resolved account.json directory so the caller can log + assert it is under
10
- * the brand install (never ~/.claude). */
11
- export function persistTelegramConfig(platformRoot, accountId, input) {
26
+ * the resolved account.json directory so the caller can log + assert it is
27
+ * under the brand install (never ~/.claude). */
28
+ export function persistTelegramBot(platformRoot, accountId, entry) {
12
29
  const accountsDir = resolve(platformRoot, "..", "data", "accounts");
13
30
  const dir = join(accountsDir, accountId);
14
31
  const configPath = join(dir, "account.json");
@@ -33,18 +50,13 @@ export function persistTelegramConfig(platformRoot, accountId, input) {
33
50
  return { ok: false, error: `account.json parse failed: ${e.message}` };
34
51
  }
35
52
  const telegram = config.telegram ?? {};
36
- if (input.botType === "admin") {
37
- telegram.adminBotToken = input.botToken;
38
- if (input.adminUsers)
39
- telegram.adminUsers = input.adminUsers;
40
- }
41
- else {
42
- telegram.publicBotToken = input.botToken;
43
- if (input.dmPolicy)
44
- telegram.dmPolicy = input.dmPolicy;
45
- if (input.allowFrom)
46
- telegram.allowFrom = input.allowFrom;
47
- }
53
+ const bots = Array.isArray(telegram.bots) ? telegram.bots : [];
54
+ const at = bots.findIndex((b) => b && typeof b === "object" && b.id === entry.id);
55
+ if (at >= 0)
56
+ bots[at] = entry;
57
+ else
58
+ bots.push(entry);
59
+ telegram.bots = bots;
48
60
  config.telegram = telegram;
49
61
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
50
62
  return { ok: true, dir };
@@ -1 +1 @@
1
- {"version":3,"file":"account-write.js","sourceRoot":"","sources":["../../src/lib/account-write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAElE;;qFAEqF;AACrF,MAAM,CAAC,MAAM,yBAAyB,GAAG,gBAAgB,CAAC;AAU1D;;;2CAG2C;AAC3C,MAAM,UAAU,qBAAqB,CACnC,YAAoB,EACpB,SAAiB,EACjB,KAAmB;IAEnB,MAAM,WAAW,GAAG,OAAO,CAAC,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IACpE,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAC7C,0EAA0E;IAC1E,4EAA4E;IAC5E,8EAA8E;IAC9E,+CAA+C;IAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9C,MAAM,UAAU,GAAG,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7E,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,6BAA6B,UAAU,EAAE,EAAE,CAAC;IACzE,CAAC;IACD,IAAI,MAA+B,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,8BAA+B,CAAW,CAAC,OAAO,EAAE,EAAE,CAAC;IACpF,CAAC;IACD,MAAM,QAAQ,GAAI,MAAM,CAAC,QAAoC,IAAI,EAAE,CAAC;IACpE,IAAI,KAAK,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;QAC9B,QAAQ,CAAC,aAAa,GAAG,KAAK,CAAC,QAAQ,CAAC;QACxC,IAAI,KAAK,CAAC,UAAU;YAAE,QAAQ,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;IAC/D,CAAC;SAAM,CAAC;QACN,QAAQ,CAAC,cAAc,GAAG,KAAK,CAAC,QAAQ,CAAC;QACzC,IAAI,KAAK,CAAC,QAAQ;YAAE,QAAQ,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;QACvD,IAAI,KAAK,CAAC,SAAS;YAAE,QAAQ,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IAC5D,CAAC;IACD,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;IAC3E,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AAC3B,CAAC"}
1
+ {"version":3,"file":"account-write.js","sourceRoot":"","sources":["../../src/lib/account-write.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /** The account's own directory under the brand install. */
2
+ export declare function accountDirFor(platformRoot: string, accountId: string): string;
3
+ /** Task 2368 — is `slug` an ACTIVE agent on this account?
4
+ *
5
+ * Duplicated from platform/ui/app/lib/claude-agent/account.ts
6
+ * `isActiveAgentSlug`, which the plugin cannot import across the
7
+ * plugin/platform boundary. Same predicate: status must be exactly "active",
8
+ * and a missing agent dir or config resolves to no status, so this also
9
+ * rejects a nonexistent slug. telegram-agent-active.test.ts asserts the two
10
+ * agree. A bot may only be paired to an agent that can actually answer. */
11
+ export declare function isActiveAgentSlug(accountDir: string, slug: string): boolean;
12
+ //# sourceMappingURL=agent-active.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-active.d.ts","sourceRoot":"","sources":["../../src/lib/agent-active.ts"],"names":[],"mappings":"AAGA,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED;;;;;;;4EAO4E;AAC5E,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAQ3E"}
@@ -0,0 +1,26 @@
1
+ import { resolve, join } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ /** The account's own directory under the brand install. */
4
+ export function accountDirFor(platformRoot, accountId) {
5
+ return join(resolve(platformRoot, "..", "data", "accounts"), accountId);
6
+ }
7
+ /** Task 2368 — is `slug` an ACTIVE agent on this account?
8
+ *
9
+ * Duplicated from platform/ui/app/lib/claude-agent/account.ts
10
+ * `isActiveAgentSlug`, which the plugin cannot import across the
11
+ * plugin/platform boundary. Same predicate: status must be exactly "active",
12
+ * and a missing agent dir or config resolves to no status, so this also
13
+ * rejects a nonexistent slug. telegram-agent-active.test.ts asserts the two
14
+ * agree. A bot may only be paired to an agent that can actually answer. */
15
+ export function isActiveAgentSlug(accountDir, slug) {
16
+ const configPath = join(accountDir, "agents", slug, "config.json");
17
+ if (!existsSync(configPath))
18
+ return false;
19
+ try {
20
+ return JSON.parse(readFileSync(configPath, "utf-8")).status === "active";
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ //# sourceMappingURL=agent-active.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-active.js","sourceRoot":"","sources":["../../src/lib/agent-active.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEnD,2DAA2D;AAC3D,MAAM,UAAU,aAAa,CAAC,YAAoB,EAAE,SAAiB;IACnE,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,SAAS,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;;;4EAO4E;AAC5E,MAAM,UAAU,iBAAiB,CAAC,UAAkB,EAAE,IAAY;IAChE,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;IACnE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,IAAI,CAAC;QACH,OAAQ,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAA0B,CAAC,MAAM,KAAK,QAAQ,CAAC;IACrG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -1,16 +1,19 @@
1
1
  export declare function configDirName(): string;
2
- /** Derive the webhook secret file path. Restricted to two known filename stems
3
- * so botType can never contribute a traversal.
2
+ /** The directory holding one secret file per registered bot. Created mode 700
3
+ * by the register tool; the secrets themselves are 600. */
4
+ export declare function telegramSecretsDir(): string;
5
+ /** Task 2368 — the webhook secret path, keyed on the bot's own Telegram id.
4
6
  *
5
- * Task 2365 the accountId suffix. Before it, one install had one secret file
6
- * per bot type, so a second account registering an admin bot overwrote the
7
- * first account's secret and broke it with no signal. The reader
8
- * (platform/ui/server/routes/telegram.ts secretPath) builds the same string;
9
- * the duplication across the plugin/platform boundary is deliberate and
10
- * ui/server/__tests__/telegram-secret-path.test.ts asserts the two agree. */
11
- export declare function resolveSecretFilePath(botType: "public" | "admin", accountId: string): string;
12
- /** The un-suffixed path this install used before Task 2365. The register tool
13
- * deletes it after a successful re-registration so a stale, install-wide
14
- * secret cannot outlive the migration. */
15
- export declare function legacySecretFilePath(botType: "public" | "admin"): string;
7
+ * Task 2365 keyed on (account, botType), which cannot express N bots in one
8
+ * account: a second admin bot in one account would overwrite the first's
9
+ * secret and break it with no signal. The bot id can, and it is the same
10
+ * value the registered URL names, so the file and the URL cannot disagree.
11
+ * The reader (platform/ui/server/routes/telegram.ts secretPath) builds the
12
+ * same string; the duplication across the plugin/platform boundary is
13
+ * deliberate and ui/server/__tests__/telegram-secret-path.test.ts asserts the
14
+ * two agree.
15
+ *
16
+ * `botId` is digits by construction (botIdFromToken rejects anything else), so
17
+ * it cannot contribute a traversal. */
18
+ export declare function resolveSecretFilePath(botId: string): string;
16
19
  //# sourceMappingURL=secret-path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"secret-path.d.ts","sourceRoot":"","sources":["../../src/lib/secret-path.ts"],"names":[],"mappings":"AAYA,wBAAgB,aAAa,IAAI,MAAM,CAWtC;AAED;;;;;;;;8EAQ8E;AAC9E,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAM5F;AAED;;2CAE2C;AAC3C,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAKxE"}
1
+ {"version":3,"file":"secret-path.d.ts","sourceRoot":"","sources":["../../src/lib/secret-path.ts"],"names":[],"mappings":"AAYA,wBAAgB,aAAa,IAAI,MAAM,CAWtC;AAED;4DAC4D;AAC5D,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;;;;;;;;wCAYwC;AACxC,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3D"}
@@ -22,29 +22,25 @@ export function configDirName() {
22
22
  }
23
23
  return ".maxy";
24
24
  }
25
- /** Derive the webhook secret file path. Restricted to two known filename stems
26
- * so botType can never contribute a traversal.
27
- *
28
- * Task 2365 — the accountId suffix. Before it, one install had one secret file
29
- * per bot type, so a second account registering an admin bot overwrote the
30
- * first account's secret and broke it with no signal. The reader
31
- * (platform/ui/server/routes/telegram.ts secretPath) builds the same string;
32
- * the duplication across the plugin/platform boundary is deliberate and
33
- * ui/server/__tests__/telegram-secret-path.test.ts asserts the two agree. */
34
- export function resolveSecretFilePath(botType, accountId) {
35
- const maxyDir = resolve(homedir(), configDirName());
36
- const stem = botType === "admin"
37
- ? ".telegram-admin-webhook-secret"
38
- : ".telegram-webhook-secret";
39
- return resolve(maxyDir, `${stem}.${accountId}`);
25
+ /** The directory holding one secret file per registered bot. Created mode 700
26
+ * by the register tool; the secrets themselves are 600. */
27
+ export function telegramSecretsDir() {
28
+ return resolve(homedir(), configDirName(), "telegram-secrets");
40
29
  }
41
- /** The un-suffixed path this install used before Task 2365. The register tool
42
- * deletes it after a successful re-registration so a stale, install-wide
43
- * secret cannot outlive the migration. */
44
- export function legacySecretFilePath(botType) {
45
- const stem = botType === "admin"
46
- ? ".telegram-admin-webhook-secret"
47
- : ".telegram-webhook-secret";
48
- return resolve(homedir(), configDirName(), stem);
30
+ /** Task 2368 the webhook secret path, keyed on the bot's own Telegram id.
31
+ *
32
+ * Task 2365 keyed on (account, botType), which cannot express N bots in one
33
+ * account: a second admin bot in one account would overwrite the first's
34
+ * secret and break it with no signal. The bot id can, and it is the same
35
+ * value the registered URL names, so the file and the URL cannot disagree.
36
+ * The reader (platform/ui/server/routes/telegram.ts secretPath) builds the
37
+ * same string; the duplication across the plugin/platform boundary is
38
+ * deliberate and ui/server/__tests__/telegram-secret-path.test.ts asserts the
39
+ * two agree.
40
+ *
41
+ * `botId` is digits by construction (botIdFromToken rejects anything else), so
42
+ * it cannot contribute a traversal. */
43
+ export function resolveSecretFilePath(botId) {
44
+ return resolve(telegramSecretsDir(), botId);
49
45
  }
50
46
  //# sourceMappingURL=secret-path.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"secret-path.js","sourceRoot":"","sources":["../../src/lib/secret-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEnD,2EAA2E;AAC3E,uEAAuE;AACvE,mCAAmC;AACnC,EAAE;AACF,wDAAwD;AACxD,6EAA6E;AAC7E,0EAA0E;AAC1E,kEAAkE;AAClE,MAAM,UAAU,aAAa;IAC3B,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC/C,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;QAC7D,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC;gBACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC;YAC3E,CAAC;YAAC,MAAM,CAAC,CAAC,wBAAwB,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;8EAQ8E;AAC9E,MAAM,UAAU,qBAAqB,CAAC,OAA2B,EAAE,SAAiB;IAClF,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,OAAO,KAAK,OAAO;QAC9B,CAAC,CAAC,gCAAgC;QAClC,CAAC,CAAC,0BAA0B,CAAC;IAC/B,OAAO,OAAO,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,SAAS,EAAE,CAAC,CAAC;AAClD,CAAC;AAED;;2CAE2C;AAC3C,MAAM,UAAU,oBAAoB,CAAC,OAA2B;IAC9D,MAAM,IAAI,GAAG,OAAO,KAAK,OAAO;QAC9B,CAAC,CAAC,gCAAgC;QAClC,CAAC,CAAC,0BAA0B,CAAC;IAC/B,OAAO,OAAO,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,EAAE,IAAI,CAAC,CAAC;AACnD,CAAC"}
1
+ {"version":3,"file":"secret-path.js","sourceRoot":"","sources":["../../src/lib/secret-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEnD,2EAA2E;AAC3E,uEAAuE;AACvE,mCAAmC;AACnC,EAAE;AACF,wDAAwD;AACxD,6EAA6E;AAC7E,0EAA0E;AAC1E,kEAAkE;AAClE,MAAM,UAAU,aAAa;IAC3B,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC/C,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;QAC7D,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC;gBACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC;YAC3E,CAAC;YAAC,MAAM,CAAC,CAAC,wBAAwB,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;4DAC4D;AAC5D,MAAM,UAAU,kBAAkB;IAChC,OAAO,OAAO,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,EAAE,kBAAkB,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;;;;;;;wCAYwC;AACxC,MAAM,UAAU,qBAAqB,CAAC,KAAa;IACjD,OAAO,OAAO,CAAC,kBAAkB,EAAE,EAAE,KAAK,CAAC,CAAC;AAC9C,CAAC"}
@@ -1,8 +1,10 @@
1
- /** Task 2365 — stamp the registering spawn's own account onto the webhook URL.
2
- * The route requires it and validates it against listValidAccounts(); an
3
- * operator transcribing it from a skill file is the failure mode this task
4
- * exists to fix, so the tool builds it. An account the caller already supplied
5
- * is overwritten: the spawn's ACCOUNT_ID is the only authority on which
6
- * account this registration belongs to. */
7
- export declare function buildWebhookUrl(webhookUrl: string, accountId: string): string;
1
+ /** Task 2368 — stamp the bot's own id onto the webhook URL.
2
+ *
3
+ * It supersedes Task 2365's `account=`: a bot id already determines its
4
+ * account, and carrying both would be two authorities for one fact. Any `bot`
5
+ * the caller supplied is overwritten and any `account` is removed the token
6
+ * this registration carries is the only authority on which bot it is for, and
7
+ * an operator transcribing either from a skill file is the failure mode this
8
+ * builder exists to remove. */
9
+ export declare function buildWebhookUrl(webhookUrl: string, botId: string): string;
8
10
  //# sourceMappingURL=webhook-url.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-url.d.ts","sourceRoot":"","sources":["../../src/lib/webhook-url.ts"],"names":[],"mappings":"AAAA;;;;;4CAK4C;AAC5C,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAI7E"}
1
+ {"version":3,"file":"webhook-url.d.ts","sourceRoot":"","sources":["../../src/lib/webhook-url.ts"],"names":[],"mappings":"AAAA;;;;;;;gCAOgC;AAChC,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKzE"}
@@ -1,12 +1,15 @@
1
- /** Task 2365 — stamp the registering spawn's own account onto the webhook URL.
2
- * The route requires it and validates it against listValidAccounts(); an
3
- * operator transcribing it from a skill file is the failure mode this task
4
- * exists to fix, so the tool builds it. An account the caller already supplied
5
- * is overwritten: the spawn's ACCOUNT_ID is the only authority on which
6
- * account this registration belongs to. */
7
- export function buildWebhookUrl(webhookUrl, accountId) {
1
+ /** Task 2368 — stamp the bot's own id onto the webhook URL.
2
+ *
3
+ * It supersedes Task 2365's `account=`: a bot id already determines its
4
+ * account, and carrying both would be two authorities for one fact. Any `bot`
5
+ * the caller supplied is overwritten and any `account` is removed the token
6
+ * this registration carries is the only authority on which bot it is for, and
7
+ * an operator transcribing either from a skill file is the failure mode this
8
+ * builder exists to remove. */
9
+ export function buildWebhookUrl(webhookUrl, botId) {
8
10
  const u = new URL(webhookUrl);
9
- u.searchParams.set("account", accountId);
11
+ u.searchParams.delete("account");
12
+ u.searchParams.set("bot", botId);
10
13
  return u.toString();
11
14
  }
12
15
  //# sourceMappingURL=webhook-url.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-url.js","sourceRoot":"","sources":["../../src/lib/webhook-url.ts"],"names":[],"mappings":"AAAA;;;;;4CAK4C;AAC5C,MAAM,UAAU,eAAe,CAAC,UAAkB,EAAE,SAAiB;IACnE,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IAC9B,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;AACtB,CAAC"}
1
+ {"version":3,"file":"webhook-url.js","sourceRoot":"","sources":["../../src/lib/webhook-url.ts"],"names":[],"mappings":"AAAA;;;;;;;gCAOgC;AAChC,MAAM,UAAU,eAAe,CAAC,UAAkB,EAAE,KAAa;IAC/D,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IAC9B,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACjC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACjC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;AACtB,CAAC"}
@@ -1,5 +1,9 @@
1
1
  interface MessageParams {
2
2
  channel: "telegram";
3
+ /** Task 2368 — which registered bot to send as. An account holds N bots, each
4
+ * with its own token, so "the account's token" is no longer a thing that can
5
+ * be resolved without naming one. */
6
+ botId: string;
3
7
  chatId: string | number;
4
8
  text: string;
5
9
  parseMode?: "HTML" | "Markdown" | "MarkdownV2";
@@ -1 +1 @@
1
- {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/tools/message.ts"],"names":[],"mappings":"AAGA,UAAU,aAAa;IACrB,OAAO,EAAE,UAAU,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,YAAY,CAAC;IAC/C,cAAc,CAAC,EAAE,KAAK,CACpB,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAC7D,CAAC;CACH;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAwC3E"}
1
+ {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/tools/message.ts"],"names":[],"mappings":"AAGA,UAAU,aAAa;IACrB,OAAO,EAAE,UAAU,CAAC;IACpB;;0CAEsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,YAAY,CAAC;IAC/C,cAAc,CAAC,EAAE,KAAK,CACpB,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAC7D,CAAC;CACH;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAiD3E"}
@@ -1,14 +1,24 @@
1
1
  import { sendMessage } from "../lib/telegram.js";
2
- import { resolveBotToken } from "../lib/account-token.js";
2
+ import { resolveBotToken, listRegisteredBots } from "../lib/account-token.js";
3
3
  export async function message(params) {
4
4
  const platformRoot = process.env.PLATFORM_ROOT;
5
5
  const accountId = process.env.ACCOUNT_ID;
6
- const botToken = platformRoot && accountId ? resolveBotToken(platformRoot, accountId) : null;
6
+ const botToken = platformRoot && accountId ? resolveBotToken(platformRoot, accountId, params.botId) : null;
7
7
  if (!botToken) {
8
+ // Task 2368 — an account holds N bots and nothing else surfaces their ids to
9
+ // an agent, so the refusal enumerates them rather than failing opaquely. A
10
+ // caller that named the wrong bot, or guessed, gets the right answer here and
11
+ // succeeds on its next call. It never picks one: which bot a message goes out
12
+ // on is the caller's decision, and choosing would put a stranger's message on
13
+ // the operator's own bot.
14
+ const registered = platformRoot && accountId ? listRegisteredBots(platformRoot, accountId) : [];
15
+ const known = registered.length === 0
16
+ ? "This account has no Telegram bot registered — run telegram:configure first."
17
+ : `This account's registered bots: ${registered.map((b) => `${b.id} (${b.role}${b.agent ? `, ${b.agent}` : ""})`).join("; ")}.`;
8
18
  return {
9
19
  sent: false,
10
20
  channel: params.channel,
11
- error: "No Telegram bot token in account.json telegram block run telegram:configure first",
21
+ error: `No Telegram bot "${params.botId}" is registered on this account. ${known}`,
12
22
  };
13
23
  }
14
24
  let replyMarkup;
@@ -1 +1 @@
1
- {"version":3,"file":"message.js","sourceRoot":"","sources":["../../src/tools/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAA+B,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAmB1D,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAAqB;IACjD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IACzC,MAAM,QAAQ,GACZ,YAAY,IAAI,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9E,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,KAAK,EACH,qFAAqF;SACxF,CAAC;IACJ,CAAC;IAED,IAAI,WAA+C,CAAC;IACpD,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC1B,WAAW,GAAG;YACZ,eAAe,EAAE,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACjD,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAChB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,aAAa,EAAE,GAAG,CAAC,YAAY;gBAC/B,GAAG,EAAE,GAAG,CAAC,GAAG;aACb,CAAC,CAAC,CACJ;SACF,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE;QACzC,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW;KACZ,CAAC,CAAC;IAEH,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,EAAE;QACf,OAAO,EAAE,UAAU;QACnB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"message.js","sourceRoot":"","sources":["../../src/tools/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAA+B,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAuB9E,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAAqB;IACjD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IACzC,MAAM,QAAQ,GACZ,YAAY,IAAI,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,YAAY,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,6EAA6E;QAC7E,2EAA2E;QAC3E,8EAA8E;QAC9E,8EAA8E;QAC9E,8EAA8E;QAC9E,0BAA0B;QAC1B,MAAM,UAAU,GAAG,YAAY,IAAI,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAChG,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,KAAK,CAAC;YACnC,CAAC,CAAC,6EAA6E;YAC/E,CAAC,CAAC,mCAAmC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;QAClI,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,KAAK,EAAE,oBAAoB,MAAM,CAAC,KAAK,oCAAoC,KAAK,EAAE;SACnF,CAAC;IACJ,CAAC;IAED,IAAI,WAA+C,CAAC;IACpD,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC1B,WAAW,GAAG;YACZ,eAAe,EAAE,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACjD,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAChB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,aAAa,EAAE,GAAG,CAAC,YAAY;gBAC/B,GAAG,EAAE,GAAG,CAAC,GAAG;aACb,CAAC,CAAC,CACJ;SACF,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE;QACzC,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW;KACZ,CAAC,CAAC;IAEH,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,EAAE;QACf,OAAO,EAAE,UAAU;QACnB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;AACJ,CAAC"}
@@ -15,22 +15,30 @@ Setup happens entirely in the admin chat — there is no settings page to click
15
15
  1. Tell the admin agent: "I want to connect a Telegram bot." The agent runs the `telegram:configure` skill.
16
16
  2. Paste the bot token from BotFather when the agent asks.
17
17
  3. Tell the agent whether this is an **admin bot** or a **public bot** (see below).
18
- 4. For an admin bot, give the agent the numeric Telegram user IDs that should have admin access. For a public bot, choose a DM policy (who can message it) and, for a whitelist, the allowed numeric IDs.
19
- 5. The agent verifies the token, registers the webhook on the brand's edge path, stores the token under the brand install, and — for a public bot — sets the agent that will respond.
20
- 6. The agent confirms the bot is connected. Message the bot; the reply comes back from the agent.
18
+ 4. For an admin bot, give the agent the numeric Telegram user IDs that should have admin access. For a public bot, choose which of your agents answers on it, a DM policy (who can message it), and, for a whitelist, the allowed numeric IDs.
19
+ 5. The agent verifies the token, registers the webhook on the brand's edge path, and stores the bot under the brand install.
20
+ 6. The agent confirms the bot is connected. Message the bot; the reply comes back from the agent you chose.
21
21
 
22
22
  The token is stored in your account config under the brand install (never in a shared `~/.claude` directory), and the webhook is registered on the `/api/telegram/` path the brand's edge admits.
23
23
 
24
- The bot belongs to the account you set it up from. The agent stamps that account onto the registered address and gives the bot its own secret, so setting up a bot for one account never disturbs another account's bot. If you connected a bot before this changed, it stops delivering until you run the setup once more; the agent can tell you which one from the standing check.
24
+ ## How Many Bots You Can Have
25
+
26
+ As many as you like. Your account holds a list of bots, and each one is its own thing: its own token, its own role, its own list of who may message it, and — for a public bot — its own agent. Adding, re-pointing or removing one bot never touches another, in your account or anyone else's.
27
+
28
+ Each bot is identified by its own Telegram id, which is the part of the token before the colon. The agent reads it off the token you paste, puts it on the registered address for you, and gives that bot its own secret file. You never type it.
29
+
30
+ Two people talking to two of your bots are two separate conversations, and so are one person talking to two of them: each bot keeps its own thread with each person.
31
+
32
+ If you connected a bot before this changed, it stops delivering until you run the setup once more. The agent can tell you which ones from the standing check.
25
33
 
26
34
  ## Admin Bot vs Public Bot
27
35
 
28
- Each Telegram bot has an **Admin routing** toggle:
36
+ Every bot is one or the other, and you say which when you set it up. It is not worked out from anything else.
29
37
 
30
- - **Admin bot** (toggle ON): Only users with admin bindings can message this bot. Everyone else is silently denied. Messages route to the admin agent. No DM policy applies — it's admin-only by design.
31
- - **Public bot** (toggle OFF): DM policy applies to everyone equally, including admins. Messages route to the public agent. Use this for customer-facing bots.
38
+ - **Admin bot**: only the numeric Telegram IDs you listed for THAT bot can message it. Everyone else is silently denied. It answers as your admin agent, with your account's tools.
39
+ - **Public bot**: customer-facing. A DM policy decides who may message it, and it answers as the agent you named when you set it up. That agent has to be one of your account's active agents; if you switch the agent off later, the bot stops answering and says so rather than falling back to another one.
32
40
 
33
- A common setup is two bots: one admin bot for the business owner, one public bot for customers.
41
+ A common setup is two bots: one admin bot for the business owner, one public bot for customers. Nothing stops you having five.
34
42
 
35
43
  ## Finding Your Telegram User ID
36
44
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: configure
3
- description: Guide the user through connecting a Telegram bot entirely via the admin chat — store the BotFather token under the brand install, register the webhook on the maxy edge-admitted path, set access policy, and select the agent. Complete PTY setup, no UI clicks.
3
+ description: Guide the user through connecting a Telegram bot entirely via the admin chat — store the BotFather token under the brand install, register the webhook on the maxy edge-admitted path, set the bot's role and access policy, and pair a public bot to one of the account's agents. An account may hold any number of bots. Complete PTY setup, no UI clicks.
4
4
  ---
5
5
 
6
6
  # Configure Telegram
@@ -15,11 +15,14 @@ User says anything about connecting, linking, or setting up Telegram, BotFather,
15
15
 
16
16
  A finished Telegram setup has all of these, and the skill is not done until each is true:
17
17
 
18
- - The bot token from BotFather is stored in `account.json`'s `telegram` block (under the brand install, never `~/.claude/`). The `telegram-webhook-register` tool writes it there and logs `[telegram-setup] op=token-write dir=… brand=…`.
19
- - The webhook is registered against a URL whose path is `/api/telegram/` (the only path the maxy edge admits). The register tool refuses any other path and logs `op=webhook-register path=… account=… admitted=false` when it does.
20
- - The registered URL carries `account=<accountId>`, naming the account this bot belongs to. The register tool appends it from the calling account, so you never type it. The receiver requires it and rejects `reason=missing-account-param`; without it, one install's accounts would all read the same bot config.
21
- - Access policy is set: admin bots carry the operator's numeric Telegram user IDs (`adminUsers`); public bots carry a DM policy (and an allowlist when the policy is `allowlist`).
22
- - For a public bot, `account.json` `defaultAgent` names an existing public agent. Without it, public inbound is dropped with `op=reject reason=no-default-agent`.
18
+ - The bot is stored as one entry in `account.json`'s `telegram.bots` array (under the brand install, never `~/.claude/`). The `telegram-webhook-register` tool writes it there and logs `[telegram-setup] op=token-write dir=… brand=…`.
19
+ - The webhook is registered against a URL whose path is `/api/telegram/` (the only path the maxy edge admits). The register tool refuses any other path and logs `op=webhook-register path=… botId=… account=… admitted=false` when it does.
20
+ - The registered URL carries `bot=<botId>`, the bot's own Telegram id. The register tool derives it from the token and appends it, so you never type it. The receiver requires it and rejects `reason=unknown-bot`; the bot id is what determines the account, so no `account=` is carried.
21
+ - The bot's `role` is stated, not inferred: `admin` or `public`. It selects the spawn path, so it is a privilege decision.
22
+ - Access policy is set **on that entry**: an admin bot carries the operator's numeric Telegram user IDs (`adminUsers`); a public bot carries a DM policy (and an allowlist when the policy is `allowlist`). Nothing is shared with the account's other bots.
23
+ - A public bot names the `agent` it answers as, and that agent is active. An admin bot names none: it reads `agents/admin` by construction.
24
+
25
+ An account may hold any number of bots. Registering, re-pointing or removing one never disturbs another, in any account.
23
26
 
24
27
  ## Flow
25
28
 
@@ -29,54 +32,48 @@ Read `references/setup-guide.md` (cited throughout this flow) with the `plugin-r
29
32
 
30
33
  Follow `references/setup-guide.md` "Creating a Bot": the operator creates the bot via @BotFather and gives you the token (e.g. `123456:ABC-DEF…`). Confirm you have it before continuing.
31
34
 
32
- ### Step 2 — Decide admin or public
35
+ ### Step 2 — Decide the role, and for a public bot its agent
36
+
37
+ Ask which role this bot serves (see `references/setup-guide.md` "Admin Bot vs Public Bot"). The role is stated by the operator, never worked out from anything else: it decides which spawn path the bot's messages take.
33
38
 
34
- Ask which role this bot serves (see `references/setup-guide.md` "Admin Bot vs Public Bot"):
39
+ - **Admin bot** only the numeric Telegram user IDs the operator names for THIS bot may message it; everything else is denied. Collect those IDs (`references/setup-guide.md` "Finding Your Telegram User ID"). It answers as the admin agent.
40
+ - **Public bot** — customer-facing; choose a DM policy (`open`, `allowlist`, or `disabled`), for `allowlist` the allowed numeric IDs, and **which of this account's agents answers on it**. Call `account-manage` to list the account's agents and let the operator pick one. The agent must be active; the register tool refuses an inactive or unknown slug.
35
41
 
36
- - **Admin bot** only the operator's numeric Telegram user IDs may message it; everything else is denied. Collect those IDs (`references/setup-guide.md` "Finding Your Telegram User ID").
37
- - **Public bot** — customer-facing; choose a DM policy (`open`, `allowlist`, or `disabled`) and, for `allowlist`, the allowed numeric IDs.
42
+ The account may already hold other bots. This one is added alongside them and shares nothing with them.
38
43
 
39
44
  ### Step 3 — Build the webhook URL
40
45
 
41
- The path MUST be `/api/telegram/` with `?bot=admin` or `?bot=public`. Use the account's domain for the host — the admin domain for an admin bot, the public domain for a public bot (read them from `brand-settings` / the account config). Example: `https://<admin-domain>/api/telegram/?bot=admin`.
46
+ The path MUST be `/api/telegram/`. Use the account's domain for the host — the admin domain for an admin bot, the public domain for a public bot (read them from `brand-settings` / the account config). Example: `https://<admin-domain>/api/telegram/`.
42
47
 
43
- Do NOT add `?account=`. The register tool appends it from the calling account, which is the only authority on which account the bot belongs to; a value you type could name a different one.
48
+ Do NOT add `?bot=` or `?account=`. The register tool derives the bot id from the token and stamps `?bot=<botId>` on for you; the token is the only authority on which bot it is, and a value typed by hand could name a different one.
44
49
 
45
50
  The register tool aborts on any other path, so do not invent `/channels/telegram` or similar.
46
51
 
47
- ### Step 4 — Register (stores token + policy + webhook + secret in one call)
52
+ ### Step 4 — Register (stores the entry + webhook + secret in one call)
48
53
 
49
54
  Call `telegram-webhook-register` with:
50
55
  - `botToken` — the BotFather token
51
56
  - `webhookUrl` — the URL from Step 3
52
- - `botType` — `admin` or `public`
57
+ - `role` — `admin` or `public`
53
58
  - admin bot: `adminUsers` — the numeric IDs from Step 2
54
- - public bot: `dmPolicy` and, for `allowlist`, `allowFrom`
55
-
56
- On success the tool has: verified the token, set the webhook with a generated secret (written to the brand configDir), persisted the token + policy into `account.json`'s `telegram` block, and logged `op=token-write` + `op=webhook-register admitted=true`. If it returns an error, read it back to the operator verbatim — the common ones are a wrong path (`admitted=false`) or a token write blocked outside the brand install (`op=token-write-rejected`).
57
-
58
- ### Step 5 — Select the agent (public bot)
59
-
60
- Public inbound routes to the account's `defaultAgent`. Set it and confirm:
61
-
62
- 1. `account-manage` to list available public agents.
63
- 2. `account-update` with `field: "defaultAgent"`, `value: "<slug>"`.
64
- 3. Re-read by calling `account-manage` (or `account-update` reports the change) and confirm `defaultAgent` is the chosen slug. The post-condition is a non-empty `defaultAgent`, not the intent to set it.
59
+ - public bot: `agent` — the slug from Step 2, plus `dmPolicy` and, for `allowlist`, `allowFrom`
65
60
 
66
- An admin bot does not need `defaultAgent` admin inbound routes to the admin agent.
61
+ On success the tool has: verified the token, derived the bot id, set the webhook with a generated secret (written to `~/.<brand>/telegram-secrets/<botId>`), persisted the entry into `account.json`'s `telegram.bots` array, and logged `op=token-write` + `op=webhook-register admitted=true`. It replaces only the entry with this bot's id, so the account's other bots are untouched. If it returns an error, read it back to the operator verbatim — the common ones are a wrong path (`admitted=false`), a public bot with no or an inactive `agent` (`op=token-write-rejected reason=no-agent|agent-inactive`), or a token write blocked outside the brand install.
67
62
 
68
- ### Step 6 — Confirm
63
+ ### Step 5 — Confirm
69
64
 
70
- Tell the operator the bot is connected, which role it has, and that messaging the bot now reaches the agent and a reply returns. For a public bot, name the agent that will respond.
65
+ Tell the operator the bot is connected, which role it has, and that messaging the bot now reaches the agent and a reply returns. For a public bot, name the agent that will respond. If the account holds other bots, say that this one is separate from them: its own people, its own agent, its own conversations.
71
66
 
72
67
  ## Failure modes (state these loudly, do not paper over)
73
68
 
74
69
  - **Wrong webhook path** — register returns an error and logs `op=webhook-register admitted=false`. Fix the URL to use `/api/telegram/`.
75
70
  - **Token write outside the brand** — `op=token-write-rejected reason=non-brand-path`. The MCP env is misconfigured; do not retry blindly — report it.
76
- - **`defaultAgent` unset on a public bot** — inbound is dropped with `[telegram-inbound] op=reject reason=no-default-agent`. Complete Step 5.
77
- - **Bad secret on inbound** — `[telegram-inbound] op=reject reason=bad-secret` means the registered secret and the stored secret diverged; re-run register.
78
- - **Bot registered before per-account webhooks** — `[telegram-inbound] op=reject reason=missing-account-param`, and `[telegram-audit] … secretFile=absent` names the account. The registered URL predates `account=`; re-run register once for that bot. There is no compatibility branch, so it 401s until you do.
79
- - **Chat id claimed by two accounts** — a scheduled dispatch logs `[schedule-inject] op=telegram-destination-ambiguous dispatched=no`. Two accounts list the same numeric ID in their `adminUsers`. Remove it from the one that should not have it; the dispatch refuses rather than guessing.
71
+ - **Paired agent switched off** — `[telegram-inbound] op=agent active=false` then `op=reject reason=agent-inactive`, and the standing `[telegram-audit] op=entry-agent-inactive` names the bot. Re-activate that agent or re-register the bot against another. There is no fall back to a different agent: answering as an agent the operator switched off is exactly what this refusal prevents.
72
+ - **Bad secret on inbound** — `[telegram-inbound] op=secret … result=mismatch` means the registered secret and the stored secret diverged; re-run register.
73
+ - **Bot registered before the bot registry** — `[telegram-inbound] op=reject reason=unknown-bot`, and `[telegram-audit] op=configured … secretFile=absent` names it. The registered URL predates `?bot=<botId>`; re-run register once for that bot. There is no compatibility branch, so it 401s until you do. The boot converter rewrites the stored config but cannot re-point a registered webhook: that is an outbound call, and the converter makes none.
74
+ - **One bot id claimed by two accounts** — inbound is refused `[telegram-inbound] op=reject reason=duplicate-bot-id`, and the standing `[telegram-audit] op=duplicate-bot-id` names it every tick. Two accounts hold the same token. Remove it from the one that should not have it; nothing picks between them.
75
+ - **Chat id claimed by two bots** — a scheduled dispatch logs `[schedule-inject] op=telegram-destination-ambiguous … dispatched=no`. Two bots, in one account or two, list the same numeric ID in their `adminUsers`. Remove it from the one that should not have it; the dispatch refuses rather than guessing which bot the person should hear from.
76
+ - **A secret file with no bot, or a bot with no secret file** — `[telegram-audit] op=secret-orphan botId=… side=file|entry`. Neither shows up any other way. `side=entry` means that bot cannot receive anything until it re-registers; `side=file` is left-over state from a bot that has been removed.
80
77
 
81
78
  ## Language
82
79
 
@@ -84,3 +81,4 @@ Use plain English:
84
81
  - "bot token" not "Bot API credential"
85
82
  - "your numeric Telegram ID" not "user_id integer"
86
83
  - "the bot is connected" not "webhook registered with secret_token"
84
+ - "which of your agents should answer on this bot" not "the agentSlug for the entry"