@agfpd/iapeer 0.4.4 → 0.4.6
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 +12 -12
- package/package.json +1 -1
- package/src/cli/index.ts +35 -4
- package/src/lifecycle/index.ts +54 -9
package/README.md
CHANGED
|
@@ -21,6 +21,13 @@ router daemon — always on; finds the recipient, checks liveness, delivers
|
|
|
21
21
|
recipient peer — asleep? the daemon wakes it and delivers on wake
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
## What makes it different
|
|
25
|
+
|
|
26
|
+
- **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 (as of 2026-06-15, `claude -p` no longer counts against the subscription's main usage limits).
|
|
27
|
+
- **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.
|
|
28
|
+
- **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.
|
|
29
|
+
- **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`.
|
|
30
|
+
|
|
24
31
|
## Quick start
|
|
25
32
|
|
|
26
33
|
Requirements: **macOS** (Linux is on the [Roadmap](#roadmap)) and at least one agent runtime — Claude Code or Codex CLI (each AI peer is a live session of one). The installer handles the rest ([bun](https://bun.sh) and `~/.local/bin` on `PATH`).
|
|
@@ -78,22 +85,15 @@ Agents reach each other with `send_to_peer`; you can also message any peer over
|
|
|
78
85
|
|
|
79
86
|
## Bypass Permissions mode
|
|
80
87
|
|
|
81
|
-
⚠️ **iapeer runs your Claude and Codex peers in bypass-permissions / no-sandbox mode — they execute tools (shell, file edits, network) without asking you to approve each action.** This is by design: a peer is a background, headless session with no human watching its terminal, so there is no one to answer a per-tool permission prompt. The launcher
|
|
88
|
+
⚠️ **iapeer runs your Claude and Codex peers in bypass-permissions / no-sandbox mode — they execute tools (shell commands, file edits, network calls) without asking you to approve each action.** This is by design: a peer is a background, headless session with no human watching its terminal, so there is no one to answer a per-tool permission prompt. The launcher starts Claude with `--dangerously-skip-permissions` and Codex with `--dangerously-bypass-approvals-and-sandbox`, and **auto-accepts the one-time "accept bypass mode" screen on your behalf** the first time a peer boots on a fresh machine.
|
|
82
89
|
|
|
83
90
|
What this means for you, stated plainly:
|
|
84
91
|
|
|
85
|
-
- A peer can run any command its runtime can run — read and write files
|
|
86
|
-
- You are trusting the model's
|
|
87
|
-
- iapeer does **not** silently weaken your own interactive `claude
|
|
92
|
+
- A peer can run any command its runtime can run — read and write files anywhere in your home directory, install software, make network calls — with **no confirmation step**. There is no sandbox between an agent and your machine.
|
|
93
|
+
- You are trusting the model's judgment, and the doctrine you give each peer, the same way you would trust a developer with a shell on your account.
|
|
94
|
+
- iapeer does **not** silently weaken your own interactive `claude` / `codex` setup. The bypass acceptance is recorded per peer, in that agent's own config; the project-MCP pre-approval is written **project-locally**, under the peer's folder — never to your global user settings.
|
|
88
95
|
|
|
89
|
-
Run iapeer on a host you control, give
|
|
90
|
-
|
|
91
|
-
## What makes it different
|
|
92
|
-
|
|
93
|
-
- **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 (as of 2026-06-15, `claude -p` no longer counts against the subscription's main usage limits).
|
|
94
|
-
- **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.
|
|
95
|
-
- **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.
|
|
96
|
-
- **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`.
|
|
96
|
+
Run iapeer on a host you control, give each peer only the access it needs, and treat a peer's instructions as security-relevant. `iapeer onboard` states this at setup; it's repeated here so the trade-off is explicit before you create your first agent. Details on every first-run dialog: [docs/07 — Install & update](docs/07-install-and-update.md#first-run-dialogs-on-a-clean-machine).
|
|
97
97
|
|
|
98
98
|
## Documentation
|
|
99
99
|
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -37,10 +37,10 @@ import {
|
|
|
37
37
|
isEphemeralPeer,
|
|
38
38
|
isStopped,
|
|
39
39
|
killSession,
|
|
40
|
-
lastActiveRuntime,
|
|
41
40
|
loadLifecycleConfig,
|
|
42
41
|
purgeIdentityState,
|
|
43
42
|
removeSessionState,
|
|
43
|
+
resolvePeerRuntime,
|
|
44
44
|
setEphemeralArmed,
|
|
45
45
|
setFreshNext,
|
|
46
46
|
setIdleReaped,
|
|
@@ -518,7 +518,10 @@ export async function newPeer(
|
|
|
518
518
|
}
|
|
519
519
|
}
|
|
520
520
|
if (runtime && !isRuntime(runtime)) throw new Error(`invalid runtime "${runtime}"`)
|
|
521
|
-
|
|
521
|
+
// Omitted runtime resolves IDENTICALLY to `attach` (resolvePeerRuntime): default_runtime
|
|
522
|
+
// anchored, sole-live refinement — so `iapeer new <peer>` and `iapeer attach <peer>` never
|
|
523
|
+
// target different runtimes (the "new had no effect, attach resurrected the other" footgun).
|
|
524
|
+
const rt: Runtime = (runtime as Runtime | undefined) ?? resolvePeerRuntime(peer, cfg)
|
|
522
525
|
if (!peer.runtimes.includes(rt)) {
|
|
523
526
|
return {
|
|
524
527
|
personality,
|
|
@@ -638,7 +641,9 @@ export async function compactPeer(
|
|
|
638
641
|
return { personality, runtime: r.value.controlled.runtime, action: 'compacted', woke: false }
|
|
639
642
|
}
|
|
640
643
|
|
|
641
|
-
|
|
644
|
+
// Asleep branch: omitted runtime resolves like new/attach (resolvePeerRuntime) — consistent
|
|
645
|
+
// default_runtime anchoring instead of the old hidden last-active-by-mtime.
|
|
646
|
+
const rt: Runtime = (runtime as Runtime | undefined) ?? resolvePeerRuntime(peer, cfg)
|
|
642
647
|
if (!peer.runtimes.includes(rt)) {
|
|
643
648
|
return {
|
|
644
649
|
personality,
|
|
@@ -1396,8 +1401,34 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1396
1401
|
env,
|
|
1397
1402
|
warn: m => errOut(`warn: ${m}\n`),
|
|
1398
1403
|
})
|
|
1404
|
+
// FU5 parity with `create`: a peer's `runtimes` must list EVERY installed agentic
|
|
1405
|
+
// runtime (the default is just one of them), so `iapeer <other-runtime>` works
|
|
1406
|
+
// without a manual add-runtime. initPeer resolves only the PRIMARY (cwd markers /
|
|
1407
|
+
// --runtime); declare the OTHER installed agentic runtime(s) too — otherwise the
|
|
1408
|
+
// registry the launch dispatch reads (folderLaunch → resolveWakeRuntime) lists the
|
|
1409
|
+
// primary only and `iapeer codex` fails "not declared" on a both-installed host.
|
|
1410
|
+
// addRuntime is capability-only (runs the runtime birth chain, reindexes the
|
|
1411
|
+
// registry from locals, NEVER flips the default); skip for an infra primary.
|
|
1412
|
+
const addedRuntimes: Runtime[] = []
|
|
1413
|
+
if (!isInfraRuntime(r.runtime)) {
|
|
1414
|
+
const { isRuntimeInstalled } = await import('../init/index.ts')
|
|
1415
|
+
const { secondaryRuntimes } = await import('../create/index.ts')
|
|
1416
|
+
const installed = (['claude', 'codex'] as Runtime[]).filter(rt => isRuntimeInstalled(rt, env))
|
|
1417
|
+
for (const rt of secondaryRuntimes(r.runtime, installed)) {
|
|
1418
|
+
try {
|
|
1419
|
+
const outcomes = await addRuntime(rt, { peer: r.personality, env })
|
|
1420
|
+
const o = outcomes.find(x => x.personality === r.personality)
|
|
1421
|
+
if (o && (o.action === 'added' || o.action === 'already')) addedRuntimes.push(rt)
|
|
1422
|
+
else errOut(`warn: could not add runtime "${rt}": ${o?.detail ?? o?.action ?? 'failed'}\n`)
|
|
1423
|
+
} catch (e) {
|
|
1424
|
+
errOut(`warn: could not add runtime "${rt}": ${e instanceof Error ? e.message : String(e)}\n`)
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
const allRuntimes = [r.runtime, ...addedRuntimes]
|
|
1399
1429
|
out(
|
|
1400
|
-
`initialized "${r.personality}" (${r.runtime}
|
|
1430
|
+
`initialized "${r.personality}" (default: ${r.runtime}; runtimes: ${allRuntimes.join(', ')}); ` +
|
|
1431
|
+
`mcp: ${r.mcpConfigPaths.join(', ') || r.codexMcpConfigPath || 'none'}` +
|
|
1401
1432
|
`${r.bootstrapped ? `; bootstrap: ${r.bootstrapped.state}` : ''}\n`,
|
|
1402
1433
|
)
|
|
1403
1434
|
return 0
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -1552,9 +1552,11 @@ export async function folderLaunch(opts: FolderLaunchOptions): Promise<WakeResul
|
|
|
1552
1552
|
}
|
|
1553
1553
|
|
|
1554
1554
|
/**
|
|
1555
|
-
* The runtime with the freshest transcript activity for a peer
|
|
1556
|
-
*
|
|
1557
|
-
*
|
|
1555
|
+
* The runtime with the freshest transcript activity for a peer. undefined when no
|
|
1556
|
+
* runtime has any activity (a never-run peer). NOTE: this is NO LONGER the attach
|
|
1557
|
+
* default — operator runtime resolution moved to `resolvePeerRuntime` (default_runtime
|
|
1558
|
+
* anchored) because last-active-by-mtime is hidden state and unreliable for codex
|
|
1559
|
+
* (rollout flushed only on a real turn). Kept as a diagnostic / `list` helper.
|
|
1558
1560
|
*/
|
|
1559
1561
|
export function lastActiveRuntime(peer: PeerRecord, cfg: LifecycleConfig): Runtime | undefined {
|
|
1560
1562
|
let best: Runtime | undefined
|
|
@@ -1573,6 +1575,46 @@ export function lastActiveRuntime(peer: PeerRecord, cfg: LifecycleConfig): Runti
|
|
|
1573
1575
|
return best
|
|
1574
1576
|
}
|
|
1575
1577
|
|
|
1578
|
+
/** Runtimes of `peer` whose session is LIVE right now (lifecycle-local liveness,
|
|
1579
|
+
* same `sessionAlive` predicate the wake/attach paths use). */
|
|
1580
|
+
function liveRuntimesOf(peer: PeerRecord, cfg: LifecycleConfig): Runtime[] {
|
|
1581
|
+
return peer.runtimes.filter(rt =>
|
|
1582
|
+
sessionAlive(buildSocketPath(rt, peer.personality, cfg.sockDir), buildProcessAddress(rt, peer.personality)),
|
|
1583
|
+
)
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* The SINGLE runtime an operator verb (new / attach / compact) acts on when the
|
|
1588
|
+
* runtime arg is OMITTED, for a peer that may declare several. PREDICTABLE by
|
|
1589
|
+
* construction and IDENTICAL across those verbs — this is the fix for the
|
|
1590
|
+
* multi-runtime footgun where `new` defaulted to `default_runtime` while `attach`
|
|
1591
|
+
* defaulted to last-active-by-mtime, so `iapeer new <peer>` freshed one runtime
|
|
1592
|
+
* while `iapeer attach <peer>` resurrected the other ("new had no effect, attach
|
|
1593
|
+
* came up on the old session").
|
|
1594
|
+
*
|
|
1595
|
+
* Precedence (never refuses — the naive flow must self-resolve, no explicit arg):
|
|
1596
|
+
* 1. sole declared runtime → it.
|
|
1597
|
+
* 2. `default_runtime` is LIVE → `default_runtime` (the configured anchor AND the
|
|
1598
|
+
* obvious running target).
|
|
1599
|
+
* 3. exactly ONE other runtime is live (default dead) → that live one.
|
|
1600
|
+
* 4. otherwise (nothing live, or 2+ live without the default) → `default_runtime`.
|
|
1601
|
+
*
|
|
1602
|
+
* The peer's runtime is governed by its CONFIGURABLE `default_runtime` (the
|
|
1603
|
+
* documented `iapeer default-runtime` lever), NOT by which transcript was touched
|
|
1604
|
+
* last — last-active-by-mtime is hidden state and unreliable for codex (its rollout
|
|
1605
|
+
* file is flushed only on a real model turn, so a just-spawned fresh codex session
|
|
1606
|
+
* reads as "older" than a stale one). To make `iapeer new/attach <peer>` (no runtime
|
|
1607
|
+
* arg) resolve to codex, set the peer's default to codex once; per-command runtime
|
|
1608
|
+
* args remain the explicit override.
|
|
1609
|
+
*/
|
|
1610
|
+
export function resolvePeerRuntime(peer: PeerRecord, cfg: LifecycleConfig): Runtime {
|
|
1611
|
+
if (peer.runtimes.length === 1) return peer.runtimes[0]
|
|
1612
|
+
const live = liveRuntimesOf(peer, cfg)
|
|
1613
|
+
if (live.includes(peer.runtime)) return peer.runtime
|
|
1614
|
+
if (live.length === 1) return live[0]
|
|
1615
|
+
return peer.runtime
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1576
1618
|
export interface AttachOptions {
|
|
1577
1619
|
personality: string
|
|
1578
1620
|
runtime?: string
|
|
@@ -1586,10 +1628,11 @@ export type AttachResult =
|
|
|
1586
1628
|
/**
|
|
1587
1629
|
* `iapeer attach <peer> [runtime]` — ensure the peer is live, then hand back the
|
|
1588
1630
|
* socket/identity for the caller to `tmux attach`. ALWAYS RESUME (contract: attach
|
|
1589
|
-
* never starts fresh). Runtime: explicit arg, else the
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1631
|
+
* never starts fresh). Runtime: explicit arg, else `resolvePeerRuntime` (the SAME
|
|
1632
|
+
* resolver `new`/`compact` use — default_runtime anchored with a sole-live
|
|
1633
|
+
* refinement), so attach and new never disagree on which runtime "peer" means. A
|
|
1634
|
+
* warm-live session is attached directly; a warm-asleep one is woken with --resume
|
|
1635
|
+
* first (fail-loud if there is nothing to resume — a never-run peer must be folder-launched).
|
|
1593
1636
|
*/
|
|
1594
1637
|
export async function attachPeer(opts: AttachOptions): Promise<AttachResult> {
|
|
1595
1638
|
const env = opts.env ?? process.env
|
|
@@ -1598,8 +1641,10 @@ export async function attachPeer(opts: AttachOptions): Promise<AttachResult> {
|
|
|
1598
1641
|
if (!peer) return { ok: false, reason: `peer "${opts.personality}" is not registered` }
|
|
1599
1642
|
const runtimeResult = resolveWakeRuntime(opts.runtime, peer)
|
|
1600
1643
|
if (opts.runtime && !runtimeResult.ok) return { ok: false, reason: runtimeResult.error.message }
|
|
1601
|
-
//
|
|
1602
|
-
|
|
1644
|
+
// Omitted runtime resolves IDENTICALLY to `new`/`compact` (resolvePeerRuntime):
|
|
1645
|
+
// default_runtime anchored, sole-live refinement, never hidden last-active — so
|
|
1646
|
+
// `iapeer attach <peer>` and `iapeer new <peer>` never disagree on the runtime.
|
|
1647
|
+
const runtime = opts.runtime ?? resolvePeerRuntime(peer, cfg)
|
|
1603
1648
|
const identity = buildProcessAddress(runtime, opts.personality)
|
|
1604
1649
|
const sock = buildSocketPath(runtime, opts.personality, cfg.sockDir)
|
|
1605
1650
|
if (sessionAlive(sock, identity)) return { ok: true, identity, socketPath: sock, woke: false, runtime }
|