@agfpd/iapeer 0.4.2 → 0.4.4
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 -0
- package/docs/07-install-and-update.md +20 -0
- package/docs/10-messaging.md +1 -1
- package/docs/ru/07-/321/203/321/201/321/202/320/260/320/275/320/276/320/262/320/272/320/260-/320/270-/320/276/320/261/320/275/320/276/320/262/320/273/320/265/320/275/320/270/320/265.md +20 -0
- package/docs/ru/10-/320/276/320/261/320/274/320/265/320/275-/321/201/320/276/320/276/320/261/321/211/320/265/320/275/320/270/321/217/320/274/320/270.md +1 -1
- package/package.json +4 -4
- package/src/cli/index.ts +41 -24
- package/src/identity/profileStandard.ts +2 -2
- package/src/init/index.ts +52 -4
- package/src/launch/adapters/claude.ts +48 -23
- package/src/onboard/index.ts +61 -3
- package/src/transport/index.ts +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,18 @@ Two ways to work with an agent locally in your terminal:
|
|
|
76
76
|
|
|
77
77
|
Agents reach each other with `send_to_peer`; you can also message any peer over Telegram.
|
|
78
78
|
|
|
79
|
+
## Bypass Permissions mode
|
|
80
|
+
|
|
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 therefore 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
|
+
|
|
83
|
+
What this means for you, stated plainly:
|
|
84
|
+
|
|
85
|
+
- A peer can run any command its runtime can run — read and write files across your home dir, install software, make network calls — with **no confirmation step**. There is no sandbox between an agent and your machine.
|
|
86
|
+
- You are trusting the model's judgement (and the doctrine/instructions you give each peer) the same way you would trust a developer with a shell on your account.
|
|
87
|
+
- iapeer does **not** silently weaken your own interactive `claude`/`codex` setup: the bypass acceptance is recorded per peer in the agent's own config, and the project-MCP pre-approval is written **project-locally** (under the peer's folder), never to your global user settings.
|
|
88
|
+
|
|
89
|
+
Run iapeer on a host you control, give peers only the access they need, and treat a peer's instructions as security-relevant. `iapeer onboard` states this at setup; it is 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).
|
|
90
|
+
|
|
79
91
|
## What makes it different
|
|
80
92
|
|
|
81
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).
|
|
@@ -47,6 +47,24 @@ You'll see the core version, the daemon's health, and the memory-slot state. The
|
|
|
47
47
|
|
|
48
48
|
A breakdown of each onboard step is in [02 — Quick start](02-quickstart.md); runtimes and memory in detail — in [09](09-runtimes.md) and [11](11-extensions-and-memory.md).
|
|
49
49
|
|
|
50
|
+
## First-run dialogs on a clean machine
|
|
51
|
+
|
|
52
|
+
On a fresh macOS host the first install → onboard → peer launch surfaces a handful of dialogs. iapeer auto-clears the agent runtimes' own first-run modals when it brings a peer up; **three** items are yours to do.
|
|
53
|
+
|
|
54
|
+
**Auto-cleared (iapeer answers them on peer boot — you do nothing):**
|
|
55
|
+
|
|
56
|
+
- **Claude** — the theme picker ("Choose the text style"), folder-trust, external-CLAUDE.md-import, and dev-channels modals (the launcher accepts the default with Enter); the **bypass-permissions accept** screen ("WARNING: Claude Code running in Bypass Permissions mode") that a *virgin* config shows the first time `--dangerously-skip-permissions` runs — its default cursor is "No, exit", so the launcher **steps the cursor to "Yes, I accept"** and confirms (never a bare Enter, which would exit); and the **project MCP-server approval** ("N new MCP servers found in this project") when the peer's cwd carries a `.mcp.json` — pre-approved at `iapeer init` via a project-local `enableAllProjectMcpServers` flag and Enter-confirmed on boot as a backstop. Claude records each accept natively, so a re-launch on the same config is dialog-free.
|
|
57
|
+
- **Codex** — folder-trust ("Do you trust the contents of this directory") is pre-trusted at peer creation and also auto-accepted on boot; the self-update offer is disabled (`check_for_update_on_startup = false`) and auto-declined; hooks-review is auto-trusted.
|
|
58
|
+
- **Bypass / no-sandbox is the operating mode, not a one-time prompt.** The launcher runs Claude with `--dangerously-skip-permissions` and Codex with `--dangerously-bypass-approvals-and-sandbox`, so peers execute tools without per-action approval (a headless peer has no human to approve each call). The one-time *acceptance* screen above is the only interactive gate; the mode itself then stays on for every turn. See the security note in the [README](../README.md#bypass-permissions-mode) — this is a deliberate, disclosed trade-off.
|
|
59
|
+
|
|
60
|
+
**Three things you do (these cannot be automated):**
|
|
61
|
+
|
|
62
|
+
1. **Log in to the runtimes — before onboard.** Sign in to Claude Code and Codex first. Their login screen is a browser OAuth flow that no headless peer can drive, so iapeer deliberately does **not** auto-answer it; a runtime that is installed but not signed in makes a peer fail its first wake loudly. `iapeer onboard` warns when it finds an unauthenticated runtime.
|
|
63
|
+
2. **Login keychain password — once, during install.** The install code-signs the binary with a stable local identity so Full Disk Access survives updates; macOS asks for your login keychain password the one time that identity is created. It is expected (the installer announces it). If declined, the binary still works — TCC prompts would just recur on updates.
|
|
64
|
+
3. **Full Disk Access — a manual grant, no prompt.** macOS TCC cannot be set by any flag, env var, or script. Without it, a peer reading or writing a TCC-protected path (an iCloud Obsidian vault, Desktop, Documents, Downloads) silently fails with EPERM — no prompt, no hang. Grant it in **System Settings → Privacy & Security → Full Disk Access** (`~/.iapeer` itself is under `$HOME` and needs nothing); onboard prints the reminder.
|
|
65
|
+
|
|
66
|
+
Gatekeeper does not gate the locally-built CLI run from a terminal (no quarantine attribute). Verified live on a truly-virgin Claude config (claude 2.1.183): the theme picker, folder-trust, the bypass-permissions accept (cursor stepped to "Yes"), and the project MCP-server approval all auto-clear in sequence, the peer reaches its input prompt, and a second launch on the same config is dialog-free; the Codex folder-trust / update auto-clearing likewise (an owner's clean-machine install + an isolated first-boot test).
|
|
67
|
+
|
|
50
68
|
## Updating
|
|
51
69
|
|
|
52
70
|
```bash
|
|
@@ -74,6 +92,8 @@ Runtimes and memory are **best-effort**: a component failure is reported and the
|
|
|
74
92
|
|
|
75
93
|
`iapeer update --foundation-only` does just step 1 (the narrow case). A pinned `iapeer update <version>` is **core-only** by design — a version pin is core-specific (downgrade, or pin a proven version, deeper than one rollback step).
|
|
76
94
|
|
|
95
|
+
> **Upgrading from before the cascade (< 0.4.2).** The cascade is driven by the *installed* binary, so the very first `iapeer update` from a pre-cascade version runs the OLD (foundation-only) logic: it upgrades the core to the cascade-capable binary but does not yet cascade runtimes + memory. Run `iapeer update` once more — now the new binary cascades the rest. This is a one-time bootstrap; from 0.4.2 onward it is a single command. (A fresh install lands on a cascade-capable binary directly, so it never needs the double run.)
|
|
96
|
+
|
|
77
97
|
## Rollback
|
|
78
98
|
|
|
79
99
|
```bash
|
package/docs/10-messaging.md
CHANGED
|
@@ -55,7 +55,7 @@ iapeer compact assistant # compact the conversation
|
|
|
55
55
|
|
|
56
56
|
`interrupt` stops a turn that's gone off-track or hung, without losing context. `compact` compacts the conversation; if the peer is asleep after a clean sleep, the daemon resumes it first, then compacts, and for an empty session it honestly answers that there's nothing to compact.
|
|
57
57
|
|
|
58
|
-
Separate are **aliases** — shortcuts like `/
|
|
58
|
+
Separate are **aliases** — shortcuts like `/alias_new`, `/alias_compact`, defined in the peer profile. These aren't control but text templates: the runtime expands an alias into its full text and delivers it to the peer as an ordinary message. So with one short command the operator runs a pre-written scenario (e.g. "save state to memory and restart fresh").
|
|
59
59
|
|
|
60
60
|
## Ephemeral workers
|
|
61
61
|
|
|
@@ -47,6 +47,24 @@ iapeer status
|
|
|
47
47
|
|
|
48
48
|
Разбор каждого шага onboard — в [02 — Быстрый старт](02-быстрый-старт.md); рантаймы и память подробно — в [09](09-рантаймы.md) и [11](11-расширения-и-память.md).
|
|
49
49
|
|
|
50
|
+
## Диалоги первого запуска на чистой машине
|
|
51
|
+
|
|
52
|
+
На свежем macOS-хосте первый цикл install → onboard → запуск пира выводит несколько диалогов. Собственные first-run-модалки агент-рантаймов iapeer авто-снимает при поднятии пира; **три** пункта — на тебе.
|
|
53
|
+
|
|
54
|
+
**Авто-снимается (iapeer отвечает на boot пира — от тебя ничего):**
|
|
55
|
+
|
|
56
|
+
- **Claude** — theme-picker («Choose the text style»), folder-trust, импорт внешнего CLAUDE.md, dev-channels (лончер принимает дефолт через Enter); экран **принятия bypass-permissions** («WARNING: Claude Code running in Bypass Permissions mode»), который *виргин*-конфиг показывает при первом запуске `--dangerously-skip-permissions` — его курсор по умолчанию стоит на «No, exit», поэтому лончер **шагает курсором на «Yes, I accept»** и подтверждает (никогда не голый Enter — он бы вышел из сессии); и **аппрув проектных MCP-серверов** («N new MCP servers found in this project»), когда cwd пира несёт `.mcp.json` — пред-одобряется на `iapeer init` через проектно-локальный флаг `enableAllProjectMcpServers` и на boot подтверждается Enter'ом как страховка. Каждый accept claude записывает нативно, поэтому повторный запуск на том же конфиге уже без диалогов.
|
|
57
|
+
- **Codex** — folder-trust («Do you trust the contents of this directory») пред-доверяется при создании пира и авто-принимается на boot; self-update-оффер отключён (`check_for_update_on_startup = false`) и авто-отклоняется; hooks-review авто-доверяется.
|
|
58
|
+
- **Bypass / no-sandbox — это режим работы, а не разовый промпт.** Лончер запускает Claude с `--dangerously-skip-permissions`, а Codex с `--dangerously-bypass-approvals-and-sandbox`, поэтому пиры выполняют инструменты без поштучного аппрува (у headless-пира нет человека, который подтверждал бы каждый вызов). Разовый экран *принятия* выше — единственный интерактивный гейт; сам режим затем остаётся включённым на каждом ходу. См. security-заметку в [README](../../README.md#bypass-permissions-mode) — это намеренный, раскрытый компромисс.
|
|
59
|
+
|
|
60
|
+
**Три пункта на тебе (автоматизировать нельзя):**
|
|
61
|
+
|
|
62
|
+
1. **Залогинься в рантаймы — ДО onboard.** Войди в Claude Code и Codex заранее. Их экран логина — браузерный OAuth, который headless-пир не пройдёт, поэтому iapeer НАМЕРЕННО его не авто-отвечает; установленный, но не залогиненный рантайм роняет первый wake пира громко. `iapeer onboard` предупреждает, если найдёт неаутентифицированный рантайм.
|
|
63
|
+
2. **Пароль логин-связки — один раз, при install.** Install подписывает бинарь стабильной локальной идентичностью, чтобы Full Disk Access пережил обновления; macOS один раз спросит пароль логин-связки при создании этой идентичности. Это ожидаемо (инсталлер анонсирует). При отказе бинарь всё равно работает — просто TCC-промпты будут повторяться на апдейтах.
|
|
64
|
+
3. **Full Disk Access — ручной грант, без промпта.** macOS TCC нельзя выставить ни флагом, ни env, ни скриптом. Без него пир, читающий/пишущий TCC-защищённый путь (iCloud Obsidian vault, Desktop, Documents, Downloads), тихо падает с EPERM — без промпта, без зависания. Выдай в **System Settings → Privacy & Security → Full Disk Access** (`~/.iapeer` сам под `$HOME` и ничего не требует); onboard напомнит.
|
|
65
|
+
|
|
66
|
+
Gatekeeper не гейтит локально собранный CLI, запускаемый из терминала (нет quarantine-атрибута). Проверено живьём на по-настоящему виргин-конфиге Claude (claude 2.1.183): theme-picker, folder-trust, принятие bypass-permissions (курсор шагнут на «Yes») и аппрув проектных MCP-серверов авто-снимаются по очереди, пир доходит до строки ввода, а повторный запуск на том же конфиге — уже без диалогов; codex folder-trust / update — аналогично (clean-install владельца + изолированный first-boot тест).
|
|
67
|
+
|
|
50
68
|
## Обновление
|
|
51
69
|
|
|
52
70
|
```bash
|
|
@@ -74,6 +92,8 @@ iapeer update --force # переустановить, даже есл
|
|
|
74
92
|
|
|
75
93
|
`iapeer update --foundation-only` делает только шаг 1 (узкий случай). Запиненный `iapeer update <версия>` — **только ядро** by design: пин версии специфичен для ядра (откат глубже одного шага или фиксация проверенной версии).
|
|
76
94
|
|
|
95
|
+
> **Обновление с версии до каскада (< 0.4.2).** Каскад исполняет *установленный* бинарь, поэтому самый первый `iapeer update` со старой версии работает по СТАРОЙ (foundation-only) логике: он поднимает ядро до каскад-способного бинаря, но рантаймы + память пока не каскадит. Запусти `iapeer update` ещё раз — теперь новый бинарь докаскадит остальное. Это разовый bootstrap; начиная с 0.4.2 — одна команда. (Свежая установка сразу ставит каскад-способный бинарь, поэтому двойной прогон ей не нужен.)
|
|
96
|
+
|
|
77
97
|
## Откат
|
|
78
98
|
|
|
79
99
|
```bash
|
|
@@ -55,7 +55,7 @@ iapeer compact assistant # сжать диалог
|
|
|
55
55
|
|
|
56
56
|
`interrupt` останавливает зашедший не туда или зависший ход, не теряя контекст. `compact` сжимает диалог; если пир спит после чистого усыпления, демон сначала продолжит диалог, потом сожмёт, а для пустой сессии честно ответит, что сжимать нечего.
|
|
57
57
|
|
|
58
|
-
Отдельно стоят **алиасы** — шорткаты вида `/
|
|
58
|
+
Отдельно стоят **алиасы** — шорткаты вида `/alias_new`, `/alias_compact`, заданные в профиле пира. Это не контроль, а заготовки текста: рантайм разворачивает алиас в полный текст и доставляет его пиру как обычное сообщение. Так оператор одной короткой командой запускает заранее прописанный сценарий (например, «сохрани состояние в память и перезапустись свежим»).
|
|
59
59
|
|
|
60
60
|
## Эфемерные воркеры
|
|
61
61
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "agfpd",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"test": "IAPEER_TEST_SANDBOX=1 bun test",
|
|
45
45
|
"typecheck": "tsc --noEmit",
|
|
46
46
|
"preversion": "npm run test && npm run typecheck",
|
|
47
|
-
"release": "npm version patch && npm publish && git push --follow-tags",
|
|
48
|
-
"release:minor": "npm version minor && npm publish && git push --follow-tags",
|
|
49
|
-
"release:major": "npm version major && npm publish && git push --follow-tags",
|
|
47
|
+
"release": "npm version patch && npm publish && git push origin main --follow-tags",
|
|
48
|
+
"release:minor": "npm version minor && npm publish && git push origin main --follow-tags",
|
|
49
|
+
"release:major": "npm version major && npm publish && git push origin main --follow-tags",
|
|
50
50
|
"prepublishOnly": "test -z \"$(git status --porcelain)\" || (echo 'release: working tree is dirty — commit or stash before release' >&2 && exit 1)"
|
|
51
51
|
},
|
|
52
52
|
"publishConfig": {
|
package/src/cli/index.ts
CHANGED
|
@@ -480,7 +480,7 @@ export interface NewPeerOutcome {
|
|
|
480
480
|
* new <peer> [runtime] — the UNCONDITIONAL fresh-restart control command
|
|
481
481
|
* (docs/Control-команды §new).
|
|
482
482
|
*
|
|
483
|
-
* The emergency lever for a HUNG or dead agent session: /
|
|
483
|
+
* The emergency lever for a HUNG or dead agent session: /alias_new covers only
|
|
484
484
|
* the COOPERATIVE path (a live peer reads the expanded prompt and runs
|
|
485
485
|
* `iapeer self-fresh`); a stuck/raving/dead peer never reads a prompt — this
|
|
486
486
|
* command restarts it MECHANICALLY, bypassing the peer entirely:
|
|
@@ -1253,9 +1253,9 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1253
1253
|
// and the memory provider's install-time sweep (`--all`). NOT slot-gated here —
|
|
1254
1254
|
// an explicit operator/provider action; only the BIRTH-time hook is slot-gated.
|
|
1255
1255
|
const state = positionals[0]
|
|
1256
|
-
if (state !== 'off' && state !== 'on') return usage(
|
|
1256
|
+
if (state !== 'off' && state !== 'on') return argErr(errOut, 'native-memory needs a state — usage: iapeer native-memory <off|on> (--peer <p> | --all)')
|
|
1257
1257
|
const peerName = typeof flags.peer === 'string' ? flags.peer : undefined
|
|
1258
|
-
if (flags.all !== true && !peerName) return
|
|
1258
|
+
if (flags.all !== true && !peerName) return argErr(errOut, 'native-memory needs a target — pass --peer <p> or --all')
|
|
1259
1259
|
const { applyNativeMemory } = await import('../launch/nativeMemory.ts')
|
|
1260
1260
|
const index = readPeersIndex({ env })
|
|
1261
1261
|
const targets = flags.all === true ? index.peers : index.peers.filter(p => p.personality === peerName)
|
|
@@ -1288,7 +1288,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1288
1288
|
// the read-only drift detector for verify pipelines — per-hook
|
|
1289
1289
|
// trusted/missing/drift, exit 1 on anything but full trust.
|
|
1290
1290
|
const target = positionals[0]
|
|
1291
|
-
if (!target) return
|
|
1291
|
+
if (!target) return argErr(errOut, 'trust-hooks needs a hooks file — usage: iapeer trust-hooks <hooks.json> [--check]')
|
|
1292
1292
|
const { preSeedCodexHooksTrust, checkCodexHooksTrust } = await import('../launch/codexHooksTrust.ts')
|
|
1293
1293
|
if (flags.check === true) {
|
|
1294
1294
|
let checks
|
|
@@ -1331,7 +1331,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1331
1331
|
// manifest), THEN deploy its declared peer-set (each → provision + per-peer
|
|
1332
1332
|
// self-config + auto-bootstrap). A runtime whose manifest declares no peers
|
|
1333
1333
|
// (telegram) is operator-add — use `iapeer create <human> --runtime telegram`.
|
|
1334
|
-
if (!positionals[0]) return usage(
|
|
1334
|
+
if (!positionals[0]) return argErr(errOut, 'install-runtime needs a runtime — usage: iapeer install-runtime <runtime> (e.g. telegram | notifier)')
|
|
1335
1335
|
const { onboardRuntime } = await import('../runtime/deploy.ts')
|
|
1336
1336
|
const r = await onboardRuntime({
|
|
1337
1337
|
runtime: positionals[0] as Runtime,
|
|
@@ -1359,7 +1359,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1359
1359
|
// restart the runtime's peers via the regular stop/start. The core's own
|
|
1360
1360
|
// `update` stays foundation-only — this is the runtimes' counterpart.
|
|
1361
1361
|
const all = flags.all === true
|
|
1362
|
-
if (!all && !positionals[0]) return
|
|
1362
|
+
if (!all && !positionals[0]) return argErr(errOut, 'update-runtime needs a runtime or --all — usage: iapeer update-runtime <runtime> | --all')
|
|
1363
1363
|
const { updateRuntime, updateAllRuntimes } = await import('../runtime/update.ts')
|
|
1364
1364
|
const results = all
|
|
1365
1365
|
? await updateAllRuntimes({ force: flags.force === true, env, warn: m => errOut(`warn: ${m}\n`) })
|
|
@@ -1406,7 +1406,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1406
1406
|
// cwd-INDEPENDENT: resolve a location (default ~/.iapeer/peers/<p> or --path),
|
|
1407
1407
|
// scaffold the folder (no-clobber), then init it. Operator-add for an infra
|
|
1408
1408
|
// human (telegram) or any agentic peer; provisions + auto-bootstraps infra.
|
|
1409
|
-
if (!positionals[0]) return
|
|
1409
|
+
if (!positionals[0]) return argErr(errOut, 'create needs a peer name — usage: iapeer create <personality> [--runtime r] [--path dir]')
|
|
1410
1410
|
const explicitRuntime = typeof flags.runtime === 'string' ? (flags.runtime as Runtime) : undefined
|
|
1411
1411
|
// FU5 — never SILENTLY default to claude when both agentic runtimes are
|
|
1412
1412
|
// installed. `--runtime` selects the DEFAULT (not the sole runtime); the
|
|
@@ -1570,7 +1570,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1570
1570
|
: positionals[0]
|
|
1571
1571
|
? [positionals[0]]
|
|
1572
1572
|
: null
|
|
1573
|
-
if (!peers) return
|
|
1573
|
+
if (!peers) return argErr(errOut, 'stop needs a peer or --all — usage: iapeer stop <peer> [runtime] | --all')
|
|
1574
1574
|
const outcomes = peers.flatMap(p => stopPeer(p, flags.all === true ? undefined : positionals[1], { env }))
|
|
1575
1575
|
for (const o of outcomes) out(`${o.personality} (${o.runtime}): ${o.action}${o.reason ? ` — ${o.reason}` : ''}\n`)
|
|
1576
1576
|
return outcomes.some(o => o.action === 'refused-foreign-launchd') ? 1 : 0
|
|
@@ -1579,9 +1579,9 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1579
1579
|
// Fleet-switch enabler (codex-parity audit): add an agentic runtime to
|
|
1580
1580
|
// existing peer(s) — full codex birth chain per target, idempotent.
|
|
1581
1581
|
const rt = positionals[0]
|
|
1582
|
-
if (!rt) return usage(
|
|
1582
|
+
if (!rt) return argErr(errOut, 'add-runtime needs a runtime — usage: iapeer add-runtime <runtime> (--peer <p> | --all)')
|
|
1583
1583
|
const peerName = typeof flags.peer === 'string' ? flags.peer : undefined
|
|
1584
|
-
if (flags.all !== true && !peerName) return
|
|
1584
|
+
if (flags.all !== true && !peerName) return argErr(errOut, 'add-runtime needs a target — pass --peer <p> or --all')
|
|
1585
1585
|
const outcomes = await addRuntime(rt, { peer: peerName, all: flags.all === true, env })
|
|
1586
1586
|
for (const o of outcomes) out(`${o.personality}: ${o.action}${o.detail ? ` — ${o.detail}` : ''}\n`)
|
|
1587
1587
|
return outcomes.some(o => o.action === 'failed') ? 1 : 0
|
|
@@ -1589,9 +1589,9 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1589
1589
|
case 'default-runtime': {
|
|
1590
1590
|
// The PRIMARY flip (routing/wake default) — the fleet-switch moment itself.
|
|
1591
1591
|
const rt = positionals[0]
|
|
1592
|
-
if (!rt) return usage(
|
|
1592
|
+
if (!rt) return argErr(errOut, 'default-runtime needs a runtime — usage: iapeer default-runtime <runtime> (--peer <p> | --all)')
|
|
1593
1593
|
const peerName = typeof flags.peer === 'string' ? flags.peer : undefined
|
|
1594
|
-
if (flags.all !== true && !peerName) return
|
|
1594
|
+
if (flags.all !== true && !peerName) return argErr(errOut, 'default-runtime needs a target — pass --peer <p> or --all')
|
|
1595
1595
|
const outcomes = await defaultRuntime(rt, { peer: peerName, all: flags.all === true, env })
|
|
1596
1596
|
for (const o of outcomes) out(`${o.personality}: ${o.action}${o.detail ? ` — ${o.detail}` : ''}\n`)
|
|
1597
1597
|
return outcomes.some(o => o.action === 'failed') ? 1 : 0
|
|
@@ -1601,7 +1601,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1601
1601
|
// §new): the emergency lever for a hung/dead session, bypasses the peer.
|
|
1602
1602
|
// Source: operator CLI, or telegram-runtime's clean-/new detect (their bot
|
|
1603
1603
|
// shells `iapeer new <peer> <runtime>` — exit 0 ⟺ fresh session up+ready).
|
|
1604
|
-
if (!positionals[0]) return
|
|
1604
|
+
if (!positionals[0]) return argErr(errOut, 'new needs a peer name — usage: iapeer new <peer> [runtime]')
|
|
1605
1605
|
const o = await newPeer(positionals[0], positionals[1], { env })
|
|
1606
1606
|
if (o.action === 'fresh') {
|
|
1607
1607
|
out(`new: ${o.runtime}-${o.personality} fresh session up\n`)
|
|
@@ -1622,7 +1622,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1622
1622
|
: positionals[0]
|
|
1623
1623
|
? [positionals[0]]
|
|
1624
1624
|
: null
|
|
1625
|
-
if (!peers) return
|
|
1625
|
+
if (!peers) return argErr(errOut, 'start needs a peer or --all — usage: iapeer start <peer> [runtime] | --all')
|
|
1626
1626
|
const outcomes = peers.flatMap(p => startPeer(p, flags.all === true ? undefined : positionals[1], { env }))
|
|
1627
1627
|
for (const o of outcomes) out(`${o.personality} (${o.runtime}): ${o.action}${o.reason ? ` — ${o.reason}` : ''}\n`)
|
|
1628
1628
|
return outcomes.some(o => o.action === 'refused-foreign-launchd') ? 1 : 0
|
|
@@ -1636,7 +1636,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1636
1636
|
: positionals[0]
|
|
1637
1637
|
? [positionals[0]]
|
|
1638
1638
|
: null
|
|
1639
|
-
if (!peers) return
|
|
1639
|
+
if (!peers) return argErr(errOut, 'refresh needs a peer or --all — usage: iapeer refresh <peer> [runtime] | --all')
|
|
1640
1640
|
const outcomes = peers.flatMap(p => refreshPeer(p, flags.all === true ? undefined : positionals[1], { env }))
|
|
1641
1641
|
for (const o of outcomes) out(`${o.personality} (${o.runtime}): ${o.action}\n`)
|
|
1642
1642
|
return 0
|
|
@@ -1645,7 +1645,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1645
1645
|
// Reap a registry record through the locked writer (the operator path over
|
|
1646
1646
|
// registry.removePeer). Idempotent on an absent peer (exit 0). Refuses a LIVE
|
|
1647
1647
|
// peer unless --force (orphaning a running session from routing is the risk).
|
|
1648
|
-
if (!positionals[0]) return
|
|
1648
|
+
if (!positionals[0]) return argErr(errOut, 'remove needs a peer name — usage: iapeer remove <peer> [--force]')
|
|
1649
1649
|
const o = await removePeerCli(positionals[0], { force: flags.force === true, env })
|
|
1650
1650
|
if (o.action === 'removed') {
|
|
1651
1651
|
out(`removed "${o.personality}" from the registry\n`)
|
|
@@ -1687,7 +1687,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1687
1687
|
const mf = flags['message-file']
|
|
1688
1688
|
message = mf === '-' ? readFileSync(0, 'utf8') : readFileSync(mf, 'utf8')
|
|
1689
1689
|
}
|
|
1690
|
-
if (!positionals[0] || message === null) return usage(
|
|
1690
|
+
if (!positionals[0] || message === null) return argErr(errOut, 'send needs a target and a message — usage: iapeer send <target> --message <text> (or --message-file <f|->)')
|
|
1691
1691
|
// --attachment is REPEATABLE; parseArgs collapses repeats (last-wins), so
|
|
1692
1692
|
// re-scan the raw rest argv to collect every attachment path (else files
|
|
1693
1693
|
// silently drop — a text-only smoke test would not catch it).
|
|
@@ -1935,7 +1935,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1935
1935
|
// Ф-F: the always-on infra entrypoint (telegram/notifier), held by launchd.
|
|
1936
1936
|
// The infra plist runs `iapeer run-infra <personality> <runtime>` (installed
|
|
1937
1937
|
// binary) instead of `bun launchdRun.ts`. cwd = the launchd WorkingDirectory.
|
|
1938
|
-
if (!positionals[0] || !positionals[1]) return usage(
|
|
1938
|
+
if (!positionals[0] || !positionals[1]) return argErr(errOut, 'run-infra needs a runtime and a personality — usage: iapeer run-infra <runtime> <personality> (internal launchd entrypoint)')
|
|
1939
1939
|
return await runAlwaysOn(positionals[0], positionals[1], process.cwd())
|
|
1940
1940
|
}
|
|
1941
1941
|
case 'self-done': {
|
|
@@ -2009,7 +2009,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2009
2009
|
case 'interrupt': {
|
|
2010
2010
|
// In-session control (Ф-E, clean-slash namespace): interrupt a stuck/raving
|
|
2011
2011
|
// turn (Escape). UNCONDITIONAL — acts on the live session.
|
|
2012
|
-
if (!positionals[0]) return
|
|
2012
|
+
if (!positionals[0]) return argErr(errOut, 'interrupt needs a peer name — usage: iapeer interrupt <peer> [runtime]')
|
|
2013
2013
|
const r = await routeControl(positionals[0], positionals[1], { name: verb })
|
|
2014
2014
|
if (!r.ok) {
|
|
2015
2015
|
errOut(`${verb}: ${r.error.message}\n`)
|
|
@@ -2022,7 +2022,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2022
2022
|
// Dialogue control: if the session is merely asleep
|
|
2023
2023
|
// after a clean idle-reap, resume the same dialogue and compact it; if there
|
|
2024
2024
|
// is no resumable dialogue, fail honestly instead of compacting a fresh one.
|
|
2025
|
-
if (!positionals[0]) return
|
|
2025
|
+
if (!positionals[0]) return argErr(errOut, 'compact needs a peer name — usage: iapeer compact <peer> [runtime]')
|
|
2026
2026
|
const o = await compactPeer(positionals[0], positionals[1], { env })
|
|
2027
2027
|
if (o.action === 'compacted') {
|
|
2028
2028
|
out(`compact → ${o.personality} (${o.runtime})${o.woke ? ' after resume' : ''}\n`)
|
|
@@ -2036,7 +2036,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2036
2036
|
// `connect telegram <peer> [--token <t>]`. The human owes only the token;
|
|
2037
2037
|
// alias/bot-add/interface/router-restart are resolved by the system. The
|
|
2038
2038
|
// FIRST message from the human to the bot activates the chat (platform rule).
|
|
2039
|
-
if (positionals[0] !== 'telegram' || !positionals[1]) return usage(
|
|
2039
|
+
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)')
|
|
2040
2040
|
const { connectTelegram } = await import('../connect/index.ts')
|
|
2041
2041
|
const r = await connectTelegram({
|
|
2042
2042
|
peer: positionals[1],
|
|
@@ -2066,7 +2066,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2066
2066
|
// per-runtime (claude project-scope IN the peer cwd / codex global) + enable +
|
|
2067
2067
|
// call the plugin's `setup` ONLY if its iapeer.json declares it. Idempotent and
|
|
2068
2068
|
// fleet-safe — claude is keyed by the peer's projectPath. `enable <plugin> [peer]`.
|
|
2069
|
-
if (!positionals[0]) return
|
|
2069
|
+
if (!positionals[0]) return argErr(errOut, 'enable needs a capability/plugin — usage: iapeer enable <plugin> [peer]')
|
|
2070
2070
|
const { enableCapability } = await import('../enable/index.ts')
|
|
2071
2071
|
const r = enableCapability({
|
|
2072
2072
|
plugin: positionals[0],
|
|
@@ -2081,7 +2081,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2081
2081
|
return r.runtimes.some(rt => rt.state === 'failed') || r.setup === 'failed' ? 1 : 0
|
|
2082
2082
|
}
|
|
2083
2083
|
case 'attach': {
|
|
2084
|
-
if (!positionals[0]) return
|
|
2084
|
+
if (!positionals[0]) return argErr(errOut, 'attach needs a peer name — usage: iapeer attach <peer> [runtime]')
|
|
2085
2085
|
const r = await attachPeer({ personality: positionals[0], runtime: positionals[1], env })
|
|
2086
2086
|
if (!r.ok) {
|
|
2087
2087
|
errOut(`attach: ${r.reason}\n`)
|
|
@@ -2157,7 +2157,14 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2157
2157
|
const id = resolveIdentity({ env })
|
|
2158
2158
|
const r = await folderLaunch({ cwd: process.cwd(), runtime: verb, env, cfg })
|
|
2159
2159
|
if (r.status === 'FAILED') {
|
|
2160
|
-
|
|
2160
|
+
// A bare token format-matches a runtime, so it dispatched here as `iapeer
|
|
2161
|
+
// <runtime>` (launch the cwd peer on that runtime). When that fails, the user
|
|
2162
|
+
// most often just MISTYPED a command — surface BOTH readings so the message is
|
|
2163
|
+
// actionable instead of a bare "runtime not declared" (FU13).
|
|
2164
|
+
errOut(
|
|
2165
|
+
`iapeer: "${verb}" — ${r.reason}\n` +
|
|
2166
|
+
' If you meant a command, run `iapeer help`. `iapeer <runtime>` launches a runtime declared for the current peer.\n',
|
|
2167
|
+
)
|
|
2161
2168
|
return 1
|
|
2162
2169
|
}
|
|
2163
2170
|
const identity = r.process_address ?? buildProcessAddress(verb, id.personality)
|
|
@@ -2169,6 +2176,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2169
2176
|
out(`launched ${identity} (fresh)\n`)
|
|
2170
2177
|
return 0
|
|
2171
2178
|
}
|
|
2179
|
+
errOut(`iapeer: unknown verb "${verb ?? ''}"\n`)
|
|
2172
2180
|
return usage(errOut)
|
|
2173
2181
|
}
|
|
2174
2182
|
}
|
|
@@ -2183,6 +2191,15 @@ function usage(errOut: (s: string) => void): number {
|
|
|
2183
2191
|
return 2
|
|
2184
2192
|
}
|
|
2185
2193
|
|
|
2194
|
+
/** A TARGETED argument error (FU13): a one-line, addressed, actionable reason for THIS
|
|
2195
|
+
* verb — never the generic top-level help wall (which reads as "nothing happened" to a
|
|
2196
|
+
* user who just mistyped one verb). `message` should name what's missing + the verb's
|
|
2197
|
+
* own form, e.g. "connect telegram needs a peer — usage: iapeer connect telegram <peer>". */
|
|
2198
|
+
function argErr(errOut: (s: string) => void, message: string): number {
|
|
2199
|
+
errOut(`iapeer: ${message}\n`)
|
|
2200
|
+
return 2
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2186
2203
|
/** Interactive default-runtime picker (FU5) — only called on a real TTY when more
|
|
2187
2204
|
* than one agentic runtime is installed and no `--runtime` was given. Accepts a
|
|
2188
2205
|
* number or the runtime name; empty input takes the first (the listed default). */
|
|
@@ -121,13 +121,13 @@ export function validateProfileStandard(raw: unknown, cwd: string): ProfileIssue
|
|
|
121
121
|
issues.push({ severity: 'error', field: 'interfaces', message: 'must be a JSON object' })
|
|
122
122
|
}
|
|
123
123
|
// Legacy top-level aliases — contract relocates to the `expansion.aliases` plugin-
|
|
124
|
-
// config section with the /
|
|
124
|
+
// config section with the /alias_* namespace (clean slash-keys are reserved for the
|
|
125
125
|
// control layer). telegram-runtime owns that migration; flagged, not an error.
|
|
126
126
|
if (obj.aliases !== undefined) {
|
|
127
127
|
issues.push({
|
|
128
128
|
severity: 'warn',
|
|
129
129
|
field: 'aliases',
|
|
130
|
-
message: 'legacy top-level `aliases` — contract relocates to expansion.aliases (/
|
|
130
|
+
message: 'legacy top-level `aliases` — contract relocates to expansion.aliases (/alias_* namespace); owner migration pending',
|
|
131
131
|
})
|
|
132
132
|
}
|
|
133
133
|
// aliases misfiled in the PASSPORT (interfaces.telegram) — an interim home.
|
package/src/init/index.ts
CHANGED
|
@@ -6,16 +6,19 @@
|
|
|
6
6
|
// THE BIG SIMPLIFICATION (install-gate, proven live): a peer gets send_to_peer
|
|
7
7
|
// purely from a project-scope `.mcp.json` pointing at the host-wide HTTP-MCP daemon —
|
|
8
8
|
// NO plugin install, NO /reload-plugins, NO GC version-snapshots (the whole legacy
|
|
9
|
-
// persistent-peer install-gate class is gone).
|
|
10
|
-
// (--dangerously-skip-permissions, as the launch primitive runs it)
|
|
11
|
-
// `.mcp.json` http server and send_to_peer is callable on its FIRST turn
|
|
9
|
+
// persistent-peer install-gate class is gone). A cold-start claude session
|
|
10
|
+
// (--dangerously-skip-permissions, as the launch primitive runs it) finds the
|
|
11
|
+
// `.mcp.json` http server and send_to_peer is callable on its FIRST turn. The server's
|
|
12
|
+
// project-approval is pre-granted by ensureClaudeProjectMcpEnabled (the
|
|
13
|
+
// enableAllProjectMcpServers flag below); on a VIRGIN config without it the launch's
|
|
14
|
+
// bootDialogKeys clears the one-time "new MCP servers found" dialog as a backstop.
|
|
12
15
|
//
|
|
13
16
|
// claude side here. codex side (`[mcp_servers.<name>]` in ~/.codex/config.toml +
|
|
14
17
|
// default_tools_approval_mode="approve") lands with its own live codex check.
|
|
15
18
|
|
|
16
19
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
17
20
|
import { homedir } from 'os'
|
|
18
|
-
import { join } from 'path'
|
|
21
|
+
import { dirname, join } from 'path'
|
|
19
22
|
import { CODEX_BEARER_ENV_VAR, CODEX_DUMMY_BEARER, IAPEER_DIR, isInfraRuntime, type Runtime } from '../core/constants.ts'
|
|
20
23
|
import { IapError } from '../core/errors.ts'
|
|
21
24
|
import {
|
|
@@ -26,6 +29,7 @@ import {
|
|
|
26
29
|
type StorageOptions,
|
|
27
30
|
} from '../storage/index.ts'
|
|
28
31
|
import { provisionPeer, type ProvisionResult } from '../provision/index.ts'
|
|
32
|
+
import { claudeSettingsPath } from '../launch/nativeMemory.ts'
|
|
29
33
|
import { launchctlBootstrap, launchdPlistPath, type BootstrapResult } from '../launch/launchd.ts'
|
|
30
34
|
import { runtimeSelfConfig, type SelfConfigResult } from '../runtime/index.ts'
|
|
31
35
|
import type { Intelligence } from '../core/constants.ts'
|
|
@@ -116,6 +120,46 @@ export function writeClaudeMcpConfig(cwd: string, personality: string, daemonUrl
|
|
|
116
120
|
return path
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Idempotently enable project-scope MCP servers for a claude peer by merging
|
|
125
|
+
* `"enableAllProjectMcpServers": true` into `<cwd>/.claude/settings.json`.
|
|
126
|
+
*
|
|
127
|
+
* Why: on a VIRGIN claude config, the FIRST launch in a cwd carrying a `.mcp.json`
|
|
128
|
+
* (writeClaudeMcpConfig above) shows a BLOCKING "N new MCP servers found in this
|
|
129
|
+
* project" approval dialog BEFORE the input prompt — a headless peer has no one to
|
|
130
|
+
* approve it, so the wake stalls. This project-LOCAL flag pre-approves the cwd's MCP
|
|
131
|
+
* servers so the dialog never appears (verified live, claude 2.1.183). It is
|
|
132
|
+
* PROJECT-scoped (this peer's cwd only) — it does NOT touch the user's GLOBAL
|
|
133
|
+
* ~/.claude/settings.json, so it never alters the user's own claude environment (the
|
|
134
|
+
* same constraint that forbids a global skipDangerousModePermissionPrompt). The launch
|
|
135
|
+
* adapter's bootDialogKeys also Enter-confirms this dialog as a belt-and-suspenders
|
|
136
|
+
* backstop, so a peer whose cwd predates this flag still boots.
|
|
137
|
+
*
|
|
138
|
+
* NO-CLOBBER merge (the file may carry foreign blocks — plugin enables, statusline
|
|
139
|
+
* wrappers, the native-memory lever): parse/patch + atomic write; a non-object
|
|
140
|
+
* settings.json is REFUSED rather than overwritten. Returns the written path, or null
|
|
141
|
+
* when already set / on a refusal / on any error (best-effort, never throws — the
|
|
142
|
+
* bootDialogKeys backstop covers the dialog regardless).
|
|
143
|
+
*/
|
|
144
|
+
export function ensureClaudeProjectMcpEnabled(cwd: string): string | null {
|
|
145
|
+
const path = claudeSettingsPath(cwd)
|
|
146
|
+
try {
|
|
147
|
+
let obj: Record<string, unknown> = {}
|
|
148
|
+
if (existsSync(path)) {
|
|
149
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as unknown
|
|
150
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null // refuse to clobber a non-object
|
|
151
|
+
obj = raw as Record<string, unknown>
|
|
152
|
+
}
|
|
153
|
+
if (obj.enableAllProjectMcpServers === true) return path // already enabled
|
|
154
|
+
obj.enableAllProjectMcpServers = true
|
|
155
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
156
|
+
writeFileAtomic(path, `${JSON.stringify(obj, null, 2)}\n`, 0o644)
|
|
157
|
+
return path
|
|
158
|
+
} catch {
|
|
159
|
+
return null // best-effort; the bootDialogKeys backstop still clears the dialog
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
119
163
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
120
164
|
// codex MCP config (~/.codex/config.toml — HOST-WIDE; the token-free recipe)
|
|
121
165
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -429,6 +473,10 @@ export async function initPeer(opts: InitPeerOptions): Promise<InitPeerResult> {
|
|
|
429
473
|
// MCP client), so it gets no `.mcp.json`.
|
|
430
474
|
if (provisioned.runtime === 'claude' || hasClaudeMarker(provisioned.cwd)) {
|
|
431
475
|
mcpConfigPaths.push(writeClaudeMcpConfig(provisioned.cwd, provisioned.personality, daemonUrl))
|
|
476
|
+
// Pre-approve the cwd's project MCP servers so the FIRST virgin-config launch does
|
|
477
|
+
// not stall on the blocking "N new MCP servers found" dialog (project-local, never
|
|
478
|
+
// touches the user's global settings; bootDialogKeys is the backstop).
|
|
479
|
+
ensureClaudeProjectMcpEnabled(provisioned.cwd)
|
|
432
480
|
}
|
|
433
481
|
// codex: write the token-free host-wide config.toml block (dummy bearer flips codex's
|
|
434
482
|
// auth gate; the OPEN daemon authenticates by the identity header). send_to_peer works
|
|
@@ -50,11 +50,27 @@ const CLAUDE_BOOT_DIALOG_MARKERS = [
|
|
|
50
50
|
'Resume from summary',
|
|
51
51
|
'Resuming the full session',
|
|
52
52
|
// First-run theme picker ("Let's get started. Choose the text style…") — appears
|
|
53
|
-
// on a CLEAN host (fresh config dir) BEFORE the input prompt
|
|
54
|
-
//
|
|
55
|
-
// cleared the peer never reaches ready. (There is NO separate "accept dangerous
|
|
56
|
-
// permissions" screen in 2.1.181 — verified absent.)
|
|
53
|
+
// on a CLEAN host (fresh config dir) BEFORE the input prompt. NOT removed by
|
|
54
|
+
// --dangerously-skip-permissions; if not cleared the peer never reaches ready.
|
|
57
55
|
'Choose the text style',
|
|
56
|
+
// Bypass-permissions ACCEPT gate ("WARNING: Claude Code running in Bypass
|
|
57
|
+
// Permissions mode"; options "1. No, exit" / "2. Yes, I accept") — shown on a
|
|
58
|
+
// VIRGIN config the FIRST time --dangerously-skip-permissions runs, BEFORE the
|
|
59
|
+
// input prompt. (An earlier note here claimed this screen was "absent in 2.1.181";
|
|
60
|
+
// that was a NON-VIRGIN observation — the host had already accepted it once, so it
|
|
61
|
+
// was masked. On a truly virgin config it IS shown and BLOCKS the boot — verified
|
|
62
|
+
// live, claude 2.1.183 via tmux. bootDialogKeys steps the cursor to "2. Yes, I
|
|
63
|
+
// accept" and confirms; a bare Enter would hit the "1. No, exit" DEFAULT and kill
|
|
64
|
+
// the peer.) Once accepted, claude records it natively and the ready banner is the
|
|
65
|
+
// lowercase "bypass permissions on" (a DISTINCT string), so this marker vanishes —
|
|
66
|
+
// it never falsely traps isInputReady, and a re-launch is dialog-free (verified).
|
|
67
|
+
'Bypass Permissions mode',
|
|
68
|
+
// Project MCP-server approval ("N new MCP servers found in this project", servers
|
|
69
|
+
// pre-checked [✔]) — shown when cwd carries a .mcp.json the config has not yet
|
|
70
|
+
// approved. Suppressed at the source by enableAllProjectMcpServers in
|
|
71
|
+
// <cwd>/.claude/settings.json (init writes it); bootDialogKeys Enter-confirms it as
|
|
72
|
+
// a backstop. Substring matches both "1 new MCP server" and "N new MCP servers".
|
|
73
|
+
'new MCP server',
|
|
58
74
|
] as const
|
|
59
75
|
|
|
60
76
|
function anyBootDialog(pane: string): boolean {
|
|
@@ -164,34 +180,43 @@ export const claudeAdapter: RuntimeAdapter = {
|
|
|
164
180
|
/**
|
|
165
181
|
* Map a visible startup dialog to the keys that clear it correctly:
|
|
166
182
|
*
|
|
167
|
-
* -
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* the
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
183
|
+
* - CURSOR-VERIFIED two-step menus ('Resume from summary …' AND 'Bypass
|
|
184
|
+
* Permissions mode') → step Down, confirm ONLY once the captured pane proves
|
|
185
|
+
* the cursor sits on "2.". Both menus share one hazard: their DEFAULT cursor is
|
|
186
|
+
* the WRONG option, so a blind Enter picks it.
|
|
187
|
+
* · Resume: default "1. Resume from summary (recommended)" COMPACTS the
|
|
188
|
+
* session (claude runs that choice as an internal /compact) — a resume must
|
|
189
|
+
* not compact.
|
|
190
|
+
* · Bypass: default "1. No, exit" makes the peer EXIT (die). "2. Yes, I
|
|
191
|
+
* accept" is the proceed path. (Virgin-config gate the first time
|
|
192
|
+
* --dangerously-skip-permissions runs; once accepted claude persists it.)
|
|
193
|
+
* The original blind ['Down','Enter'] burst (0e67e1f) put BOTH keys in one pty
|
|
194
|
+
* chunk; under load the TUI dropped the Down and the Enter confirmed the DEFAULT
|
|
195
|
+
* (proven for resume: pane frame ❯ on option 1 → "❯ /compact"). Fix: ONE key
|
|
196
|
+
* per boot-iteration — Down while the cursor is NOT yet on "2.", Enter ONLY
|
|
197
|
+
* after the captured pane PROVES it ("❯ 2."). A swallowed Down self-heals next
|
|
198
|
+
* iteration; Enter can never hit the bad default. If the layout ever changes the
|
|
199
|
+
* loop Downs until the boot deadline and the wake fails LOUD.
|
|
200
|
+
* - SINGLE-CONFIRM modals (folder-trust / external-import / dev-channels / theme /
|
|
201
|
+
* project MCP-server approval) → ['Enter']: the default-highlighted option IS
|
|
202
|
+
* the proceed path, so a bare Enter clears each. The MCP approval pre-checks the
|
|
203
|
+
* servers [✔] (Enter enables them, Esc rejects all) — it is also suppressed at
|
|
204
|
+
* the source by enableAllProjectMcpServers (init), so the Enter here is a
|
|
205
|
+
* backstop. (claude-start.sh:341.)
|
|
185
206
|
* - anything else (incl. the post-select "Resuming…" load state) → null (wait).
|
|
186
207
|
*/
|
|
187
208
|
bootDialogKeys(pane: string): string[] | null {
|
|
188
|
-
if (pane.includes('Resume from summary')) {
|
|
209
|
+
if (pane.includes('Resume from summary') || pane.includes('Bypass Permissions mode')) {
|
|
189
210
|
return /❯\s*2\./.test(pane) ? ['Enter'] : ['Down']
|
|
190
211
|
}
|
|
191
212
|
if (
|
|
192
213
|
pane.includes('trust this folder') ||
|
|
193
214
|
pane.includes('Allow external CLAUDE.md file imports?') ||
|
|
194
215
|
pane.includes('I am using this for local development') ||
|
|
216
|
+
// Project MCP-server approval (servers pre-checked [✔]; Enter confirms, Esc
|
|
217
|
+
// rejects all). enableAllProjectMcpServers normally suppresses it; this is the
|
|
218
|
+
// backstop. Matches both "1 new MCP server" and "N new MCP servers".
|
|
219
|
+
pane.includes('new MCP server') ||
|
|
195
220
|
// First-run theme picker (clean host): the default-highlighted row is always a
|
|
196
221
|
// valid theme (any choice is harmless for a headless peer), so a bare Enter
|
|
197
222
|
// accepts it. Without this the picker blocks the boot forever on a fresh host.
|
package/src/onboard/index.ts
CHANGED
|
@@ -46,6 +46,14 @@ export interface OnboardOptions {
|
|
|
46
46
|
/** Restrict to these runtimes (default both). */
|
|
47
47
|
runtimes?: OnboardRuntime[]
|
|
48
48
|
env?: NodeJS.ProcessEnv
|
|
49
|
+
/** Injectable (tests). Default registerMarketplace. */
|
|
50
|
+
register?: (runtime: OnboardRuntime, env: NodeJS.ProcessEnv) => { ok: boolean; detail?: string }
|
|
51
|
+
/** Injectable (tests). Default isMarketplaceRegistered. */
|
|
52
|
+
isRegistered?: (runtime: OnboardRuntime, env: NodeJS.ProcessEnv) => boolean
|
|
53
|
+
/** Injectable (tests). Default Bun.sleepSync — the pause before the one transient retry. */
|
|
54
|
+
sleep?: (ms: number) => void
|
|
55
|
+
/** Delay (ms) before the single non-timeout retry. Default 2000. */
|
|
56
|
+
retryDelayMs?: number
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -192,13 +200,17 @@ export function refreshMarketplace(
|
|
|
192
200
|
export function onboardHost(opts: OnboardOptions = {}): OnboardResult {
|
|
193
201
|
const env = opts.env ?? process.env
|
|
194
202
|
const runtimes = opts.runtimes ?? (['claude', 'codex'] as OnboardRuntime[])
|
|
203
|
+
const isRegistered = opts.isRegistered ?? isMarketplaceRegistered
|
|
204
|
+
const register = opts.register ?? registerMarketplace
|
|
205
|
+
const sleep = opts.sleep ?? ((ms: number) => Bun.sleepSync(ms))
|
|
206
|
+
const retryDelayMs = opts.retryDelayMs ?? 2000
|
|
195
207
|
const marketplaces: OnboardRuntimeResult[] = []
|
|
196
208
|
for (const runtime of runtimes) {
|
|
197
209
|
if (!isExecutable(runtimeBin(runtime, env), env)) {
|
|
198
210
|
marketplaces.push({ runtime, state: 'runtime-missing' })
|
|
199
211
|
continue
|
|
200
212
|
}
|
|
201
|
-
if (
|
|
213
|
+
if (isRegistered(runtime, env)) {
|
|
202
214
|
marketplaces.push({ runtime, state: 'already-registered' })
|
|
203
215
|
continue
|
|
204
216
|
}
|
|
@@ -206,13 +218,59 @@ export function onboardHost(opts: OnboardOptions = {}): OnboardResult {
|
|
|
206
218
|
marketplaces.push({ runtime, state: 'would-register' })
|
|
207
219
|
continue
|
|
208
220
|
}
|
|
209
|
-
|
|
210
|
-
marketplaces.push({ runtime, state: r.ok ? 'registered' : 'failed', detail: r.detail })
|
|
221
|
+
marketplaces.push(resolveMarketplaceAdd(runtime, env, register, isRegistered, sleep, retryDelayMs))
|
|
211
222
|
}
|
|
212
223
|
const noop = marketplaces.every(m => m.state !== 'registered')
|
|
213
224
|
return { marketplaces, noop }
|
|
214
225
|
}
|
|
215
226
|
|
|
227
|
+
/**
|
|
228
|
+
* One `marketplace add` with first-run-transient resilience (Arthur's fresh-run found a
|
|
229
|
+
* scary red "failed" on a step that actually self-heals — the failure is transient, the
|
|
230
|
+
* marketplace ends up registered). Layered:
|
|
231
|
+
* (a) SELF-HEAL: if the add reports failure but a re-check shows the marketplace IS
|
|
232
|
+
* present (it landed despite a non-zero tail / a race), report `registered`.
|
|
233
|
+
* (c) ONE retry after a short delay — only for a NON-timeout transient (a network/git
|
|
234
|
+
* blip on the GitHub clone). NEVER on a timeout: a wedged / macOS-launch-approval-
|
|
235
|
+
* parked binary would just re-wedge, doubling the wait.
|
|
236
|
+
* (b) on a timeout signature, report `failed` with the macOS-approval advisory; on any
|
|
237
|
+
* other persistent failure, a transient-retry hint. The marketplace is OPTIONAL for
|
|
238
|
+
* core function, so a clear actionable line beats a bare "failed".
|
|
239
|
+
*/
|
|
240
|
+
function resolveMarketplaceAdd(
|
|
241
|
+
runtime: OnboardRuntime,
|
|
242
|
+
env: NodeJS.ProcessEnv,
|
|
243
|
+
register: (r: OnboardRuntime, e: NodeJS.ProcessEnv) => { ok: boolean; detail?: string },
|
|
244
|
+
isRegistered: (r: OnboardRuntime, e: NodeJS.ProcessEnv) => boolean,
|
|
245
|
+
sleep: (ms: number) => void,
|
|
246
|
+
retryDelayMs: number,
|
|
247
|
+
): OnboardRuntimeResult {
|
|
248
|
+
const first = register(runtime, env)
|
|
249
|
+
if (first.ok) return { runtime, state: 'registered' }
|
|
250
|
+
// (a) self-heal — present despite the reported error?
|
|
251
|
+
if (isRegistered(runtime, env)) {
|
|
252
|
+
return { runtime, state: 'registered', detail: 'add reported an error, but the marketplace is present' }
|
|
253
|
+
}
|
|
254
|
+
const timedOut = /timed out/i.test(first.detail ?? '')
|
|
255
|
+
if (!timedOut) {
|
|
256
|
+
// (c) one retry for a non-timeout transient.
|
|
257
|
+
sleep(retryDelayMs)
|
|
258
|
+
const second = register(runtime, env)
|
|
259
|
+
if (second.ok || isRegistered(runtime, env)) return { runtime, state: 'registered', detail: 'succeeded on retry' }
|
|
260
|
+
return {
|
|
261
|
+
runtime,
|
|
262
|
+
state: 'failed',
|
|
263
|
+
detail: `${second.detail ?? first.detail} — transient (network/git)? re-run \`iapeer onboard\`; the marketplace is optional for core function`,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// (b) timeout signature → the known macOS launch-approval park.
|
|
267
|
+
return {
|
|
268
|
+
runtime,
|
|
269
|
+
state: 'failed',
|
|
270
|
+
detail: `${first.detail} — macOS may be verifying the runtime binary; approve it in System Settings → Privacy & Security, then re-run \`iapeer onboard\``,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
216
274
|
/**
|
|
217
275
|
* macOS Full Disk Access (TCC) advisory printed at the END of onboard. The runtime permission layer
|
|
218
276
|
* (`--add-dir` / `additionalDirectories` for claude, the sandbox flags for codex) governs what the
|
package/src/transport/index.ts
CHANGED
|
@@ -936,7 +936,7 @@ export interface ControlResult {
|
|
|
936
936
|
* acts on a running session — a non-live peer has nothing to interrupt) then
|
|
937
937
|
* executeControlOnTarget. Without a runtime, resolves the single live runtime (2+ →
|
|
938
938
|
* "specify runtime"). The clean-slash control namespace (interrupt/compact/…) is the
|
|
939
|
-
* caller's; /
|
|
939
|
+
* caller's; /alias_* expansions are message, not control (docs/Control §namespace).
|
|
940
940
|
*/
|
|
941
941
|
export async function routeControl(personality: string, runtime: string | undefined, command: ControlCommand): Promise<Result<ControlResult>> {
|
|
942
942
|
if (!isValidName(personality)) {
|