@agfpd/iapeer 0.1.0
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/bin/iapeer +25 -0
- package/package.json +37 -0
- package/src/cli/cli.test.ts +130 -0
- package/src/cli/index.ts +608 -0
- package/src/cli/listTui.test.ts +70 -0
- package/src/cli/listTui.ts +165 -0
- package/src/codec/codec.test.ts +271 -0
- package/src/codec/index.ts +217 -0
- package/src/core/constants.test.ts +21 -0
- package/src/core/constants.ts +180 -0
- package/src/core/errors.ts +20 -0
- package/src/core/index.ts +3 -0
- package/src/core/normalize.test.ts +98 -0
- package/src/core/normalize.ts +89 -0
- package/src/core/socket.ts +63 -0
- package/src/create/create.test.ts +143 -0
- package/src/create/index.ts +178 -0
- package/src/daemon/daemon-http.test.ts +114 -0
- package/src/daemon/daemon.test.ts +103 -0
- package/src/daemon/index.ts +439 -0
- package/src/daemon/main.test.ts +194 -0
- package/src/daemon/main.ts +230 -0
- package/src/enable/enable.test.ts +92 -0
- package/src/enable/index.ts +381 -0
- package/src/identity/identity.test.ts +262 -0
- package/src/identity/index.ts +603 -0
- package/src/index.ts +27 -0
- package/src/init/index.ts +408 -0
- package/src/init/init.test.ts +171 -0
- package/src/init/runtime-resolve.test.ts +49 -0
- package/src/install/index.ts +84 -0
- package/src/install/install.test.ts +31 -0
- package/src/launch/adapters/claude.ts +250 -0
- package/src/launch/adapters/codex.ts +329 -0
- package/src/launch/adapters/notifier.ts +90 -0
- package/src/launch/adapters/telegram.ts +130 -0
- package/src/launch/bootstrap.test.ts +56 -0
- package/src/launch/composeSystemPrompt.layers.test.ts +319 -0
- package/src/launch/composeSystemPrompt.test.ts +98 -0
- package/src/launch/composeSystemPrompt.ts +261 -0
- package/src/launch/index.ts +253 -0
- package/src/launch/launch.test.ts +233 -0
- package/src/launch/launchd.test.ts +363 -0
- package/src/launch/launchd.ts +375 -0
- package/src/launch/launchdRun.ts +168 -0
- package/src/launch/sockdir.test.ts +70 -0
- package/src/launch/types.ts +300 -0
- package/src/lifecycle/index.ts +840 -0
- package/src/lifecycle/lifecycle.test.ts +496 -0
- package/src/onboard/index.ts +135 -0
- package/src/onboard/onboard.test.ts +39 -0
- package/src/provision/index.ts +170 -0
- package/src/provision/provision.test.ts +104 -0
- package/src/registry/index.ts +453 -0
- package/src/registry/registry.test.ts +400 -0
- package/src/runtime/deploy.ts +230 -0
- package/src/runtime/index.ts +191 -0
- package/src/runtime/runtime.test.ts +226 -0
- package/src/storage/index.ts +331 -0
- package/src/storage/peers-home.test.ts +34 -0
- package/src/storage/storage.test.ts +65 -0
- package/src/transport/index.ts +522 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// Launch — the single session-bring-up primitive and the per-runtime adapter
|
|
2
|
+
// contract. launch.launch is runtime-AGNOSTIC (env + argv + tmux new-session +
|
|
3
|
+
// pipe-pane + self-TTL + boot + ready-gate + first-message delivery); the
|
|
4
|
+
// runtime specifics (argv flags, system-prompt mechanism, boot dialogs, ready
|
|
5
|
+
// markers, activity proxy, permission dialogs, resume) live behind RuntimeAdapter.
|
|
6
|
+
//
|
|
7
|
+
// Ownership split (blueprint §1, §7): launch = HOW to bring up ONE session;
|
|
8
|
+
// lifecycle = WHEN / HOW MANY (wake/lock/reap/supervise). lifecycle.wakeOrSpawn
|
|
9
|
+
// calls launch.launch; launch never decides whether or when to wake. The launch
|
|
10
|
+
// path carries NO currency (no marketplace/plugin update) — that is install-time
|
|
11
|
+
// (blueprint §0.6 fast-wake).
|
|
12
|
+
//
|
|
13
|
+
// This interface is FROZEN here (single author) so the per-runtime adapters can
|
|
14
|
+
// be implemented independently against a known contract.
|
|
15
|
+
|
|
16
|
+
import type { Intelligence, Runtime } from '../core/constants.ts'
|
|
17
|
+
import type { PublicPeerSummary } from '../registry/index.ts'
|
|
18
|
+
|
|
19
|
+
export type { PublicPeerSummary } from '../registry/index.ts'
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// composeSystemPrompt — the layered Канал-A merge (docs/Сборка системного
|
|
23
|
+
// промпта — слои и каналы.md). Four layers, general → specific (local overrides
|
|
24
|
+
// global):
|
|
25
|
+
// 1. System YAML (identity + host facts) — jq-GOLDEN, byte-for-byte.
|
|
26
|
+
// 2. iapeer doctrine: ~/.iapeer/IAPEER.md (global) + <cwd>/.iapeer/IAPEER.md (local).
|
|
27
|
+
// 3. Normalized peer registry (publicPeerSummary, exactly 5 fields).
|
|
28
|
+
// 4. Plugin user-settings: every OTHER <DOMAIN>.md at the .iapeer/ root, global
|
|
29
|
+
// + local merged per domain. Custom files (SPAWNER_INSTRUCTIONS.md, …) flow
|
|
30
|
+
// in organically here — no special-casing.
|
|
31
|
+
// composeSystemPrompt is a PURE renderer over already-gathered data; the FS
|
|
32
|
+
// discovery lives in gatherPromptInput (mirrors the bash split: shell read the
|
|
33
|
+
// files, the renderer just laid out bytes).
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/** One Layer-4 domain: the global + local halves of a single `<DOMAIN>.md` pair
|
|
37
|
+
* (either may be absent). `domain` is the filename stem, used only for stable
|
|
38
|
+
* ordering — it is NOT emitted (the merge is organic, per the contract). */
|
|
39
|
+
export interface PromptDomainBlock {
|
|
40
|
+
domain: string
|
|
41
|
+
/** ~/.iapeer/<DOMAIN>.md content (general). */
|
|
42
|
+
global?: string
|
|
43
|
+
/** <cwd>/.iapeer/<DOMAIN>.md content (specific — overrides global). */
|
|
44
|
+
local?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ComposePromptInput {
|
|
48
|
+
personality: string
|
|
49
|
+
description: string
|
|
50
|
+
cwd: string
|
|
51
|
+
/** System facts (claude-start.sh:228-247). */
|
|
52
|
+
platform: string
|
|
53
|
+
osVersion: string
|
|
54
|
+
user: string
|
|
55
|
+
hostname: string
|
|
56
|
+
today: string
|
|
57
|
+
/** Layer 2 local: <cwd>/.iapeer/IAPEER.md content. '' when absent. */
|
|
58
|
+
peerDoctrine: string
|
|
59
|
+
/** Layer 2 global, OPTIONAL: ~/.iapeer/IAPEER.md content (sits between the YAML
|
|
60
|
+
* block and the per-peer doctrine so per-peer overrides global). Existence-
|
|
61
|
+
* gated: a present-but-empty file → '', an absent file → undefined. */
|
|
62
|
+
globalDoctrine?: string
|
|
63
|
+
/** Layer 3: the normalized peer registry. Empty/omitted → the layer emits
|
|
64
|
+
* nothing (and the output stays byte-identical to the legacy YAML+doctrine). */
|
|
65
|
+
peers?: PublicPeerSummary[]
|
|
66
|
+
/** Layer 4: every non-IAPEER `<DOMAIN>.md` pair at the .iapeer/ root. Empty/
|
|
67
|
+
* omitted → the layer emits nothing. */
|
|
68
|
+
pluginDomains?: PromptDomainBlock[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Compose the merged system prompt, byte-for-byte equivalent to the claude-start
|
|
73
|
+
* jq pipeline:
|
|
74
|
+
*
|
|
75
|
+
* ---\n
|
|
76
|
+
* personality: <jq @json>\n
|
|
77
|
+
* description: <jq @json>\n
|
|
78
|
+
* peer-cwd: <jq @json>\n
|
|
79
|
+
* platform: <jq @json>\n
|
|
80
|
+
* os_version: <jq @json>\n
|
|
81
|
+
* user: <jq @json>\n
|
|
82
|
+
* hostname: <jq @json>\n
|
|
83
|
+
* today: <jq @json>\n
|
|
84
|
+
* ---\n
|
|
85
|
+
* \n
|
|
86
|
+
* [globalDoctrine + "\n" — only when present]
|
|
87
|
+
* peerDoctrine
|
|
88
|
+
* [\n\n + registry section — only when peers.length > 0]
|
|
89
|
+
* [\n\n + merged domains — only when pluginDomains is non-empty]
|
|
90
|
+
*
|
|
91
|
+
* Layers 1+2 are byte-for-byte the legacy jq output; layers 3+4 are appended
|
|
92
|
+
* (each as a `\n\n`-separated section) ONLY when they have content, so a peer
|
|
93
|
+
* with no registry and no extra domains produces the exact legacy bytes.
|
|
94
|
+
*
|
|
95
|
+
* Each YAML value is a JSON string literal (jq @json: JSON.stringify), which is
|
|
96
|
+
* also a valid YAML double-quoted scalar — safe against colons/quotes/newlines.
|
|
97
|
+
* The keys use hyphen `peer-cwd` and underscore `os_version` exactly as the bash.
|
|
98
|
+
*/
|
|
99
|
+
export type ComposeSystemPrompt = (input: ComposePromptInput) => string
|
|
100
|
+
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
// Launch spec + adapter config
|
|
103
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export interface LaunchAdapterConfig {
|
|
106
|
+
claudeBin: string
|
|
107
|
+
codexBin: string
|
|
108
|
+
/** telegram-runtime launch binary (router runtime). */
|
|
109
|
+
telegramBin?: string
|
|
110
|
+
/** notifier-runtime launch binary (router runtime, infra/always-on). */
|
|
111
|
+
notifierBin?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface LaunchSpec {
|
|
115
|
+
personality: string
|
|
116
|
+
runtime: Runtime
|
|
117
|
+
cwd: string
|
|
118
|
+
/** `<runtime>-<personality>` — the tmux session name + socket stem. */
|
|
119
|
+
identity: string
|
|
120
|
+
/** Socket path (`/tmp/tmux-iap-<identity>.sock`). */
|
|
121
|
+
socketPath: string
|
|
122
|
+
/** Composed system-prompt file path (tui runtimes that usesDoctrine). */
|
|
123
|
+
systemPromptFile?: string
|
|
124
|
+
/** Resume the newest transcript/session for this cwd (adapter validates). */
|
|
125
|
+
resume?: boolean
|
|
126
|
+
/** Pre-resolved resume ref (claude `--resume <uuid>`); set by resolveResume. */
|
|
127
|
+
resumeRef?: string
|
|
128
|
+
/** Free-form extra CLI args (PEER_START_ARGS). */
|
|
129
|
+
extraArgs?: string[]
|
|
130
|
+
/** Peer intelligence (artificial/natural/absent). Used to enforce an adapter's
|
|
131
|
+
* intelligence gate at launch (telegram requires natural). Optional: a doctrine-
|
|
132
|
+
* less throwaway may omit it, but an adapter that declares requiresIntelligence
|
|
133
|
+
* then refuses (cannot confirm the required nature). */
|
|
134
|
+
intelligence?: Intelligence
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
138
|
+
// Delivery markers — the tui submit surface (owned by the adapter, 07.06 refactor)
|
|
139
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Markers the transport submit path (submitIntoTui) needs to detect that a
|
|
143
|
+
* bracketed paste landed in the input row before pressing Enter. Contract refactor
|
|
144
|
+
* 07.06 (docs/Рантайм-адаптеры): the ADAPTER owns delivery markers — they moved out
|
|
145
|
+
* of the transport's hardcoded `PROMPT_GLYPHS = ['❯','›']` union so the generic
|
|
146
|
+
* submit logic carries NO runtime strings and a new runtime ships its own glyph
|
|
147
|
+
* with its adapter, not by editing transport.
|
|
148
|
+
*/
|
|
149
|
+
export interface DeliveryMarkers {
|
|
150
|
+
/** Glyph(s) at column 0 of the rendered input-prompt row (claude '❯', codex '›').
|
|
151
|
+
* submitIntoTui locates the prompt row by these. A router has no submit surface
|
|
152
|
+
* → empty array. */
|
|
153
|
+
promptGlyphs: string[]
|
|
154
|
+
/** Extra "bracketed paste landed" indicators beyond the envelope's own tail-marker
|
|
155
|
+
* (claude '[Pasted text' / '[Image #'). Optional — when absent the tail-marker is
|
|
156
|
+
* the sole landed-signal. */
|
|
157
|
+
pastePatterns?: RegExp[]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
// Control commands (Ф-E, docs/Control-команды). The SECOND daemon channel: an
|
|
162
|
+
// in-session control command (interrupt / compact) is mapped by the target's adapter
|
|
163
|
+
// to a tmux send-keys sequence and performed UNCONDITIONALLY (immediate, NOT gated on
|
|
164
|
+
// ready — the point is to interrupt / drive, not to wait). System commands (list /
|
|
165
|
+
// status) are the daemon's own and do not reach the adapter.
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/** An abstract in-session control command (`interrupt`, `compact`, runtime-specific). */
|
|
169
|
+
export interface ControlCommand {
|
|
170
|
+
name: string
|
|
171
|
+
args?: readonly string[]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The runtime mechanism for a control command: a sequence of `tmux send-keys`
|
|
175
|
+
* arg-lists, performed IN ORDER on the target session. */
|
|
176
|
+
export interface ControlPlan {
|
|
177
|
+
/** Each inner array is one `tmux send-keys -t <addr>` call's trailing args
|
|
178
|
+
* (e.g. ['Escape'] or ['-l', '/compact'] then ['Enter']). */
|
|
179
|
+
sequence: string[][]
|
|
180
|
+
/** Pause (ms) between sequence steps — e.g. typed text must settle before Enter. */
|
|
181
|
+
stepDelayMs?: number
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
// RuntimeAdapter — per-runtime "HOW to launch / observe one session"
|
|
186
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export interface RuntimeAdapter {
|
|
189
|
+
runtime: Runtime
|
|
190
|
+
/** 'tui' (claude/codex — pane boot/ready/dialogs) | 'router' (telegram — no TUI phases). */
|
|
191
|
+
kind: 'tui' | 'router'
|
|
192
|
+
/** Does this runtime consume a composed system-prompt doctrine? (tui yes, router no). */
|
|
193
|
+
usesDoctrine: boolean
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Delivery markers for the transport submit path (submitIntoTui) — the input-
|
|
197
|
+
* prompt glyph(s) and optional "paste landed" patterns. The adapter OWNS them
|
|
198
|
+
* (07.06 refactor): transport reads them from here instead of a hardcoded glyph
|
|
199
|
+
* union. A router declares `{ promptGlyphs: [] }` (no submit surface).
|
|
200
|
+
*/
|
|
201
|
+
deliveryMarkers: DeliveryMarkers
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* If set, launch REFUSES unless the peer's intelligence equals this value
|
|
205
|
+
* (fail-loud). telegram → 'natural' — it is a human channel; launching an
|
|
206
|
+
* artificial/absent peer on it is a category error (PP held a FATAL guard in two
|
|
207
|
+
* places). Most adapters omit it (no nature gate). Source of intelligence →
|
|
208
|
+
* docs/Идентичность; enforced by the launch primitive against LaunchSpec.intelligence.
|
|
209
|
+
*/
|
|
210
|
+
requiresIntelligence?: Intelligence
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Build the runtime argv: binary + flags, wiring systemPromptFile per-runtime
|
|
214
|
+
* - claude: `--dangerously-skip-permissions [--disallowedTools …] --system-prompt-file <f> [extra]`
|
|
215
|
+
* - codex: `[resume --last] --no-alt-screen -C <cwd> -c model_instructions_file=<f> --dangerously-bypass-approvals-and-sandbox [extra]`
|
|
216
|
+
* - telegram: `telegram-runtime run …` (no doctrine).
|
|
217
|
+
* NO currency on this path.
|
|
218
|
+
*/
|
|
219
|
+
buildArgv(spec: LaunchSpec, cfg: LaunchAdapterConfig): string[]
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* If a known startup dialog is visible in `pane`, return the tmux send-keys
|
|
223
|
+
* args to clear it (e.g. ['Enter'] or ['Down','Enter']); else null. (tui only).
|
|
224
|
+
*/
|
|
225
|
+
bootDialogKeys(pane: string): string[] | null
|
|
226
|
+
|
|
227
|
+
/** Is the input surface ready for the first message? (tui: ready marker present
|
|
228
|
+
* AND startup dialogs gone). Router runtimes return true (no input surface). */
|
|
229
|
+
isInputReady(pane: string): boolean
|
|
230
|
+
|
|
231
|
+
/** Newest activity-proxy mtime for the ready-gate AND idle accounting:
|
|
232
|
+
* claude transcript / codex session jsonl mtime; null for a router (no proxy). */
|
|
233
|
+
newestActivityMtime(cwd: string): number | null
|
|
234
|
+
|
|
235
|
+
/** Is a permission/approval dialog open in the pane (headless autopilot)? */
|
|
236
|
+
permissionDialogActive(pane: string): boolean
|
|
237
|
+
/** tmux send-keys args to affirm a permission dialog (claude ['Enter'];
|
|
238
|
+
* codex ['Down','Enter'] to pick "allow for session"). */
|
|
239
|
+
permissionDialogKeys(): string[]
|
|
240
|
+
|
|
241
|
+
/** Resume preflight — validate a resume request fail-loud (never silent fresh).
|
|
242
|
+
* Returns the resolved ref (claude uuid) or ok:false with a reason. */
|
|
243
|
+
resolveResume(cwd: string): { ok: boolean; ref?: string; reason?: string }
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Map an abstract in-session control command to this runtime's mechanism (a tmux
|
|
247
|
+
* send-keys sequence — ControlPlan), or null when the runtime does not support it
|
|
248
|
+
* (the daemon/CLI surfaces an explicit refusal). Ф-E, docs/Control-команды:
|
|
249
|
+
* - tui (claude/codex): `interrupt` → ['Escape'] (claude ×1; codex ×1-2, snapped
|
|
250
|
+
* live), `compact` → type '/compact' then Enter. Declares the supported set.
|
|
251
|
+
* - router (telegram/notifier): no TUI turn → null for everything (refuse).
|
|
252
|
+
* Performed UNCONDITIONALLY (immediate, not ready-gated) — the point is to
|
|
253
|
+
* interrupt / drive a possibly-stuck session, exactly when normal delivery wouldn't.
|
|
254
|
+
*/
|
|
255
|
+
executeControl(command: ControlCommand): ControlPlan | null
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
259
|
+
// launch primitive
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
export interface LaunchConfig extends LaunchAdapterConfig {
|
|
263
|
+
sockDir: string
|
|
264
|
+
bootDeadlineSecs: number
|
|
265
|
+
readyGateSecs: number
|
|
266
|
+
maxAgeSecs: number
|
|
267
|
+
/** Log dir for pipe-pane output. */
|
|
268
|
+
logDir: string
|
|
269
|
+
env?: NodeJS.ProcessEnv
|
|
270
|
+
/**
|
|
271
|
+
* Always-on bring-up (infra runtimes held by launchd KeepAlive): SKIP the
|
|
272
|
+
* session self-TTL so the session is not auto-killed at maxAgeSecs. The TTL is
|
|
273
|
+
* a warm-on-demand zombie bound; an always-on session's lifecycle is owned by
|
|
274
|
+
* launchd (KeepAlive respawns it), so the self-kill timer must not fire.
|
|
275
|
+
*/
|
|
276
|
+
alwaysOn?: boolean
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface LaunchResult {
|
|
280
|
+
status: 'READY' | 'FAILED'
|
|
281
|
+
identity: string
|
|
282
|
+
process_address: string
|
|
283
|
+
reason?: string
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Bring up ONE session: pre-clean stale tmux server → tmux new-session -d with
|
|
288
|
+
* adapter.buildArgv → pipe-pane → session self-TTL → boot (answer dialogs via
|
|
289
|
+
* adapter, wait for adapter.isInputReady, deliver the first message via
|
|
290
|
+
* send-keys -l) → ready-gate (adapter.newestActivityMtime strictly advances).
|
|
291
|
+
* Runtime-agnostic; all specifics come from the adapter. Returns READY/FAILED.
|
|
292
|
+
* `firstMessage` (the task / routed envelope) is delivered as the boot message;
|
|
293
|
+
* a router runtime skips the TUI boot/ready phases.
|
|
294
|
+
*/
|
|
295
|
+
export type LaunchFn = (
|
|
296
|
+
spec: LaunchSpec,
|
|
297
|
+
adapter: RuntimeAdapter,
|
|
298
|
+
firstMessage: string,
|
|
299
|
+
cfg: LaunchConfig,
|
|
300
|
+
) => Promise<LaunchResult>
|