@agfpd/iapeer 0.4.66 → 0.4.68
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/docs/16-tray.md +16 -8
- package/docs/ru/16-tray.md +14 -6
- package/package.json +1 -1
- package/src/cli/index.ts +15 -0
- package/src/tray/index.ts +9 -3
- package/src/tray/install.ts +34 -3
- package/src/tray/render.ts +52 -38
package/docs/16-tray.md
CHANGED
|
@@ -26,17 +26,25 @@ activation that also installs SwiftBar.app when it is absent.
|
|
|
26
26
|
`●` live · `○` asleep · `✕` stopped — green when any runtime is live. Badges: `👤`
|
|
27
27
|
a human is attached, `🔒` launchd-managed (H4 read-only), `⏳N` an ephemeral peer's
|
|
28
28
|
queue depth.
|
|
29
|
-
- **Click
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
- **Click an agent peer → Terminal.app running `iapeer attach <peer>`.** The row itself is
|
|
30
|
+
the attach action (no submenu, so a click attaches rather than merely expanding). The
|
|
31
|
+
handoff goes through `iapeer tray attach-term`, which writes a per-peer `.command`
|
|
32
|
+
launcher and `open`s it — `open <file>.command` hands the file to Terminal.app with
|
|
33
|
+
**no Automation/Accessibility permission prompt**. This deliberately avoids SwiftBar's
|
|
34
|
+
own `terminal=true`, whose Terminal launch drives a `Cmd-T` System-Events keystroke that
|
|
35
|
+
silently no-ops unless SwiftBar holds Accessibility permission. **Launchd-managed infra
|
|
36
|
+
peers (telegram / notifier, `🔒`) are not pty-attachable** — their rows are plain status
|
|
37
|
+
lines. Lifecycle actions (Wake / Stop / Start / New / Interrupt / Refresh / Compact) live
|
|
38
|
+
in the **Manage** submenu, each a `POST /fleet/v1/peers/<peer>/<cmd>` via `iapeer tray cmd`.
|
|
34
39
|
|
|
35
40
|
The list updates itself: the plugin is **streamable** — it subscribes to the event stream
|
|
36
41
|
and re-fetches the snapshot on every change (a peer waking, an operator stop/start, a
|
|
37
|
-
death), coalescing bursts. A
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
death), coalescing bursts. A heartbeat keeps ages fresh and self-heals a missed event; the
|
|
43
|
+
stream reconnects across daemon restarts. Every emitted menu block is prefixed with the
|
|
44
|
+
`~~~` stream separator (SwiftBar's default leading-separator mode = replace), including the
|
|
45
|
+
first — otherwise a manual Refresh, which `terminate()`s and re-`invoke()`s the plugin
|
|
46
|
+
without clearing SwiftBar's held content, would append the restarted block and render the
|
|
47
|
+
whole dashboard twice.
|
|
40
48
|
|
|
41
49
|
## Verbs
|
|
42
50
|
|
package/docs/ru/16-tray.md
CHANGED
|
@@ -25,15 +25,23 @@ SwiftBar.app, если его нет.
|
|
|
25
25
|
`●` живой · `○` спит · `✕` остановлен — зелёный, если хоть один рантайм живой. Бейджи:
|
|
26
26
|
`👤` подключён человек, `🔒` под launchd (H4 read-only), `⏳N` глубина очереди эфемерного
|
|
27
27
|
пира.
|
|
28
|
-
- **Клик по
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
- **Клик по агент-пиру → Terminal.app с `iapeer attach <peer>`.** Строка сама и есть действие
|
|
29
|
+
attach (без подменю — клик аттачит, а не раскрывает). Передача идёт через `iapeer tray
|
|
30
|
+
attach-term`: пишет per-peer `.command`-лаунчер и `open`'ит его — `open <файл>.command`
|
|
31
|
+
отдаёт файл Terminal.app **без запроса Automation/Accessibility**. Это намеренно обходит
|
|
32
|
+
родной `terminal=true` SwiftBar, чей запуск Terminal бьёт `Cmd-T` кейстроком через System
|
|
33
|
+
Events и молча не срабатывает без права Accessibility у SwiftBar. **Launchd-инфра-пиры
|
|
34
|
+
(telegram / notifier, `🔒`) не pty-аттачатся** — их строки просто статус. Lifecycle-действия
|
|
35
|
+
(Wake / Stop / Start / New / Interrupt / Refresh / Compact) — в подменю **Manage**, каждое —
|
|
36
|
+
`POST /fleet/v1/peers/<peer>/<cmd>` через `iapeer tray cmd`.
|
|
32
37
|
|
|
33
38
|
Список обновляется сам: плагин **streamable** — подписан на поток событий и перечитывает
|
|
34
39
|
снапшот на каждое изменение (пробуждение пира, стоп/старт оператором, смерть), склеивая
|
|
35
|
-
всплески. Хартбит
|
|
36
|
-
переподключается через рестарты демона.
|
|
40
|
+
всплески. Хартбит держит возрасты свежими и самозалечивает пропущенное событие; поток
|
|
41
|
+
переподключается через рестарты демона. Каждый выдаваемый блок меню предваряется сепаратором
|
|
42
|
+
`~~~` (дефолтный leading-режим SwiftBar = замена), включая первый — иначе ручной Refresh,
|
|
43
|
+
который `terminate()`'ит и заново `invoke()`'ит плагин НЕ очищая удерживаемый SwiftBar контент,
|
|
44
|
+
пришил бы перезапущенный блок и отрисовал бы весь дашборд дважды.
|
|
37
45
|
|
|
38
46
|
## Verb'ы
|
|
39
47
|
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -2505,6 +2505,21 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2505
2505
|
out(await tray.renderTrayOnce(env))
|
|
2506
2506
|
return 0
|
|
2507
2507
|
}
|
|
2508
|
+
case 'attach-term': {
|
|
2509
|
+
// Terminal handoff for a tray click: open a system Terminal running
|
|
2510
|
+
// `iapeer attach <peer>` via an `open`ed `.command` (no Accessibility/
|
|
2511
|
+
// Automation TCC — unlike SwiftBar's own terminal=true).
|
|
2512
|
+
const peer = positionals[1]
|
|
2513
|
+
if (!peer) return argErr(errOut, 'usage: iapeer tray attach-term <peer> [runtime]')
|
|
2514
|
+
try {
|
|
2515
|
+
const r = tray.trayAttachTerm({ env, personality: peer, runtime: positionals[2] })
|
|
2516
|
+
out(`opening Terminal → iapeer attach ${peer} (${r.cmdFile})\n`)
|
|
2517
|
+
} catch (e) {
|
|
2518
|
+
errOut(`tray attach-term: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
2519
|
+
return 1
|
|
2520
|
+
}
|
|
2521
|
+
return 0
|
|
2522
|
+
}
|
|
2508
2523
|
case 'cmd': {
|
|
2509
2524
|
const command = positionals[1]
|
|
2510
2525
|
const peer = positionals[2]
|
package/src/tray/index.ts
CHANGED
|
@@ -72,14 +72,20 @@ export async function streamTray(
|
|
|
72
72
|
out: (s: string) => void = s => process.stdout.write(s),
|
|
73
73
|
signal?: AbortSignal,
|
|
74
74
|
): Promise<void> {
|
|
75
|
-
let first = true
|
|
76
75
|
// Serialize all emits through a promise chain so a `~~~`+block is never interleaved.
|
|
77
76
|
let chain: Promise<void> = Promise.resolve()
|
|
78
77
|
const emit = (): Promise<void> => {
|
|
79
78
|
chain = chain.then(async () => {
|
|
80
79
|
const block = await renderTrayOnce(env)
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// EVERY block — including the first — is prefixed with the `~~~` stream separator.
|
|
81
|
+
// SwiftBar's default (leading-separator) handler sets content = <text after the
|
|
82
|
+
// last ~~~>, i.e. REPLACE; a block WITHOUT a leading ~~~ is APPENDED to the
|
|
83
|
+
// current content. On a refresh (SwiftBar terminate()s the process WITHOUT
|
|
84
|
+
// clearing content, then re-invoke()s), the restarted process's first block would
|
|
85
|
+
// otherwise append to the stale pre-refresh menu → the whole dashboard renders
|
|
86
|
+
// twice. Always leading with ~~~ makes every emit a clean replace. (root: SwiftBar
|
|
87
|
+
// v2.0.1 StreamablePlugin.onOutputUpdate.)
|
|
88
|
+
out(`~~~\n${block}`)
|
|
83
89
|
})
|
|
84
90
|
return chain
|
|
85
91
|
}
|
package/src/tray/install.ts
CHANGED
|
@@ -100,9 +100,10 @@ export function buildPluginShim(binPath: string): string {
|
|
|
100
100
|
'# <xbar.author>iapeer</xbar.author>',
|
|
101
101
|
'# <xbar.desc>iapeer multi-agent fleet — live peer dashboard over the daemon fleet API.</xbar.desc>',
|
|
102
102
|
'# <swiftbar.type>streamable</swiftbar.type>',
|
|
103
|
-
'
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
// NB: deliberately NOT hiding SwiftBar's own service items (Run-in-Terminal /
|
|
104
|
+
// Disable / Last-Updated / Preferences / Refresh). They are the right-click "do
|
|
105
|
+
// stuff" menu the owner expects; hiding them made the icon feel dead. Minimalism is
|
|
106
|
+
// for OUR content (status + peer rows), not SwiftBar's utility menu.
|
|
106
107
|
// The dashboard ONLY ever talks to the local daemon (unix socket / loopback), so it
|
|
107
108
|
// must never route through an HTTP proxy. Bun's fetch caches proxy config at process
|
|
108
109
|
// start and ignores NO_PROXY for loopback, so clearing the vars must happen in the
|
|
@@ -259,6 +260,36 @@ export function uninstallTray(
|
|
|
259
260
|
return { removed, refreshed }
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Open a system Terminal attached to a peer WITHOUT SwiftBar's `terminal=true`. SwiftBar
|
|
265
|
+
* v2's Terminal.app launch drives a `Cmd-T` keystroke via System Events, which silently
|
|
266
|
+
* no-ops unless SwiftBar holds Accessibility permission — so tray attach clicks appeared
|
|
267
|
+
* dead even after the Automation→Terminal grant. Instead: write a per-peer `.command`
|
|
268
|
+
* launcher and `open` it. `open <file>.command` hands the file to Terminal.app (its
|
|
269
|
+
* default handler) and runs it — a document-open, needing NO Automation/Accessibility
|
|
270
|
+
* TCC. This is the tray's terminal handoff (docs/15: attach is client-side). The peer
|
|
271
|
+
* name is validated (it is interpolated into a shell script). Returns the launcher path.
|
|
272
|
+
*/
|
|
273
|
+
export function trayAttachTerm(opts: {
|
|
274
|
+
env?: NodeJS.ProcessEnv
|
|
275
|
+
personality: string
|
|
276
|
+
runtime?: string
|
|
277
|
+
run?: Runner
|
|
278
|
+
}): { cmdFile: string } {
|
|
279
|
+
const env = opts.env ?? process.env
|
|
280
|
+
const run = opts.run ?? defaultRun
|
|
281
|
+
const p = opts.personality
|
|
282
|
+
if (!/^[a-z][a-z0-9-]*$/.test(p)) throw new Error(`invalid peer name "${p}"`)
|
|
283
|
+
const rt = opts.runtime && /^[a-z][a-z0-9-]*$/.test(opts.runtime) ? opts.runtime : undefined
|
|
284
|
+
const binPath = iapeerBinPath(env)
|
|
285
|
+
const dir = join(resolveGlobalRoot(env), 'tray')
|
|
286
|
+
mkdirSync(dir, { recursive: true })
|
|
287
|
+
const cmdFile = join(dir, `attach-${p}.command`)
|
|
288
|
+
writeFileSync(cmdFile, `#!/bin/bash\nexec "${binPath}" attach ${p}${rt ? ` ${rt}` : ''}\n`, { mode: 0o755 })
|
|
289
|
+
run('open', [cmdFile])
|
|
290
|
+
return { cmdFile }
|
|
291
|
+
}
|
|
292
|
+
|
|
262
293
|
export interface TrayStatus {
|
|
263
294
|
daemon: { fleet: boolean; version?: string; sock?: string; tcp?: string }
|
|
264
295
|
swiftbar: { installed: boolean; pluginDir?: string }
|
package/src/tray/render.ts
CHANGED
|
@@ -79,11 +79,6 @@ function line(depth: number, text: string, p: Record<string, string | number | b
|
|
|
79
79
|
return `${'--'.repeat(depth)}${text}${params(p)}`
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/** A menu separator at submenu `depth`: the `--`×depth prefix + `---` (SwiftBar: a
|
|
83
|
-
* top-level separator is `---`, a depth-1 one is `-----`, etc.). */
|
|
84
|
-
function separator(depth: number): string {
|
|
85
|
-
return `${'--'.repeat(depth)}---`
|
|
86
|
-
}
|
|
87
82
|
|
|
88
83
|
// ── time helpers ────────────────────────────────────────────────────────────────
|
|
89
84
|
|
|
@@ -154,17 +149,25 @@ export function renderSwiftBar(snapshot: TraySnapshot, opts: RenderOptions): str
|
|
|
154
149
|
if (providers) lines.push(line(0, providers, { color: COLOR.meta }))
|
|
155
150
|
lines.push('---')
|
|
156
151
|
|
|
157
|
-
// ── peers ──
|
|
152
|
+
// ── peers (each row is a DIRECT attach action — click → Terminal) ──
|
|
158
153
|
if (peers.length === 0) {
|
|
159
154
|
lines.push(line(0, 'no peers registered', { color: COLOR.meta }))
|
|
160
155
|
}
|
|
161
156
|
for (const p of peers) {
|
|
162
|
-
lines.push(
|
|
157
|
+
lines.push(renderPeerRow(p, bin, now))
|
|
163
158
|
}
|
|
164
159
|
|
|
160
|
+
// ── Manage submenu (lifecycle commands per peer; kept off the rows so a row click
|
|
161
|
+
// attaches instead of just expanding a submenu) ──
|
|
162
|
+
lines.push('---')
|
|
163
|
+
lines.push(...renderManage(peers, bin))
|
|
164
|
+
|
|
165
165
|
// ── footer ──
|
|
166
|
+
// No explicit "Refresh" item: the plugin is streamable + SSE-driven, so the menu is
|
|
167
|
+
// always current on open — a manual refresh gives the user nothing and (being a
|
|
168
|
+
// SwiftBar refresh) just closes the dropdown. The emergency "restart the plugin" lever
|
|
169
|
+
// lives in SwiftBar's own service menu (right-click → Refresh).
|
|
166
170
|
lines.push('---')
|
|
167
|
-
lines.push(line(0, 'Refresh', { refresh: true, sfimage: 'arrow.clockwise' }))
|
|
168
171
|
lines.push(line(0, `iapeer · fleet dashboard`, { color: COLOR.meta, size: 11 }))
|
|
169
172
|
return lines.join('\n') + '\n'
|
|
170
173
|
}
|
|
@@ -184,7 +187,23 @@ function renderProviders(host: TraySnapshot['host']): string {
|
|
|
184
187
|
return seg.join(' · ')
|
|
185
188
|
}
|
|
186
189
|
|
|
187
|
-
|
|
190
|
+
/** A peer is attachable when it is NOT launchd-managed: launchd-owned infra peers
|
|
191
|
+
* (telegram / notifier) have no attachable pty session, so their rows carry no attach
|
|
192
|
+
* action (a plain status line). Agent peers (claude / codex) are attachable. */
|
|
193
|
+
function attachable(p: TrayPeer): boolean {
|
|
194
|
+
return !p.launchd_managed
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* One peer row. For an ATTACHABLE peer the row IS a DIRECT attach action (no submenu,
|
|
199
|
+
* so a click attaches instead of merely expanding): clicking runs `iapeer tray
|
|
200
|
+
* attach-term <peer>` in the BACKGROUND (terminal=false), which opens a `.command` via
|
|
201
|
+
* `open` — a system-Terminal handoff that needs NO Accessibility/Automation TCC. This
|
|
202
|
+
* deliberately AVOIDS SwiftBar's own `terminal=true`, whose Terminal.app launch drives a
|
|
203
|
+
* `Cmd-T` System-Events keystroke that silently no-ops without Accessibility permission.
|
|
204
|
+
* Infra (launchd) rows are plain status lines. Lifecycle lives in the Manage submenu.
|
|
205
|
+
*/
|
|
206
|
+
function renderPeerRow(p: TrayPeer, bin: string, now: number): string {
|
|
188
207
|
const agg = aggregate(p)
|
|
189
208
|
const detail = p.runtimes.map(r => `${r.runtime}${GLYPH[r.status]}`).join(' ')
|
|
190
209
|
const badges =
|
|
@@ -194,36 +213,31 @@ function renderPeerRow(p: TrayPeer, bin: string, now: number): string[] {
|
|
|
194
213
|
const ageStr = p.last_active_ms ? ` ${fmtAge(p.last_active_ms, now)}` : ''
|
|
195
214
|
const label = `${p.personality} ${detail}${badges}${ageStr}`
|
|
196
215
|
const rowColor = agg === 'live' ? COLOR.live : COLOR.meta
|
|
216
|
+
if (!attachable(p)) return line(0, label, { color: rowColor })
|
|
217
|
+
return line(0, label, { color: rowColor, bash: bin, param1: 'tray', param2: 'attach-term', param3: p.personality, terminal: false })
|
|
218
|
+
}
|
|
197
219
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
out.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
param1: 'tray',
|
|
220
|
-
param2: 'cmd',
|
|
221
|
-
param3: c.cmd,
|
|
222
|
-
param4: p.personality,
|
|
223
|
-
terminal: false,
|
|
224
|
-
refresh: true,
|
|
225
|
-
}),
|
|
226
|
-
)
|
|
220
|
+
/** The Manage submenu: one sub-submenu per peer with its lifecycle commands, each a
|
|
221
|
+
* `iapeer tray cmd <c> <peer>` → POST /fleet/v1/peers/<peer>/<c> (terminal=false,
|
|
222
|
+
* refresh to reflect the outcome). Kept OFF the peer rows so a row click attaches. */
|
|
223
|
+
function renderManage(peers: TrayPeer[], bin: string): string[] {
|
|
224
|
+
const out: string[] = [line(0, 'Manage', { sfimage: 'slider.horizontal.3' })]
|
|
225
|
+
for (const p of peers) {
|
|
226
|
+
out.push(line(1, p.personality))
|
|
227
|
+
for (const c of MANAGE_COMMANDS) {
|
|
228
|
+
// No refresh=true: the SSE stream reflects the command's lifecycle event on its
|
|
229
|
+
// own (streamable is always fresh); a refresh would only restart the stream.
|
|
230
|
+
out.push(
|
|
231
|
+
line(2, c.label, {
|
|
232
|
+
bash: bin,
|
|
233
|
+
param1: 'tray',
|
|
234
|
+
param2: 'cmd',
|
|
235
|
+
param3: c.cmd,
|
|
236
|
+
param4: p.personality,
|
|
237
|
+
terminal: false,
|
|
238
|
+
}),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
227
241
|
}
|
|
228
242
|
return out
|
|
229
243
|
}
|