@agfpd/iapeer 0.2.98 → 0.3.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 (36) hide show
  1. package/README.md +21 -7
  2. package/docs/01-overview.md +86 -0
  3. package/docs/02-quickstart.md +110 -0
  4. package/docs/03-peers-runtimes-identity.md +117 -0
  5. package/docs/04-lifecycle-and-daemon.md +101 -0
  6. package/docs/05-system-prompt.md +82 -0
  7. package/docs/06-storage.md +115 -0
  8. package/docs/07-install-and-update.md +87 -0
  9. package/docs/08-peer-management.md +119 -0
  10. package/docs/09-runtimes.md +103 -0
  11. package/docs/10-messaging.md +69 -0
  12. package/docs/11-extensions-and-memory.md +60 -0
  13. package/docs/12-cli-reference.md +96 -0
  14. package/docs/13-architecture-and-contract.md +190 -0
  15. package/docs/14-configuration-and-logs.md +91 -0
  16. package/docs/README.md +97 -0
  17. package/docs/ru/01-/320/276/320/261/320/267/320/276/321/200.md +86 -0
  18. package/docs/ru/02-/320/261/321/213/321/201/321/202/321/200/321/213/320/271-/321/201/321/202/320/260/321/200/321/202.md +110 -0
  19. package/docs/ru/03-/320/277/320/270/321/200/321/213-/321/200/320/260/320/275/321/202/320/260/320/271/320/274/321/213-/320/270/320/264/320/265/320/275/321/202/320/270/321/207/320/275/320/276/321/201/321/202/321/214.md +117 -0
  20. package/docs/ru/04-/320/266/320/270/320/267/320/275/320/265/320/275/320/275/321/213/320/271-/321/206/320/270/320/272/320/273-/320/270-/320/264/320/265/320/274/320/276/320/275.md +101 -0
  21. package/docs/ru/05-/321/201/320/270/321/201/321/202/320/265/320/274/320/275/321/213/320/271-/320/277/321/200/320/276/320/274/320/277/321/202.md +82 -0
  22. package/docs/ru/06-/321/205/321/200/320/260/320/275/320/265/320/275/320/270/320/265.md +115 -0
  23. 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 +87 -0
  24. package/docs/ru/08-/321/203/320/277/321/200/320/260/320/262/320/273/320/265/320/275/320/270/320/265-/320/277/320/270/321/200/320/260/320/274/320/270.md +119 -0
  25. package/docs/ru/09-/321/200/320/260/320/275/321/202/320/260/320/271/320/274/321/213.md +103 -0
  26. 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 +69 -0
  27. package/docs/ru/11-/321/200/320/260/321/201/321/210/320/270/321/200/320/265/320/275/320/270/321/217-/320/270-/320/277/320/260/320/274/321/217/321/202/321/214.md +60 -0
  28. package/docs/ru/12-cli-reference.md +96 -0
  29. package/docs/ru/13-/320/260/321/200/321/205/320/270/321/202/320/265/320/272/321/202/321/203/321/200/320/260-/320/270-/320/272/320/276/320/275/321/202/321/200/320/260/320/272/321/202.md +190 -0
  30. package/docs/ru/14-/320/272/320/276/320/275/321/204/320/270/320/263/321/203/321/200/320/260/321/206/320/270/321/217-/320/270-/320/266/321/203/321/200/320/275/320/260/320/273/321/213.md +91 -0
  31. package/docs/ru/README.md +97 -0
  32. package/package.json +8 -2
  33. package/src/cli/index.ts +20 -0
  34. package/src/tui/onboard/app.tsx +260 -0
  35. package/src/tui/onboard/run.tsx +72 -0
  36. package/tsconfig.json +3 -1
