@zswarm/core 0.3.0 → 0.4.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.
@@ -337,11 +337,26 @@ export function buildCrewClientScript(action, params) {
337
337
  PS_CLIENT_PREAMBLE,
338
338
  `$marker = '${marker}'`,
339
339
  psClientPidLine(marker),
340
+ "$all = @(Get-CimInstance Win32_Process)",
340
341
  "$closed = @()",
341
342
  "foreach ($clientPid in $clientPids) {",
342
- " # The window is the launcher's console, so the tree has to go with it:",
343
- " # /T reaches the attached `zellij` client that shares that console.",
344
- " & taskkill.exe /PID $clientPid /T /F 2>$null | Out-Null",
343
+ " # The window is the launcher's console, so its tree goes with it: the",
344
+ " # attached `zellij` client shares that console. The Zellij server is in the",
345
+ " # tree too (the client that created the session spawned it) and must stay,",
346
+ " # or the session dies with its window: skip it and everything under it.",
347
+ " # A child is never older than its parent, so a reused parent PID never",
348
+ " # links an unrelated process.",
349
+ " $tree = New-Object System.Collections.Generic.List[object]",
350
+ " $queue = New-Object System.Collections.Generic.Queue[object]",
351
+ " $queue.Enqueue(($all | Where-Object { $_.ProcessId -eq $clientPid } | Select-Object -First 1))",
352
+ " while ($queue.Count -gt 0) {",
353
+ " $proc = $queue.Dequeue()",
354
+ " if (-not $proc) { continue }",
355
+ " if ($proc.Name -eq 'zellij.exe' -and $proc.CommandLine -match '(^|\\s)--server(\\s|=)') { continue }",
356
+ " $tree.Add($proc)",
357
+ " foreach ($child in @($all | Where-Object { $_.ParentProcessId -eq $proc.ProcessId -and $_.CreationDate -ge $proc.CreationDate })) { $queue.Enqueue($child) }",
358
+ " }",
359
+ " for ($i = $tree.Count - 1; $i -ge 0; $i--) { Stop-Process -Id $tree[$i].ProcessId -Force -ErrorAction SilentlyContinue }",
345
360
  " $closed += $clientPid",
346
361
  "}",
347
362
  "Write-ZswarmJson @{ closed = @($closed); count = $closed.Count }",
package/dist/host/crew.js CHANGED
@@ -212,6 +212,14 @@ export async function crewStart(session, deps = {}) {
212
212
  // `zellij --layout ... attach --create`, which `crew client` starts. That
213
213
  // window is what `crew start` reopens when it is gone.
214
214
  const client = windowsClient(deps, env);
215
+ // A live session that already has its layout is the crew: keep it and only
216
+ // make sure its window is there. Rebuilding would restart every pane.
217
+ if ((await sessions(env)).includes(session)
218
+ && checkLayout(await panes(session, env), files.first, names).state === "ok") {
219
+ const opened = await client.ensure(session);
220
+ log(`session ${session} already up; client window ${opened.opened ? "opened" : "already open"}`);
221
+ return 0;
222
+ }
215
223
  for (let attempt = 1; attempt <= 3; attempt++) {
216
224
  zellij(["delete-session", session], env); // clears only a dead session of this name
217
225
  await client.close(session); // a window left over from the previous attempt
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export { buildServeTaskScript, DEFAULT_SERVE_INSTALL_TIMEOUT_MS, installServeLog
24
24
  export { allocateLoopbackPort, assertSafeSshTunnelOpts, buildSshTunnelArgv, createServeTunnelManager, DEFAULT_SSH_PORT, DEFAULT_SSH_SERVE_PORT, describeServeTarget, formatSshServeTarget, forwardServe, isSshServeTarget, parseSshServeTarget, redactSshUserinfo, serveTunnelCacheKey, SSH_SERVE_REMOTE_HOST, SSH_TUNNEL_KEEPALIVE_COUNT, SSH_TUNNEL_KEEPALIVE_INTERVAL_S, stripControllerRouting, type AcquireServeTunnelOptions, type ParsedSshServeTarget, type ServeTunnelHandle, type ServeTunnelManager, type ServeTunnelManagerOptions, } from "./ops/serve-tunnel.js";
25
25
  export { parseTabList, resolveTab, type ZellijTab } from "./zellij/tabs.js";
26
26
  export { selectTargets } from "./ops/broadcast.js";
27
+ export { originFromServeRequest, originSuffix, resolveSenderOrigin, sanitizeOriginPart, type SenderOrigin, } from "./ops/delivery.js";
27
28
  export { diffScreens, cursorKey } from "./ops/tail.js";
28
29
  export { classify, lastLine, mapPool, peerStatus, DEFAULT_STATUS_TIMEOUT_MS, STATUS_DUMP_CONCURRENCY, type PeerState, } from "./ops/status.js";
29
30
  export { normalizeScreen, unfoldScreen, truncateDumpText, DEFAULT_DUMP_MAX_CHARS, DEFAULT_WAIT_MAX_CHARS, } from "./ops/util.js";
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ export { buildServeTaskScript, DEFAULT_SERVE_INSTALL_TIMEOUT_MS, installServeLog
24
24
  export { allocateLoopbackPort, assertSafeSshTunnelOpts, buildSshTunnelArgv, createServeTunnelManager, DEFAULT_SSH_PORT, DEFAULT_SSH_SERVE_PORT, describeServeTarget, formatSshServeTarget, forwardServe, isSshServeTarget, parseSshServeTarget, redactSshUserinfo, serveTunnelCacheKey, SSH_SERVE_REMOTE_HOST, SSH_TUNNEL_KEEPALIVE_COUNT, SSH_TUNNEL_KEEPALIVE_INTERVAL_S, stripControllerRouting, } from "./ops/serve-tunnel.js";
25
25
  export { parseTabList, resolveTab } from "./zellij/tabs.js";
26
26
  export { selectTargets } from "./ops/broadcast.js";
27
+ export { originFromServeRequest, originSuffix, resolveSenderOrigin, sanitizeOriginPart, } from "./ops/delivery.js";
27
28
  export { diffScreens, cursorKey } from "./ops/tail.js";
28
29
  export { classify, lastLine, mapPool, peerStatus, DEFAULT_STATUS_TIMEOUT_MS, STATUS_DUMP_CONCURRENCY, } from "./ops/status.js";
29
30
  export { normalizeScreen, unfoldScreen, truncateDumpText, DEFAULT_DUMP_MAX_CHARS, DEFAULT_WAIT_MAX_CHARS, } from "./ops/util.js";
@@ -4,6 +4,37 @@ import type { ZellijClient } from "../zellij/client.js";
4
4
  import type { ZellijPane } from "../zellij/panes.js";
5
5
  import type { Clock } from "./types.js";
6
6
  export type Submitted = boolean | "queued" | "unverified" | "not-delivered";
7
+ /**
8
+ * Where a message came from: the sending machine, the sender's Zellij session
9
+ * and pane, and whether this host received the op over `serve`. Every part is
10
+ * optional so an older caller (or a host with no Zellij env) still works.
11
+ */
12
+ export type SenderOrigin = {
13
+ /** Sending machine. */
14
+ host: string | null;
15
+ /** Sender's Zellij session. */
16
+ session: string | null;
17
+ /** Sender's pane id, e.g. "terminal_3". */
18
+ pane: string | null;
19
+ /** "serve" only when this host received the op over serve. */
20
+ via: "serve" | null;
21
+ };
22
+ /**
23
+ * Clean one origin part for the `[zswarm from=…]` header: non-strings and
24
+ * blanks become null, header-breaking characters become spaces, runs of
25
+ * whitespace collapse, and the result is capped at 64 chars.
26
+ */
27
+ export declare function sanitizeOriginPart(raw: unknown): string | null;
28
+ /** The origin of the caller's own process, read from its environment. */
29
+ export declare function resolveSenderOrigin(env: NodeJS.ProcessEnv): SenderOrigin;
30
+ /**
31
+ * The origin carried by a serve request. Only the serve worker trusts it: a
32
+ * local caller cannot claim another pane. Missing/garbage input (an older
33
+ * client) keeps every part null but still records that serve carried it.
34
+ */
35
+ export declare function originFromServeRequest(raw: unknown): SenderOrigin;
36
+ /** " @ session/pane on host via serve" (or the parts that are known). */
37
+ export declare function originSuffix(origin: SenderOrigin | null | undefined): string;
7
38
  export type DeliveryResult = {
8
39
  to: string;
9
40
  ok: boolean;
@@ -143,4 +174,6 @@ export declare function deliverTo(client: ZellijClient, state: StateStore, args:
143
174
  firstCallMs?: number;
144
175
  /** The observation timeout the scaled windows are capped by. */
145
176
  timeoutMs?: number;
177
+ /** Sender's environment; direct callers (restart, relay) fall back to it. */
178
+ env?: NodeJS.ProcessEnv;
146
179
  }): Promise<DeliveryResult>;
@@ -1,5 +1,80 @@
1
+ import { hostname } from "node:os";
1
2
  import { busyMarker, queuedPrompt, resolveHarness } from "../harness.js";
3
+ import { resolveSelfPaneId, resolveSelfSession } from "../zellij/session.js";
2
4
  import { isTrue, numberArg, optionalString } from "./util.js";
5
+ /** Cap for one origin part; keeps a spoofed value from wrapping the header. */
6
+ const ORIGIN_PART_MAX = 64;
7
+ /**
8
+ * Clean one origin part for the `[zswarm from=…]` header: non-strings and
9
+ * blanks become null, header-breaking characters become spaces, runs of
10
+ * whitespace collapse, and the result is capped at 64 chars.
11
+ */
12
+ export function sanitizeOriginPart(raw) {
13
+ if (typeof raw !== "string")
14
+ return null;
15
+ const cleaned = raw.replace(/[\r\n[\]]+/g, " ").replace(/\s+/g, " ").trim();
16
+ if (!cleaned)
17
+ return null;
18
+ return cleaned.length <= ORIGIN_PART_MAX
19
+ ? cleaned
20
+ : cleaned.slice(0, ORIGIN_PART_MAX).trimEnd();
21
+ }
22
+ /** The origin of the caller's own process, read from its environment. */
23
+ export function resolveSenderOrigin(env) {
24
+ const configured = env.ZSWARM_HOST;
25
+ const host = configured === undefined
26
+ ? sanitizeOriginPart(hostname().split(".")[0])
27
+ : configured === "-"
28
+ ? null
29
+ : sanitizeOriginPart(configured);
30
+ return {
31
+ host,
32
+ session: sanitizeOriginPart(resolveSelfSession(env)),
33
+ pane: sanitizeOriginPart(resolveSelfPaneId(env)),
34
+ via: null,
35
+ };
36
+ }
37
+ /**
38
+ * The origin carried by a serve request. Only the serve worker trusts it: a
39
+ * local caller cannot claim another pane. Missing/garbage input (an older
40
+ * client) keeps every part null but still records that serve carried it.
41
+ */
42
+ export function originFromServeRequest(raw) {
43
+ const record = raw !== null && typeof raw === "object" && !Array.isArray(raw)
44
+ ? raw
45
+ : {};
46
+ return {
47
+ host: sanitizeOriginPart(record.host),
48
+ session: sanitizeOriginPart(record.session),
49
+ pane: sanitizeOriginPart(record.pane),
50
+ via: "serve",
51
+ };
52
+ }
53
+ /** " @ session/pane on host via serve" (or the parts that are known). */
54
+ export function originSuffix(origin) {
55
+ if (!origin)
56
+ return "";
57
+ const { session, pane, host, via } = origin;
58
+ const loc = session && pane ? `${session}/${pane}` : (pane ?? session ?? null);
59
+ return ((loc ? " @ " + loc : "") +
60
+ (host ? " on " + host : "") +
61
+ (via === "serve" ? " via serve" : ""));
62
+ }
63
+ /**
64
+ * An args-supplied origin, cleaned again (a direct caller may skip dispatch),
65
+ * or null when it is absent/not an object.
66
+ */
67
+ function originArg(raw) {
68
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
69
+ return null;
70
+ const record = raw;
71
+ return {
72
+ host: sanitizeOriginPart(record.host),
73
+ session: sanitizeOriginPart(record.session),
74
+ pane: sanitizeOriginPart(record.pane),
75
+ via: record.via === "serve" ? "serve" : null,
76
+ };
77
+ }
3
78
  /** Fixed send windows before adaptive scaling; explicit flags always win. */
4
79
  export const SEND_BUDGETS = {
5
80
  /** Pause before checking the paste landed. */
@@ -97,7 +172,9 @@ export function attachKnownSender(args, env) {
97
172
  return { ...args, from: sender.label };
98
173
  }
99
174
  export function bodyText(client, args, body) {
100
- return isTrue(args.raw) ? body : client.formatPeerMessage(senderLabel(args), body);
175
+ return isTrue(args.raw)
176
+ ? body
177
+ : client.formatPeerMessage(senderLabel(args), body, originArg(args.origin));
101
178
  }
102
179
  /**
103
180
  * Caller `submit=` always wins. With none given, the pane's harness picks
@@ -292,6 +369,10 @@ export async function submitAfterEnter(client, input) {
292
369
  export async function deliverTo(client, state, args, input) {
293
370
  const { session, pane, body, op, at, clock, firstCallMs = 0, timeoutMs = 30_000 } = input;
294
371
  const from = senderLabel(args);
372
+ // Dispatch stamps args.origin for the header; a direct caller (restart
373
+ // handoff, relay) has none and is described by its own environment.
374
+ const origin = originArg(args.origin) ?? resolveSenderOrigin(input.env ?? process.env);
375
+ const headerArgs = { ...args, origin };
295
376
  const override = optionalString(args.harness);
296
377
  const mode = resolveSubmitMode(args, pane);
297
378
  const profile = resolveHarness(pane, override);
@@ -313,7 +394,7 @@ export async function deliverTo(client, state, args, input) {
313
394
  const paste = () => client.injectPane({
314
395
  session,
315
396
  paneId: pane.id,
316
- text: bodyText(client, args, body),
397
+ text: bodyText(client, headerArgs, body),
317
398
  });
318
399
  let delivered = await paste();
319
400
  let submitted = await verifySubmit(client, {
@@ -371,6 +452,10 @@ export async function deliverTo(client, state, args, input) {
371
452
  session,
372
453
  to: delivered.paneId,
373
454
  from,
455
+ fromHost: origin.host,
456
+ fromSession: origin.session,
457
+ fromPane: origin.pane,
458
+ via: origin.via,
374
459
  bytes: body.length,
375
460
  ok: false,
376
461
  detail: "submitted=not-delivered",
@@ -392,6 +477,10 @@ export async function deliverTo(client, state, args, input) {
392
477
  session,
393
478
  to: delivered.paneId,
394
479
  from,
480
+ fromHost: origin.host,
481
+ fromSession: origin.session,
482
+ fromPane: origin.pane,
483
+ via: origin.via,
395
484
  bytes: body.length,
396
485
  ok: true,
397
486
  detail: `submitted=${submitted}`,
@@ -414,6 +503,10 @@ export async function deliverTo(client, state, args, input) {
414
503
  session,
415
504
  to: pane.id,
416
505
  from,
506
+ fromHost: origin.host,
507
+ fromSession: origin.session,
508
+ fromPane: origin.pane,
509
+ via: origin.via,
417
510
  bytes: body.length,
418
511
  ok: false,
419
512
  detail: `${code}: ${message}`,
@@ -12,7 +12,7 @@ import { broadcast } from "./broadcast.js";
12
12
  import { busChanged, busOp, busScreens, busSnapshot, busWait, planBus } from "./bus.js";
13
13
  import { dumpLayoutOp, focusTarget, listTabsOp, renameTarget, stackTargets, } from "./panes.js";
14
14
  import { peerCheckpoint, peerDiff } from "./review.js";
15
- import { attachKnownSender, deliverTo, scaleBudgets, withSenderLabel, selfPaneTitle } from "./delivery.js";
15
+ import { attachKnownSender, deliverTo, originFromServeRequest, resolveSenderOrigin, scaleBudgets, withSenderLabel, selfPaneTitle, } from "./delivery.js";
16
16
  import { assertNotPlugin, assertNotSelf, assertExpectedScreen, assertWritable, } from "./guards.js";
17
17
  import { readDeliveryLog } from "./log.js";
18
18
  import { awaitSignal, listSignals, postSignal } from "./signals.js";
@@ -754,6 +754,13 @@ async function pruneExitedSessions(client, listed, args) {
754
754
  export async function dispatchZswarm(args, injected, deps = {}) {
755
755
  try {
756
756
  const env = resolveInvocationEnv(args, deps.env ?? process.env);
757
+ // A local caller can never claim another pane or `via serve`: its origin is
758
+ // always read from the environment it runs in. Only the serve worker trusts
759
+ // the request's origin, sanitized, and stamps it `via serve`.
760
+ args =
761
+ deps.viaServe === true
762
+ ? { ...args, origin: originFromServeRequest(args.origin) }
763
+ : { ...args, origin: resolveSenderOrigin(env) };
757
764
  const context = invocationContext(args, env, injected?.transport);
758
765
  const result = await dispatchOperation(args, injected, deps, context, env);
759
766
  if (context.transport === "serve" && result.context) {
@@ -25,6 +25,12 @@ export type DispatchDeps = {
25
25
  state?: StateStore;
26
26
  policy?: Policy;
27
27
  env?: NodeJS.ProcessEnv;
28
+ /**
29
+ * True when this dispatch is the local worker behind `zswarm serve`: the
30
+ * request's own origin is trusted and stamped `via serve`, never replaced by
31
+ * the worker host's environment.
32
+ */
33
+ viaServe?: boolean;
28
34
  /** MCP cancellation; aborted waits stop instead of running to timeout. */
29
35
  signal?: AbortSignal;
30
36
  /** Process-owned ssh:// LocalForward manager. CLI disposes; MCP reuses. */
package/dist/schema.js CHANGED
@@ -281,7 +281,7 @@ export const PARAMS = [
281
281
  type: "string",
282
282
  flags: ["--from", "-f"],
283
283
  ops: ["send", "broadcast"],
284
- description: "send: sender label in the [zswarm from=…] prefix (default: ZSWARM_FROM, else the sending pane's title, else swarm)",
284
+ description: "send: sender label in the [zswarm from=…] prefix (default: ZSWARM_FROM, else the sending pane's title, else swarm). zswarm appends the sender's session/pane, host (ZSWARM_HOST) and `via serve`",
285
285
  },
286
286
  {
287
287
  name: "raw",
@@ -648,7 +648,7 @@ export function cliUsage() {
648
648
  const value = param.type === "boolean" ? "" : param.type === "number" ? " N" : " VALUE";
649
649
  lines.push(` ${param.flags.join(", ").padEnd(28)}${value.trim().padEnd(6)}${param.description}`);
650
650
  }
651
- 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", "");
651
+ 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, ZSWARM_HOST, 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", "");
652
652
  return lines.join("\n");
653
653
  }
654
654
  /**
package/dist/state.d.ts CHANGED
@@ -9,6 +9,14 @@ export type LogEntry = {
9
9
  session?: string | null;
10
10
  to?: string | null;
11
11
  from?: string | null;
12
+ /** Sender's machine when the send was logged. */
13
+ fromHost?: string | null;
14
+ /** Sender's Zellij session. */
15
+ fromSession?: string | null;
16
+ /** Sender's pane id, e.g. "terminal_3". */
17
+ fromPane?: string | null;
18
+ /** "serve" when this host received the op over serve. */
19
+ via?: "serve" | null;
12
20
  bytes?: number;
13
21
  ok: boolean;
14
22
  detail?: string | null;
@@ -2,6 +2,7 @@ import { ListingCache } from "./cache.js";
2
2
  import { buildClosePaneArgs, buildDumpArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, type WaitRequest, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PipeInput } from "./args.js";
3
3
  import { resolveTab, type ZellijTab } from "./tabs.js";
4
4
  import { type IpcDiscoveryState } from "../exec.js";
5
+ import { type SenderOrigin } from "../ops/delivery.js";
5
6
  import { type ZellijExecFn } from "./binary.js";
6
7
  import { normalizePaneId, resolvePane, type ZellijPane } from "./panes.js";
7
8
  import { type ZellijSession, type ZellijSessionResolve } from "./session.js";
@@ -183,7 +184,7 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
183
184
  }>;
184
185
  resolveTab: typeof resolveTab;
185
186
  normalizePaneId: typeof normalizePaneId;
186
- formatPeerMessage: (from: string, body: string) => string;
187
+ formatPeerMessage: (from: string, body: string, origin?: SenderOrigin | null) => string;
187
188
  buildListPanesArgs: typeof buildListPanesArgs;
188
189
  buildPasteArgs: typeof buildPasteArgs;
189
190
  buildSendEnterArgs: typeof buildSendEnterArgs;
@@ -4,6 +4,7 @@ import { buildClosePaneArgs, buildDumpArgs, buildDumpLayoutArgs, buildFocusPaneA
4
4
  import { DEFAULT_BUS_TIMEOUT_MS, parseBusReply, parseChangedReply, parseScrollbackReply, parseWaitReply, } from "./bus.js";
5
5
  import { parseTabList, resolveTab } from "./tabs.js";
6
6
  import { createSshExec } from "../exec.js";
7
+ import { originSuffix } from "../ops/delivery.js";
7
8
  import { DEFAULT_TIMEOUT_MS, NOT_FOUND_EXIT, defaultExec, ensureZellijProbes, identityCacheKey, resolveSshTarget, resolveZellijBinary, sanitizeZellijEnv, zellijExecDetails, zellijMissingError, } from "./binary.js";
8
9
  import { normalizePaneId, parsePaneList, resolvePane, } from "./panes.js";
9
10
  import { isZellijNoSessionsOutput, parseSessionList, resolveSelfPaneId, resolveSelfSession, sessionFromEnv, sessionFromList, } from "./session.js";
@@ -363,9 +364,9 @@ export function createZellijClient(options = {}) {
363
364
  };
364
365
  }
365
366
  /** Visible prefix so peer CLIs can tell zSwarm injects from human prompts. */
366
- function formatPeerMessage(from, body) {
367
+ function formatPeerMessage(from, body, origin) {
367
368
  const sender = from.trim() || "swarm";
368
- return `[zswarm from=${sender}]\n${body}`;
369
+ return `[zswarm from=${sender}${originSuffix(origin)}]\n${body}`;
369
370
  }
370
371
  return {
371
372
  zellijPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zswarm/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "zSwarm Zellij client and shared ops dispatch",
6
6
  "exports": {