@agfpd/iapeer 0.4.22 → 0.4.24
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.
- package/CHANGELOG.md +29 -1
- package/package.json +1 -1
- package/src/cli/index.ts +44 -24
- package/src/connect/index.ts +43 -15
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.4.24] - 2026-06-22
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`iapeer connect telegram <peer>` (and the `enable telegram` alias) no longer fails for a
|
|
18
|
+
peer whose personality isn't a valid Telegram @username** (e.g. a hyphen: "impact-finder").
|
|
19
|
+
connect passed the PERSONALITY as the bot @username to telegram-runtime (which keys bots by
|
|
20
|
+
`<bot-username>`), so a hyphenated personality was rejected ("must be a telegram @username").
|
|
21
|
+
The bot @username is TOKEN-derived (set in @BotFather), never the personality. connect now
|
|
22
|
+
resolves it from the token via Telegram **getMe** and uses that for `bot add` / `interface
|
|
23
|
+
bot` / storage; the human still owes only the token. An invalid/unreachable token fails
|
|
24
|
+
early with a getMe-rejected message. (The getMe lives in the telegram-glue module
|
|
25
|
+
`connect/index.ts` — the foundation core stays Telegram-API-free.)
|
|
26
|
+
|
|
27
|
+
## [0.4.23] - 2026-06-22
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **`iapeer enable telegram <peer>`** — a discoverability alias for `iapeer connect telegram
|
|
32
|
+
<peer>` (give an existing peer a Telegram bot face). A layperson reaches for "enable
|
|
33
|
+
telegram", not "connect"; both now work and route through the same flow (bot add →
|
|
34
|
+
interface → router restart; asks only the token). telegram is a CHANNEL, not a marketplace
|
|
35
|
+
plugin, so the alias routes to the connect flow, not `enableCapability`. The `connect`
|
|
36
|
+
help line documents the alias. No telegram-runtime change (the mechanism already worked) —
|
|
37
|
+
purely CLI discoverability.
|
|
38
|
+
|
|
13
39
|
## [0.4.22] - 2026-06-22
|
|
14
40
|
|
|
15
41
|
### Added
|
|
@@ -340,7 +366,9 @@ Initial public release — the foundation core of the iapeer multi-agent ecosyst
|
|
|
340
366
|
- **Heterogeneous peers** — AI agents (Claude, Codex), humans (Telegram), and services
|
|
341
367
|
(timer = cron, watcher = event) are all first-class and addressed the same way.
|
|
342
368
|
|
|
343
|
-
[Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.
|
|
369
|
+
[Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.24...HEAD
|
|
370
|
+
[0.4.24]: https://github.com/agfpd/iapeer/compare/v0.4.23...v0.4.24
|
|
371
|
+
[0.4.23]: https://github.com/agfpd/iapeer/compare/v0.4.22...v0.4.23
|
|
344
372
|
[0.4.22]: https://github.com/agfpd/iapeer/compare/v0.4.21...v0.4.22
|
|
345
373
|
[0.4.21]: https://github.com/agfpd/iapeer/compare/v0.4.20...v0.4.21
|
|
346
374
|
[0.4.20]: https://github.com/agfpd/iapeer/compare/v0.4.19...v0.4.20
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -875,6 +875,41 @@ export async function removePeerCli(
|
|
|
875
875
|
}
|
|
876
876
|
}
|
|
877
877
|
|
|
878
|
+
/**
|
|
879
|
+
* `connect telegram <peer>` core — give a peer a Telegram bot face (interfaces.telegram).
|
|
880
|
+
* Shared by the `connect telegram` verb AND the `enable telegram <peer>` discoverability
|
|
881
|
+
* alias (telegram is a CHANNEL, not a marketplace plugin, so both route here, not through
|
|
882
|
+
* enableCapability). Returns the process exit code. The human owes only the token (prompted
|
|
883
|
+
* when omitted on a tty); the system does bot-add → interface → router restart.
|
|
884
|
+
*/
|
|
885
|
+
async function connectTelegramVerb(
|
|
886
|
+
peer: string,
|
|
887
|
+
token: string | undefined,
|
|
888
|
+
env: NodeJS.ProcessEnv,
|
|
889
|
+
out: (s: string) => void,
|
|
890
|
+
errOut: (s: string) => void,
|
|
891
|
+
): Promise<number> {
|
|
892
|
+
const { connectTelegram } = await import('../connect/index.ts')
|
|
893
|
+
const r = await connectTelegram({ peer, token, env })
|
|
894
|
+
if (r.state === 'noop-same-token') {
|
|
895
|
+
out(`connect telegram ${r.peer}: ${r.detail}\n`)
|
|
896
|
+
return 0
|
|
897
|
+
}
|
|
898
|
+
if (r.state !== 'connected') {
|
|
899
|
+
errOut(`connect telegram ${r.peer}: ${r.state}${r.detail ? ` — ${r.detail}` : ''}\n`)
|
|
900
|
+
return 1
|
|
901
|
+
}
|
|
902
|
+
const rs = r.restart!
|
|
903
|
+
out(`bot ${r.username ?? `for "${r.peer}"`} added + interfaced to "${r.peer}"\n`)
|
|
904
|
+
out(
|
|
905
|
+
rs.state === 'restarted'
|
|
906
|
+
? `router restarted — credentials loaded\n`
|
|
907
|
+
: `router restart ${rs.state}${rs.detail ? ` — ${rs.detail}` : ''} (the channel stays dead until the router restarts)\n`,
|
|
908
|
+
)
|
|
909
|
+
out(`activation: send the bot ${r.username ?? '(see @BotFather)'} its FIRST message — Telegram does not let a bot start the chat\n`)
|
|
910
|
+
return rs.state === 'restarted' ? 0 : 1
|
|
911
|
+
}
|
|
912
|
+
|
|
878
913
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
879
914
|
// rename — first-class peer-identity rename (parity with remove/create). Wraps
|
|
880
915
|
// renamePeer (registry + per-cwd profile, atomic, inside the lock) and KEEPS the
|
|
@@ -1168,7 +1203,7 @@ const VERBS: ReadonlyArray<{ sig: string; desc: string }> = [
|
|
|
1168
1203
|
desc: 'manual IAP send (fallback)',
|
|
1169
1204
|
},
|
|
1170
1205
|
{ sig: '<runtime>', desc: "launch the cwd's peer FRESH; on a TTY drop into it (like attach), else detached" },
|
|
1171
|
-
{ sig: 'connect telegram <peer> [--token <t>]', desc: 'attach a telegram bot to a peer (bot add → interface → router restart; asks only the token)' },
|
|
1206
|
+
{ sig: 'connect telegram <peer> [--token <t>]', desc: 'attach a telegram bot to a peer (bot add → interface → router restart; asks only the token). Alias: `iapeer enable telegram <peer>`' },
|
|
1172
1207
|
{ sig: 'enable <plugin> [peer] [--no-setup]', desc: 'install + enable an agfpd capability for a peer' },
|
|
1173
1208
|
{
|
|
1174
1209
|
sig: 'new <peer> [runtime]',
|
|
@@ -2287,31 +2322,16 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2287
2322
|
// alias/bot-add/interface/router-restart are resolved by the system. The
|
|
2288
2323
|
// FIRST message from the human to the bot activates the chat (platform rule).
|
|
2289
2324
|
if (positionals[0] !== 'telegram' || !positionals[1]) return argErr(errOut, 'connect needs "telegram <peer>" — usage: iapeer connect telegram <peer> [--token <t>] (e.g. iapeer connect telegram boris)')
|
|
2290
|
-
|
|
2291
|
-
const r = await connectTelegram({
|
|
2292
|
-
peer: positionals[1],
|
|
2293
|
-
token: typeof flags.token === 'string' ? flags.token : undefined,
|
|
2294
|
-
env,
|
|
2295
|
-
})
|
|
2296
|
-
if (r.state === 'noop-same-token') {
|
|
2297
|
-
out(`connect telegram ${r.peer}: ${r.detail}\n`)
|
|
2298
|
-
return 0
|
|
2299
|
-
}
|
|
2300
|
-
if (r.state !== 'connected') {
|
|
2301
|
-
errOut(`connect telegram ${r.peer}: ${r.state}${r.detail ? ` — ${r.detail}` : ''}\n`)
|
|
2302
|
-
return 1
|
|
2303
|
-
}
|
|
2304
|
-
const rs = r.restart!
|
|
2305
|
-
out(`bot ${r.username ?? `for "${r.peer}"`} added + interfaced to "${r.peer}"\n`)
|
|
2306
|
-
out(
|
|
2307
|
-
rs.state === 'restarted'
|
|
2308
|
-
? `router restarted — credentials loaded\n`
|
|
2309
|
-
: `router restart ${rs.state}${rs.detail ? ` — ${rs.detail}` : ''} (the channel stays dead until the router restarts)\n`,
|
|
2310
|
-
)
|
|
2311
|
-
out(`activation: send the bot ${r.username ?? '(see @BotFather)'} its FIRST message — Telegram does not let a bot start the chat\n`)
|
|
2312
|
-
return rs.state === 'restarted' ? 0 : 1
|
|
2325
|
+
return await connectTelegramVerb(positionals[1], typeof flags.token === 'string' ? flags.token : undefined, env, out, errOut)
|
|
2313
2326
|
}
|
|
2314
2327
|
case 'enable': {
|
|
2328
|
+
// DISCOVERABILITY ALIAS: `enable telegram <peer>` → `connect telegram <peer>`
|
|
2329
|
+
// (a layperson reaches for "enable telegram"; telegram is a CHANNEL, not a
|
|
2330
|
+
// marketplace plugin, so it routes to the connect flow, NOT enableCapability).
|
|
2331
|
+
if (positionals[0] === 'telegram') {
|
|
2332
|
+
if (!positionals[1]) return argErr(errOut, 'enable telegram needs a peer — usage: iapeer enable telegram <peer> [--token <t>] (alias of `iapeer connect telegram <peer>`)')
|
|
2333
|
+
return await connectTelegramVerb(positionals[1], typeof flags.token === 'string' ? flags.token : undefined, env, out, errOut)
|
|
2334
|
+
}
|
|
2315
2335
|
// Per-peer capability install (contract Установка §3): install <plugin>@agfpd
|
|
2316
2336
|
// per-runtime (claude project-scope IN the peer cwd / codex global) + enable +
|
|
2317
2337
|
// call the plugin's `setup` ONLY if its iapeer.json declares it. Idempotent and
|
package/src/connect/index.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
//
|
|
6
6
|
// The human owes EXACTLY ONE external fact: the bot token (prompt walks them
|
|
7
7
|
// through @BotFather → /newbot). Everything else the system resolves: the bot
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// @username via Telegram getMe(token) — the bot key is TOKEN-derived, NOT the peer
|
|
9
|
+
// personality (a personality may contain a hyphen, structurally invalid as a
|
|
10
|
+
// @username; telegram-runtime keys bots by <bot-username>), `telegram-runtime bot add`
|
|
11
|
+
// (also validates the token), `telegram-runtime interface bot` (profile merge;
|
|
12
|
+
// precondition: the peer is registered), then the MANDATORY
|
|
11
13
|
// router-session restart (the live poller reads bots/ ONCE at start, no fs-watch —
|
|
12
14
|
// without the restart the channel is dead both ways), and the activation hint:
|
|
13
15
|
// the FIRST message from the human to the bot opens the chat (a Telegram platform
|
|
@@ -51,6 +53,9 @@ export interface ConnectTelegramOptions {
|
|
|
51
53
|
runTg?: TgRunner
|
|
52
54
|
/** Injectable router restart (tests). Default: stopPeer→startPeer strictly in order. */
|
|
53
55
|
restart?: RestartFn
|
|
56
|
+
/** Injectable bot-@username resolver (tests). Default: Telegram getMe(token). Returns the
|
|
57
|
+
* bare @username (no leading @) or null on a rejected/unreachable token. */
|
|
58
|
+
resolveUsername?: (token: string) => Promise<string | null>
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
export interface ConnectTelegramResult {
|
|
@@ -60,6 +65,7 @@ export interface ConnectTelegramResult {
|
|
|
60
65
|
| 'refused-no-token' // non-tty and no --token (or an empty tty answer)
|
|
61
66
|
| 'unregistered-peer' // precondition: the peer must be in the registry
|
|
62
67
|
| 'runtime-missing' // no telegram runtime manifest — install it first
|
|
68
|
+
| 'bad-token' // Telegram getMe rejected the token (invalid/revoked) or was unreachable
|
|
63
69
|
| 'bot-add-failed' // telegram-runtime bot add exited non-zero (incl. getMe refusal)
|
|
64
70
|
| 'interface-failed' // telegram-runtime interface bot exited non-zero
|
|
65
71
|
peer: string
|
|
@@ -93,6 +99,26 @@ function readBotEnv(alias: string, env: NodeJS.ProcessEnv): string | null {
|
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the bot's REAL @username from the token via Telegram getMe — the bot identity
|
|
104
|
+
* is TOKEN-derived (BotFather sets it), NOT the peer personality. telegram-runtime keys
|
|
105
|
+
* bots by `<bot-username>` (`bot add <bot-username>`, `interface bot <bot-username>`), and a
|
|
106
|
+
* personality can contain a hyphen which is structurally invalid as a @username — so the
|
|
107
|
+
* personality can NEVER be the bot-username. Returns the bare username (no leading @) or
|
|
108
|
+
* null on a rejected/unreachable token. The token goes literally in the URL path
|
|
109
|
+
* (Telegram tokens are `<digits>:<urlsafe>`; no encoding).
|
|
110
|
+
*/
|
|
111
|
+
async function defaultResolveBotUsername(token: string): Promise<string | null> {
|
|
112
|
+
try {
|
|
113
|
+
const r = await fetch(`https://api.telegram.org/bot${token}/getMe`)
|
|
114
|
+
const j = (await r.json()) as { ok?: boolean; result?: { username?: unknown } }
|
|
115
|
+
const u = j?.ok === true && typeof j.result?.username === 'string' ? j.result.username.trim().replace(/^@/, '') : ''
|
|
116
|
+
return u || null
|
|
117
|
+
} catch {
|
|
118
|
+
return null // network error / non-JSON → treat as unresolvable (bad-token)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
96
122
|
/** Resolve the telegram-runtime CLI bin from the manifest's selfConfig command (the
|
|
97
123
|
* package's own absolute bin — PATH-independent, launchd-safe). */
|
|
98
124
|
function resolveTgBin(env: NodeJS.ProcessEnv): string | null {
|
|
@@ -173,8 +199,17 @@ export async function connectTelegram(opts: ConnectTelegramOptions): Promise<Con
|
|
|
173
199
|
if (!token) return { state: 'refused-no-token', peer, detail: 'no answer — nothing connected' }
|
|
174
200
|
}
|
|
175
201
|
|
|
202
|
+
// Resolve the bot's REAL @username from the token (getMe) — NOT the personality. The
|
|
203
|
+
// bot-username is token-derived; a personality with a hyphen (e.g. "impact-finder") is
|
|
204
|
+
// structurally invalid as a @username and telegram-runtime (which keys bots by
|
|
205
|
+
// <bot-username>) rejects it. getMe also validates the token early (bad token → fail here).
|
|
206
|
+
const resolveUsername = opts.resolveUsername ?? defaultResolveBotUsername
|
|
207
|
+
const botUsername = await resolveUsername(token)
|
|
208
|
+
if (!botUsername) {
|
|
209
|
+
return { state: 'bad-token', peer, detail: 'Telegram getMe rejected the token (invalid/revoked) or was unreachable — recheck the token with @BotFather' }
|
|
210
|
+
}
|
|
176
211
|
const runTg = opts.runTg ?? defaultRunTg(tgBin)
|
|
177
|
-
const alias =
|
|
212
|
+
const alias = botUsername // bot key = the REAL @username from getMe (telegram-runtime keys bots by <bot-username>)
|
|
178
213
|
const before = readBotEnv(alias, env)
|
|
179
214
|
|
|
180
215
|
// (1) bot add — token → bots/<alias>/.env. Owner adds getMe validation here: an
|
|
@@ -183,17 +218,10 @@ export async function connectTelegram(opts: ConnectTelegramOptions): Promise<Con
|
|
|
183
218
|
if (add.status !== 0) {
|
|
184
219
|
return { state: 'bot-add-failed', peer, detail: (add.stderr || add.stdout || `exit ${add.status}`).trim() }
|
|
185
220
|
}
|
|
186
|
-
// @username: the
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
const envAfterAdd = readBotEnv(alias, env)
|
|
191
|
-
const envUser = envAfterAdd?.match(/^TELEGRAM_BOT_USERNAME=(.+)$/m)?.[1]?.trim()
|
|
192
|
-
const username = envUser
|
|
193
|
-
? envUser.startsWith('@')
|
|
194
|
-
? envUser
|
|
195
|
-
: `@${envUser}`
|
|
196
|
-
: add.stdout.match(/@[A-Za-z0-9_]{3,}/)?.[0]
|
|
221
|
+
// @username for display / the activation hint: the bot key (alias) IS the
|
|
222
|
+
// getMe-resolved username, so the display form is simply `@<alias>` — no need to
|
|
223
|
+
// re-parse bots/<alias>/.env (telegram-runtime writes the same value there).
|
|
224
|
+
const username = `@${alias}`
|
|
197
225
|
|
|
198
226
|
// (2) interface bot — merge the channel binding into the peer's profile.
|
|
199
227
|
const iface = runTg(['interface', 'bot', alias, '--peer', peer], env)
|