@agfpd/iapeer 0.2.80 → 0.2.81
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/README.md +62 -14
- package/package.json +1 -1
- package/src/cli/index.ts +7 -2
- package/src/identity/index.ts +13 -0
- package/src/init/index.ts +57 -4
package/README.md
CHANGED
|
@@ -1,29 +1,77 @@
|
|
|
1
1
|
# iapeer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Host-level core for a team of AI agents on one machine.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@agfpd/iapeer)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](#quick-start)
|
|
6
8
|
|
|
7
|
-
iapeer
|
|
9
|
+
iapeer keeps a team of AI agents, humans, and services on one host **always reachable — every one of them a single message away.** Each participant is a **peer**: a Claude Code or Codex CLI agent, a person on Telegram, or a scheduler/watcher service. Any agent reaches any other with one call — `send_to_peer(<name>, <text>)` — and a peer that is asleep is still reachable: the daemon wakes it and delivers. Agents on Claude and on Codex — different AI runtimes from different vendors — talk as equal peers; humans take part over Telegram.
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## How it works
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
```text
|
|
14
|
+
send_to_peer(name, text) ← from any peer
|
|
15
|
+
│
|
|
16
|
+
▼
|
|
17
|
+
router daemon — always on; finds the recipient, checks liveness, delivers
|
|
18
|
+
│
|
|
19
|
+
▼
|
|
20
|
+
recipient peer — asleep? the daemon wakes it and delivers on wake
|
|
21
|
+
```
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
## Quick start
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
Requirements: **macOS**, [bun](https://bun.sh) on `PATH`, `~/.local/bin` on `PATH`, and at least one agent runtime installed — Claude Code or Codex CLI (each AI peer is a live session of one).
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
```sh
|
|
28
|
+
# 1. install — the iapeer binary and the ~/.iapeer layout
|
|
29
|
+
npx @agfpd/iapeer
|
|
18
30
|
|
|
19
|
-
|
|
31
|
+
# 2. set up the host — starts the router daemon, then adds scheduler peers, Telegram, and shared memory
|
|
32
|
+
iapeer onboard
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
# 3. create your first agent
|
|
35
|
+
iapeer create assistant --runtime claude
|
|
22
36
|
|
|
23
|
-
|
|
37
|
+
# 4. talk to it live in your terminal — detach with Ctrl-]
|
|
38
|
+
iapeer attach assistant
|
|
39
|
+
```
|
|
24
40
|
|
|
25
|
-
|
|
41
|
+
**Already have a project folder?** In step 3, use `init` instead of `create`: `cd` into the project and register that folder itself as a peer.
|
|
26
42
|
|
|
27
|
-
|
|
43
|
+
```sh
|
|
44
|
+
cd ~/projects/myagent
|
|
45
|
+
iapeer init
|
|
46
|
+
```
|
|
28
47
|
|
|
29
|
-
|
|
48
|
+
The peer's name is the folder name, normalized (`myagent`). `create` sets up a fresh folder under `~/.iapeer/peers/<name>`; `init` turns the folder you are in into a peer. The setup is identical either way.
|
|
49
|
+
|
|
50
|
+
Two ways to work with an agent locally in your terminal:
|
|
51
|
+
|
|
52
|
+
- **Join its live session** — `iapeer attach assistant`. The daemon wakes the peer first if it is asleep; detach with `Ctrl-]` and the session keeps running.
|
|
53
|
+
- **Open a fresh session in the peer's folder** — go to the peer's directory and launch its runtime:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
cd ~/.iapeer/peers/assistant
|
|
57
|
+
iapeer claude
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This starts a full session with the peer's identity and doctrine and drops you straight into it. In effect, `iapeer claude` is `claude` for that peer — the same interactive CLI, but the session has a stable identity, its own memory, and an address other peers can reach.
|
|
61
|
+
|
|
62
|
+
Agents reach each other with `send_to_peer`; you can also message any peer over Telegram.
|
|
63
|
+
|
|
64
|
+
## What makes it different
|
|
65
|
+
|
|
66
|
+
- **Real interactive CLI sessions, not headless one-shots.** Each AI peer is a full Claude Code or Codex CLI session, not a disposable `claude -p` / `codex exec` call.
|
|
67
|
+
- **Warm-on-demand lifecycle.** A live session answers fast; after about an hour idle the daemon closes it; the next message brings it back with its context resumed. The message flow drives the lifecycle — not a human's presence.
|
|
68
|
+
- **One identity across runtimes, with one memory.** A peer's `personality` is decoupled from its runtime — the same peer runs on Claude or Codex, switched with a command. By default (set up by `iapeer onboard`) it carries one shared memory (iapeer-memory) keyed to the personality, not the runtime: one personality, one memory, whatever runtime it runs on.
|
|
69
|
+
- **Heterogeneous peers, one protocol.** AI agents, humans (Telegram), and services (timer = cron, watcher = event) are all first-class, addressed the same way over an always-on MCP daemon that exposes exactly one tool, `send_to_peer`.
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
[`docs/`](https://github.com/agfpd/iapeer/tree/main/docs) — the product side (what it is and how to use it); [`docs/internals/`](https://github.com/agfpd/iapeer/tree/main/docs/internals) — the mechanics from the inside. This repository is the implementation.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
Apache-2.0. Platform: macOS / launchd. iapeer is the foundation core of a larger ecosystem; runtime and capability plugins build on top of it.
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -915,7 +915,7 @@ const USAGE = `usage: iapeer <verb> [args]
|
|
|
915
915
|
status host snapshot: version, daemon health, memory slot (<provider> | none)
|
|
916
916
|
install-runtime <runtime> [--package pkg] [--npx] npx-install a runtime package + deploy its declared peer-set
|
|
917
917
|
update-runtime <runtime> | --all [--force] version-gate → re-install + re-provision declared set → restart the runtime's peers
|
|
918
|
-
init [cwd] [--
|
|
918
|
+
init [cwd] [--runtime r] [--description d] onboard the CURRENT folder as a peer (name = folder name; identity + MCP + doctrine)
|
|
919
919
|
create <personality> [--runtime r] [--path dir] [--bin abs] create a peer anywhere (default ~/.iapeer/peers/<p>) + provision
|
|
920
920
|
list [--json] registered peers + per-runtime liveness
|
|
921
921
|
verify [--json] [--fix] profile-standard conformance + index↔local drift (--fix self-heals the index + migrates legacy runtime→default_runtime in local profiles)
|
|
@@ -1182,11 +1182,16 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1182
1182
|
// cwd-DEPENDENT: onboard the CURRENT folder (or positional cwd) as a peer —
|
|
1183
1183
|
// identity + MCP wiring + doctrine, runtime resolved from the cwd's markers
|
|
1184
1184
|
// when not explicit. Auto-bootstraps an infra plist unless --no-bootstrap.
|
|
1185
|
+
// A peer's name IS its folder name (normalize(basename(cwd))) — personality ↔ folder
|
|
1186
|
+
// is 1:1, so `--personality` is gone: name the folder, don't pass a separate name.
|
|
1187
|
+
if (flags.personality !== undefined) {
|
|
1188
|
+
errOut("init: --personality was removed — a peer's name is its folder name; rename the folder to set the name\n")
|
|
1189
|
+
return 2
|
|
1190
|
+
}
|
|
1185
1191
|
const { initPeer } = await import('../init/index.ts')
|
|
1186
1192
|
const r = await initPeer({
|
|
1187
1193
|
cwd: positionals[0] ?? process.cwd(),
|
|
1188
1194
|
runtime: typeof flags.runtime === 'string' ? (flags.runtime as Runtime) : undefined,
|
|
1189
|
-
personality: typeof flags.personality === 'string' ? flags.personality : undefined,
|
|
1190
1195
|
description: typeof flags.description === 'string' ? flags.description : undefined,
|
|
1191
1196
|
runtimeBin: typeof flags.bin === 'string' ? flags.bin : undefined,
|
|
1192
1197
|
bootstrap: flags['no-bootstrap'] === true ? false : undefined,
|
package/src/identity/index.ts
CHANGED
|
@@ -415,6 +415,19 @@ export function ensurePeerProfile(options: EnsurePeerProfileOptions): PeerProfil
|
|
|
415
415
|
let personality: string
|
|
416
416
|
if (options.personality !== undefined) {
|
|
417
417
|
personality = validatePersonality(options.personality, 'personality')
|
|
418
|
+
// INVARIANT (personality ↔ folder is 1:1): an explicitly-supplied personality MUST
|
|
419
|
+
// equal the normalized cwd basename. Otherwise the registry record (personality) and
|
|
420
|
+
// a manual `cd <cwd>; <runtime>` session (which self-derives identity from the cwd
|
|
421
|
+
// basename) would disagree. The only ways to reach a mismatch are a runtime manifest
|
|
422
|
+
// `peers[].path` or `create --path <dir>` whose basename ≠ personality — both are
|
|
423
|
+
// misconfigurations, surfaced loudly here rather than written as a latent drift.
|
|
424
|
+
const expected = normalizeNameCandidate(basename(cwd))
|
|
425
|
+
if (personality !== expected) {
|
|
426
|
+
throw new IapError(
|
|
427
|
+
`personality "${personality}" must equal the normalized cwd basename "${expected}" ` +
|
|
428
|
+
`(personality ↔ folder is 1:1) — name the folder to match, or drop the explicit name. cwd: ${cwd}`,
|
|
429
|
+
)
|
|
430
|
+
}
|
|
418
431
|
const collision = peerCollision(personality, cwd, peers)
|
|
419
432
|
if (collision) {
|
|
420
433
|
throw new IapError(
|
package/src/init/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
|
|
|
17
17
|
import { homedir } from 'os'
|
|
18
18
|
import { join } from 'path'
|
|
19
19
|
import { CODEX_BEARER_ENV_VAR, CODEX_DUMMY_BEARER, IAPEER_DIR, isInfraRuntime, type Runtime } from '../core/constants.ts'
|
|
20
|
+
import { IapError } from '../core/errors.ts'
|
|
20
21
|
import {
|
|
21
22
|
ensureLocalIapScaffold,
|
|
22
23
|
pluginStateDir,
|
|
@@ -257,11 +258,63 @@ export function ensureDoctrineTemplate(cwd: string): { path: string; created: bo
|
|
|
257
258
|
* the old inconsistency where a `.codex`-only folder defaulted to claude as primary
|
|
258
259
|
* yet got a codex config — now the primary matches the markers, deterministically.
|
|
259
260
|
*/
|
|
260
|
-
|
|
261
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Is an AGENTIC runtime installed on this host? Checks (any hit ⇒ installed): env
|
|
263
|
+
* override (IAPEER_<RT>_BIN), the native installer location (~/.local/bin/claude), the
|
|
264
|
+
* bare name on PATH, and the runtime's global config dir (~/.claude / ~/.codex — a
|
|
265
|
+
* runtime that has ever run leaves one). SANDBOX: under IAPEER_TEST_SANDBOX it reports
|
|
266
|
+
* `true` (never probes the real host) — mirror of the install-side guards; a hermetic
|
|
267
|
+
* test that wants the real branches injects its own predicate into resolvePrimaryRuntime.
|
|
268
|
+
*/
|
|
269
|
+
export function isRuntimeInstalled(runtime: Runtime, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
270
|
+
if (env.IAPEER_TEST_SANDBOX === '1' || process.env.IAPEER_TEST_SANDBOX === '1') return true
|
|
271
|
+
const override = runtime === 'claude' ? env.IAPEER_CLAUDE_BIN : runtime === 'codex' ? env.IAPEER_CODEX_BIN : undefined
|
|
272
|
+
if (override?.trim()) return true
|
|
273
|
+
const home = env.HOME?.trim() || homedir()
|
|
274
|
+
if (runtime === 'claude' && existsSync(join(home, '.local', 'bin', 'claude'))) return true
|
|
275
|
+
if (Bun.which(runtime)) return true
|
|
276
|
+
if (runtime === 'claude' && existsSync(join(home, '.claude'))) return true
|
|
277
|
+
if (runtime === 'codex' && existsSync(join(home, '.codex'))) return true
|
|
278
|
+
return false
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const AGENTIC_RUNTIMES: Runtime[] = ['claude', 'codex']
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Resolve a peer's primary AGENTIC runtime — install-aware:
|
|
285
|
+
* 1. explicit (`--runtime`) → use it, but VALIDATE it is installed (clear error instead
|
|
286
|
+
* of a silent launch failure later on an uninstalled runtime).
|
|
287
|
+
* 2. else a cwd runtime MARKER (.claude / .codex) → that runtime. The folder's already-
|
|
288
|
+
* established config takes PRECEDENCE over install-presence (a folder configured for
|
|
289
|
+
* X stays X even if Y is also installed; markers are not re-validated).
|
|
290
|
+
* 3. else from what is INSTALLED: exactly one → it; none → clear error (don't silently
|
|
291
|
+
* default to claude on a host without it); both → claude (deterministic default —
|
|
292
|
+
* resolution runs in non-interactive paths, so no prompt; `--runtime` picks codex).
|
|
293
|
+
* `isInstalled` is injectable for hermetic tests.
|
|
294
|
+
*/
|
|
295
|
+
export function resolvePrimaryRuntime(
|
|
296
|
+
cwd: string,
|
|
297
|
+
explicit?: Runtime,
|
|
298
|
+
isInstalled: (rt: Runtime) => boolean = rt => isRuntimeInstalled(rt),
|
|
299
|
+
): Runtime {
|
|
300
|
+
if (explicit) {
|
|
301
|
+
if (!isInstalled(explicit)) {
|
|
302
|
+
throw new IapError(
|
|
303
|
+
`runtime "${explicit}" is not installed on this host — install it (Claude Code / Codex CLI) or choose an installed runtime`,
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
return explicit
|
|
307
|
+
}
|
|
262
308
|
if (existsSync(join(cwd, '.claude'))) return 'claude'
|
|
263
309
|
if (existsSync(join(cwd, '.codex'))) return 'codex'
|
|
264
|
-
|
|
310
|
+
const installed = AGENTIC_RUNTIMES.filter(isInstalled)
|
|
311
|
+
if (installed.length === 1) return installed[0]!
|
|
312
|
+
if (installed.length === 0) {
|
|
313
|
+
throw new IapError(
|
|
314
|
+
`no agentic runtime installed (Claude Code or Codex CLI) — install one, then create/init the peer`,
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
return 'claude' // both installed → deterministic default; pass --runtime codex to pick codex
|
|
265
318
|
}
|
|
266
319
|
|
|
267
320
|
export interface InitPeerOptions {
|
|
@@ -319,7 +372,7 @@ export async function initPeer(opts: InitPeerOptions): Promise<InitPeerResult> {
|
|
|
319
372
|
// Runtime-aware + CONSISTENT: an explicit runtime wins; else the cwd's markers
|
|
320
373
|
// decide the primary (resolvePrimaryRuntime), so the primary always matches the
|
|
321
374
|
// config that gets written (no `.codex`-only-but-primary-claude mismatch).
|
|
322
|
-
const runtime = resolvePrimaryRuntime(opts.cwd, opts.runtime)
|
|
375
|
+
const runtime = resolvePrimaryRuntime(opts.cwd, opts.runtime, rt => isRuntimeInstalled(rt, env))
|
|
323
376
|
const provisioned = await provisionPeer({
|
|
324
377
|
cwd: opts.cwd,
|
|
325
378
|
runtime,
|