@zswarm/core 0.1.5 → 0.1.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.
@@ -1,6 +1,7 @@
1
1
  export type PaneDirection = "right" | "left" | "up" | "down";
2
2
  export type NewPaneInput = {
3
3
  session: string;
4
+ timeoutMs?: number;
4
5
  command?: string[];
5
6
  cwd?: string | null;
6
7
  name?: string | null;
@@ -13,6 +14,7 @@ export type NewPaneInput = {
13
14
  };
14
15
  export type NewTabInput = {
15
16
  session: string;
17
+ timeoutMs?: number;
16
18
  command?: string[];
17
19
  cwd?: string | null;
18
20
  name?: string | null;
@@ -6,6 +6,53 @@ export declare const DEFAULT_TIMEOUT_MS = 15000;
6
6
  export { NOT_FOUND_EXIT };
7
7
  /** Expand a leading `~/` or `~\` using USERPROFILE/HOME. */
8
8
  export declare function expandHomePath(input: string, env?: NodeJS.ProcessEnv): string;
9
+ /**
10
+ * True when a path (or basename) is the zswarm CLI rather than Zellij.
11
+ * Setting ZSWARM_BIN to zswarm makes `list-sessions --short` fail with
12
+ * "unknown arg: --short" because zswarm re-parses argv as its own CLI.
13
+ */
14
+ export declare function looksLikeZswarmBinary(path: string): boolean;
15
+ export declare function assertZellijBinaryPath(path: string): void;
16
+ /**
17
+ * ZSWARM_SSH is a destination, not a full ssh argv.
18
+ * Accept `user@host` or an SSH config alias; put flags in ZSWARM_SSH_OPTS.
19
+ */
20
+ export declare function validateSshDestination(raw: string): string;
21
+ export declare function validateSshMode(raw: string): "ssh" | "interactive";
22
+ /** True when `--version` output is positively Zellij. */
23
+ export declare function isZellijVersionOutput(stdout: string, stderr?: string): boolean;
24
+ /**
25
+ * Cache key covering the resolved binary and SSH routing that can change the
26
+ * actual remote executable (opts, mode, remote bin).
27
+ */
28
+ export declare function identityCacheKey(zellijPath: string, ssh?: {
29
+ host: string;
30
+ options: string[];
31
+ mode?: string;
32
+ remoteBin?: string;
33
+ /** SSH client binary (`SshTarget.ssh`). */
34
+ ssh?: string;
35
+ sshBin?: string;
36
+ remoteShell?: string;
37
+ } | null): string;
38
+ /**
39
+ * Confirm the resolved binary is Zellij. Only verified identities are cached.
40
+ * Transport timeouts leave the cache empty so a later call can retry.
41
+ * Returns false when verification is unresolved; only true is cached.
42
+ * In-flight probes are never shared — each caller supplies its own timeout.
43
+ */
44
+ export declare function ensureZellijIdentity(exec: ExecFn, zellijPath: string, timeoutMs?: number, cacheKey?: string, signal?: AbortSignal): Promise<boolean>;
45
+ export declare function ensureZellijCapabilities(exec: ExecFn, zellijPath: string, timeoutMs?: number, cacheKey?: string, signal?: AbortSignal): Promise<boolean>;
46
+ /**
47
+ * Identity and capability are independent reads. Run them together under one
48
+ * remaining budget; do not share in-flight work with another caller.
49
+ */
50
+ export declare function ensureZellijProbes(exec: ExecFn, zellijPath: string, timeoutMs?: number, cacheKey?: string, signal?: AbortSignal): Promise<{
51
+ identity: boolean;
52
+ capabilities: boolean;
53
+ }>;
54
+ /** Test helper: drop cached identity/capability probes. */
55
+ export declare function resetZellijIdentityCache(): void;
9
56
  export declare function resolveZellijBinary(env?: NodeJS.ProcessEnv): string;
10
57
  export declare function sanitizeZellijEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
11
58
  /**
@@ -1,6 +1,7 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { delimiter, join } from "node:path";
3
+ import { basename, delimiter, join } from "node:path";
4
+ import { ZellijError } from "../errors.js";
4
5
  import { createExec, createSshExec, NOT_FOUND_EXIT, } from "../exec.js";
5
6
  import { applyIpcTmpEnv } from "./ipc.js";
6
7
  export { createSshExec };
@@ -16,15 +17,206 @@ export function expandHomePath(input, env = process.env) {
16
17
  return trimmed;
17
18
  return join(home, trimmed.slice(2));
18
19
  }
20
+ /**
21
+ * True when a path (or basename) is the zswarm CLI rather than Zellij.
22
+ * Setting ZSWARM_BIN to zswarm makes `list-sessions --short` fail with
23
+ * "unknown arg: --short" because zswarm re-parses argv as its own CLI.
24
+ */
25
+ export function looksLikeZswarmBinary(path) {
26
+ const trimmed = path.trim();
27
+ const base = basename(trimmed).toLowerCase();
28
+ // Windows paths on a Linux host still use backslashes in env values.
29
+ const winBase = trimmed.replace(/\\/g, "/").split("/").pop()?.toLowerCase() ?? base;
30
+ const name = winBase || base;
31
+ if (/^zswarm(\.(exe|cmd|js|mjs|cjs))?$/.test(name))
32
+ return true;
33
+ // Native binaries are never the Node CLI wrapper.
34
+ if (/\.(exe|dll|so|dylib)$/i.test(name))
35
+ return false;
36
+ if (name === "zellij")
37
+ return false;
38
+ // Shebang wrappers may keep another basename; sniff a short readable prefix.
39
+ try {
40
+ if (!existsSync(trimmed))
41
+ return false;
42
+ const head = readFileSync(trimmed, { encoding: "utf8" }).slice(0, 400);
43
+ if (head.includes("\0"))
44
+ return false;
45
+ return /@zswarm\/cli/.test(head) || /usage:\s*zswarm/i.test(head);
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ export function assertZellijBinaryPath(path) {
52
+ if (!looksLikeZswarmBinary(path))
53
+ return;
54
+ throw new ZellijError("zellij_wrong_bin", `ZSWARM_BIN/ZSWARM_PATH points at zswarm (${path}), not Zellij. Set it to the zellij binary (e.g. ~/.local/bin/zellij)`);
55
+ }
56
+ /**
57
+ * ZSWARM_SSH is a destination, not a full ssh argv.
58
+ * Accept `user@host` or an SSH config alias; put flags in ZSWARM_SSH_OPTS.
59
+ */
60
+ export function validateSshDestination(raw) {
61
+ const host = raw.trim();
62
+ if (!host) {
63
+ throw new ZellijError("bad_ssh", "ZSWARM_SSH is empty");
64
+ }
65
+ if (/^ssh(\s|$)/i.test(host)) {
66
+ throw new ZellijError("bad_ssh", `ZSWARM_SSH should be user@host or an SSH alias, not a full ssh command. Put flags in ZSWARM_SSH_OPTS. Got: ${JSON.stringify(host)}`);
67
+ }
68
+ // Leading dashes are SSH options (`-V`, `-F…`, `-o…`), not destinations.
69
+ if (host.startsWith("-")) {
70
+ throw new ZellijError("bad_ssh", `ZSWARM_SSH looks like an SSH option (${JSON.stringify(host)}); put flags in ZSWARM_SSH_OPTS and set ZSWARM_SSH to user@host or an alias`);
71
+ }
72
+ if (/\s/.test(host)) {
73
+ throw new ZellijError("bad_ssh", `ZSWARM_SSH must be a single destination (user@host or alias); put options in ZSWARM_SSH_OPTS. Got: ${JSON.stringify(host)}`);
74
+ }
75
+ if (/[;|&$<>()]/.test(host)) {
76
+ throw new ZellijError("bad_ssh", `ZSWARM_SSH contains shell metacharacters; expected user@host or an SSH alias. Got: ${JSON.stringify(host)}`);
77
+ }
78
+ return host;
79
+ }
80
+ export function validateSshMode(raw) {
81
+ const mode = raw.trim().toLowerCase();
82
+ if (!mode || mode === "ssh")
83
+ return "ssh";
84
+ if (mode === "interactive")
85
+ return "interactive";
86
+ throw new ZellijError("bad_ssh_mode", `ZSWARM_SSH_MODE must be "interactive" or "ssh" (or unset); got ${JSON.stringify(raw.trim())}`);
87
+ }
88
+ /** Cache of verified `zellij --version` probes keyed by target identity. */
89
+ const identityCache = new Set();
90
+ /** True when `--version` output is positively Zellij. */
91
+ export function isZellijVersionOutput(stdout, stderr = "") {
92
+ const text = `${stdout}\n${stderr}`;
93
+ return /\bzellij\s+\d+\.\d+/i.test(text) || /^\s*zellij\b/im.test(stdout);
94
+ }
95
+ /**
96
+ * Cache key covering the resolved binary and SSH routing that can change the
97
+ * actual remote executable (opts, mode, remote bin).
98
+ */
99
+ export function identityCacheKey(zellijPath, ssh) {
100
+ if (!ssh)
101
+ return JSON.stringify([zellijPath]);
102
+ return JSON.stringify([
103
+ zellijPath,
104
+ ssh.host,
105
+ ssh.remoteBin ?? "",
106
+ ssh.mode ?? "ssh",
107
+ ssh.options,
108
+ ssh.sshBin ?? ssh.ssh ?? "",
109
+ ssh.remoteShell ?? "",
110
+ ]);
111
+ }
112
+ /**
113
+ * Confirm the resolved binary is Zellij. Only verified identities are cached.
114
+ * Transport timeouts leave the cache empty so a later call can retry.
115
+ * Returns false when verification is unresolved; only true is cached.
116
+ * In-flight probes are never shared — each caller supplies its own timeout.
117
+ */
118
+ export async function ensureZellijIdentity(exec, zellijPath, timeoutMs = 3_000, cacheKey = zellijPath, signal) {
119
+ if (identityCache.has(cacheKey))
120
+ return true;
121
+ if (signal?.aborted)
122
+ return false;
123
+ assertZellijBinaryPath(zellijPath);
124
+ const result = await exec(["--version"], {
125
+ timeoutMs: Math.min(timeoutMs, 5_000),
126
+ signal,
127
+ });
128
+ const text = `${result.stdout}\n${result.stderr}`;
129
+ if (/usage:\s*zswarm/i.test(text) || /unknown arg:/i.test(text)) {
130
+ throw new ZellijError("zellij_wrong_bin", `resolved binary is zswarm, not Zellij (${zellijPath}). Set ZSWARM_BIN to the zellij executable`);
131
+ }
132
+ if (result.code === NOT_FOUND_EXIT) {
133
+ throw new ZellijError("zellij_missing", `zellij binary not found (${zellijPath}); install Zellij ≥ 0.42, add it to PATH, or set ZSWARM_BIN / ZSWARM_PATH`);
134
+ }
135
+ if (result.code === 0) {
136
+ if (signal?.aborted)
137
+ return false;
138
+ if (isZellijVersionOutput(result.stdout, result.stderr)) {
139
+ identityCache.add(cacheKey);
140
+ return true;
141
+ }
142
+ throw new ZellijError("zellij_wrong_bin", `resolved binary is not Zellij (${zellijPath}); --version returned: ${result.stdout.trim() || result.stderr.trim() || "empty"}`);
143
+ }
144
+ // Timeout / transport failure: leave unresolved so a later call retries.
145
+ return false;
146
+ }
147
+ /**
148
+ * Probe that the target understands the session-list flags we rely on.
149
+ * Cached with the same key as identity once verified.
150
+ * Returns false when the transport cannot verify support.
151
+ */
152
+ const capabilityCache = new Set();
153
+ export async function ensureZellijCapabilities(exec, zellijPath, timeoutMs = 3_000, cacheKey = zellijPath, signal) {
154
+ if (capabilityCache.has(cacheKey))
155
+ return true;
156
+ if (signal?.aborted)
157
+ return false;
158
+ const result = await exec(["list-sessions", "--help"], {
159
+ timeoutMs: Math.min(timeoutMs, 5_000),
160
+ signal,
161
+ });
162
+ const text = `${result.stdout}\n${result.stderr}`;
163
+ if (/usage:\s*zswarm/i.test(text)) {
164
+ throw new ZellijError("zellij_wrong_bin", `resolved binary is zswarm, not Zellij (${zellijPath})`);
165
+ }
166
+ if (result.code === NOT_FOUND_EXIT) {
167
+ throw new ZellijError("zellij_missing", `zellij binary not found (${zellijPath})`);
168
+ }
169
+ // Require the flags this client always passes.
170
+ if (result.code === 0 &&
171
+ /--no-formatting/i.test(text) &&
172
+ /list-sessions/i.test(text)) {
173
+ if (signal?.aborted)
174
+ return false;
175
+ capabilityCache.add(cacheKey);
176
+ return true;
177
+ }
178
+ if (result.code === 0) {
179
+ throw new ZellijError("zellij_incompatible", `Zellij at ${zellijPath} does not advertise list-sessions --no-formatting; upgrade Zellij (≥ 0.42) or zswarm`);
180
+ }
181
+ // Soft: leave unresolved on transport failure.
182
+ return false;
183
+ }
184
+ /**
185
+ * Identity and capability are independent reads. Run them together under one
186
+ * remaining budget; do not share in-flight work with another caller.
187
+ */
188
+ export async function ensureZellijProbes(exec, zellijPath, timeoutMs = 3_000, cacheKey = zellijPath, signal) {
189
+ if (signal?.aborted)
190
+ return { identity: false, capabilities: false };
191
+ const probeTimeout = Math.min(timeoutMs, 5_000);
192
+ // Settle both so a wrong-bin throw does not leave the other SSH probe running.
193
+ const settled = await Promise.allSettled([
194
+ ensureZellijIdentity(exec, zellijPath, probeTimeout, cacheKey, signal),
195
+ ensureZellijCapabilities(exec, zellijPath, probeTimeout, cacheKey, signal),
196
+ ]);
197
+ if (settled[0].status === "rejected")
198
+ throw settled[0].reason;
199
+ if (settled[1].status === "rejected")
200
+ throw settled[1].reason;
201
+ return { identity: settled[0].value, capabilities: settled[1].value };
202
+ }
203
+ /** Test helper: drop cached identity/capability probes. */
204
+ export function resetZellijIdentityCache() {
205
+ identityCache.clear();
206
+ capabilityCache.clear();
207
+ }
19
208
  export function resolveZellijBinary(env = process.env) {
20
209
  const fromEnv = expandHomePath((env.ZSWARM_BIN ?? env.ZSWARM_PATH ?? env.ZELLIJ_BIN ?? "")
21
210
  .trim()
22
211
  .replace(/^['"]|['"]$/g, ""), env);
23
212
  if (fromEnv && existsSync(fromEnv)) {
213
+ assertZellijBinaryPath(fromEnv);
24
214
  if (/\.cmd$/i.test(fromEnv)) {
25
215
  const exe = fromEnv.replace(/\.cmd$/i, ".exe");
26
- if (existsSync(exe))
216
+ if (existsSync(exe)) {
217
+ assertZellijBinaryPath(exe);
27
218
  return exe;
219
+ }
28
220
  const wingetExe = join(env.LOCALAPPDATA ||
29
221
  join(env.USERPROFILE || env.HOME || homedir(), "AppData", "Local"), "Zellij", "zellij.exe");
30
222
  if (existsSync(wingetExe))
@@ -112,17 +304,18 @@ export function parseSshOpts(raw) {
112
304
  return out;
113
305
  }
114
306
  export function resolveSshTarget(env = process.env) {
115
- const host = env.ZSWARM_SSH?.trim();
116
- if (!host)
307
+ const raw = env.ZSWARM_SSH?.trim();
308
+ if (!raw)
117
309
  return null;
310
+ const host = validateSshDestination(raw);
118
311
  const options = parseSshOpts(env.ZSWARM_SSH_OPTS ?? "");
119
312
  if (!options.some((o) => o.startsWith("BatchMode"))) {
120
313
  options.unshift("-o", "BatchMode=yes");
121
314
  }
122
315
  const shellRaw = (env.ZSWARM_REMOTE_SHELL ?? "").trim().toLowerCase();
123
- const modeRaw = (env.ZSWARM_SSH_MODE ?? "").trim().toLowerCase();
316
+ const mode = validateSshMode(env.ZSWARM_SSH_MODE ?? "");
124
317
  const tmpRaw = env.ZSWARM_TMP?.trim();
125
- const interactive = modeRaw === "interactive";
318
+ const interactive = mode === "interactive";
126
319
  return {
127
320
  ssh: env.ZSWARM_SSH_BIN?.trim() || "ssh",
128
321
  host,
@@ -130,7 +323,7 @@ export function resolveSshTarget(env = process.env) {
130
323
  options,
131
324
  // Interactive tasks do not inherit the desktop TEMP; discover it unless set.
132
325
  tmp: tmpRaw || (interactive ? "auto" : undefined),
133
- mode: interactive ? "interactive" : "ssh",
326
+ mode,
134
327
  remoteShell: shellRaw === "cmd" || shellRaw === "sh" ? shellRaw : undefined,
135
328
  };
136
329
  }
@@ -23,6 +23,8 @@ export type BusSnapshot = {
23
23
  paneUpdates: number;
24
24
  tabUpdates: number;
25
25
  tabs: string[];
26
+ /** Stable IDs by tab position; absent on older running plugin instances. */
27
+ tabIds?: (number | null)[];
26
28
  panes: BusPane[];
27
29
  };
28
30
  export type BusMarker = {
@@ -74,7 +74,7 @@ export function parseChangedReply(stdout) {
74
74
  if (!row || typeof row !== "object")
75
75
  continue;
76
76
  const p = row;
77
- if (typeof p.id !== "string")
77
+ if (typeof p.id !== "string" || typeof p.changed !== "boolean" || typeof p.first !== "boolean" || !Array.isArray(p.viewport))
78
78
  continue;
79
79
  panes.push({
80
80
  id: p.id,
@@ -186,6 +186,7 @@ function toSnapshot(value) {
186
186
  tabs: Array.isArray(value.tabs)
187
187
  ? value.tabs.filter((t) => typeof t === "string")
188
188
  : [],
189
+ ...(Array.isArray(value.tabIds) ? { tabIds: value.tabIds.map((id) => typeof id === "number" && Number.isInteger(id) && id >= 0 ? id : null) } : {}),
189
190
  panes,
190
191
  };
191
192
  }
@@ -290,7 +291,7 @@ export function busToPanes(snapshot) {
290
291
  command: pane.command,
291
292
  cwd: null,
292
293
  tabName: snapshot.tabs[pane.tab] ?? null,
293
- tabId: pane.tab,
294
+ tabId: snapshot.tabIds?.[pane.tab] ?? null,
294
295
  focused: pane.focused,
295
296
  exited: pane.exited,
296
297
  floating: false,
@@ -0,0 +1,24 @@
1
+ /** Short-lived, completed observations only. Never shares a caller's promise or budget. */
2
+ export declare class ListingCache {
3
+ private readonly limit;
4
+ private entries;
5
+ private scopes;
6
+ constructor(limit?: number);
7
+ begin(scope: string): {
8
+ state: {
9
+ version: number;
10
+ revision?: string;
11
+ };
12
+ version: number;
13
+ };
14
+ get<T>(key: string, now: number): T | undefined;
15
+ put<T>(key: string, scope: string, ticket: ReturnType<ListingCache["begin"]>, value: T, ttl: number, now: number): void;
16
+ invalidate(scope: string): void;
17
+ observe(scope: string, revision: string): boolean;
18
+ clear(): void;
19
+ }
20
+ export declare const sharedListings: ListingCache;
21
+ export declare function executorIdentity(exec?: object): number | null;
22
+ /** Environment fields that select local sockets or an SSH identity/config. */
23
+ export declare function routingEnvironment(env: NodeJS.ProcessEnv): Record<string, string | null>;
24
+ export declare function listingTtl(env: NodeJS.ProcessEnv): number;
@@ -0,0 +1,90 @@
1
+ /** Short-lived, completed observations only. Never shares a caller's promise or budget. */
2
+ export class ListingCache {
3
+ limit;
4
+ entries = new Map();
5
+ scopes = new Map();
6
+ constructor(limit = 512) {
7
+ this.limit = limit;
8
+ }
9
+ begin(scope) {
10
+ let state = this.scopes.get(scope);
11
+ if (!state) {
12
+ state = { version: 0 };
13
+ this.scopes.set(scope, state);
14
+ if (this.scopes.size > this.limit) {
15
+ const oldest = this.scopes.keys().next().value;
16
+ this.scopes.delete(oldest);
17
+ for (const [key, entry] of this.entries)
18
+ if (entry.scope === oldest)
19
+ this.entries.delete(key);
20
+ }
21
+ }
22
+ return { state, version: state.version };
23
+ }
24
+ get(key, now) {
25
+ const entry = this.entries.get(key);
26
+ if (!entry)
27
+ return undefined;
28
+ if (now >= entry.expiresAt) {
29
+ this.entries.delete(key);
30
+ return undefined;
31
+ }
32
+ return structuredClone(entry.value);
33
+ }
34
+ put(key, scope, ticket, value, ttl, now) {
35
+ // A mutation/event during this read must not resurrect an older manifest.
36
+ if (ttl <= 0 || this.scopes.get(scope) !== ticket.state || ticket.state.version !== ticket.version)
37
+ return;
38
+ this.entries.delete(key);
39
+ this.entries.set(key, { scope, value: structuredClone(value), expiresAt: now + ttl });
40
+ while (this.entries.size > this.limit)
41
+ this.entries.delete(this.entries.keys().next().value);
42
+ }
43
+ invalidate(scope) {
44
+ this.begin(scope).state.version++;
45
+ for (const [key, entry] of this.entries)
46
+ if (entry.scope === scope)
47
+ this.entries.delete(key);
48
+ }
49
+ observe(scope, revision) {
50
+ const { state } = this.begin(scope);
51
+ if (state.revision === revision)
52
+ return false;
53
+ this.invalidate(scope);
54
+ state.revision = revision;
55
+ return true;
56
+ }
57
+ clear() {
58
+ this.entries.clear();
59
+ this.scopes.clear();
60
+ }
61
+ }
62
+ export const sharedListings = new ListingCache();
63
+ const executors = new WeakMap();
64
+ let nextExecutor = 0;
65
+ export function executorIdentity(exec) {
66
+ if (!exec)
67
+ return null;
68
+ let id = executors.get(exec);
69
+ if (id === undefined) {
70
+ id = ++nextExecutor;
71
+ executors.set(exec, id);
72
+ }
73
+ return id;
74
+ }
75
+ /** Environment fields that select local sockets or an SSH identity/config. */
76
+ export function routingEnvironment(env) {
77
+ return Object.fromEntries([
78
+ "ZSWARM_TMP", "ZELLIJ_SOCKET_DIR", "TMPDIR", "TEMP", "TMP", "XDG_RUNTIME_DIR",
79
+ "HOME", "USERPROFILE", "USER", "USERNAME", "LOGNAME", "USERDOMAIN", "SSH_AUTH_SOCK",
80
+ "PATH", "Path", "PATHEXT", "XDG_CONFIG_HOME", "ZELLIJ_CONFIG_DIR",
81
+ "ZELLIJ_SESSION_NAME", "ZSWARM_SESSION",
82
+ ].map((key) => [key, env[key] ?? null]));
83
+ }
84
+ export function listingTtl(env) {
85
+ const value = env.ZSWARM_CACHE_TTL_MS;
86
+ if (value === undefined || value.trim() === "")
87
+ return 500;
88
+ const ttl = Number(value);
89
+ return Number.isFinite(ttl) ? Math.max(0, Math.min(5000, ttl)) : 500;
90
+ }
@@ -1,24 +1,50 @@
1
+ import { ListingCache } from "./cache.js";
1
2
  import { buildClosePaneArgs, buildDumpArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, type WaitRequest, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PipeInput } from "./args.js";
2
3
  import { resolveTab, type ZellijTab } from "./tabs.js";
4
+ import { type IpcDiscoveryState } from "../exec.js";
3
5
  import { type ZellijExecFn } from "./binary.js";
4
6
  import { normalizePaneId, resolvePane, type ZellijPane } from "./panes.js";
5
- import { type ZellijSessionResolve } from "./session.js";
7
+ import { type ZellijSession, type ZellijSessionResolve } from "./session.js";
6
8
  export type ZellijClientOptions = {
7
9
  exec?: ZellijExecFn;
8
10
  zellijPath?: string;
9
11
  timeoutMs?: number;
10
12
  env?: NodeJS.ProcessEnv;
11
13
  signal?: AbortSignal;
14
+ /** Skip the one-time `zellij --version` identity probe (tests). */
15
+ skipIdentityProbe?: boolean;
16
+ /** Completed listing cache shared by long-lived MCP/serve calls; false bypasses reads. */
17
+ cache?: ListingCache | false;
18
+ /** Clock for cache TTLs; process deadlines continue using wall time. */
19
+ now?: () => number;
12
20
  };
13
- /** Thin, stateless wrapper over the `zellij` binary. */
21
+ export type ZellijTransport = {
22
+ kind: "local" | "ssh";
23
+ mode: "local" | "ssh" | "interactive";
24
+ host?: string;
25
+ remoteBin?: string;
26
+ /** Configured or resolved IPC temp (`auto` until discovery succeeds). */
27
+ tmp?: string;
28
+ /** Outcome of `ZSWARM_TMP=auto` discovery when SSH routing is active. */
29
+ ipc?: IpcDiscoveryState;
30
+ };
31
+ /** Zellij wrapper with bounded caches of completed discovery observations. */
14
32
  export declare function createZellijClient(options?: ZellijClientOptions): {
15
33
  zellijPath: string;
16
34
  selfPaneId: string | null;
35
+ contextKey: () => string;
36
+ invalidateListings: (session: string) => void;
37
+ observeManifest: (session: string, revision: string) => void;
38
+ readonly transport: ZellijTransport;
17
39
  /** A `file:` plugin url only names a path on the machine running Zellij. */
18
40
  remote: boolean;
19
- listSessions: () => Promise<string[]>;
20
- resolveSession: (explicit?: string | null) => Promise<ZellijSessionResolve>;
21
- listPanes: (session: string) => Promise<ZellijPane[]>;
41
+ listSessions: (callTimeoutMs?: number, opts?: {
42
+ fresh?: boolean;
43
+ }) => Promise<ZellijSession[]>;
44
+ resolveSession: (explicit?: string | null, callTimeoutMs?: number) => Promise<ZellijSessionResolve>;
45
+ listPanes: (session: string, callTimeoutMs?: number, opts?: {
46
+ fresh?: boolean;
47
+ }) => Promise<ZellijPane[]>;
22
48
  resolvePane: typeof resolvePane;
23
49
  injectPane: (input: {
24
50
  session: string;
@@ -32,6 +58,7 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
32
58
  session: string;
33
59
  paneId: string;
34
60
  full?: boolean;
61
+ timeoutMs?: number;
35
62
  }) => Promise<{
36
63
  paneId: string;
37
64
  session: string;
@@ -75,6 +102,7 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
75
102
  session: string;
76
103
  paneId: string;
77
104
  name: string;
105
+ timeoutMs?: number;
78
106
  }) => Promise<{
79
107
  paneId: string;
80
108
  session: string;
@@ -96,7 +124,9 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
96
124
  paneId: string;
97
125
  session: string;
98
126
  }>;
99
- listTabs: (session: string) => Promise<ZellijTab[]>;
127
+ listTabs: (session: string, callTimeoutMs?: number, opts?: {
128
+ fresh?: boolean;
129
+ }) => Promise<ZellijTab[]>;
100
130
  dumpLayout: (session: string) => Promise<string>;
101
131
  stackPanes: (input: {
102
132
  session: string;
@@ -138,6 +168,7 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
138
168
  url: string;
139
169
  configKey: string;
140
170
  panes: string[];
171
+ timeoutMs?: number;
141
172
  }) => Promise<{
142
173
  code: number;
143
174
  stdout: string;