@delorenj/pjangler 1.5.40 โ†’ 1.5.42

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 (2) hide show
  1. package/dist/index.js +493 -53
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6064,8 +6064,8 @@ var init_capture = __esm({
6064
6064
 
6065
6065
  // src/index.ts
6066
6066
  import { spawnSync as spawnSync18 } from "node:child_process";
6067
- import { existsSync as existsSync26, readdirSync as readdirSync13, readFileSync as readFileSync31, statSync as statSync9 } from "node:fs";
6068
- import { basename as basename11, join as join36, resolve as resolve25 } from "node:path";
6067
+ import { existsSync as existsSync26, readdirSync as readdirSync14, readFileSync as readFileSync32, statSync as statSync9 } from "node:fs";
6068
+ import { basename as basename12, join as join37, resolve as resolve25 } from "node:path";
6069
6069
  import { Command as Command3, CommanderError, Option } from "commander";
6070
6070
 
6071
6071
  // src/recipes/Recipe.ts
@@ -21155,7 +21155,7 @@ var ZCRANK_LOG_CAP = 500;
21155
21155
  var TAB_STATUS_SUFFIX = /[\s๐ŸŸข๐ŸŸก๐Ÿ”ด๐Ÿ””โšซโœณโœปโ—ยทโ”€]+$/u;
21156
21156
  var defaultShell = (command, args) => {
21157
21157
  const result2 = spawnSync17(command, args, { encoding: "utf8", timeout: 1e4 });
21158
- if (result2.error) return { stdout: "", status: 127 };
21158
+ if (result2.error) return { stdout: "", status: result2.error.code === "ETIMEDOUT" ? 124 : 127 };
21159
21159
  return { stdout: result2.stdout ?? "", status: result2.status ?? 1, stderr: result2.stderr ?? "" };
21160
21160
  };
21161
21161
  function tabDisplay(name) {
@@ -21382,48 +21382,43 @@ function liveTabs(deps = {}) {
21382
21382
  return [];
21383
21383
  }
21384
21384
  }
21385
- function livePanes(deps = {}) {
21386
- const shell = deps.shell ?? defaultShell;
21387
- const result2 = shell("env", [
21388
- "-u",
21389
- "ZELLIJ",
21390
- "-u",
21391
- "ZELLIJ_SESSION_NAME",
21392
- "timeout",
21393
- "8s",
21394
- "zellij",
21395
- "-s",
21396
- ZCRANK_SESSION,
21397
- "action",
21398
- "list-panes",
21399
- "--json",
21400
- "--all"
21401
- ]);
21402
- if (result2.status !== 0 || !result2.stdout.trim()) return [];
21385
+ function parseListPanes(stdout) {
21386
+ if (!stdout.trim()) return null;
21387
+ let parsed;
21403
21388
  try {
21404
- const parsed = JSON.parse(result2.stdout);
21405
- if (!Array.isArray(parsed)) return [];
21406
- const panes = [];
21407
- for (const entry of parsed) {
21408
- if (typeof entry !== "object" || entry === null) continue;
21409
- const e = entry;
21410
- const id = Number(e.id);
21411
- if (!Number.isFinite(id)) continue;
21412
- const tabId = Number(e.tab_id);
21413
- const tabPosition = Number(e.tab_position);
21414
- panes.push({
21415
- id,
21416
- tabId: e.tab_id !== void 0 && e.tab_id !== null && Number.isInteger(tabId) ? tabId : void 0,
21417
- tabPosition: e.tab_position !== void 0 && e.tab_position !== null && Number.isInteger(tabPosition) ? tabPosition : void 0,
21418
- tabName: String(e.tab_name ?? ""),
21419
- isPlugin: e.is_plugin === true,
21420
- cwd: typeof e.pane_cwd === "string" && e.pane_cwd.trim() ? e.pane_cwd : void 0
21421
- });
21422
- }
21423
- return panes;
21389
+ parsed = JSON.parse(stdout);
21424
21390
  } catch {
21425
- return [];
21391
+ return null;
21426
21392
  }
21393
+ if (!Array.isArray(parsed)) return null;
21394
+ const panes = [];
21395
+ for (const entry of parsed) {
21396
+ if (typeof entry !== "object" || entry === null) continue;
21397
+ const e = entry;
21398
+ const id = Number(e.id);
21399
+ if (!Number.isFinite(id)) continue;
21400
+ const tabId = Number(e.tab_id);
21401
+ const tabPosition = Number(e.tab_position);
21402
+ panes.push({
21403
+ id,
21404
+ tabId: e.tab_id !== void 0 && e.tab_id !== null && Number.isInteger(tabId) ? tabId : void 0,
21405
+ tabPosition: e.tab_position !== void 0 && e.tab_position !== null && Number.isInteger(tabPosition) ? tabPosition : void 0,
21406
+ tabName: String(e.tab_name ?? ""),
21407
+ isPlugin: e.is_plugin === true,
21408
+ cwd: typeof e.pane_cwd === "string" && e.pane_cwd.trim() ? e.pane_cwd : void 0,
21409
+ title: typeof e.title === "string" ? e.title : null
21410
+ });
21411
+ }
21412
+ return panes;
21413
+ }
21414
+ function runListPanes(session, shell) {
21415
+ return shell("env", ["-u", "ZELLIJ", "-u", "ZELLIJ_SESSION_NAME", "timeout", "8s", "zellij", "-s", session, "action", "list-panes", "--json", "--all"]);
21416
+ }
21417
+ function livePanes(deps = {}) {
21418
+ const shell = deps.shell ?? defaultShell;
21419
+ const result2 = runListPanes(deps.session ?? ZCRANK_SESSION, shell);
21420
+ if (result2.status !== 0) return [];
21421
+ return (parseListPanes(result2.stdout) ?? []).map(({ title: _title, ...pane }) => pane);
21427
21422
  }
21428
21423
  function projectIdentity(dir, deps = {}) {
21429
21424
  const target = (dir ?? "").trim();
@@ -21967,6 +21962,420 @@ function createCrankController(deps) {
21967
21962
  };
21968
21963
  }
21969
21964
 
21965
+ // src/crank/panes.ts
21966
+ import { readFileSync as readFileSync31, readdirSync as readdirSync13, readlinkSync as readlinkSync9 } from "node:fs";
21967
+ import { homedir as homedir16 } from "node:os";
21968
+ import { basename as basename11, join as join36 } from "node:path";
21969
+ var AGENT_CLIS = [
21970
+ "claude",
21971
+ "codex",
21972
+ "copilot",
21973
+ "opencode",
21974
+ "aider",
21975
+ "hermes",
21976
+ "agy",
21977
+ "kimi-code",
21978
+ "kimi-co",
21979
+ "gemini",
21980
+ "openclaw",
21981
+ // Not in Deckard's list: the Kimi binary on the operator box is `~/.kimi-code/bin/kimi` (argv[0]
21982
+ // basename `kimi`), which neither `kimi-code` nor `kimi-co` prefixes. Listed after them so the
21983
+ // reported name stays `kimi-code` where Deckard would say it.
21984
+ "kimi"
21985
+ ];
21986
+ function agentCli(exe, argv) {
21987
+ if (argv.includes("mcp-server") || argv.includes(" mcp ")) return null;
21988
+ const byName = AGENT_CLIS.find((pat) => exe === pat || exe.startsWith(pat));
21989
+ if (byName) return byName;
21990
+ const tokens = argv.split(" ").filter(Boolean);
21991
+ if (INTERPRETER.test(exe)) {
21992
+ const program2 = interpretedProgram(exe, tokens);
21993
+ if (!program2) return null;
21994
+ if (program2.module) {
21995
+ const top = (program2.module.split(".")[0] ?? "").replace(/_/g, "-");
21996
+ return AGENT_CLIS.find((pat) => top === pat || PATH_SUFFIXES.some((suffix) => top === `${pat}-${suffix}`)) ?? null;
21997
+ }
21998
+ return PATH_PASS.find(([, re]) => re.test(program2.script))?.[0] ?? null;
21999
+ }
22000
+ const versioned = /\/([^/]+)\/versions\/[^/]+$/.exec(tokens[0] ?? "");
22001
+ return versioned ? AGENT_CLIS.find((pat) => versioned[1] === pat) ?? null : null;
22002
+ }
22003
+ var INTERPRETER = /^(?:node(?:js)?|bun|deno|tsx|python[\d.]*|ruby|perl|bash|sh|zsh|dash|fish|ksh)$/;
22004
+ var SHELL = /^(?:bash|sh|zsh|dash|fish|ksh)$/;
22005
+ var PYTHON = /^python[\d.]*$/;
22006
+ var VALUE_OPTIONS = [
22007
+ [/^(?:node(?:js)?|bun|deno|tsx)$/, /* @__PURE__ */ new Set(["-r", "--require", "--import", "--loader", "--experimental-loader", "-C", "--conditions", "--inspect-port", "--debug-port", "--input-type", "--title", "--env-file", "--disable-warning", "--watch-path", "--stack-size", "--max-old-space-size", "--redirect-warnings", "--openssl-config", "--icu-data-dir", "--tls-cipher-list", "--secure-heap", "--diagnostic-dir", "--report-dir", "--cpu-prof-dir", "--heap-prof-dir"])],
22008
+ [PYTHON, /* @__PURE__ */ new Set(["-W", "-X", "-Q", "--check-hash-based-pycs"])],
22009
+ [SHELL, /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"])]
22010
+ ];
22011
+ function interpretedProgram(exe, tokens) {
22012
+ const takesValue = VALUE_OPTIONS.find(([re]) => re.test(exe))?.[1] ?? /* @__PURE__ */ new Set();
22013
+ for (let i = 1; i < tokens.length; i++) {
22014
+ const token = tokens[i];
22015
+ if (token === "-m" || PYTHON.test(exe) && /^-[BEIOPRSbdhiqsuvx]*m$/.test(token)) return tokens[i + 1] ? { module: tokens[i + 1], script: "" } : null;
22016
+ if (token === "-e" || token === "-p" || token === "-pe" || token === "-ep" || token === "--eval" || token === "--print") return null;
22017
+ if (SHELL.test(exe) && /^-[a-zA-Z]*c[a-zA-Z]*$/.test(token)) return null;
22018
+ if (PYTHON.test(exe) && /^-[BEIOPRSbdhiqsuvx]*c$/.test(token)) return null;
22019
+ if (token.startsWith("-") || token.startsWith("+")) {
22020
+ if (takesValue.has(token)) i++;
22021
+ continue;
22022
+ }
22023
+ return { script: token };
22024
+ }
22025
+ return null;
22026
+ }
22027
+ var PATH_SUFFIXES = ["code", "cli", "agent"];
22028
+ var PATH_PASS = AGENT_CLIS.map((pat) => {
22029
+ const name = pat.replace(/[-]/g, "\\-");
22030
+ const end = "(?=$|[ /.])";
22031
+ return [pat, new RegExp(`/${name}(?:${end}|-(?:${PATH_SUFFIXES.join("|")})${end})`)];
22032
+ });
22033
+ function parseProcStat(text2) {
22034
+ const close = text2.lastIndexOf(")");
22035
+ const open = text2.indexOf("(");
22036
+ if (close < 0 || open < 0 || open > close) return null;
22037
+ const comm = text2.slice(open + 1, close);
22038
+ const fields = text2.slice(close + 1).trim().split(/\s+/);
22039
+ const state = fields[0];
22040
+ const ppid = Number(fields[1]);
22041
+ const pgrp = Number(fields[2]);
22042
+ const ttyNr = Number(fields[4]);
22043
+ const tpgid = Number(fields[5]);
22044
+ const startTime = fields[19];
22045
+ if (!state || !startTime || ![ppid, pgrp, ttyNr, tpgid].every(Number.isInteger)) return null;
22046
+ return { comm, state, ppid, pgrp, ttyNr, tpgid, startTime };
22047
+ }
22048
+ function readProcStat(pid) {
22049
+ try {
22050
+ return parseProcStat(readFileSync31(`/proc/${pid}/stat`, "latin1"));
22051
+ } catch {
22052
+ return null;
22053
+ }
22054
+ }
22055
+ function readTag(pid) {
22056
+ let raw;
22057
+ try {
22058
+ raw = readFileSync31(`/proc/${pid}/environ`);
22059
+ } catch {
22060
+ return null;
22061
+ }
22062
+ let paneId = null;
22063
+ let session = null;
22064
+ let start = 0;
22065
+ while (start <= raw.length) {
22066
+ let end = raw.indexOf(0, start);
22067
+ if (end < 0) end = raw.length;
22068
+ const eq = raw.indexOf(61, start);
22069
+ if (eq >= 0 && eq < end) {
22070
+ const key = raw.toString("latin1", start, eq);
22071
+ if (key === "ZELLIJ_PANE_ID") {
22072
+ const value = raw.toString("utf8", eq + 1, end);
22073
+ paneId = /^\d+$/.test(value) ? Number(value) : null;
22074
+ } else if (key === "ZELLIJ_SESSION_NAME") {
22075
+ session = raw.toString("utf8", eq + 1, end);
22076
+ }
22077
+ }
22078
+ start = end + 1;
22079
+ }
22080
+ return paneId === null ? null : { paneId, session };
22081
+ }
22082
+ function readArgv(pid) {
22083
+ try {
22084
+ const parts = readFileSync31(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean);
22085
+ return { argv: parts.join(" "), exe: parts[0] ? basename11(parts[0]) : "" };
22086
+ } catch {
22087
+ return { argv: "", exe: "" };
22088
+ }
22089
+ }
22090
+ var ProcScanner = class {
22091
+ known = /* @__PURE__ */ new Map();
22092
+ enumerate;
22093
+ /**
22094
+ * `pids` replaces the enumeration of /proc: the scanner then looks at exactly those pids and no
22095
+ * others. Production never passes it (the default reads every pid of the box, which is the
22096
+ * point). A test that spawns its own processes passes its own pids, so that processes OTHER
22097
+ * tests or agents have on the box (session-less tagged ones are visible to every scan by
22098
+ * design) cannot change its answer.
22099
+ */
22100
+ constructor(options = {}) {
22101
+ this.enumerate = options.pids;
22102
+ }
22103
+ /** How many pids the cache holds (negatives included). For the cost report and tests. */
22104
+ cached() {
22105
+ return this.known.size;
22106
+ }
22107
+ /** Is `pid` in the cache, as a tagged process or a remembered negative? */
22108
+ has(pid) {
22109
+ return this.known.has(pid);
22110
+ }
22111
+ /**
22112
+ * Every process in `session` that carries a pane tag. A process whose ZELLIJ_SESSION_NAME names
22113
+ * a DIFFERENT session is dropped; one with no session name at all is kept (`session: null`),
22114
+ * because zellij has not always set it and dropping it would lose real agents (Deckard's rule).
22115
+ * That also keeps processes some other tool started under `env -u ZELLIJ_SESSION_NAME` (e.g.
22116
+ * a `zellij action list-panes` run from a pane): they carry a pane id and no session, and are
22117
+ * not agents, so classification discards them.
22118
+ */
22119
+ scan(session) {
22120
+ let pids;
22121
+ if (this.enumerate) {
22122
+ pids = [...this.enumerate()];
22123
+ } else {
22124
+ try {
22125
+ pids = readdirSync13("/proc").filter((name) => /^\d+$/.test(name)).map(Number);
22126
+ } catch {
22127
+ return [];
22128
+ }
22129
+ }
22130
+ const alive = /* @__PURE__ */ new Set();
22131
+ const out = [];
22132
+ for (const pid of pids) {
22133
+ alive.add(pid);
22134
+ const stat = readProcStat(pid);
22135
+ if (!stat) continue;
22136
+ let slot = this.known.get(pid);
22137
+ if (!slot || slot.startTime !== stat.startTime || slot.comm !== stat.comm) {
22138
+ const tag2 = readTag(pid);
22139
+ slot = { startTime: stat.startTime, comm: stat.comm, tag: tag2, ...tag2 ? readArgv(pid) : { argv: "", exe: "" } };
22140
+ this.known.set(pid, slot);
22141
+ } else if (slot.tag && stat.state !== "Z") {
22142
+ Object.assign(slot, readArgv(pid));
22143
+ }
22144
+ const tag = slot.tag;
22145
+ if (!tag) continue;
22146
+ if (tag.session !== null && tag.session !== session) continue;
22147
+ let cwd = null;
22148
+ try {
22149
+ cwd = readlinkSync9(`/proc/${pid}/cwd`);
22150
+ } catch {
22151
+ }
22152
+ out.push({ pid, paneId: tag.paneId, session: tag.session, argv: slot.argv, exe: slot.exe, cwd, stat });
22153
+ }
22154
+ for (const pid of this.known.keys()) if (!alive.has(pid)) this.known.delete(pid);
22155
+ return out.sort((a, b) => a.pid - b.pid);
22156
+ }
22157
+ };
22158
+ var PANE_STATUSES = ["busy", "idle", "shell"];
22159
+ function claudeSessionsDir(env2 = process.env) {
22160
+ const override = env2.PJ_CLAUDE_SESSIONS_DIR?.trim();
22161
+ return override || join36(homedir16(), ".claude", "sessions");
22162
+ }
22163
+ function readClaudeSession(pid, stat, dir = claudeSessionsDir()) {
22164
+ const none = (reason) => ({ kind: null, sessionId: null, reason, status: "unknown", statusUpdatedAt: null });
22165
+ let raw;
22166
+ try {
22167
+ raw = readFileSync31(join36(dir, `${pid}.json`), "utf8");
22168
+ } catch {
22169
+ return none("no-session-file");
22170
+ }
22171
+ let data;
22172
+ try {
22173
+ data = JSON.parse(raw);
22174
+ } catch {
22175
+ return none("unreadable-session-file");
22176
+ }
22177
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return none("unreadable-session-file");
22178
+ const file = data;
22179
+ const procStart = file.procStart === void 0 || file.procStart === null ? "" : String(file.procStart);
22180
+ if (!stat || !procStart || procStart !== stat.startTime) return none("stale-session-file");
22181
+ const sessionId = typeof file.sessionId === "string" && file.sessionId ? file.sessionId : null;
22182
+ const status = typeof file.status === "string" && PANE_STATUSES.includes(file.status) ? file.status : "unknown";
22183
+ const at = Number(file.statusUpdatedAt);
22184
+ return {
22185
+ kind: typeof file.kind === "string" ? file.kind : null,
22186
+ sessionId,
22187
+ reason: sessionId ? null : "no-session-id",
22188
+ status,
22189
+ statusUpdatedAt: file.statusUpdatedAt !== void 0 && file.statusUpdatedAt !== null && Number.isFinite(at) ? at : null
22190
+ };
22191
+ }
22192
+ function listPanesFailure(session, result2) {
22193
+ const said = (result2.stderr ?? "").trim().split("\n")[0]?.trim();
22194
+ if (result2.status === 127) return `zellij is not installed or not on PATH (exit 127)${said ? `: ${said}` : ""}`;
22195
+ if (result2.status === 124) return `zellij list-panes for session "${session}" timed out (exit 124): the zellij server may be wedged`;
22196
+ if (said && /not found|no active session|no session/i.test(said)) return `zellij has no session "${session}" (exit ${result2.status}): ${said}`;
22197
+ return `zellij list-panes for session "${session}" failed (exit ${result2.status})${said ? `: ${said}` : ""}`;
22198
+ }
22199
+ function readPaneTable(session, shell = defaultShell) {
22200
+ const panes = /* @__PURE__ */ new Map();
22201
+ const result2 = runListPanes(session, shell);
22202
+ if (result2.status !== 0) return { ok: false, error: listPanesFailure(session, result2), panes };
22203
+ const listed = parseListPanes(result2.stdout);
22204
+ if (!listed) return { ok: false, error: `zellij list-panes for session "${session}" printed no pane list`, panes };
22205
+ if (!listed.length) return { ok: false, error: `zellij list-panes for session "${session}" listed no panes at all: not a live session`, panes };
22206
+ for (const pane of listed) {
22207
+ if (pane.isPlugin || !Number.isInteger(pane.id)) continue;
22208
+ panes.set(pane.id, { tabId: pane.tabId ?? null, tabPosition: pane.tabPosition ?? null, tabName: pane.tabName, title: pane.title });
22209
+ }
22210
+ return { ok: true, error: null, panes };
22211
+ }
22212
+ var IDENTITY_BUDGET_MS2 = 2e4;
22213
+ function paneTerminal(inPane, serverParent) {
22214
+ const live = inPane.filter((p) => p.stat.state !== "Z");
22215
+ const pidsInPane = new Set(live.map((p) => p.pid));
22216
+ const roots = live.filter((p) => !pidsInPane.has(p.stat.ppid));
22217
+ const underServer = roots.filter((p) => serverParent(p.stat.ppid));
22218
+ const ttys = new Set((underServer.length ? underServer : roots).map((p) => p.stat.ttyNr).filter((tty) => tty > 0));
22219
+ return ttys.size === 1 ? [...ttys][0] : null;
22220
+ }
22221
+ function chooseCandidate(candidates) {
22222
+ const start = (c) => Number(c.proc.stat.startTime);
22223
+ const trusted = (c) => c.claude?.sessionId ? 1 : 0;
22224
+ const background = (c) => c.claude?.kind === "bg" ? 1 : 0;
22225
+ return [...candidates].sort(
22226
+ (a, b) => Number(b.foreground) - Number(a.foreground) || background(a) - background(b) || (a.foreground && b.foreground ? trusted(b) - trusted(a) : 0) || start(b) - start(a) || b.proc.pid - a.proc.pid
22227
+ )[0];
22228
+ }
22229
+ function inventoryPanes(options = {}) {
22230
+ const fromOption = options.session?.trim();
22231
+ const fromEnv = process.env.ZELLIJ_SESSION_NAME?.trim();
22232
+ const session = fromOption || fromEnv || ZCRANK_SESSION;
22233
+ const sessionSource = fromOption ? "option" : fromEnv ? "ZELLIJ_SESSION_NAME" : "default";
22234
+ const shell = options.shell ?? defaultShell;
22235
+ const sessionsDir = options.sessionsDir ?? claudeSessionsDir();
22236
+ const scanner = options.scanner ?? new ProcScanner();
22237
+ const identityCache = options.identityCache ?? /* @__PURE__ */ new Map();
22238
+ const budget = { totalMs: IDENTITY_BUDGET_MS2 };
22239
+ const procs = scanner.scan(session);
22240
+ const table = readPaneTable(session, shell);
22241
+ if (!table.ok) {
22242
+ return { session, sessionSource, zellijReadable: false, error: table.error, unmappedPaneIds: [], panes: [] };
22243
+ }
22244
+ const taggedByPane = /* @__PURE__ */ new Map();
22245
+ for (const proc of procs) {
22246
+ const list = taggedByPane.get(proc.paneId);
22247
+ if (list) list.push(proc);
22248
+ else taggedByPane.set(proc.paneId, [proc]);
22249
+ }
22250
+ const serverMemo = /* @__PURE__ */ new Map();
22251
+ const isZellijServer = (pid) => {
22252
+ let known = serverMemo.get(pid);
22253
+ if (known === void 0) {
22254
+ const { exe, argv } = readArgv(pid);
22255
+ known = exe === "zellij" && argv.includes(" --server");
22256
+ serverMemo.set(pid, known);
22257
+ }
22258
+ return known;
22259
+ };
22260
+ const byPane = /* @__PURE__ */ new Map();
22261
+ for (const [paneId, inPane] of taggedByPane) {
22262
+ const tty = paneTerminal(inPane, isZellijServer);
22263
+ for (const proc of inPane) {
22264
+ if (proc.stat.state === "Z") continue;
22265
+ const agent = agentCli(proc.exe, proc.argv);
22266
+ if (!agent) continue;
22267
+ const claude = agent === "claude" ? readClaudeSession(proc.pid, proc.stat, sessionsDir) : null;
22268
+ const foreground = tty !== null && proc.stat.ttyNr === tty && proc.stat.tpgid > 0 && proc.stat.pgrp === proc.stat.tpgid;
22269
+ const candidate = { proc, agent, foreground, claude };
22270
+ const list = byPane.get(paneId);
22271
+ if (list) list.push(candidate);
22272
+ else byPane.set(paneId, [candidate]);
22273
+ }
22274
+ }
22275
+ const rows = [];
22276
+ const unmappedPaneIds = [];
22277
+ for (const [paneId, candidates] of byPane) {
22278
+ if (!table.panes.has(paneId)) {
22279
+ unmappedPaneIds.push(paneId);
22280
+ continue;
22281
+ }
22282
+ const chosen = chooseCandidate(candidates);
22283
+ const entry = table.panes.get(paneId);
22284
+ const cwd = chosen.proc.cwd;
22285
+ const info = chosen.claude ?? { kind: null, sessionId: null, reason: "unresolved", status: "unknown", statusUpdatedAt: null };
22286
+ rows.push({
22287
+ position: entry?.tabPosition ?? Number.MAX_SAFE_INTEGER,
22288
+ row: {
22289
+ session,
22290
+ tabId: entry?.tabId ?? null,
22291
+ tabName: entry?.tabName ?? null,
22292
+ paneId,
22293
+ paneTitle: entry?.title ?? null,
22294
+ pid: chosen.proc.pid,
22295
+ agent: chosen.agent,
22296
+ cwd,
22297
+ project: (cwd ? projectIdentity(cwd, { shell, cache: identityCache, budget }) : void 0) ?? null,
22298
+ sessionId: info.sessionId,
22299
+ sessionReason: info.sessionId ? null : info.reason,
22300
+ status: info.status,
22301
+ statusUpdatedAt: info.statusUpdatedAt,
22302
+ nested: candidates.filter((c) => c !== chosen).map((c) => ({ pid: c.proc.pid, agent: c.agent })).sort((a, b) => a.pid - b.pid)
22303
+ }
22304
+ });
22305
+ }
22306
+ if (options.all) {
22307
+ for (const [paneId, entry] of table.panes) {
22308
+ if (byPane.has(paneId)) continue;
22309
+ rows.push({
22310
+ position: entry.tabPosition ?? Number.MAX_SAFE_INTEGER,
22311
+ row: {
22312
+ session,
22313
+ tabId: entry.tabId,
22314
+ tabName: entry.tabName,
22315
+ paneId,
22316
+ paneTitle: entry.title,
22317
+ pid: null,
22318
+ agent: null,
22319
+ cwd: null,
22320
+ project: null,
22321
+ sessionId: null,
22322
+ sessionReason: null,
22323
+ status: "unknown",
22324
+ statusUpdatedAt: null,
22325
+ nested: []
22326
+ }
22327
+ });
22328
+ }
22329
+ }
22330
+ rows.sort((a, b) => a.position - b.position || a.row.paneId - b.row.paneId);
22331
+ return { session, sessionSource, zellijReadable: true, error: null, unmappedPaneIds: unmappedPaneIds.sort((a, b) => a - b), panes: rows.map((r) => r.row) };
22332
+ }
22333
+ function tilde(path) {
22334
+ if (!path) return "-";
22335
+ const home = homedir16();
22336
+ return path === home ? "~" : path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
22337
+ }
22338
+ function displayWidth(text2) {
22339
+ let width = 0;
22340
+ for (const { segment } of new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(text2)) width += clusterWidth(segment);
22341
+ return width;
22342
+ }
22343
+ var EMOJI_PRESENTATION = /^\p{Emoji_Presentation}/u;
22344
+ var EMOJI_BASE = /^\p{Emoji}/u;
22345
+ var REGIONAL_INDICATOR = /^\p{Regional_Indicator}/u;
22346
+ var ZERO_WIDTH_ONLY = /^[\p{Mn}\p{Me}\p{Cf}\p{Cc}]+$/u;
22347
+ function clusterWidth(cluster) {
22348
+ if (ZERO_WIDTH_ONLY.test(cluster)) return 0;
22349
+ const cp = cluster.codePointAt(0);
22350
+ if (REGIONAL_INDICATOR.test(cluster)) return [...cluster].length >= 2 ? 2 : 1;
22351
+ if (EMOJI_PRESENTATION.test(cluster)) return 2;
22352
+ if (cluster.includes("\uFE0F") && EMOJI_BASE.test(cluster)) return 2;
22353
+ const wide = cp >= 4352 && cp <= 4447 || // Hangul Jamo
22354
+ cp >= 11904 && cp <= 42191 && cp !== 12351 || // CJK, kana, Yi
22355
+ cp >= 44032 && cp <= 55203 || // Hangul syllables
22356
+ cp >= 63744 && cp <= 64255 || // CJK compatibility
22357
+ cp >= 65072 && cp <= 65135 || // CJK compatibility forms
22358
+ cp >= 65280 && cp <= 65376 || // fullwidth forms
22359
+ cp >= 65504 && cp <= 65510 || cp >= 131072 && cp <= 262141;
22360
+ return wide ? 2 : 1;
22361
+ }
22362
+ function formatPaneTable(inventory) {
22363
+ const header = ["TAB", "PANE", "PID", "AGENT", "PROJECT", "STATUS", "SESSION", "NESTED"];
22364
+ const rows = inventory.panes.map((p) => [
22365
+ p.tabName ?? "-",
22366
+ String(p.paneId),
22367
+ p.pid === null ? "-" : String(p.pid),
22368
+ p.agent ?? "(no agent)",
22369
+ tilde(p.project ?? p.cwd),
22370
+ p.agent ? p.status : "-",
22371
+ p.sessionId ?? (p.agent ? `- (${p.sessionReason})` : "-"),
22372
+ p.nested.length ? p.nested.map((n) => `${n.agent}:${n.pid}`).join(",") : "-"
22373
+ ]);
22374
+ const widths = header.map((h, i) => Math.max(displayWidth(h), ...rows.map((r) => displayWidth(r[i] ?? ""))));
22375
+ const line = (cells) => cells.map((c, i) => c + " ".repeat(Math.max(0, (widths[i] ?? 0) - displayWidth(c)))).join(" ").trimEnd();
22376
+ return [line(header), ...rows.map(line)].join("\n");
22377
+ }
22378
+
21970
22379
  // src/crank/cli.ts
21971
22380
  var loadCrankScreens = () => loadScreen("./crank/tui.js", import.meta.url);
21972
22381
  var xmark = `${red(glyph.fail)}`;
@@ -22010,6 +22419,9 @@ function registerCrankCli(program2) {
22010
22419
  crankCmd.command("tui").description("Full-screen crank dashboard: toggle, interval, last runs, countdown to the next run, log").action(async () => {
22011
22420
  await runTui();
22012
22421
  });
22422
+ crankCmd.command("panes").description("Read-only inventory of the live agent panes in this zellij session: pane -> pid -> agent CLI -> session id/status (from /proc, not tab names)").option("--json", "Emit the inventory as JSON").option("--all", "Also list terminal panes that run no agent").action((options) => {
22423
+ runPanes(options);
22424
+ });
22013
22425
  crankCmd.command("list").description("Every registry project with crank ON/OFF and its resolved tab, plus orphan zcrank keys").action(() => {
22014
22426
  try {
22015
22427
  const store = redis();
@@ -22140,6 +22552,34 @@ function runDisable() {
22140
22552
  fail(error, "crank");
22141
22553
  }
22142
22554
  }
22555
+ function runPanes(options) {
22556
+ try {
22557
+ const inventory = inventoryPanes({ all: options.all === true });
22558
+ const sessionNote = inventory.sessionSource === "default" ? `not inside zellij ($ZELLIJ_SESSION_NAME is unset): using the default session "${inventory.session}"` : inventory.sessionSource === "ZELLIJ_SESSION_NAME" ? `session "${inventory.session}" (from $ZELLIJ_SESSION_NAME)` : `session "${inventory.session}"`;
22559
+ if (options.json) {
22560
+ console.log(JSON.stringify(inventory, null, 2));
22561
+ }
22562
+ if (!inventory.zellijReadable) {
22563
+ console.error(`${xmark} ${inventory.error ?? "zellij is unreadable"} \u2014 ${sessionNote}`);
22564
+ process.exit(1);
22565
+ }
22566
+ if (options.json) {
22567
+ if (inventory.unmappedPaneIds.length) console.error(` ${dim(`${inventory.unmappedPaneIds.length} pane id(s) with agent processes are not listed by zellij: ${inventory.unmappedPaneIds.join(", ")}`)}`);
22568
+ return;
22569
+ }
22570
+ if (inventory.sessionSource === "default") console.error(` ${yellow(glyph.warn)} ${dim(sessionNote)}`);
22571
+ if (inventory.unmappedPaneIds.length) {
22572
+ console.error(` ${dim(`ignored ${inventory.unmappedPaneIds.length} pane id(s) with agent processes that zellij does not list (a closed pane, a pane that closed during the scan, or a stale daemon environment): ${inventory.unmappedPaneIds.join(", ")}`)}`);
22573
+ }
22574
+ if (!inventory.panes.length) {
22575
+ console.log(` ${dim(`no live agent panes in session ${inventory.session}`)}`);
22576
+ return;
22577
+ }
22578
+ console.log(formatPaneTable(inventory));
22579
+ } catch (error) {
22580
+ fail(error, "crank panes");
22581
+ }
22582
+ }
22143
22583
  async function runTui() {
22144
22584
  try {
22145
22585
  if (!isInteractive()) {
@@ -22185,7 +22625,7 @@ async function promptForRuleIds(rules) {
22185
22625
  function readJson2(path) {
22186
22626
  if (!existsSync26(path)) return void 0;
22187
22627
  try {
22188
- const parsed = JSON.parse(readFileSync31(path, "utf8"));
22628
+ const parsed = JSON.parse(readFileSync32(path, "utf8"));
22189
22629
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
22190
22630
  } catch {
22191
22631
  return void 0;
@@ -22202,9 +22642,9 @@ function packageNameToProjectName(value) {
22202
22642
  return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
22203
22643
  }
22204
22644
  function deriveProjectDefaults(targetDir) {
22205
- const manifest = readJson2(join36(targetDir, ".project.json"));
22206
- const pkg = readJson2(join36(targetDir, "package.json"));
22207
- const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename11(targetDir)) || "Project";
22645
+ const manifest = readJson2(join37(targetDir, ".project.json"));
22646
+ const pkg = readJson2(join37(targetDir, "package.json"));
22647
+ const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename12(targetDir)) || "Project";
22208
22648
  const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
22209
22649
  return {
22210
22650
  name,
@@ -22278,7 +22718,7 @@ function actionNeedsRun(plan, kind, syncMode) {
22278
22718
  if (!action || action.kind !== "project.write-manifest") return false;
22279
22719
  const next = `${JSON.stringify(action.manifest, null, 2)}
22280
22720
  `;
22281
- return !existsSync26(action.path) || readFileSync31(action.path, "utf8") !== next;
22721
+ return !existsSync26(action.path) || readFileSync32(action.path, "utf8") !== next;
22282
22722
  }
22283
22723
  if (kind === "copier.copy.commonproject") return true;
22284
22724
  if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
@@ -22335,11 +22775,11 @@ async function resolveProjectInitTarget(name, options) {
22335
22775
  if (!targetDir && cwdGitRoot) {
22336
22776
  targetDir = cwdGitRoot;
22337
22777
  }
22338
- if (!targetDir && !name && existsSync26(join36(cwd, ".project.json"))) {
22778
+ if (!targetDir && !name && existsSync26(join37(cwd, ".project.json"))) {
22339
22779
  targetDir = cwd;
22340
22780
  }
22341
22781
  if (!targetDir && interactive) {
22342
- const promptedName = await promptTextValue("Project name", packageNameToProjectName(basename11(cwd)));
22782
+ const promptedName = await promptTextValue("Project name", packageNameToProjectName(basename12(cwd)));
22343
22783
  targetDir = resolve25(await promptTextValue("Project directory", projectTargetDirUnder(promptedName, cwd)));
22344
22784
  name = promptedName;
22345
22785
  }
@@ -22349,14 +22789,14 @@ async function resolveProjectInitTarget(name, options) {
22349
22789
  const targetExists = existsSync26(targetDir);
22350
22790
  if (targetExists && !statSync9(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
22351
22791
  const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
22352
- const alreadyScaffolded = targetExists && (existsSync26(join36(targetDir, ".project.json")) || existsSync26(join36(targetDir, ".copier-answers.yml")));
22792
+ const alreadyScaffolded = targetExists && (existsSync26(join37(targetDir, ".project.json")) || existsSync26(join37(targetDir, ".copier-answers.yml")));
22353
22793
  const syncMode = Boolean(
22354
22794
  targetGitRoot && resolve25(targetGitRoot) === resolve25(targetDir) || alreadyScaffolded
22355
22795
  );
22356
- if (targetExists && !syncMode && !options.force && readdirSync13(targetDir).length > 0) {
22796
+ if (targetExists && !syncMode && !options.force && readdirSync14(targetDir).length > 0) {
22357
22797
  throw new Error(`Target already exists and is not empty: ${targetDir} (pass --force to render over it)`);
22358
22798
  }
22359
- const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename11(targetDir)) ?? "Project", description: "" };
22799
+ const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename12(targetDir)) ?? "Project", description: "" };
22360
22800
  if (!name && interactive && !syncMode) {
22361
22801
  name = await promptTextValue("Project name", defaults.name);
22362
22802
  }
@@ -22871,7 +23311,7 @@ program.command("reindex").argument("[project-id]", "Canonical project ID (defau
22871
23311
  else if (options.all) result2 = registryRequest(location, "POST", "/v1/reindex", {});
22872
23312
  else {
22873
23313
  const repo = projectId ? getProject(loadProjectRegistry(location), projectId).repo_path : resolveProject(process.cwd()).root;
22874
- result2 = registryRequest(location, "POST", "/v1/index", { manifest_path: join36(repo, ".project.json") });
23314
+ result2 = registryRequest(location, "POST", "/v1/index", { manifest_path: join37(repo, ".project.json") });
22875
23315
  }
22876
23316
  const statuses = result2.__registry_status;
22877
23317
  const stale = Object.entries(statuses ?? {}).filter(([, item]) => item.status !== "ok");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.5.40",
3
+ "version": "1.5.42",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",