@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.
Files changed (63) hide show
  1. package/bin/iapeer +25 -0
  2. package/package.json +37 -0
  3. package/src/cli/cli.test.ts +130 -0
  4. package/src/cli/index.ts +608 -0
  5. package/src/cli/listTui.test.ts +70 -0
  6. package/src/cli/listTui.ts +165 -0
  7. package/src/codec/codec.test.ts +271 -0
  8. package/src/codec/index.ts +217 -0
  9. package/src/core/constants.test.ts +21 -0
  10. package/src/core/constants.ts +180 -0
  11. package/src/core/errors.ts +20 -0
  12. package/src/core/index.ts +3 -0
  13. package/src/core/normalize.test.ts +98 -0
  14. package/src/core/normalize.ts +89 -0
  15. package/src/core/socket.ts +63 -0
  16. package/src/create/create.test.ts +143 -0
  17. package/src/create/index.ts +178 -0
  18. package/src/daemon/daemon-http.test.ts +114 -0
  19. package/src/daemon/daemon.test.ts +103 -0
  20. package/src/daemon/index.ts +439 -0
  21. package/src/daemon/main.test.ts +194 -0
  22. package/src/daemon/main.ts +230 -0
  23. package/src/enable/enable.test.ts +92 -0
  24. package/src/enable/index.ts +381 -0
  25. package/src/identity/identity.test.ts +262 -0
  26. package/src/identity/index.ts +603 -0
  27. package/src/index.ts +27 -0
  28. package/src/init/index.ts +408 -0
  29. package/src/init/init.test.ts +171 -0
  30. package/src/init/runtime-resolve.test.ts +49 -0
  31. package/src/install/index.ts +84 -0
  32. package/src/install/install.test.ts +31 -0
  33. package/src/launch/adapters/claude.ts +250 -0
  34. package/src/launch/adapters/codex.ts +329 -0
  35. package/src/launch/adapters/notifier.ts +90 -0
  36. package/src/launch/adapters/telegram.ts +130 -0
  37. package/src/launch/bootstrap.test.ts +56 -0
  38. package/src/launch/composeSystemPrompt.layers.test.ts +319 -0
  39. package/src/launch/composeSystemPrompt.test.ts +98 -0
  40. package/src/launch/composeSystemPrompt.ts +261 -0
  41. package/src/launch/index.ts +253 -0
  42. package/src/launch/launch.test.ts +233 -0
  43. package/src/launch/launchd.test.ts +363 -0
  44. package/src/launch/launchd.ts +375 -0
  45. package/src/launch/launchdRun.ts +168 -0
  46. package/src/launch/sockdir.test.ts +70 -0
  47. package/src/launch/types.ts +300 -0
  48. package/src/lifecycle/index.ts +840 -0
  49. package/src/lifecycle/lifecycle.test.ts +496 -0
  50. package/src/onboard/index.ts +135 -0
  51. package/src/onboard/onboard.test.ts +39 -0
  52. package/src/provision/index.ts +170 -0
  53. package/src/provision/provision.test.ts +104 -0
  54. package/src/registry/index.ts +453 -0
  55. package/src/registry/registry.test.ts +400 -0
  56. package/src/runtime/deploy.ts +230 -0
  57. package/src/runtime/index.ts +191 -0
  58. package/src/runtime/runtime.test.ts +226 -0
  59. package/src/storage/index.ts +331 -0
  60. package/src/storage/peers-home.test.ts +34 -0
  61. package/src/storage/storage.test.ts +65 -0
  62. package/src/transport/index.ts +522 -0
  63. 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>