@hwip/cli 0.0.1 → 0.1.1
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/LICENSE +190 -0
- package/NOTICE +11 -0
- package/README.md +1983 -0
- package/dist/agent-list.d.ts +76 -0
- package/dist/agent-list.js +102 -0
- package/dist/agent-read.d.ts +105 -0
- package/dist/agent-read.js +309 -0
- package/dist/agent-rules-fetch.d.ts +51 -0
- package/dist/agent-rules-fetch.js +275 -0
- package/dist/agent-rules.default.d.ts +30 -0
- package/dist/agent-rules.default.js +305 -0
- package/dist/agent-session.d.ts +23 -0
- package/dist/agent-session.js +44 -0
- package/dist/agent-state.d.ts +84 -0
- package/dist/agent-state.js +188 -0
- package/dist/agent-target.d.ts +60 -0
- package/dist/agent-target.js +81 -0
- package/dist/agents.d.ts +31 -0
- package/dist/agents.js +75 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +76 -0
- package/dist/args.d.ts +74 -0
- package/dist/args.js +254 -0
- package/dist/ask-context-attach.d.ts +73 -0
- package/dist/ask-context-attach.js +151 -0
- package/dist/ask-context-screen.d.ts +21 -0
- package/dist/ask-context-screen.js +49 -0
- package/dist/ask-context-switch.d.ts +31 -0
- package/dist/ask-context-switch.js +74 -0
- package/dist/ask-context.d.ts +84 -0
- package/dist/ask-context.js +172 -0
- package/dist/ask-pending.d.ts +47 -0
- package/dist/ask-pending.js +221 -0
- package/dist/ask-seal.d.ts +120 -0
- package/dist/ask-seal.js +179 -0
- package/dist/ask.d.ts +107 -0
- package/dist/ask.js +551 -0
- package/dist/blocked-prompt-seal.d.ts +39 -0
- package/dist/blocked-prompt-seal.js +92 -0
- package/dist/blocked-prompt.d.ts +62 -0
- package/dist/blocked-prompt.js +518 -0
- package/dist/channel.d.ts +67 -0
- package/dist/channel.js +136 -0
- package/dist/check-update.d.ts +28 -0
- package/dist/check-update.js +153 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +813 -0
- package/dist/codex-config.d.ts +24 -0
- package/dist/codex-config.js +169 -0
- package/dist/codex-transcript.d.ts +59 -0
- package/dist/codex-transcript.js +373 -0
- package/dist/command-refusal.d.ts +56 -0
- package/dist/command-refusal.js +127 -0
- package/dist/command-scan.d.ts +46 -0
- package/dist/command-scan.js +154 -0
- package/dist/command-seal.d.ts +57 -0
- package/dist/command-seal.js +88 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +150 -0
- package/dist/config.d.ts +156 -0
- package/dist/config.js +392 -0
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.js +28 -0
- package/dist/crypto.d.ts +278 -0
- package/dist/crypto.js +698 -0
- package/dist/desktop-notify.d.ts +33 -0
- package/dist/desktop-notify.js +49 -0
- package/dist/device-directory.d.ts +36 -0
- package/dist/device-directory.js +97 -0
- package/dist/device-presence.d.ts +17 -0
- package/dist/device-presence.js +62 -0
- package/dist/downloads.d.ts +29 -0
- package/dist/downloads.js +106 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +28 -0
- package/dist/file-crypto.d.ts +94 -0
- package/dist/file-crypto.js +278 -0
- package/dist/file-view-budget.d.ts +15 -0
- package/dist/file-view-budget.js +34 -0
- package/dist/file-view-switch.d.ts +28 -0
- package/dist/file-view-switch.js +122 -0
- package/dist/file-view.d.ts +108 -0
- package/dist/file-view.js +327 -0
- package/dist/gate.d.ts +141 -0
- package/dist/gate.js +336 -0
- package/dist/gemini-transcript.d.ts +32 -0
- package/dist/gemini-transcript.js +189 -0
- package/dist/http-failure.d.ts +98 -0
- package/dist/http-failure.js +349 -0
- package/dist/hwip-core.generated.d.ts +7 -0
- package/dist/hwip-core.generated.js +14 -0
- package/dist/hwip-hook.d.ts +77 -0
- package/dist/hwip-hook.js +340 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/input-sequencer.d.ts +65 -0
- package/dist/input-sequencer.js +142 -0
- package/dist/insecure-url.d.ts +16 -0
- package/dist/insecure-url.js +76 -0
- package/dist/installer.d.ts +96 -0
- package/dist/installer.js +850 -0
- package/dist/inventory-offload.d.ts +49 -0
- package/dist/inventory-offload.js +209 -0
- package/dist/inventory-worker.d.ts +2 -0
- package/dist/inventory-worker.js +27 -0
- package/dist/jsonc-doc.d.ts +106 -0
- package/dist/jsonc-doc.js +354 -0
- package/dist/keep-awake.d.ts +32 -0
- package/dist/keep-awake.js +46 -0
- package/dist/key-changes.d.ts +48 -0
- package/dist/key-changes.js +140 -0
- package/dist/lan-auth.d.ts +140 -0
- package/dist/lan-auth.js +217 -0
- package/dist/lan-endpoint.d.ts +41 -0
- package/dist/lan-endpoint.js +115 -0
- package/dist/lan-receiver.d.ts +122 -0
- package/dist/lan-receiver.js +510 -0
- package/dist/lan-sender.d.ts +76 -0
- package/dist/lan-sender.js +184 -0
- package/dist/lan-transfer.d.ts +36 -0
- package/dist/lan-transfer.js +54 -0
- package/dist/listener-device-id.d.ts +20 -0
- package/dist/listener-device-id.js +133 -0
- package/dist/listener-log.d.ts +4 -0
- package/dist/listener-log.js +23 -0
- package/dist/listener-process.d.ts +200 -0
- package/dist/listener-process.js +470 -0
- package/dist/listener-service.d.ts +225 -0
- package/dist/listener-service.js +682 -0
- package/dist/listener-worktree-cleanup.d.ts +27 -0
- package/dist/listener-worktree-cleanup.js +62 -0
- package/dist/listener.d.ts +1325 -0
- package/dist/listener.js +5681 -0
- package/dist/login-address.d.ts +25 -0
- package/dist/login-address.js +97 -0
- package/dist/login-page.d.ts +29 -0
- package/dist/login-page.js +93 -0
- package/dist/login.d.ts +98 -0
- package/dist/login.js +388 -0
- package/dist/mcp-command.d.ts +45 -0
- package/dist/mcp-command.js +50 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +29 -0
- package/dist/node-support.d.ts +19 -0
- package/dist/node-support.js +38 -0
- package/dist/notify-failure.d.ts +57 -0
- package/dist/notify-failure.js +150 -0
- package/dist/opencode-transcript.d.ts +139 -0
- package/dist/opencode-transcript.js +390 -0
- package/dist/optional-features.d.ts +6 -0
- package/dist/optional-features.js +19 -0
- package/dist/pane-fit.d.ts +40 -0
- package/dist/pane-fit.js +119 -0
- package/dist/peer-key.d.ts +33 -0
- package/dist/peer-key.js +34 -0
- package/dist/pi-transcript.d.ts +26 -0
- package/dist/pi-transcript.js +255 -0
- package/dist/presence.d.ts +14 -0
- package/dist/presence.js +124 -0
- package/dist/private-files.d.ts +51 -0
- package/dist/private-files.js +218 -0
- package/dist/project-switch.d.ts +19 -0
- package/dist/project-switch.js +60 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.js +21 -0
- package/dist/remote-agents.d.ts +410 -0
- package/dist/remote-agents.js +1218 -0
- package/dist/remote-hook.d.ts +12 -0
- package/dist/remote-hook.js +196 -0
- package/dist/replace-file.d.ts +50 -0
- package/dist/replace-file.js +328 -0
- package/dist/schedule-command.d.ts +35 -0
- package/dist/schedule-command.js +260 -0
- package/dist/schedule-launch.d.ts +15 -0
- package/dist/schedule-launch.js +24 -0
- package/dist/schedule-ready.d.ts +44 -0
- package/dist/schedule-ready.js +71 -0
- package/dist/schedule-remote.d.ts +77 -0
- package/dist/schedule-remote.js +167 -0
- package/dist/schedule-runner.d.ts +89 -0
- package/dist/schedule-runner.js +452 -0
- package/dist/schedule.d.ts +149 -0
- package/dist/schedule.js +428 -0
- package/dist/secret-mask.d.ts +29 -0
- package/dist/secret-mask.js +245 -0
- package/dist/self-register.d.ts +45 -0
- package/dist/self-register.js +67 -0
- package/dist/send.d.ts +56 -0
- package/dist/send.js +104 -0
- package/dist/session-diff.d.ts +161 -0
- package/dist/session-diff.js +445 -0
- package/dist/session-registry.d.ts +143 -0
- package/dist/session-registry.js +216 -0
- package/dist/sessions-report.d.ts +59 -0
- package/dist/sessions-report.js +99 -0
- package/dist/shell-audit.d.ts +34 -0
- package/dist/shell-audit.js +47 -0
- package/dist/subagent-transcripts.d.ts +167 -0
- package/dist/subagent-transcripts.js +437 -0
- package/dist/subscriber-trust.d.ts +128 -0
- package/dist/subscriber-trust.js +397 -0
- package/dist/templates.d.ts +105 -0
- package/dist/templates.js +871 -0
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.js +65 -0
- package/dist/tmux-liveness.d.ts +45 -0
- package/dist/tmux-liveness.js +71 -0
- package/dist/tmux-pane.d.ts +55 -0
- package/dist/tmux-pane.js +126 -0
- package/dist/tmux-socket.d.ts +20 -0
- package/dist/tmux-socket.js +292 -0
- package/dist/todo-fold.d.ts +31 -0
- package/dist/todo-fold.js +102 -0
- package/dist/transcript-tail.d.ts +312 -0
- package/dist/transcript-tail.js +718 -0
- package/dist/trust-command.d.ts +49 -0
- package/dist/trust-command.js +200 -0
- package/dist/trust-lock.d.ts +26 -0
- package/dist/trust-lock.js +94 -0
- package/dist/trust-pins.d.ts +37 -0
- package/dist/trust-pins.js +82 -0
- package/dist/turn-recorder-wire.d.ts +70 -0
- package/dist/turn-recorder-wire.js +165 -0
- package/dist/turn-recorder.d.ts +190 -0
- package/dist/turn-recorder.js +695 -0
- package/dist/turn-ring.d.ts +93 -0
- package/dist/turn-ring.js +418 -0
- package/dist/turn-watch.d.ts +177 -0
- package/dist/turn-watch.js +630 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +55 -0
- package/dist/uninstall.js +507 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +421 -0
- package/dist/verify.d.ts +47 -0
- package/dist/verify.js +396 -0
- package/dist/worktree.d.ts +144 -0
- package/dist/worktree.js +310 -0
- package/dist/wrapper.d.ts +254 -0
- package/dist/wrapper.js +704 -0
- package/dist/ws-url.d.ts +109 -0
- package/dist/ws-url.js +202 -0
- package/package.json +78 -10
- package/bin/hwip.js +0 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmux is not optional for `hwip cc` — every remote-agent subcommand runs the
|
|
3
|
+
* agent inside a named tmux session so the listener (and the phone picker) can
|
|
4
|
+
* reach it. Nothing in the install flow used to mention that, so a machine
|
|
5
|
+
* without tmux looked fully set up until the first `hwip cc` failed with an
|
|
6
|
+
* error from a program the user was never told they needed.
|
|
7
|
+
*/
|
|
8
|
+
/** What this machine can do about a missing tmux. */
|
|
9
|
+
export type TmuxInstallPlan = {
|
|
10
|
+
readonly kind: 'present';
|
|
11
|
+
readonly path: string;
|
|
12
|
+
}
|
|
13
|
+
/** Safe to run unattended — no root, user-writable prefix. */
|
|
14
|
+
| {
|
|
15
|
+
readonly kind: 'command';
|
|
16
|
+
readonly run: readonly string[];
|
|
17
|
+
readonly label: string;
|
|
18
|
+
}
|
|
19
|
+
/** Needs root, or there is no package manager we recognise. Tell, don't do. */
|
|
20
|
+
| {
|
|
21
|
+
readonly kind: 'manual';
|
|
22
|
+
readonly hint: string;
|
|
23
|
+
};
|
|
24
|
+
export declare const planTmuxInstall: (deps: {
|
|
25
|
+
resolve: (cmd: string) => string | null;
|
|
26
|
+
platform: NodeJS.Platform;
|
|
27
|
+
}) => TmuxInstallPlan;
|
|
28
|
+
/**
|
|
29
|
+
* Act on the plan. Everything the outside world touches is injected, because
|
|
30
|
+
* the interesting branch — "offer, then run a package manager" — is otherwise
|
|
31
|
+
* only reachable through an interactive install that no test can drive.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ensureTmux: (deps: {
|
|
34
|
+
plan: TmuxInstallPlan;
|
|
35
|
+
nonInteractive: boolean;
|
|
36
|
+
askConfirm: (message: string) => Promise<boolean>;
|
|
37
|
+
run: (argv: readonly string[]) => void;
|
|
38
|
+
report: {
|
|
39
|
+
ok: (m: string) => void;
|
|
40
|
+
fail: (m: string) => void;
|
|
41
|
+
note: (m: string) => void;
|
|
42
|
+
};
|
|
43
|
+
}) => Promise<void>;
|
|
44
|
+
//# sourceMappingURL=tmux-install.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* tmux is not optional for `hwip cc` — every remote-agent subcommand runs the
|
|
4
|
+
* agent inside a named tmux session so the listener (and the phone picker) can
|
|
5
|
+
* reach it. Nothing in the install flow used to mention that, so a machine
|
|
6
|
+
* without tmux looked fully set up until the first `hwip cc` failed with an
|
|
7
|
+
* error from a program the user was never told they needed.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ensureTmux = exports.planTmuxInstall = void 0;
|
|
11
|
+
/** Package managers that need root, and the line to hand the user for each. */
|
|
12
|
+
const ROOT_PACKAGE_MANAGERS = [
|
|
13
|
+
{ bin: 'apt-get', hint: 'sudo apt install tmux' },
|
|
14
|
+
{ bin: 'dnf', hint: 'sudo dnf install tmux' },
|
|
15
|
+
{ bin: 'pacman', hint: 'sudo pacman -S tmux' },
|
|
16
|
+
{ bin: 'zypper', hint: 'sudo zypper install tmux' },
|
|
17
|
+
{ bin: 'apk', hint: 'sudo apk add tmux' },
|
|
18
|
+
];
|
|
19
|
+
const planTmuxInstall = (deps) => {
|
|
20
|
+
const path = deps.resolve('tmux');
|
|
21
|
+
if (path)
|
|
22
|
+
return { kind: 'present', path };
|
|
23
|
+
// tmux has no native Windows build. `hwip cc` is POSIX-only by way of tmux,
|
|
24
|
+
// so point at WSL rather than at a package that cannot be installed.
|
|
25
|
+
if (deps.platform === 'win32') {
|
|
26
|
+
return { kind: 'manual', hint: '`hwip cc` needs tmux, which has no Windows build — run it under WSL' };
|
|
27
|
+
}
|
|
28
|
+
if (deps.platform === 'darwin') {
|
|
29
|
+
return deps.resolve('brew')
|
|
30
|
+
? { kind: 'command', run: ['brew', 'install', 'tmux'], label: 'brew install tmux' }
|
|
31
|
+
: { kind: 'manual', hint: 'install Homebrew (brew.sh), then: brew install tmux' };
|
|
32
|
+
}
|
|
33
|
+
const rootManager = ROOT_PACKAGE_MANAGERS.find((pm) => deps.resolve(pm.bin));
|
|
34
|
+
if (rootManager)
|
|
35
|
+
return { kind: 'manual', hint: rootManager.hint };
|
|
36
|
+
return { kind: 'manual', hint: 'install tmux with this system\'s package manager' };
|
|
37
|
+
};
|
|
38
|
+
exports.planTmuxInstall = planTmuxInstall;
|
|
39
|
+
/**
|
|
40
|
+
* Act on the plan. Everything the outside world touches is injected, because
|
|
41
|
+
* the interesting branch — "offer, then run a package manager" — is otherwise
|
|
42
|
+
* only reachable through an interactive install that no test can drive.
|
|
43
|
+
*/
|
|
44
|
+
const ensureTmux = async (deps) => {
|
|
45
|
+
const { plan, report } = deps;
|
|
46
|
+
if (plan.kind === 'present')
|
|
47
|
+
return report.ok(`tmux found (${plan.path})`);
|
|
48
|
+
if (plan.kind === 'manual')
|
|
49
|
+
return report.fail(`tmux not found — \`hwip cc\` needs it. ${plan.hint}`);
|
|
50
|
+
if (deps.nonInteractive) {
|
|
51
|
+
return report.note(`tmux not found — \`hwip cc\` needs it. Run: ${plan.label}`);
|
|
52
|
+
}
|
|
53
|
+
const wanted = await deps.askConfirm(`tmux is missing and \`hwip cc\` needs it — run \`${plan.label}\`? (enter to confirm)`);
|
|
54
|
+
if (!wanted) {
|
|
55
|
+
return report.note(`tmux not installed — \`hwip cc\` will not work until you run: ${plan.label}`);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
deps.run(plan.run);
|
|
59
|
+
report.ok('tmux installed');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
report.fail(`\`${plan.label}\` failed — run it by hand`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
exports.ensureTmux = ensureTmux;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SessionLiveness } from './session-registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Every session name the tmux server currently holds, or null when there was no
|
|
4
|
+
* server to ask — the binary is missing, or nothing answers on the socket this
|
|
5
|
+
* process resolved.
|
|
6
|
+
*
|
|
7
|
+
* Null is not an empty list. `has-session` cannot tell the two apart: against a
|
|
8
|
+
* socket with nobody behind it it exits 1 — the same answer it gives for a name
|
|
9
|
+
* that ended (measured 2026-09-18). That matters because socket discovery falls
|
|
10
|
+
* back to the default socket, and a cron or launchd process that cannot see the
|
|
11
|
+
* user's server would otherwise call every live session an ended one.
|
|
12
|
+
*/
|
|
13
|
+
export declare const liveSessionNames: () => string[] | null;
|
|
14
|
+
/**
|
|
15
|
+
* The three-state answer, for callers that destroy something on "ended".
|
|
16
|
+
*
|
|
17
|
+
* "tmux says no such session" and "no tmux answered" are not the same fact, and
|
|
18
|
+
* only one of them means the session is over. Collapsing them lets a missing
|
|
19
|
+
* binary or a socket nobody is behind read as "everything has ended" — which,
|
|
20
|
+
* on the delete path, is an irreversible deletion justified by a failure to ask.
|
|
21
|
+
*
|
|
22
|
+
* Asking for the whole list rather than `has-session -t =<name>` is what makes
|
|
23
|
+
* the distinction available at all, and the name comparison is then exact by
|
|
24
|
+
* construction — a tmux TARGET falls back to prefix and fnmatch matching, so
|
|
25
|
+
* with `hwip-hwipit` running an ended `hwip-hwip` used to answer "running".
|
|
26
|
+
*/
|
|
27
|
+
export declare const sessionLiveness: (name: string) => SessionLiveness;
|
|
28
|
+
/**
|
|
29
|
+
* Whether this machine currently holds a session by this name. A subagent
|
|
30
|
+
* pane cannot be asked with has-session: tmux would parse `hwip-x.48` as
|
|
31
|
+
* session `hwip-x`, pane index 48 and answer for the WRONG thing (or exit 1
|
|
32
|
+
* for a missing index) — so subs are answered from the sweep's target map,
|
|
33
|
+
* which is exactly what "a pane we can address without ambiguity" means.
|
|
34
|
+
*
|
|
35
|
+
* An in-process subagent has no pane to address at all, so it is answered from
|
|
36
|
+
* the transcript map instead. Both maps are the same statement — the last sweep
|
|
37
|
+
* saw this thing — and the split is which of the two the sweep could record.
|
|
38
|
+
*/
|
|
39
|
+
/** Exported for the same reason as `sessionLiveness` above: the subagent branch is
|
|
40
|
+
* the load-bearing one, and a test has to be able to reach it.
|
|
41
|
+
* Unknown means "not running" here, which is what its callers — resume,
|
|
42
|
+
* exit, stream targeting — already do with a session they cannot find. The
|
|
43
|
+
* paths that DELETE ask `sessionLiveness` instead and refuse on unknown. */
|
|
44
|
+
export declare const sessionExists: (name: string) => boolean;
|
|
45
|
+
//# sourceMappingURL=tmux-liveness.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionExists = exports.sessionLiveness = exports.liveSessionNames = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const tmux_pane_js_1 = require("./tmux-pane.js");
|
|
6
|
+
const tmux_socket_js_1 = require("./tmux-socket.js");
|
|
7
|
+
/**
|
|
8
|
+
* Every session name the tmux server currently holds, or null when there was no
|
|
9
|
+
* server to ask — the binary is missing, or nothing answers on the socket this
|
|
10
|
+
* process resolved.
|
|
11
|
+
*
|
|
12
|
+
* Null is not an empty list. `has-session` cannot tell the two apart: against a
|
|
13
|
+
* socket with nobody behind it it exits 1 — the same answer it gives for a name
|
|
14
|
+
* that ended (measured 2026-09-18). That matters because socket discovery falls
|
|
15
|
+
* back to the default socket, and a cron or launchd process that cannot see the
|
|
16
|
+
* user's server would otherwise call every live session an ended one.
|
|
17
|
+
*/
|
|
18
|
+
const liveSessionNames = () => {
|
|
19
|
+
const probe = (0, child_process_1.spawnSync)('tmux', (0, tmux_socket_js_1.tmuxArgs)(['list-sessions', '-F', '#{session_name}']), {
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
22
|
+
});
|
|
23
|
+
// A server with zero sessions does not exist — tmux exits with the last one
|
|
24
|
+
// — so a non-zero status here is always "no server answered", never "none".
|
|
25
|
+
if (probe.error || probe.status !== 0)
|
|
26
|
+
return null;
|
|
27
|
+
return (probe.stdout ?? '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
28
|
+
};
|
|
29
|
+
exports.liveSessionNames = liveSessionNames;
|
|
30
|
+
/**
|
|
31
|
+
* The three-state answer, for callers that destroy something on "ended".
|
|
32
|
+
*
|
|
33
|
+
* "tmux says no such session" and "no tmux answered" are not the same fact, and
|
|
34
|
+
* only one of them means the session is over. Collapsing them lets a missing
|
|
35
|
+
* binary or a socket nobody is behind read as "everything has ended" — which,
|
|
36
|
+
* on the delete path, is an irreversible deletion justified by a failure to ask.
|
|
37
|
+
*
|
|
38
|
+
* Asking for the whole list rather than `has-session -t =<name>` is what makes
|
|
39
|
+
* the distinction available at all, and the name comparison is then exact by
|
|
40
|
+
* construction — a tmux TARGET falls back to prefix and fnmatch matching, so
|
|
41
|
+
* with `hwip-hwipit` running an ended `hwip-hwip` used to answer "running".
|
|
42
|
+
*/
|
|
43
|
+
const sessionLiveness = (name) => {
|
|
44
|
+
if ((0, tmux_pane_js_1.isSubagentSessionName)(name)) {
|
|
45
|
+
const known = tmux_pane_js_1.targetsBySession.has(name) || tmux_pane_js_1.subagentTranscriptsBySession.has(name);
|
|
46
|
+
return known ? 'running' : 'ended';
|
|
47
|
+
}
|
|
48
|
+
const names = (0, exports.liveSessionNames)();
|
|
49
|
+
if (names === null)
|
|
50
|
+
return 'unknown';
|
|
51
|
+
return names.includes(name) ? 'running' : 'ended';
|
|
52
|
+
};
|
|
53
|
+
exports.sessionLiveness = sessionLiveness;
|
|
54
|
+
/**
|
|
55
|
+
* Whether this machine currently holds a session by this name. A subagent
|
|
56
|
+
* pane cannot be asked with has-session: tmux would parse `hwip-x.48` as
|
|
57
|
+
* session `hwip-x`, pane index 48 and answer for the WRONG thing (or exit 1
|
|
58
|
+
* for a missing index) — so subs are answered from the sweep's target map,
|
|
59
|
+
* which is exactly what "a pane we can address without ambiguity" means.
|
|
60
|
+
*
|
|
61
|
+
* An in-process subagent has no pane to address at all, so it is answered from
|
|
62
|
+
* the transcript map instead. Both maps are the same statement — the last sweep
|
|
63
|
+
* saw this thing — and the split is which of the two the sweep could record.
|
|
64
|
+
*/
|
|
65
|
+
/** Exported for the same reason as `sessionLiveness` above: the subagent branch is
|
|
66
|
+
* the load-bearing one, and a test has to be able to reach it.
|
|
67
|
+
* Unknown means "not running" here, which is what its callers — resume,
|
|
68
|
+
* exit, stream targeting — already do with a session they cannot find. The
|
|
69
|
+
* paths that DELETE ask `sessionLiveness` instead and refuse on unknown. */
|
|
70
|
+
const sessionExists = (name) => (0, exports.sessionLiveness)(name) === 'running';
|
|
71
|
+
exports.sessionExists = sessionExists;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type RegisteredRemoteAgent } from './remote-agents.js';
|
|
2
|
+
export interface PaneInfo {
|
|
3
|
+
currentCommand: string | null;
|
|
4
|
+
startCommand: string | null;
|
|
5
|
+
currentPath: string | null;
|
|
6
|
+
panePid: number | null;
|
|
7
|
+
}
|
|
8
|
+
export declare const FIELD_SEP = "\u241F";
|
|
9
|
+
/** Session name → pinned pane id, replaced wholesale by each inventory sweep. */
|
|
10
|
+
export declare let targetsBySession: Map<string, string>;
|
|
11
|
+
/**
|
|
12
|
+
* A subagent session name: `<session>.<pane id digits>` (`hwip-hwip.48`).
|
|
13
|
+
* tmux forbids `.` in session names, so the shape cannot collide; `#` would
|
|
14
|
+
* break the server's stream-sub keys and `:` is the web key separator.
|
|
15
|
+
*/
|
|
16
|
+
export declare const isSubagentSessionName: (name: string) => boolean;
|
|
17
|
+
/** Store the latest sweep's name → pane-id map (see `CollectResult.targets`). */
|
|
18
|
+
export declare const recordTargets: (targets: Record<string, string> | null | undefined) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Subagent name → the transcript it writes, for the ones that have no pane
|
|
21
|
+
* (`CollectResult.subagentTranscripts`). The addressing map above answers for
|
|
22
|
+
* pane subagents; this one answers for in-process ones, and it is deliberately
|
|
23
|
+
* NOT `targets`: a row in there is a pane every write path may address, and
|
|
24
|
+
* these rows have nothing to address.
|
|
25
|
+
*/
|
|
26
|
+
export declare let subagentTranscriptsBySession: Map<string, string>;
|
|
27
|
+
export declare const recordSubagentTranscripts: (paths: Record<string, string> | null | undefined) => void;
|
|
28
|
+
/** The transcript of an in-process subagent, or null for anything else. */
|
|
29
|
+
export declare const subagentTranscriptFor: (name: string) => string | null;
|
|
30
|
+
/**
|
|
31
|
+
* The tmux target for a wire session name: its pinned pane id when the last
|
|
32
|
+
* sweep saw the session, else the name itself (a session younger than one
|
|
33
|
+
* sweep, or tmux unreachable at startup — today's resolution, active pane).
|
|
34
|
+
*
|
|
35
|
+
* A SUBAGENT name is the one exception: it resolves ONLY through the map, and
|
|
36
|
+
* an unknown one is null — because tmux parses `hwip-hwip.48` as session
|
|
37
|
+
* `hwip-hwip`, pane INDEX 48, and silently falls back to the active pane when
|
|
38
|
+
* that index is missing (measured: `display -t hwip-hwip.99` → main pane,
|
|
39
|
+
* exit 0). Every caller treats null as "gone": no spawn, refusal, or capture
|
|
40
|
+
* failure — never a write to whichever pane happens to be focused.
|
|
41
|
+
*/
|
|
42
|
+
export declare const tmuxTargetFor: (name: string) => string | null;
|
|
43
|
+
export declare const readPaneInfo: (session: string) => PaneInfo;
|
|
44
|
+
/**
|
|
45
|
+
* Identify the agent from the tmux pane. Prefer `pane_start_command`
|
|
46
|
+
* because the foreground process is usually `node`/`python3` (the
|
|
47
|
+
* interpreter), which doesn't tell us *what* was launched. Fall back to
|
|
48
|
+
* `pane_current_command` when start_command is empty — tmux clears
|
|
49
|
+
* start_command in some re-attach cases, especially when a pre-existing
|
|
50
|
+
* session was joined via `tmux new -A` instead of being created fresh.
|
|
51
|
+
* That fallback is safe because only the literal binaries registered in
|
|
52
|
+
* remote-agents.ts are accepted as a match.
|
|
53
|
+
*/
|
|
54
|
+
export declare const detectRemoteAgent: (info: PaneInfo) => RegisteredRemoteAgent | null;
|
|
55
|
+
//# sourceMappingURL=tmux-pane.d.ts.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectRemoteAgent = exports.readPaneInfo = exports.tmuxTargetFor = exports.subagentTranscriptFor = exports.recordSubagentTranscripts = exports.subagentTranscriptsBySession = exports.recordTargets = exports.isSubagentSessionName = exports.targetsBySession = exports.FIELD_SEP = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const remote_agents_js_1 = require("./remote-agents.js");
|
|
7
|
+
const tmux_socket_js_1 = require("./tmux-socket.js");
|
|
8
|
+
// U+241F "Symbol for Unit Separator" — a *printable* Unicode glyph
|
|
9
|
+
// (3-byte UTF-8) that visually represents the C0 Unit Separator but is
|
|
10
|
+
// itself a normal character. Critical detail: tmux 3.5a's `-F` format
|
|
11
|
+
// escapes raw control bytes (0x00-0x1F) like `\037` for terminal safety,
|
|
12
|
+
// which broke an earlier `'\x1f'` separator — the byte we passed never
|
|
13
|
+
// arrived at the consumer end. A printable Unicode char passes through
|
|
14
|
+
// verbatim and won't appear in any real session name or filesystem path.
|
|
15
|
+
exports.FIELD_SEP = '␟';
|
|
16
|
+
// ─── Pane targets ───────────────────────────────────────────────────
|
|
17
|
+
// A bare `-t <session>` resolves to whichever pane has FOCUS, so the moment a
|
|
18
|
+
// second pane exists in a `hwip-*` session (pi-interactive-subagents splits
|
|
19
|
+
// one off for every subagent), capture and input silently follow the user's
|
|
20
|
+
// focus instead of the agent. The inventory sweep pins each session to its
|
|
21
|
+
// main pane id, and every `-t` call site goes through `tmuxTargetFor` so the
|
|
22
|
+
// pinning cannot be bypassed by one call site forgetting it.
|
|
23
|
+
/** Session name → pinned pane id, replaced wholesale by each inventory sweep. */
|
|
24
|
+
exports.targetsBySession = new Map();
|
|
25
|
+
/**
|
|
26
|
+
* A subagent session name: `<session>.<pane id digits>` (`hwip-hwip.48`).
|
|
27
|
+
* tmux forbids `.` in session names, so the shape cannot collide; `#` would
|
|
28
|
+
* break the server's stream-sub keys and `:` is the web key separator.
|
|
29
|
+
*/
|
|
30
|
+
const isSubagentSessionName = (name) => /^hwip-.+\.\d+$/.test(name);
|
|
31
|
+
exports.isSubagentSessionName = isSubagentSessionName;
|
|
32
|
+
/** Store the latest sweep's name → pane-id map (see `CollectResult.targets`). */
|
|
33
|
+
const recordTargets = (targets) => {
|
|
34
|
+
exports.targetsBySession = new Map(Object.entries(targets ?? {}));
|
|
35
|
+
};
|
|
36
|
+
exports.recordTargets = recordTargets;
|
|
37
|
+
/**
|
|
38
|
+
* Subagent name → the transcript it writes, for the ones that have no pane
|
|
39
|
+
* (`CollectResult.subagentTranscripts`). The addressing map above answers for
|
|
40
|
+
* pane subagents; this one answers for in-process ones, and it is deliberately
|
|
41
|
+
* NOT `targets`: a row in there is a pane every write path may address, and
|
|
42
|
+
* these rows have nothing to address.
|
|
43
|
+
*/
|
|
44
|
+
exports.subagentTranscriptsBySession = new Map();
|
|
45
|
+
const recordSubagentTranscripts = (paths) => {
|
|
46
|
+
exports.subagentTranscriptsBySession = new Map(Object.entries(paths ?? {}));
|
|
47
|
+
};
|
|
48
|
+
exports.recordSubagentTranscripts = recordSubagentTranscripts;
|
|
49
|
+
/** The transcript of an in-process subagent, or null for anything else. */
|
|
50
|
+
const subagentTranscriptFor = (name) => exports.subagentTranscriptsBySession.get(name) ?? null;
|
|
51
|
+
exports.subagentTranscriptFor = subagentTranscriptFor;
|
|
52
|
+
/**
|
|
53
|
+
* The tmux target for a wire session name: its pinned pane id when the last
|
|
54
|
+
* sweep saw the session, else the name itself (a session younger than one
|
|
55
|
+
* sweep, or tmux unreachable at startup — today's resolution, active pane).
|
|
56
|
+
*
|
|
57
|
+
* A SUBAGENT name is the one exception: it resolves ONLY through the map, and
|
|
58
|
+
* an unknown one is null — because tmux parses `hwip-hwip.48` as session
|
|
59
|
+
* `hwip-hwip`, pane INDEX 48, and silently falls back to the active pane when
|
|
60
|
+
* that index is missing (measured: `display -t hwip-hwip.99` → main pane,
|
|
61
|
+
* exit 0). Every caller treats null as "gone": no spawn, refusal, or capture
|
|
62
|
+
* failure — never a write to whichever pane happens to be focused.
|
|
63
|
+
*/
|
|
64
|
+
const tmuxTargetFor = (name) => {
|
|
65
|
+
const hit = exports.targetsBySession.get(name);
|
|
66
|
+
if (hit !== undefined)
|
|
67
|
+
return hit;
|
|
68
|
+
return (0, exports.isSubagentSessionName)(name) ? null : name;
|
|
69
|
+
};
|
|
70
|
+
exports.tmuxTargetFor = tmuxTargetFor;
|
|
71
|
+
const readPaneInfo = (session) => {
|
|
72
|
+
const target = (0, exports.tmuxTargetFor)(session);
|
|
73
|
+
if (target === null)
|
|
74
|
+
return { currentCommand: null, startCommand: null, currentPath: null, panePid: null };
|
|
75
|
+
const r = (0, child_process_1.spawnSync)('tmux', (0, tmux_socket_js_1.tmuxArgs)(['display-message', '-p', '-t', target,
|
|
76
|
+
`#{pane_current_command}${exports.FIELD_SEP}#{pane_start_command}${exports.FIELD_SEP}#{pane_current_path}${exports.FIELD_SEP}#{pane_pid}`]), {
|
|
77
|
+
encoding: 'utf-8',
|
|
78
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
79
|
+
});
|
|
80
|
+
if (r.status !== 0)
|
|
81
|
+
return { currentCommand: null, startCommand: null, currentPath: null, panePid: null };
|
|
82
|
+
const parts = (r.stdout ?? '').trim().split(exports.FIELD_SEP);
|
|
83
|
+
if (parts.length !== 4)
|
|
84
|
+
return { currentCommand: null, startCommand: null, currentPath: null, panePid: null };
|
|
85
|
+
const [current, start, path, pid] = parts;
|
|
86
|
+
const parsedPid = Number(pid);
|
|
87
|
+
return {
|
|
88
|
+
currentCommand: current || null,
|
|
89
|
+
startCommand: start || null,
|
|
90
|
+
currentPath: path || null,
|
|
91
|
+
panePid: Number.isInteger(parsedPid) && parsedPid > 0 ? parsedPid : null,
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
exports.readPaneInfo = readPaneInfo;
|
|
95
|
+
/**
|
|
96
|
+
* Strip the surrounding quotes tmux uses when serialising commands
|
|
97
|
+
* with spaces. `tmux display-message -p '#{pane_start_command}'`
|
|
98
|
+
* outputs `"claude --resume xxx"` (literal double-quotes around the
|
|
99
|
+
* whole shell-command form the wrapper passed). Without unwrapping,
|
|
100
|
+
* the leading `"` made the basename check fail and the listener
|
|
101
|
+
* skipped the session as 'no agent in pane'.
|
|
102
|
+
*/
|
|
103
|
+
const unwrapQuotes = (cmd) => {
|
|
104
|
+
const m = cmd.match(/^"(.+)"$/) || cmd.match(/^'(.+)'$/);
|
|
105
|
+
return m ? m[1] : cmd;
|
|
106
|
+
};
|
|
107
|
+
const firstTokenBasename = (cmd) => {
|
|
108
|
+
if (!cmd)
|
|
109
|
+
return '';
|
|
110
|
+
const stripped = unwrapQuotes(cmd.trim());
|
|
111
|
+
return (0, path_1.basename)(stripped.split(/\s+/)[0] || '');
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Identify the agent from the tmux pane. Prefer `pane_start_command`
|
|
115
|
+
* because the foreground process is usually `node`/`python3` (the
|
|
116
|
+
* interpreter), which doesn't tell us *what* was launched. Fall back to
|
|
117
|
+
* `pane_current_command` when start_command is empty — tmux clears
|
|
118
|
+
* start_command in some re-attach cases, especially when a pre-existing
|
|
119
|
+
* session was joined via `tmux new -A` instead of being created fresh.
|
|
120
|
+
* That fallback is safe because only the literal binaries registered in
|
|
121
|
+
* remote-agents.ts are accepted as a match.
|
|
122
|
+
*/
|
|
123
|
+
const detectRemoteAgent = (info) => (0, remote_agents_js_1.matchAgentByPaneCommand)(firstTokenBasename(info.startCommand))
|
|
124
|
+
?? (0, remote_agents_js_1.matchAgentByPaneCommand)(firstTokenBasename(info.currentCommand))
|
|
125
|
+
?? null;
|
|
126
|
+
exports.detectRemoteAgent = detectRemoteAgent;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark the cached socket as no longer trustworthy — call this when a
|
|
3
|
+
* tmux command fails against the cached path. The next findTmuxSocket()
|
|
4
|
+
* will redo full discovery (default probe → /var/folders walk → lsof
|
|
5
|
+
* fallback) instead of returning a stale answer. Without this the
|
|
6
|
+
* listener wedged at "reported 0 session(s)" forever after a tmux
|
|
7
|
+
* server restart, even when a new server was live and discoverable.
|
|
8
|
+
*/
|
|
9
|
+
export declare const invalidateTmuxSocketCache: () => void;
|
|
10
|
+
/**
|
|
11
|
+
* One name per socket file: the path with its symlinks resolved, or the path
|
|
12
|
+
* as given when nothing is there to resolve. macOS reaches the same directory
|
|
13
|
+
* as `/tmp/…` and `/private/tmp/…`, and as `/var/folders/…` and
|
|
14
|
+
* `/private/var/folders/…`; without this each dead socket there is probed
|
|
15
|
+
* once per spelling.
|
|
16
|
+
*/
|
|
17
|
+
export declare const socketKey: (path: string) => string;
|
|
18
|
+
/** Prepend `-S <socket>` when we've discovered a non-default tmux server. */
|
|
19
|
+
export declare const tmuxArgs: (args: string[]) => string[];
|
|
20
|
+
//# sourceMappingURL=tmux-socket.d.ts.map
|