@zswarm/core 0.2.5 → 0.2.7

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/dist/schema.js CHANGED
@@ -15,6 +15,7 @@ export const OP_NAMES = [
15
15
  "keys",
16
16
  "interrupt",
17
17
  "spawn",
18
+ "restart",
18
19
  "close",
19
20
  "worktrees",
20
21
  "unworktree",
@@ -42,6 +43,7 @@ export const TARGET_OPS = [
42
43
  "status",
43
44
  "keys",
44
45
  "interrupt",
46
+ "restart",
45
47
  "close",
46
48
  "rename",
47
49
  "focus",
@@ -57,25 +59,29 @@ export const PARAMS = [
57
59
  {
58
60
  name: "to",
59
61
  type: "string",
60
- flags: ["--to", "-t"],
62
+ flags: ["--to", "-t", "--pane"],
63
+ ops: [...TARGET_OPS, "broadcast", "log"],
61
64
  description: "target pane: id (3 / terminal_3) or unique title/command; broadcast takes a comma list; log filters by it",
62
65
  },
63
66
  {
64
67
  name: "all",
65
68
  type: "boolean",
66
69
  flags: ["--all", "-a"],
70
+ ops: ["broadcast", "sessions"],
67
71
  description: "broadcast: every terminal pane in the session; sessions: include EXITED resurrectable sessions",
68
72
  },
69
73
  {
70
74
  name: "live",
71
75
  type: "boolean",
72
76
  flags: ["--live", "--active"],
77
+ ops: ["sessions"],
73
78
  description: "sessions: only live (non-EXITED) sessions — this is the default; use --all to include EXITED",
74
79
  },
75
80
  {
76
81
  name: "exited",
77
82
  type: "boolean",
78
83
  flags: ["--exited"],
84
+ ops: ["sessions"],
79
85
  cliOnly: true,
80
86
  description: "sessions: list only EXITED (resurrectable) sessions; rows carry name, exited, age and ageSeconds",
81
87
  },
@@ -83,6 +89,7 @@ export const PARAMS = [
83
89
  name: "pruneExited",
84
90
  type: "boolean",
85
91
  flags: ["--prune-exited"],
92
+ ops: ["sessions"],
86
93
  cliOnly: true,
87
94
  description: "sessions: delete EXITED sessions (all, or those matching --older-than) with zellij delete-session; a live session is never removed",
88
95
  },
@@ -90,6 +97,7 @@ export const PARAMS = [
90
97
  name: "olderThan",
91
98
  type: "string",
92
99
  flags: ["--older-than"],
100
+ ops: ["sessions"],
93
101
  cliOnly: true,
94
102
  description: "sessions --prune-exited: only delete sessions created at least this long ago (e.g. 7d, 12h, 30m; default all exited)",
95
103
  },
@@ -97,6 +105,7 @@ export const PARAMS = [
97
105
  name: "dryRun",
98
106
  type: "boolean",
99
107
  flags: ["--dry-run"],
108
+ ops: ["sessions"],
100
109
  cliOnly: true,
101
110
  description: "sessions --prune-exited: report what would be deleted without deleting",
102
111
  },
@@ -104,6 +113,7 @@ export const PARAMS = [
104
113
  name: "json",
105
114
  type: "boolean",
106
115
  flags: ["--json"],
116
+ ops: ["sessions"],
107
117
  cliOnly: true,
108
118
  description: "sessions: rows are name, exited, age and ageSeconds; the CLI prints JSON either way",
109
119
  },
@@ -123,60 +133,70 @@ export const PARAMS = [
123
133
  name: "group",
124
134
  type: "string",
125
135
  flags: ["--group", "-g"],
136
+ ops: ["broadcast"],
126
137
  description: "broadcast: narrow the selection to panes whose title or command contains this",
127
138
  },
128
139
  {
129
140
  name: "channel",
130
141
  type: "string",
131
142
  flags: ["--channel"],
143
+ ops: ["signal", "await"],
132
144
  description: "signal/await: channel name",
133
145
  },
134
146
  {
135
147
  name: "payload",
136
148
  type: "string",
137
149
  flags: ["--payload"],
150
+ ops: ["signal"],
138
151
  description: "signal: short note stored with the post",
139
152
  },
140
153
  {
141
154
  name: "count",
142
155
  type: "number",
143
156
  flags: ["--count"],
157
+ ops: ["await"],
144
158
  description: "await: how many posts to wait for (default 1)",
145
159
  },
146
160
  {
147
161
  name: "clear",
148
162
  type: "boolean",
149
163
  flags: ["--clear"],
164
+ ops: ["signal", "bus", "serve"],
150
165
  description: "signal: reset the channel (all channels when none is given); bus: forget the installed plugin; serve: stop and unregister the owned Windows zswarm-serve logon task, or the Linux/macOS serve service, if present",
151
166
  },
152
167
  {
153
168
  name: "install",
154
169
  type: "boolean",
155
170
  flags: ["--install"],
171
+ ops: ["bus", "serve"],
156
172
  description: "bus: load the event-bus plugin in a pane so its permission prompt can be answered, then remember it; serve: register the current-user Windows Interactive logon task, or on Linux/macOS a systemd user unit (with linger) or launchd job whose token lives in ~/.zswarm/serve/, and wait for authenticated hello plus host session visibility",
157
173
  },
158
174
  {
159
175
  name: "runAs",
160
176
  type: "string",
161
177
  flags: ["--run-as"],
178
+ ops: ["serve"],
162
179
  description: "serve --install/--clear on Linux/macOS, run as root: install the service for this user (a LaunchDaemon with UserName on macOS, the user's systemd units on Linux), for accounts that never log in",
163
180
  },
164
181
  {
165
182
  name: "reset",
166
183
  type: "boolean",
167
184
  flags: ["--reset"],
185
+ ops: ["tail"],
168
186
  description: "tail: forget the stored cursor and return the whole screen",
169
187
  },
170
188
  {
171
189
  name: "sampleMs",
172
190
  type: "number",
173
191
  flags: ["--sample-ms"],
192
+ ops: ["status"],
174
193
  description: "status: explicitly sample twice with this gap (fallback default 400); 0 reports running/exited only; default prefers bus changes",
175
194
  },
176
195
  {
177
196
  name: "sinceLast",
178
197
  type: "boolean",
179
198
  flags: ["--since-last"],
199
+ ops: ["status"],
180
200
  description: "status: classify changes since the previous bus observation (default when bus available); first observation is unknown unless a prompt is recognized",
181
201
  },
182
202
  {
@@ -195,33 +215,60 @@ export const PARAMS = [
195
215
  name: "limit",
196
216
  type: "number",
197
217
  flags: ["--limit"],
218
+ ops: ["log"],
198
219
  description: "log: how many entries to return (default 20)",
199
220
  },
200
221
  {
201
222
  name: "since",
202
223
  type: "string",
203
224
  flags: ["--since"],
225
+ ops: ["log"],
204
226
  description: "log: only entries at or after this epoch millisecond",
205
227
  },
206
228
  {
207
229
  name: "failed",
208
230
  type: "boolean",
209
231
  flags: ["--failed"],
232
+ ops: ["log"],
210
233
  description: "log: only deliveries that did not land",
211
234
  },
212
235
  {
213
236
  name: "body",
214
237
  type: "string",
215
238
  flags: ["--body", "-b", "--text"],
239
+ ops: ["send", "broadcast"],
216
240
  description: "send: message body",
217
241
  },
218
242
  {
219
243
  name: "bodyFile",
220
244
  type: "string",
221
245
  flags: ["--body-file"],
246
+ ops: ["send"],
222
247
  cliOnly: true,
223
248
  description: "send: read UTF-8 on the caller from PATH, or - for stdin; cannot combine with --body/--text",
224
249
  },
250
+ {
251
+ name: "handoff",
252
+ type: "string",
253
+ flags: [],
254
+ ops: ["restart"],
255
+ description: "restart: handoff text to deliver to the relaunched agent (MCP inline form; the CLI fills this from --handoff-file)",
256
+ },
257
+ {
258
+ name: "handoffFile",
259
+ type: "string",
260
+ flags: ["--handoff-file"],
261
+ ops: ["restart"],
262
+ cliOnly: true,
263
+ description: "restart: read the handoff UTF-8 on the caller from PATH, or - for stdin; a short handoff is pasted, a long one becomes a pointer line, and --handoff-self writes the agent's handoff to this path",
264
+ },
265
+ {
266
+ name: "handoffSelf",
267
+ type: "boolean",
268
+ flags: ["--handoff-self"],
269
+ ops: ["restart"],
270
+ description: "restart: before exiting, ask the running agent to write its handoff to the file and print a marker, then deliver it after relaunch",
271
+ },
225
272
  {
226
273
  name: "text",
227
274
  type: "string",
@@ -232,36 +279,42 @@ export const PARAMS = [
232
279
  name: "from",
233
280
  type: "string",
234
281
  flags: ["--from", "-f"],
282
+ ops: ["send", "broadcast"],
235
283
  description: "send: sender label in the [zswarm from=…] prefix (default: ZSWARM_FROM, else the sending pane's title, else swarm)",
236
284
  },
237
285
  {
238
286
  name: "raw",
239
287
  type: "boolean",
240
288
  flags: ["--raw"],
289
+ ops: ["send", "broadcast"],
241
290
  description: "send: skip the peer prefix (default false)",
242
291
  },
243
292
  {
244
293
  name: "full",
245
294
  type: "boolean",
246
295
  flags: ["--full"],
296
+ ops: ["dump", "wait", "tail"],
247
297
  description: "dump/wait: include full scrollback (default false)",
248
298
  },
249
299
  {
250
300
  name: "max",
251
301
  type: "number",
252
302
  flags: ["--max"],
303
+ ops: ["dump", "wait", "tail", "layout", "diff"],
253
304
  description: "dump/wait: max text chars (dump 8000, wait 2000; keeps tail, 0 = unlimited)",
254
305
  },
255
306
  {
256
307
  name: "head",
257
308
  type: "boolean",
258
309
  flags: ["--head"],
310
+ ops: ["dump"],
259
311
  description: "dump: keep the start instead of the tail when truncating",
260
312
  },
261
313
  {
262
314
  name: "for",
263
315
  type: "string",
264
316
  flags: ["--for"],
317
+ ops: ["wait"],
265
318
  values: ["idle", "match", "either"],
266
319
  description: "wait: stop on a quiet screen, on a match, or whichever lands first (default: match if match= given, else idle)",
267
320
  },
@@ -269,42 +322,49 @@ export const PARAMS = [
269
322
  name: "match",
270
323
  type: "string",
271
324
  flags: ["--match", "-m"],
325
+ ops: ["wait"],
272
326
  description: "wait: text to look for in the pane screen",
273
327
  },
274
328
  {
275
329
  name: "regex",
276
330
  type: "boolean",
277
331
  flags: ["--regex"],
332
+ ops: ["wait"],
278
333
  description: "wait: treat match as a regex (default false)",
279
334
  },
280
335
  {
281
336
  name: "ignoreCase",
282
337
  type: "boolean",
283
338
  flags: ["--ignore-case"],
339
+ ops: ["wait"],
284
340
  description: "wait: case-insensitive match (default false)",
285
341
  },
286
342
  {
287
343
  name: "idleMs",
288
344
  type: "number",
289
345
  flags: ["--idle-ms"],
346
+ ops: ["wait"],
290
347
  description: "wait: screen must be unchanged this long to count as idle (default 2000)",
291
348
  },
292
349
  {
293
350
  name: "pollMs",
294
351
  type: "number",
295
352
  flags: ["--poll-ms"],
353
+ ops: ["wait"],
296
354
  description: "wait: fallback poll interval (default 150; bus path polls at 50ms)",
297
355
  },
298
356
  {
299
357
  name: "timeoutMs",
300
358
  type: "number",
301
359
  flags: ["--timeout-ms"],
302
- description: "wait: timeout (default 60000); status/spawn: overall deadline (default 30000); doctor: overall deadline (default 10000), including Tailscale/SSH/hello/host checks; serve --install: overall install/readiness deadline (default 30000)",
360
+ ops: ["wait", "status", "spawn", "restart", "doctor", "serve", "send", "dump", "tail", "keys", "interrupt", "close", "await"],
361
+ description: "wait: timeout (default 60000); status/spawn/restart: overall deadline (default 30000); doctor: overall deadline (default 10000), including Tailscale/SSH/hello/host checks; serve --install: overall install/readiness deadline (default 30000)",
303
362
  },
304
363
  {
305
364
  name: "keys",
306
365
  type: "stringOrArray",
307
366
  flags: ["--key", "--keys", "-k"],
367
+ ops: ["keys", "interrupt"],
308
368
  repeat: true,
309
369
  description: 'keys: key specs, one per entry — "Ctrl c", "Esc", "F1", "Up". A bare string is one key; comma-separate for several.',
310
370
  },
@@ -312,72 +372,84 @@ export const PARAMS = [
312
372
  name: "chars",
313
373
  type: "string",
314
374
  flags: ["--chars"],
375
+ ops: ["keys"],
315
376
  description: "keys: literal characters to type instead of key specs (no Enter unless enter=true)",
316
377
  },
317
378
  {
318
379
  name: "enter",
319
380
  type: "boolean",
320
381
  flags: ["--enter"],
382
+ ops: ["keys"],
321
383
  description: "keys: press Enter after the keys/chars",
322
384
  },
323
385
  {
324
386
  name: "hard",
325
387
  type: "boolean",
326
388
  flags: ["--hard"],
389
+ ops: ["interrupt"],
327
390
  description: "interrupt: send Ctrl c instead of the default Esc",
328
391
  },
329
392
  {
330
393
  name: "command",
331
394
  type: "stringOrArray",
332
395
  flags: ["--command", "--cmd", "-c"],
333
- description: "spawn: program to run in the new pane, argv-style (no shell). Empty starts a plain shell.",
396
+ ops: ["spawn", "restart"],
397
+ description: "spawn: program to run in the new pane, argv-style (no shell). Empty starts a plain shell. restart: command to type in a shell pane instead of the profile's launch command",
334
398
  },
335
399
  {
336
400
  name: "cwd",
337
401
  type: "string",
338
402
  flags: ["--cwd"],
403
+ ops: ["spawn", "worktrees", "unworktree", "diff", "checkpoint"],
339
404
  description: "spawn: working directory for the new pane; worktrees/unworktree: any directory inside the repo",
340
405
  },
341
406
  {
342
407
  name: "worktree",
343
408
  type: "string",
344
409
  flags: ["--worktree", "-w"],
410
+ ops: ["spawn", "unworktree"],
345
411
  description: "spawn: branch to give the peer its own git worktree (overrides cwd); unworktree: alias for branch",
346
412
  },
347
413
  {
348
414
  name: "worktreeRoot",
349
415
  type: "string",
350
416
  flags: ["--worktree-root"],
417
+ ops: ["spawn", "unworktree"],
351
418
  description: "where worktrees live (default <repo>-worktrees beside the repo, or ZSWARM_WORKTREE_ROOT)",
352
419
  },
353
420
  {
354
421
  name: "baseRef",
355
422
  type: "string",
356
423
  flags: ["--base-ref", "--base"],
424
+ ops: ["spawn"],
357
425
  description: "spawn: ref to branch from when the worktree branch is new",
358
426
  },
359
427
  {
360
428
  name: "path",
361
429
  type: "string",
362
430
  flags: ["--path"],
431
+ ops: ["unworktree", "diff", "checkpoint"],
363
432
  description: "unworktree: worktree path to remove",
364
433
  },
365
434
  {
366
435
  name: "branch",
367
436
  type: "string",
368
437
  flags: ["--branch"],
438
+ ops: ["unworktree", "diff", "checkpoint"],
369
439
  description: "unworktree: remove the worktree holding this branch",
370
440
  },
371
441
  {
372
442
  name: "name",
373
443
  type: "string",
374
444
  flags: ["--name", "-n"],
445
+ ops: ["spawn", "rename"],
375
446
  description: "spawn: pane (or tab) name; rename: the new name",
376
447
  },
377
448
  {
378
449
  name: "submit",
379
450
  type: "string",
380
451
  flags: ["--submit"],
452
+ ops: ["send", "broadcast"],
381
453
  values: ["auto", "double-enter", "none"],
382
454
  description: 'send/broadcast: auto verifies the paste actually submitted and presses Enter again if not (default). The result reports submitted: true, "queued" (held behind a running turn), false (still in the composer), "unverified" (the screen changed but the body is not visible), or "not-delivered" (the pane was unchanged apart from the composer, so the op fails not_delivered and a resend is safe)',
383
455
  },
@@ -385,48 +457,56 @@ export const PARAMS = [
385
457
  name: "observeMs",
386
458
  type: "number",
387
459
  flags: ["--observe-ms"],
460
+ ops: ["send", "dump", "tail", "wait", "keys", "interrupt", "close", "spawn"],
388
461
  description: "spawn: observe creation/alias for up to 3000ms; pane lookup: retry absence for 1000ms (scaled up to 5x when the first Zellij call is slow); 0 disables retries",
389
462
  },
390
463
  {
391
464
  name: "settleMs",
392
465
  type: "number",
393
466
  flags: ["--settle-ms"],
467
+ ops: ["send", "broadcast"],
394
468
  description: "send/broadcast: pause before checking the paste landed (default 300, scaled up to 5x when the first Zellij call is slow; explicit value wins)",
395
469
  },
396
470
  {
397
471
  name: "confirm",
398
472
  type: "boolean",
399
473
  flags: ["--confirm"],
474
+ ops: ["send", "broadcast"],
400
475
  description: 'send/broadcast: after the send, dump once more and report what it sees; paste again only on "not-delivered" (positive evidence nothing landed), press Enter when the body is still in the composer, and never paste the body twice',
401
476
  },
402
477
  {
403
478
  name: "ifIdle",
404
479
  type: "boolean",
405
480
  flags: ["--if-idle"],
481
+ ops: ["send"],
406
482
  description: "send: refuse a pane that a relay has leased (pane_leased) or whose harness is visibly working (pane_busy) instead of pasting into it",
407
483
  },
408
484
  {
409
485
  name: "expect",
410
486
  type: "string",
411
487
  flags: ["--expect"],
488
+ ops: ["send", "keys", "interrupt"],
412
489
  description: "send/keys/interrupt: case-insensitive substring required on the current screen immediately before input",
413
490
  },
414
491
  {
415
492
  name: "message",
416
493
  type: "string",
417
494
  flags: ["--message"],
495
+ ops: ["checkpoint"],
418
496
  description: "checkpoint: commit message",
419
497
  },
420
498
  {
421
499
  name: "stat",
422
500
  type: "boolean",
423
501
  flags: ["--stat"],
502
+ ops: ["diff"],
424
503
  description: "diff: stat only, no patch body",
425
504
  },
426
505
  {
427
506
  name: "direction",
428
507
  type: "string",
429
508
  flags: ["--direction", "-d"],
509
+ ops: ["spawn"],
430
510
  values: ["right", "left", "up", "down"],
431
511
  description: "spawn: split direction",
432
512
  },
@@ -434,66 +514,77 @@ export const PARAMS = [
434
514
  name: "floating",
435
515
  type: "boolean",
436
516
  flags: ["--floating"],
517
+ ops: ["spawn"],
437
518
  description: "spawn: open the pane floating",
438
519
  },
439
520
  {
440
521
  name: "width",
441
522
  type: "string",
442
523
  flags: ["--width"],
524
+ ops: ["spawn"],
443
525
  description: "spawn: floating pane width (e.g. 80 or 50%)",
444
526
  },
445
527
  {
446
528
  name: "height",
447
529
  type: "string",
448
530
  flags: ["--height"],
531
+ ops: ["spawn"],
449
532
  description: "spawn: floating pane height (e.g. 20 or 40%)",
450
533
  },
451
534
  {
452
535
  name: "tab",
453
536
  type: "string",
454
537
  flags: ["--tab"],
538
+ ops: ["broadcast", "rename", "spawn"],
455
539
  description: "tab name: broadcast targets it, rename retitles it, spawn opens the new pane's tab by it",
456
540
  },
457
541
  {
458
542
  name: "newTab",
459
543
  type: "boolean",
460
544
  flags: ["--new-tab"],
545
+ ops: ["spawn"],
461
546
  description: "spawn: open a new tab instead of splitting",
462
547
  },
463
548
  {
464
549
  name: "layout",
465
550
  type: "string",
466
551
  flags: ["--layout", "-l"],
552
+ ops: ["spawn"],
467
553
  description: "spawn: layout name for the new tab (newTab=true)",
468
554
  },
469
555
  {
470
556
  name: "closeOnExit",
471
557
  type: "boolean",
472
558
  flags: ["--close-on-exit"],
559
+ ops: ["spawn"],
473
560
  description: "spawn: close the pane when its command exits",
474
561
  },
475
562
  {
476
563
  name: "allowSelf",
477
564
  type: "boolean",
478
565
  flags: ["--allow-self"],
566
+ ops: ["send", "keys", "close", "interrupt", "broadcast"],
479
567
  description: "send/keys/close: allow targeting zswarm's own pane (default false)",
480
568
  },
481
569
  {
482
570
  name: "force",
483
571
  type: "boolean",
484
572
  flags: ["--force"],
485
- description: "send/keys: write to a pane whose command has exited; unworktree: remove a busy or dirty worktree; bus: close orphan bus panes and reload this session's plugin",
573
+ ops: ["send", "keys", "unworktree", "bus", "interrupt", "broadcast", "restart"],
574
+ description: "send/keys: write to a pane whose command has exited; restart: restart a pane that a relay has leased; unworktree: remove a busy or dirty worktree; bus: close orphan bus panes and reload this session's plugin",
486
575
  },
487
576
  {
488
577
  name: "listen",
489
578
  type: "string",
490
579
  flags: ["--listen"],
580
+ ops: ["serve"],
491
581
  description: "serve: bind address (default 127.0.0.1:9419). Loopback needs no Tailscale; a non-loopback literal must be a verified local Tailscale IP (see docs/tailscale.md). Reach via ZSWARM_SERVE / --serve (direct host:port, private Tailscale Serve tcp:// frontend, or ssh:// to remote loopback)",
492
582
  },
493
583
  {
494
584
  name: "verbose",
495
585
  type: "boolean",
496
586
  flags: ["--verbose", "-v"],
587
+ ops: ["list", "send", "spawn", "status"],
497
588
  description: "list/send/spawn: include cwd/focus/exited/floating (and the pane on send/spawn)",
498
589
  },
499
590
  ];
@@ -533,7 +624,7 @@ export function mcpInputSchema() {
533
624
  }
534
625
  export const MCP_TOOL_DESCRIPTION = `zSwarm Zellij pane coordination (op=${OP_NAMES.join("|")}). ` +
535
626
  "List panes, send text into a CLI pane (paste+Enter), block until a pane goes idle or prints a match, " +
536
- "send raw keys, open or close panes, and give a peer its own git worktree. " +
627
+ "send raw keys, open or close panes, restart an agent in its pane with an optional handoff, and give a peer its own git worktree. " +
537
628
  "Same host as Zellij, or ZSWARM_SSH / ZSWARM_SERVE for a remote crew.";
538
629
  const FLAG_INDEX = new Map(PARAMS.flatMap((p) => p.flags.map((flag) => [flag, p])));
539
630
  export function cliUsage() {
@@ -552,6 +643,49 @@ export function cliUsage() {
552
643
  lines.push("", "Guards: writes refuse zswarm's own pane (--allow-self) and exited panes (--force). --expect requires the screen to contain a substring first.", "Bus: `zswarm bus --install` once per Zellij session. `--force` closes orphan bus panes and reloads; do not use it as a retry.", "Remote: ZSWARM_SSH (+ ZSWARM_TMP=auto or ZSWARM_SSH_MODE=interactive on Windows). Or run `zswarm serve --listen` next to Zellij and set ZSWARM_SERVE / --serve (host:port, tcp://, or ssh://user@host?servePort=9419) plus ZSWARM_SERVE_TOKEN. Serve defaults to loopback and always requires a token; an explicit local Tailscale IP is allowed only after host verification. Private raw TCP Tailscale Serve keeps the backend on 127.0.0.1 behind `tailscale serve --tcp=…` (docs/tailscale.md). ssh:// does not start remote serve. Windows default recipe: `zswarm serve --install` (verified readiness).", "Hosts (CLI only, see docs/hosts.md): `zswarm slot`, `zswarm crew`, `zswarm relay`, `zswarm host`; `zswarm <command> --help` for each.", "Doctor: `zswarm doctor --session crew` inspects local, --ssh, and --serve routes without installs, pane changes, or plugin launch. See docs/doctor.md and docs/tailscale.md.", "Env: ZSWARM_BIN, ZSWARM_PATH, ZSWARM_SESSION, ZSWARM_SELF_PANE, ZSWARM_FROM, ZELLIJ_PANE_ID, ZELLIJ_SESSION_NAME, ZSWARM_BUS, ZSWARM_BUS_PLUGIN, ZSWARM_SSH, ZSWARM_SSH_BIN, ZSWARM_SSH_OPTS, ZSWARM_TMP, ZSWARM_SSH_MODE, ZSWARM_SERVE, ZSWARM_SERVE_TOKEN, ZSWARM_TAILSCALE_BIN, ZSWARM_CACHE_TTL_MS", "");
553
644
  return lines.join("\n");
554
645
  }
646
+ /**
647
+ * True when argv asks for help: `--help` or `-h` as a flag of its own, not the
648
+ * value of a flag that takes one (`--body -h` sends "-h") and not after `--`.
649
+ */
650
+ export function wantsHelp(argv) {
651
+ const valued = new Set(PARAMS.filter((p) => p.type !== "boolean").flatMap((p) => p.flags));
652
+ for (let i = 0; i < argv.length; i++) {
653
+ const arg = argv[i];
654
+ if (arg === "--")
655
+ return false;
656
+ if (arg === "--help" || arg === "-h")
657
+ return true;
658
+ if (valued.has(arg))
659
+ i++; // its value may be anything, "-h" included
660
+ }
661
+ return false;
662
+ }
663
+ /**
664
+ * Per-command help for `zswarm <op> --help`: the usage line, the positional
665
+ * shorthand, and only the flags whose `ops` include this op. A flag without
666
+ * `ops` (session, --local, --ssh, --fresh, --serve) applies to every op.
667
+ * Render from the same PARAMS table that parses the flags, so they cannot drift.
668
+ */
669
+ export function commandUsage(op) {
670
+ const lines = [`usage: zswarm ${op} [options]`, ""];
671
+ const positional = op === "send"
672
+ ? " positional: first bare argument is --to, second is --body"
673
+ : TARGET_OPS.includes(op)
674
+ ? " positional: first bare argument is --to"
675
+ : undefined;
676
+ if (positional)
677
+ lines.push(positional, "");
678
+ for (const param of PARAMS) {
679
+ if (param.flags.length === 0)
680
+ continue;
681
+ if (param.ops && !param.ops.includes(op))
682
+ continue;
683
+ const value = param.type === "boolean" ? "" : param.type === "number" ? " N" : " VALUE";
684
+ lines.push(` ${param.flags.join(", ").padEnd(28)}${value.trim().padEnd(6)}${param.description}`);
685
+ }
686
+ lines.push("");
687
+ return lines.join("\n");
688
+ }
555
689
  /** Turn argv (without the op) into dispatch args, driven by PARAMS. */
556
690
  /**
557
691
  * Pull the op out of argv, tolerating flags before it.
@@ -561,7 +695,7 @@ export function cliUsage() {
561
695
  * an unknown op. Only positions that already errored are affected: if argv[0]
562
696
  * is a real op it wins, so `send reviewer list` still sends the word "list".
563
697
  */
564
- function extractOp(argv) {
698
+ export function extractOp(argv) {
565
699
  const first = argv[0];
566
700
  if (first && !first.startsWith("-")) {
567
701
  return { op: first, rest: argv.slice(1) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zswarm/core",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "description": "zSwarm Zellij client and shared ops dispatch",
6
6
  "exports": {