@@ -0,0 +1,260 @@
1
+ // OnboardApp — the interactive onboard wizard (Ink). Owns the interactive +
2
+ // live-progress part and EXITS; the inherited-stdio memory install + the final
3
+ // summary run post-Ink (see run.tsx). Drives the SAME backend functions as the
4
+ // linear non-interactive path (no logic duplication), honoring the phase
5
+ // principles: explicit prompts (what to press), visible progress on otherwise
6
+ // silent steps (daemon start), and a clear next-step instead of a cryptic code.
7
+
8
+ import React, { useEffect, useState } from 'react'
9
+ import { Box, Text, useApp, useInput, useStdin } from 'ink'
10
+ import { ONBOARD_SECURITY_WARNING } from '../../onboard/risk.ts'
11
+ import { onboardHost, runtimeAuthNote, tccFullDiskAccessNote, type OnboardRuntime } from '../../onboard/index.ts'
12
+ import { probeFullDiskAccess } from '../../status/index.ts'
13
+ import { ensureDaemonStarted } from '../../daemon/main.ts'
14
+ import { iapeerBinPath } from '../../install/index.ts'
15
+
16
+ /** What the wizard hands back to run.tsx for the post-Ink phase (memory install +
17
+ * summary). */
18
+ export interface WizardResult {
19
+ code: number
20
+ memoryConsent: boolean
21
+ advisories: string[]
22
+ summary: string[]
23
+ }
24
+
25
+ type Phase = 'gate' | 'running' | 'memory' | 'declined' | 'finishing'
26
+ type StepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'fail'
27
+ interface Step {
28
+ key: string
29
+ label: string
30
+ status: StepStatus
31
+ detail?: string
32
+ }
33
+
34
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
35
+
36
+ function useSpinnerFrame(active: boolean): string {
37
+ const [i, setI] = useState(0)
38
+ useEffect(() => {
39
+ if (!active) return
40
+ const t = setInterval(() => setI(n => (n + 1) % SPINNER.length), 80)
41
+ return () => clearInterval(t)
42
+ }, [active])
43
+ return SPINNER[i]
44
+ }
45
+
46
+ function glyph(status: StepStatus, spin: string): string {
47
+ switch (status) {
48
+ case 'running':
49
+ return spin
50
+ case 'ok':
51
+ return '✓'
52
+ case 'warn':
53
+ return '!'
54
+ case 'fail':
55
+ return '✗'
56
+ default:
57
+ return '·'
58
+ }
59
+ }
60
+
61
+ function color(status: StepStatus): string {
62
+ switch (status) {
63
+ case 'ok':
64
+ return 'green'
65
+ case 'warn':
66
+ return 'yellow'
67
+ case 'fail':
68
+ return 'red'
69
+ case 'running':
70
+ return 'cyan'
71
+ default:
72
+ return 'gray'
73
+ }
74
+ }
75
+
76
+ export function OnboardApp({
77
+ env,
78
+ onResult,
79
+ }: {
80
+ env: NodeJS.ProcessEnv
81
+ onResult: (r: WizardResult) => void
82
+ }): React.ReactElement {
83
+ const { exit } = useApp()
84
+ const { isRawModeSupported } = useStdin()
85
+ const [phase, setPhase] = useState<Phase>('gate')
86
+ const [steps, setSteps] = useState<Step[]>([])
87
+ const [advisories, setAdvisories] = useState<string[]>([])
88
+ const spin = useSpinnerFrame(phase === 'running')
89
+
90
+ const finish = (memoryConsent: boolean): void => {
91
+ const failed = steps.some(s => s.status === 'fail')
92
+ const summary = steps.map(s => {
93
+ const mark = s.status === 'ok' ? '✓' : s.status === 'warn' ? '!' : s.status === 'fail' ? '✗' : '·'
94
+ return ` ${mark} ${s.label}${s.detail ? ` — ${s.detail}` : ''}`
95
+ })
96
+ summary.unshift(failed ? 'Onboard finished with errors:' : 'Onboard complete:')
97
+ onResult({ code: failed ? 1 : 0, memoryConsent, advisories, summary })
98
+ setPhase('finishing')
99
+ setTimeout(() => exit(), 30)
100
+ }
101
+
102
+ // Gate input — explicit y/N.
103
+ useInput(
104
+ (input, key) => {
105
+ if (input === 'y' || input === 'Y') setPhase('running')
106
+ else if (input === 'n' || input === 'N' || key.escape) setPhase('declined')
107
+ },
108
+ { isActive: phase === 'gate' },
109
+ )
110
+
111
+ // Memory consent — DEFAULT-YES ([Y/n]: Enter or y → install).
112
+ useInput(
113
+ (input, key) => {
114
+ if (input === 'n' || input === 'N') finish(false)
115
+ else if (input === 'y' || input === 'Y' || key.return) finish(true)
116
+ },
117
+ { isActive: phase === 'memory' },
118
+ )
119
+
120
+ // Declined → exit 1 (no host mutation).
121
+ useEffect(() => {
122
+ if (phase !== 'declined') return
123
+ onResult({ code: 1, memoryConsent: false, advisories: [], summary: ['onboard aborted — risk not accepted.'] })
124
+ const t = setTimeout(() => exit(), 30)
125
+ return () => clearTimeout(t)
126
+ }, [phase, onResult, exit])
127
+
128
+ // Running phase — drive the backend steps sequentially with live status.
129
+ useEffect(() => {
130
+ if (phase !== 'running') return
131
+ let cancelled = false
132
+ const set = (k: string, patch: Partial<Step>) =>
133
+ setSteps(prev => prev.map(s => (s.key === k ? { ...s, ...patch } : s)))
134
+ setSteps([
135
+ { key: 'daemon', label: 'Start router daemon', status: 'pending' },
136
+ { key: 'market', label: 'Register marketplace (claude, codex)', status: 'pending' },
137
+ { key: 'auth', label: 'Check runtime sign-in', status: 'pending' },
138
+ { key: 'fda', label: 'Check macOS Full Disk Access', status: 'pending' },
139
+ ])
140
+
141
+ // Backend steps use synchronous spawnSync (launchctl, runtime plugin list) —
142
+ // those block the event loop, so we yield (tick) after each status change so
143
+ // Ink flushes a frame and steps paint as they progress, not all at once.
144
+ // (Intra-step spinner can't animate while a single spawnSync blocks — a known
145
+ // limitation until those backend calls go async; the step still shows.)
146
+ const tick = () => new Promise<void>(r => setTimeout(r, 0))
147
+
148
+ void (async () => {
149
+ const adv: string[] = []
150
+ set('daemon', { status: 'running' })
151
+ await tick()
152
+ try {
153
+ const d = await ensureDaemonStarted({ env })
154
+ const ok = d.state !== 'failed' && d.healthy !== false
155
+ set('daemon', { status: ok ? 'ok' : 'fail', detail: d.detail ?? d.state })
156
+ } catch (e) {
157
+ set('daemon', { status: 'fail', detail: e instanceof Error ? e.message : String(e) })
158
+ }
159
+ if (cancelled) return
160
+ await tick()
161
+
162
+ set('market', { status: 'running' })
163
+ await tick()
164
+ const r = onboardHost({ env })
165
+ const installed: OnboardRuntime[] = []
166
+ for (const m of r.marketplaces) if (m.state !== 'runtime-missing') installed.push(m.runtime)
167
+ const mFail = r.marketplaces.some(m => m.state === 'failed')
168
+ set('market', {
169
+ status: mFail ? 'fail' : 'ok',
170
+ detail: r.marketplaces.map(m => `${m.runtime}:${m.state}`).join(' '),
171
+ })
172
+ if (cancelled) return
173
+ await tick()
174
+
175
+ set('auth', { status: 'running' })
176
+ await tick()
177
+ if (installed.length === 0) {
178
+ set('auth', { status: 'warn', detail: 'no runtime installed' })
179
+ adv.push(
180
+ '⚠ SETUP INCOMPLETE — no agent runtime found. iapeer needs Claude Code or Codex CLI\n' +
181
+ ' to run peers. Install one, sign in, then re-run `iapeer onboard`.',
182
+ )
183
+ } else {
184
+ const notes = installed.map(rt => runtimeAuthNote(rt, env)).filter((n): n is string => n != null)
185
+ set('auth', {
186
+ status: notes.length ? 'warn' : 'ok',
187
+ detail: notes.length ? `${notes.length} runtime(s) not signed in` : installed.join(', '),
188
+ })
189
+ adv.push(...notes)
190
+ }
191
+ if (cancelled) return
192
+
193
+ set('fda', { status: 'running' })
194
+ await tick()
195
+ const fda = probeFullDiskAccess(env)
196
+ const tcc = tccFullDiskAccessNote({ fda, binPath: iapeerBinPath(env) })
197
+ set('fda', { status: tcc ? 'warn' : 'ok', detail: fda === true ? 'granted' : tcc ? 'not granted' : 'n/a' })
198
+ if (tcc) adv.push(tcc)
199
+ if (cancelled) return
200
+
201
+ setAdvisories(adv)
202
+ setPhase('memory')
203
+ })()
204
+
205
+ return () => {
206
+ cancelled = true
207
+ }
208
+ }, [phase, env])
209
+
210
+ if (isRawModeSupported === false) {
211
+ return (
212
+ <Text color="yellow">
213
+ iapeer onboard needs an interactive terminal. Open a normal terminal and run: iapeer onboard
214
+ </Text>
215
+ )
216
+ }
217
+
218
+ if (phase === 'gate') {
219
+ return (
220
+ <Box flexDirection="column">
221
+ <Box borderStyle="round" borderColor="yellow" paddingX={1} flexDirection="column">
222
+ <Text color="yellow" bold>
223
+ Security warning — please read.
224
+ </Text>
225
+ <Text>{ONBOARD_SECURITY_WARNING.replace(/^Security warning — please read\.\n\n/, '')}</Text>
226
+ </Box>
227
+ <Box marginTop={1}>
228
+ <Text bold>Continue? </Text>
229
+ <Text dimColor>[y/N] </Text>
230
+ </Box>
231
+ </Box>
232
+ )
233
+ }
234
+
235
+ if (phase === 'declined') {
236
+ return <Text color="red">onboard aborted — risk not accepted.</Text>
237
+ }
238
+
239
+ // running | memory | finishing
240
+ return (
241
+ <Box flexDirection="column">
242
+ <Text bold>Setting up the host…</Text>
243
+ <Box flexDirection="column" marginTop={1}>
244
+ {steps.map(s => (
245
+ <Box key={s.key}>
246
+ <Text color={color(s.status)}>{glyph(s.status, spin)} </Text>
247
+ <Text>{s.label}</Text>
248
+ {s.detail ? <Text dimColor> — {s.detail}</Text> : null}
249
+ </Box>
250
+ ))}
251
+ </Box>
252
+ {phase === 'memory' ? (
253
+ <Box marginTop={1} flexDirection="column">
254
+ <Text>Install the shared-memory provider (@agfpd/iapeer-memory)?</Text>
255
+ <Text dimColor>It gives your agents a shared knowledge vault. [Y/n] </Text>
256
+ </Box>
257
+ ) : null}
258
+ </Box>
259
+ )
260
+ }
@@ -0,0 +1,72 @@
1
+ // runOnboardWizard — the Ink entry point for the interactive onboard wizard.
2
+ //
3
+ // Boundary (контракт «интерактив только в реальном TTY», фаза TUI-редизайн):
4
+ // the wizard runs ONLY in a real interactive terminal. The CLI routes here only
5
+ // when stdin AND stdout are TTYs and no non-interactive flag is set; install.sh
6
+ // never launches it (it prints a next-step to open a normal terminal). As a
7
+ // belt-and-suspenders guard against the readline-class wedge (Bun raw-mode on a
8
+ // redirect-opened /dev/tty reports isTTY=true but never receives input), we also
9
+ // fail CLOSED here: no real TTY → return WIZARD_NOT_INTERACTIVE so the caller
10
+ // falls back to the linear non-interactive path instead of rendering raw and
11
+ // hanging.
12
+ //
13
+ // Structure: Ink owns the interactive + live-progress part (gate, daemon/
14
+ // marketplace/auth/FDA steps, memory consent) and EXITS. The memory provider
15
+ // install — an INHERITED-STDIO subprocess (it owns its own tty questions) — runs
16
+ // AFTER Ink has unmounted, so the two never share the terminal at once. The final
17
+ // summary is plain text post-Ink (it folds in the memory outcome).
18
+
19
+ import React from 'react'
20
+ import { render } from 'ink'
21
+ import { OnboardApp, type WizardResult } from './app.tsx'
22
+
23
+ /** Sentinel: the environment is not a real interactive TTY — caller should fall
24
+ * back to the linear non-interactive onboard path (never render raw + hang). */
25
+ export const WIZARD_NOT_INTERACTIVE = -1
26
+
27
+ export interface OnboardWizardOptions {
28
+ env?: NodeJS.ProcessEnv
29
+ }
30
+
31
+ /** Render the interactive onboard wizard. Resolves with the process exit code, or
32
+ * WIZARD_NOT_INTERACTIVE when there is no real TTY to drive it. */
33
+ export async function runOnboardWizard(opts: OnboardWizardOptions = {}): Promise<number> {
34
+ const env = opts.env ?? process.env
35
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
36
+ return WIZARD_NOT_INTERACTIVE
37
+ }
38
+
39
+ let result: WizardResult = { code: 0, memoryConsent: false, advisories: [], summary: [] }
40
+ try {
41
+ const app = render(<OnboardApp env={env} onResult={r => (result = r)} />)
42
+ await app.waitUntilExit()
43
+ } catch {
44
+ return WIZARD_NOT_INTERACTIVE
45
+ }
46
+
47
+ // Post-Ink: the memory provider install owns the terminal (inherited stdio).
48
+ // Ink is fully unmounted now, so there is no contention.
49
+ if (result.memoryConsent) {
50
+ const { onboardMemoryProvider } = await import('../../onboard/memory.ts')
51
+ process.stdout.write('\nInstalling shared-memory provider (@agfpd/iapeer-memory)…\n')
52
+ try {
53
+ const mem = await onboardMemoryProvider({ env })
54
+ const label = mem.provider ? `${mem.provider.provider} ${mem.provider.version}` : 'none'
55
+ process.stdout.write(`memory: ${mem.state}${mem.detail ? ` — ${mem.detail}` : ''} (slot: ${label})\n`)
56
+ } catch (e) {
57
+ process.stdout.write(`memory: step failed — ${e instanceof Error ? e.message : String(e)}\n`)
58
+ }
59
+ } else {
60
+ process.stdout.write('\nmemory: skipped — set it up later with `iapeer onboard` or `npx @agfpd/iapeer-memory init`\n')
61
+ }
62
+
63
+ // Final summary (plain text, post-Ink).
64
+ process.stdout.write('\n')
65
+ for (const line of result.summary) process.stdout.write(line + '\n')
66
+ if (result.advisories.length > 0) {
67
+ process.stdout.write('\n')
68
+ for (const a of result.advisories) process.stdout.write(a + '\n')
69
+ }
70
+ process.stdout.write('\nNext: create your first agent — `iapeer create <name> --runtime claude`\n')
71
+ return result.code
72
+ }
package/tsconfig.json CHANGED
@@ -4,6 +4,8 @@
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
6
  "allowImportingTsExtensions": true,
7
+ "jsx": "react-jsx",
8
+ "jsxImportSource": "react",
7
9
  "verbatimModuleSyntax": true,
8
10
  "noEmit": true,
9
11
  "strict": true,
@@ -14,5 +16,5 @@
14
16
  "types": ["bun", "node"],
15
17
  "lib": ["ESNext"]
16
18
  },
17
- "include": ["src/**/*.ts"]
19
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
18
20
  }