@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,76 @@
|
|
|
1
|
+
import type { AgentState } from './agent-state.js';
|
|
2
|
+
/**
|
|
3
|
+
* `hwip agent list` — the agent sessions running on THIS machine. It looks at tmux
|
|
4
|
+
* and the screens through the listener's own sweep and nothing else: no server,
|
|
5
|
+
* no relay, no API key, and nothing typed into a session. Another PC's sessions
|
|
6
|
+
* stay out of reach on purpose (the E2E trust boundary).
|
|
7
|
+
*
|
|
8
|
+
* The sweep (`collectSessionsVerbose`) ends by refreshing the local session record
|
|
9
|
+
* (`$XDG_STATE_HOME/hwip/known-sessions.json`, the listener's resume whitelist). That
|
|
10
|
+
* write belongs to the listener alone: the file is written in place, so a second writer
|
|
11
|
+
* can leave the listener a half-written file. `liveLocalAgentDeps` switches it off for
|
|
12
|
+
* this process (`keepRegistryUntouched`), which keeps `listener.ts` as it is.
|
|
13
|
+
*/
|
|
14
|
+
/** The part of a listener session row this command reads. */
|
|
15
|
+
export interface ListedSession {
|
|
16
|
+
name: string;
|
|
17
|
+
attached: boolean;
|
|
18
|
+
agentKind: string;
|
|
19
|
+
parentName?: string;
|
|
20
|
+
project: string;
|
|
21
|
+
label?: string | null;
|
|
22
|
+
providerSessionName?: string | null;
|
|
23
|
+
state?: AgentState;
|
|
24
|
+
lastActivityAt?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface LocalAgentDeps {
|
|
27
|
+
/** One sweep of the local tmux server: every `hwip-*` session with its kind and state. */
|
|
28
|
+
collect: () => ListedSession[];
|
|
29
|
+
deviceId: () => string;
|
|
30
|
+
/** The key of the session this process runs in, or null outside tmux. */
|
|
31
|
+
selfKey: () => string | null;
|
|
32
|
+
/** Whether tmux answered at all. Asked only when the sweep found nothing. */
|
|
33
|
+
tmuxVisible: () => boolean;
|
|
34
|
+
/** Awaited, so a large JSON line is flushed before the process exits. */
|
|
35
|
+
out: (text: string) => void | Promise<void>;
|
|
36
|
+
err: (line: string) => void;
|
|
37
|
+
}
|
|
38
|
+
/** One row of the `--json` contract (version 1). */
|
|
39
|
+
export interface AgentListEntry {
|
|
40
|
+
key: string;
|
|
41
|
+
name: string;
|
|
42
|
+
agent: string;
|
|
43
|
+
project: string;
|
|
44
|
+
label: string | null;
|
|
45
|
+
providerName: string | null;
|
|
46
|
+
state: AgentState | null;
|
|
47
|
+
attached: boolean;
|
|
48
|
+
lastActivityAt: string | null;
|
|
49
|
+
self: boolean;
|
|
50
|
+
}
|
|
51
|
+
export type AgentListResult = {
|
|
52
|
+
ok: true;
|
|
53
|
+
v: 1;
|
|
54
|
+
device: string;
|
|
55
|
+
sessions: AgentListEntry[];
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
code: 'NO_TMUX';
|
|
59
|
+
message: string;
|
|
60
|
+
};
|
|
61
|
+
export declare const NO_TMUX_MESSAGE = "no tmux server answered \u2014 no agent sessions can be listed or read on this machine";
|
|
62
|
+
export declare const listLocalAgents: (deps: LocalAgentDeps) => AgentListResult;
|
|
63
|
+
export declare const runAgentList: (opts: {
|
|
64
|
+
json: boolean;
|
|
65
|
+
}, deps: LocalAgentDeps) => Promise<number>;
|
|
66
|
+
/** Write to stdout and wait for it: `main` calls `process.exit` right after, which cuts off a pipe's tail. */
|
|
67
|
+
export declare const writeStdout: (text: string) => Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* The real sweep. The listener's log goes quiet first (its tmux socket search
|
|
70
|
+
* prints to stdout, which `--json` must keep to one object) and the OTA agent
|
|
71
|
+
* rules are read from the cache, else every non-claude session reports no state
|
|
72
|
+
* — the same two steps the inventory worker takes before it sweeps.
|
|
73
|
+
*/
|
|
74
|
+
export declare const liveLocalAgentDeps: () => Promise<LocalAgentDeps>;
|
|
75
|
+
export declare const handleAgentList: (args: Record<string, string | boolean>) => Promise<number>;
|
|
76
|
+
//# sourceMappingURL=agent-list.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleAgentList = exports.liveLocalAgentDeps = exports.writeStdout = exports.runAgentList = exports.listLocalAgents = exports.NO_TMUX_MESSAGE = void 0;
|
|
4
|
+
const args_js_1 = require("./args.js");
|
|
5
|
+
const agent_session_js_1 = require("./agent-session.js");
|
|
6
|
+
const agent_target_js_1 = require("./agent-target.js");
|
|
7
|
+
const listener_device_id_js_1 = require("./listener-device-id.js");
|
|
8
|
+
exports.NO_TMUX_MESSAGE = 'no tmux server answered — no agent sessions can be listed or read on this machine';
|
|
9
|
+
const listLocalAgents = (deps) => {
|
|
10
|
+
const device = deps.deviceId();
|
|
11
|
+
const self = deps.selfKey();
|
|
12
|
+
const sessions = deps
|
|
13
|
+
.collect()
|
|
14
|
+
.filter(agent_target_js_1.isAgentTarget)
|
|
15
|
+
.map((s) => ({
|
|
16
|
+
key: `${device}:${s.name}`,
|
|
17
|
+
name: s.name,
|
|
18
|
+
agent: s.agentKind,
|
|
19
|
+
project: s.project,
|
|
20
|
+
label: s.label ?? null,
|
|
21
|
+
providerName: s.providerSessionName ?? null,
|
|
22
|
+
state: s.state ?? null,
|
|
23
|
+
attached: s.attached,
|
|
24
|
+
lastActivityAt: s.lastActivityAt ?? null,
|
|
25
|
+
self: `${device}:${s.name}` === self,
|
|
26
|
+
}))
|
|
27
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
28
|
+
// An empty sweep is either "no agent runs" or "tmux was not reachable"; only the second is an error.
|
|
29
|
+
if (sessions.length === 0 && !deps.tmuxVisible())
|
|
30
|
+
return { ok: false, code: 'NO_TMUX', message: exports.NO_TMUX_MESSAGE };
|
|
31
|
+
return { ok: true, v: 1, device, sessions };
|
|
32
|
+
};
|
|
33
|
+
exports.listLocalAgents = listLocalAgents;
|
|
34
|
+
/** The agent names its session itself (`/rename`), so the text may hold escape sequences a terminal would act on. */
|
|
35
|
+
// eslint-disable-next-line no-control-regex -- stripping them is the point
|
|
36
|
+
const printable = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ');
|
|
37
|
+
const renderList = (sessions) => {
|
|
38
|
+
if (sessions.length === 0)
|
|
39
|
+
return 'no agent sessions on this machine';
|
|
40
|
+
const rows = sessions.map((s) => [s.key, s.agent, s.state ?? '-', `${printable(s.providerName ?? s.label ?? '-')}${s.self ? ' (this session)' : ''}`]);
|
|
41
|
+
const table = [['KEY', 'AGENT', 'STATE', 'NAME'], ...rows];
|
|
42
|
+
const widths = [0, 1, 2].map((col) => Math.max(...table.map((r) => r[col].length)));
|
|
43
|
+
return table.map((r) => [...widths.map((w, col) => r[col].padEnd(w)), r[3]].join(' ')).join('\n');
|
|
44
|
+
};
|
|
45
|
+
const runAgentList = async (opts, deps) => {
|
|
46
|
+
const result = (0, exports.listLocalAgents)(deps);
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
if (opts.json)
|
|
49
|
+
await deps.out(JSON.stringify({ v: 1, error: result.code, message: result.message }));
|
|
50
|
+
else
|
|
51
|
+
deps.err(`hwip agent list: ${result.message}`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
if (opts.json)
|
|
55
|
+
await deps.out(JSON.stringify({ v: result.v, device: result.device, sessions: result.sessions }));
|
|
56
|
+
else
|
|
57
|
+
await deps.out(renderList(result.sessions));
|
|
58
|
+
return 0;
|
|
59
|
+
};
|
|
60
|
+
exports.runAgentList = runAgentList;
|
|
61
|
+
/** Write to stdout and wait for it: `main` calls `process.exit` right after, which cuts off a pipe's tail. */
|
|
62
|
+
const writeStdout = (text) => new Promise((resolve) => {
|
|
63
|
+
process.stdout.write(`${text}\n`, () => resolve());
|
|
64
|
+
});
|
|
65
|
+
exports.writeStdout = writeStdout;
|
|
66
|
+
/**
|
|
67
|
+
* The real sweep. The listener's log goes quiet first (its tmux socket search
|
|
68
|
+
* prints to stdout, which `--json` must keep to one object) and the OTA agent
|
|
69
|
+
* rules are read from the cache, else every non-claude session reports no state
|
|
70
|
+
* — the same two steps the inventory worker takes before it sweeps.
|
|
71
|
+
*/
|
|
72
|
+
const liveLocalAgentDeps = async () => {
|
|
73
|
+
const { collectSessionsVerbose, liveSessionNames, silenceListenerLog } = await import('./listener.js');
|
|
74
|
+
const { syncManifestFromCache } = await import('./agent-rules-fetch.js');
|
|
75
|
+
const { keepRegistryUntouched } = await import('./session-registry.js');
|
|
76
|
+
silenceListenerLog();
|
|
77
|
+
keepRegistryUntouched();
|
|
78
|
+
return {
|
|
79
|
+
collect: () => {
|
|
80
|
+
syncManifestFromCache();
|
|
81
|
+
return collectSessionsVerbose().sessions;
|
|
82
|
+
},
|
|
83
|
+
deviceId: listener_device_id_js_1.listenerDeviceId,
|
|
84
|
+
selfKey: () => {
|
|
85
|
+
const context = (0, agent_session_js_1.agentSessionContext)();
|
|
86
|
+
return context ? `${context.agentDeviceId}:${context.agentSessionName}` : null;
|
|
87
|
+
},
|
|
88
|
+
tmuxVisible: () => liveSessionNames() !== null,
|
|
89
|
+
out: exports.writeStdout,
|
|
90
|
+
err: (line) => console.error(line),
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
exports.liveLocalAgentDeps = liveLocalAgentDeps;
|
|
94
|
+
const handleAgentList = async (args) => {
|
|
95
|
+
// `--turns` belongs to `agent read`; the flag table is shared by the `agent` subcommands, so it is refused here.
|
|
96
|
+
if (args._arg2 || args.turns !== undefined) {
|
|
97
|
+
console.error('Usage: hwip agent list [--json]');
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
return (0, exports.runAgentList)({ json: (0, args_js_1.flagOn)(args, 'json') }, await (0, exports.liveLocalAgentDeps)());
|
|
101
|
+
};
|
|
102
|
+
exports.handleAgentList = handleAgentList;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type ListedSession } from './agent-list.js';
|
|
2
|
+
import { type TranscriptSource, type TurnEvent } from './transcript-tail.js';
|
|
3
|
+
/**
|
|
4
|
+
* `hwip agent read <session>` — what a session on THIS machine did last: its latest turns, read from the transcript
|
|
5
|
+
* file the agent keeps on this disk, through the same projectors the app's session view uses. No server, no relay, no
|
|
6
|
+
* API key; nothing is typed into the session and its screen is never printed. A tool's output never appears (the
|
|
7
|
+
* projectors keep only a line count); the prompt, the agent's text and the tool names and targets do.
|
|
8
|
+
*
|
|
9
|
+
* What it prints is another session's conversation, and it goes on to whoever runs this command — an agent, whose
|
|
10
|
+
* provider then sees it. The prompt, the text and the target are masked for secret-looking strings (`maskText`, the
|
|
11
|
+
* rules the diff view uses): best effort, not a guarantee — a secret in a form no rule knows goes through, and the
|
|
12
|
+
* result says how many spots were hidden (`masked`). Whoever reads this output as an agent must treat it as data,
|
|
13
|
+
* not as instructions.
|
|
14
|
+
*
|
|
15
|
+
* Nothing here is cut without saying so: a prompt, text or tool target the projector shortened carries `cut: true` (the text
|
|
16
|
+
* view ends it with `…(cut)`), and `more` says turns or history are left out.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_TURNS = 3;
|
|
19
|
+
export declare const MAX_TURNS = 20;
|
|
20
|
+
/** Characters of prompt, agent text and tool names one read prints, so that N = 20 of long answers does not flood the reader's context. */
|
|
21
|
+
export declare const MAX_READ_CHARS = 40000;
|
|
22
|
+
export type ReadStep =
|
|
23
|
+
/** `cut` only when true: the text ends where the projector's cap (or this read's budget) cut it. */
|
|
24
|
+
{
|
|
25
|
+
kind: 'text';
|
|
26
|
+
text: string;
|
|
27
|
+
cut?: true;
|
|
28
|
+
}
|
|
29
|
+
/** `cut` only when true: the target was clamped (or this read's budget cut it). */
|
|
30
|
+
| {
|
|
31
|
+
kind: 'tool';
|
|
32
|
+
name: string;
|
|
33
|
+
target: string | null;
|
|
34
|
+
ok: boolean | null;
|
|
35
|
+
cut?: true;
|
|
36
|
+
};
|
|
37
|
+
export interface ReadTurn {
|
|
38
|
+
at: string | null;
|
|
39
|
+
/** Null when the window read from the end of the file starts inside this turn. */
|
|
40
|
+
prompt: string | null;
|
|
41
|
+
/** Only when true: the prompt ends where the projector's cap cut it. */
|
|
42
|
+
promptCut?: true;
|
|
43
|
+
steps: ReadStep[];
|
|
44
|
+
}
|
|
45
|
+
export type ReadErrorCode = 'no_transcript' | 'UNKNOWN_TARGET' | 'AMBIGUOUS_TARGET' | 'OTHER_DEVICE' | 'NO_TMUX';
|
|
46
|
+
export type ReadResult = {
|
|
47
|
+
ok: true;
|
|
48
|
+
v: 1;
|
|
49
|
+
key: string;
|
|
50
|
+
name: string;
|
|
51
|
+
agent: string;
|
|
52
|
+
turns: ReadTurn[];
|
|
53
|
+
more: boolean;
|
|
54
|
+
masked: number;
|
|
55
|
+
} | {
|
|
56
|
+
ok: false;
|
|
57
|
+
code: ReadErrorCode;
|
|
58
|
+
message: string;
|
|
59
|
+
};
|
|
60
|
+
export interface AgentReadDeps {
|
|
61
|
+
device: () => string;
|
|
62
|
+
/** Names of the tmux sessions, or null when tmux did not answer. */
|
|
63
|
+
liveNames: () => string[] | null;
|
|
64
|
+
isSubagent: (name: string) => boolean;
|
|
65
|
+
/** The slow sweep (a screen capture per session); only for words that are not an exact session name. */
|
|
66
|
+
inventory: () => ListedSession[];
|
|
67
|
+
/** The agent kind running in the session's pane, from tmux alone. */
|
|
68
|
+
agentOf: (name: string) => string | null;
|
|
69
|
+
transcript: (name: string, onMiss: (reason: string) => void) => TranscriptSource | null;
|
|
70
|
+
/** The transcript's first read — the last `TRANSCRIPT_BACKFILL_BYTES` of a file, or the source reader's backfill — and its size; null when it cannot be read. */
|
|
71
|
+
readTail: (source: TranscriptSource) => {
|
|
72
|
+
lines: string[];
|
|
73
|
+
size: number;
|
|
74
|
+
} | null;
|
|
75
|
+
/** Awaited, so a large JSON line is flushed before the process exits. */
|
|
76
|
+
out: (text: string) => void | Promise<void>;
|
|
77
|
+
err: (line: string) => void;
|
|
78
|
+
}
|
|
79
|
+
/** Events into turns: a turn starts at each prompt. A tool and its result become one step, `ok` null until the result is seen. */
|
|
80
|
+
export declare const groupTurns: (events: readonly TurnEvent[]) => ReadTurn[];
|
|
81
|
+
/**
|
|
82
|
+
* The last `n` turns, within `maxChars` of text: the oldest turns go first, and the newest always stays. A newest turn that is
|
|
83
|
+
* over the budget on its own keeps its prompt and its latest steps; the step that crosses the line is cut at its end.
|
|
84
|
+
*/
|
|
85
|
+
export declare const limitTurns: (turns: ReadTurn[], n: number, maxChars?: number) => {
|
|
86
|
+
turns: ReadTurn[];
|
|
87
|
+
cut: boolean;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The prompt, the agent's text and the tool targets, masked — after `limitTurns`, so what is masked is what would be
|
|
91
|
+
* printed. The tool name is the agent's own vocabulary and is left as it is.
|
|
92
|
+
*/
|
|
93
|
+
export declare const maskTurns: (turns: readonly ReadTurn[]) => {
|
|
94
|
+
turns: ReadTurn[];
|
|
95
|
+
masked: number;
|
|
96
|
+
};
|
|
97
|
+
export declare const readLocalAgent: (deps: AgentReadDeps, target: string, turnsWanted: number) => ReadResult;
|
|
98
|
+
export declare const runAgentRead: (opts: {
|
|
99
|
+
session: string;
|
|
100
|
+
turns: number;
|
|
101
|
+
json: boolean;
|
|
102
|
+
}, deps: AgentReadDeps) => Promise<number>;
|
|
103
|
+
export declare const liveAgentReadDeps: () => Promise<AgentReadDeps>;
|
|
104
|
+
export declare const handleAgentRead: (args: Record<string, string | boolean>) => Promise<number>;
|
|
105
|
+
//# sourceMappingURL=agent-read.d.ts.map
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleAgentRead = exports.liveAgentReadDeps = exports.runAgentRead = exports.readLocalAgent = exports.maskTurns = exports.limitTurns = exports.groupTurns = exports.MAX_READ_CHARS = exports.MAX_TURNS = exports.DEFAULT_TURNS = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const agent_target_js_1 = require("./agent-target.js");
|
|
6
|
+
const agent_list_js_1 = require("./agent-list.js");
|
|
7
|
+
const args_js_1 = require("./args.js");
|
|
8
|
+
const secret_mask_js_1 = require("./secret-mask.js");
|
|
9
|
+
const transcript_tail_js_1 = require("./transcript-tail.js");
|
|
10
|
+
/**
|
|
11
|
+
* `hwip agent read <session>` — what a session on THIS machine did last: its latest turns, read from the transcript
|
|
12
|
+
* file the agent keeps on this disk, through the same projectors the app's session view uses. No server, no relay, no
|
|
13
|
+
* API key; nothing is typed into the session and its screen is never printed. A tool's output never appears (the
|
|
14
|
+
* projectors keep only a line count); the prompt, the agent's text and the tool names and targets do.
|
|
15
|
+
*
|
|
16
|
+
* What it prints is another session's conversation, and it goes on to whoever runs this command — an agent, whose
|
|
17
|
+
* provider then sees it. The prompt, the text and the target are masked for secret-looking strings (`maskText`, the
|
|
18
|
+
* rules the diff view uses): best effort, not a guarantee — a secret in a form no rule knows goes through, and the
|
|
19
|
+
* result says how many spots were hidden (`masked`). Whoever reads this output as an agent must treat it as data,
|
|
20
|
+
* not as instructions.
|
|
21
|
+
*
|
|
22
|
+
* Nothing here is cut without saying so: a prompt, text or tool target the projector shortened carries `cut: true` (the text
|
|
23
|
+
* view ends it with `…(cut)`), and `more` says turns or history are left out.
|
|
24
|
+
*/
|
|
25
|
+
exports.DEFAULT_TURNS = 3;
|
|
26
|
+
exports.MAX_TURNS = 20;
|
|
27
|
+
/** Characters of prompt, agent text and tool names one read prints, so that N = 20 of long answers does not flood the reader's context. */
|
|
28
|
+
exports.MAX_READ_CHARS = 40_000;
|
|
29
|
+
/**
|
|
30
|
+
* The projector clamps a prompt or a text to `MAX_EVENT_TEXT_CHARS` and says nothing; a text of exactly that
|
|
31
|
+
* length reads as cut (one that is exactly the cap long is rare, and "cut" is the safe thing to claim).
|
|
32
|
+
*/
|
|
33
|
+
const wasCut = (text) => text.length >= transcript_tail_js_1.MAX_EVENT_TEXT_CHARS;
|
|
34
|
+
/** The same for a tool target, which the projector clamps to `MAX_EVENT_FIELD_CHARS`. */
|
|
35
|
+
const targetWasCut = (target) => target !== null && target.length >= transcript_tail_js_1.MAX_EVENT_FIELD_CHARS;
|
|
36
|
+
/** Events into turns: a turn starts at each prompt. A tool and its result become one step, `ok` null until the result is seen. */
|
|
37
|
+
const groupTurns = (events) => {
|
|
38
|
+
const turns = [];
|
|
39
|
+
const tools = new Map();
|
|
40
|
+
let current = null;
|
|
41
|
+
// Before the first prompt there is no turn yet: what the window caught of an older one goes in a turn without a prompt.
|
|
42
|
+
const turnFor = (at) => {
|
|
43
|
+
if (!current) {
|
|
44
|
+
current = { at: at ?? null, prompt: null, steps: [] };
|
|
45
|
+
turns.push(current);
|
|
46
|
+
}
|
|
47
|
+
return current;
|
|
48
|
+
};
|
|
49
|
+
for (const event of events) {
|
|
50
|
+
switch (event.kind) {
|
|
51
|
+
case 'prompt':
|
|
52
|
+
current = { at: event.at ?? null, prompt: event.text, ...(wasCut(event.text) ? { promptCut: true } : {}), steps: [] };
|
|
53
|
+
turns.push(current);
|
|
54
|
+
break;
|
|
55
|
+
case 'text':
|
|
56
|
+
turnFor(event.at).steps.push({ kind: 'text', text: event.text, ...(wasCut(event.text) ? { cut: true } : {}) });
|
|
57
|
+
break;
|
|
58
|
+
case 'tool': {
|
|
59
|
+
const target = event.target ?? null;
|
|
60
|
+
const step = { kind: 'tool', name: event.name, target, ok: null, ...(targetWasCut(target) ? { cut: true } : {}) };
|
|
61
|
+
tools.set(event.id, step);
|
|
62
|
+
turnFor(event.at).steps.push(step);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case 'tool_result': {
|
|
66
|
+
const step = tools.get(event.id);
|
|
67
|
+
if (step)
|
|
68
|
+
step.ok = event.ok;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case 'msg':
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return turns;
|
|
76
|
+
};
|
|
77
|
+
exports.groupTurns = groupTurns;
|
|
78
|
+
/** What a step prints, in characters: a tool counts its name and target, so a turn of nothing but tool calls is bounded too. */
|
|
79
|
+
const stepChars = (step) => (step.kind === 'text' ? step.text.length : step.name.length + (step.target?.length ?? 0));
|
|
80
|
+
const textChars = (turn) => (turn.prompt?.length ?? 0) + turn.steps.reduce((sum, s) => sum + stepChars(s), 0);
|
|
81
|
+
/**
|
|
82
|
+
* The last `n` turns, within `maxChars` of text: the oldest turns go first, and the newest always stays. A newest turn that is
|
|
83
|
+
* over the budget on its own keeps its prompt and its latest steps; the step that crosses the line is cut at its end.
|
|
84
|
+
*/
|
|
85
|
+
const limitTurns = (turns, n, maxChars = exports.MAX_READ_CHARS) => {
|
|
86
|
+
let kept = turns.slice(-n);
|
|
87
|
+
let cut = kept.length < turns.length;
|
|
88
|
+
let total = kept.reduce((sum, t) => sum + textChars(t), 0);
|
|
89
|
+
while (kept.length > 1 && total > maxChars) {
|
|
90
|
+
total -= textChars(kept[0]);
|
|
91
|
+
kept = kept.slice(1);
|
|
92
|
+
cut = true;
|
|
93
|
+
}
|
|
94
|
+
const newest = kept[kept.length - 1];
|
|
95
|
+
if (newest && total > maxChars) {
|
|
96
|
+
let budget = Math.max(0, maxChars - (newest.prompt?.length ?? 0));
|
|
97
|
+
const steps = [];
|
|
98
|
+
for (let i = newest.steps.length - 1; i >= 0; i--) {
|
|
99
|
+
const step = newest.steps[i];
|
|
100
|
+
if (stepChars(step) <= budget) {
|
|
101
|
+
steps.push(step);
|
|
102
|
+
budget -= stepChars(step);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// The step that crosses the line: a text is cut at its end, a tool call is not shown half.
|
|
106
|
+
if (step.kind === 'text' && budget > 0)
|
|
107
|
+
steps.push({ kind: 'text', text: step.text.slice(0, budget), cut: true });
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
kept = [...kept.slice(0, -1), { ...newest, steps: steps.reverse() }];
|
|
111
|
+
cut = true;
|
|
112
|
+
}
|
|
113
|
+
return { turns: kept, cut };
|
|
114
|
+
};
|
|
115
|
+
exports.limitTurns = limitTurns;
|
|
116
|
+
/** A token cut by a cap can be a fragment the rules do not know; the last word goes with the cut. Bounded: a longer run is a blob, not a word. */
|
|
117
|
+
const MAX_FRAGMENT_CHARS = 300;
|
|
118
|
+
const MASK = '***';
|
|
119
|
+
const withoutCutFragment = (text) => {
|
|
120
|
+
let end = text.length;
|
|
121
|
+
while (end > 0 && text.length - end < MAX_FRAGMENT_CHARS && !/\s/.test(text[end - 1]))
|
|
122
|
+
end--;
|
|
123
|
+
return text.slice(0, end);
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* One field, masked. A cut one then loses the word the cut may have split — unless that word was masked: the rules
|
|
127
|
+
* read the fragment whole there, and trimming first would show the head of a long token (a JWT) that masking hides.
|
|
128
|
+
*/
|
|
129
|
+
const maskField = (text, cut) => {
|
|
130
|
+
const result = (0, secret_mask_js_1.maskText)(text);
|
|
131
|
+
return cut && !result.text.endsWith(MASK) ? { text: withoutCutFragment(result.text), masked: result.masked } : result;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* The prompt, the agent's text and the tool targets, masked — after `limitTurns`, so what is masked is what would be
|
|
135
|
+
* printed. The tool name is the agent's own vocabulary and is left as it is.
|
|
136
|
+
*/
|
|
137
|
+
const maskTurns = (turns) => {
|
|
138
|
+
let masked = 0;
|
|
139
|
+
const field = (text, cut) => {
|
|
140
|
+
const result = maskField(text, cut === true);
|
|
141
|
+
masked += result.masked;
|
|
142
|
+
return result.text;
|
|
143
|
+
};
|
|
144
|
+
const out = turns.map((turn) => ({
|
|
145
|
+
...turn,
|
|
146
|
+
prompt: turn.prompt === null ? null : field(turn.prompt, turn.promptCut),
|
|
147
|
+
steps: turn.steps.map((step) => step.kind === 'text' ? { ...step, text: field(step.text, step.cut) } : { ...step, target: step.target === null ? null : field(step.target, step.cut) }),
|
|
148
|
+
}));
|
|
149
|
+
return { turns: out, masked };
|
|
150
|
+
};
|
|
151
|
+
exports.maskTurns = maskTurns;
|
|
152
|
+
/** `<deviceId>:<tmuxName>`, the key `agent list` prints. A tmux name has no `:`, so a word with one is a key. */
|
|
153
|
+
const KEY_FORM = /^(dev_[A-Za-z0-9_-]+):(.+)$/;
|
|
154
|
+
const fail = (code, message) => ({ ok: false, code, message });
|
|
155
|
+
const readLocalAgent = (deps, target, turnsWanted) => {
|
|
156
|
+
const device = deps.device();
|
|
157
|
+
const asKey = KEY_FORM.exec(target);
|
|
158
|
+
if (asKey && asKey[1] !== device) {
|
|
159
|
+
return fail('OTHER_DEVICE', `${target} is on another machine; hwip agent read reads this machine's sessions only`);
|
|
160
|
+
}
|
|
161
|
+
// An exact session name needs no sweep: the sweep captures every pane (seconds), and only a label or an agent's own name needs it.
|
|
162
|
+
const exact = asKey ? asKey[2] : target;
|
|
163
|
+
let name;
|
|
164
|
+
let agent;
|
|
165
|
+
if (exact.startsWith('hwip-') && !deps.isSubagent(exact) && deps.liveNames()?.includes(exact)) {
|
|
166
|
+
name = exact;
|
|
167
|
+
agent = deps.agentOf(exact);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const rows = deps.inventory();
|
|
171
|
+
if (rows.length === 0 && deps.liveNames() === null)
|
|
172
|
+
return fail('NO_TMUX', agent_list_js_1.NO_TMUX_MESSAGE);
|
|
173
|
+
try {
|
|
174
|
+
const resolved = (0, agent_target_js_1.resolveAgentTarget)([{ deviceId: device, agentSessions: rows.map((r) => ({ name: r.name, label: r.label, providerSessionName: r.providerSessionName, parentName: r.parentName, agentKind: r.agentKind })) }], target);
|
|
175
|
+
name = resolved.name;
|
|
176
|
+
agent = rows.find((r) => r.name === name && (0, agent_target_js_1.isAgentTarget)(r))?.agentKind ?? null;
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
if (e instanceof agent_target_js_1.AgentTargetError)
|
|
180
|
+
return fail(e.code === 'AMBIGUOUS_TARGET' ? 'AMBIGUOUS_TARGET' : 'UNKNOWN_TARGET', e.message);
|
|
181
|
+
throw e;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let reason = 'no reason given';
|
|
185
|
+
const source = deps.transcript(name, (why) => {
|
|
186
|
+
reason = why;
|
|
187
|
+
});
|
|
188
|
+
if (!source)
|
|
189
|
+
return fail('no_transcript', `no transcript for ${name}: ${reason}`);
|
|
190
|
+
const tail = deps.readTail(source);
|
|
191
|
+
if (!tail)
|
|
192
|
+
return fail('no_transcript', `no transcript for ${name}: ${source.read ? 'its session store is unreadable' : 'the transcript file is unreadable'}`);
|
|
193
|
+
const limited = (0, exports.limitTurns)((0, exports.groupTurns)(source.project(tail.lines, {})), turnsWanted);
|
|
194
|
+
const safe = (0, exports.maskTurns)(limited.turns);
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
v: 1,
|
|
198
|
+
key: `${device}:${name}`,
|
|
199
|
+
name,
|
|
200
|
+
agent: agent ?? 'unknown',
|
|
201
|
+
turns: safe.turns,
|
|
202
|
+
more: limited.cut || tail.size > transcript_tail_js_1.TRANSCRIPT_BACKFILL_BYTES,
|
|
203
|
+
masked: safe.masked,
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
exports.readLocalAgent = readLocalAgent;
|
|
207
|
+
/** Text for a terminal: the transcript holds whatever the agent and the user typed, and an escape sequence in it would act on this terminal. */
|
|
208
|
+
// eslint-disable-next-line no-control-regex -- stripping them is the point
|
|
209
|
+
const printable = (value) => value.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, ' ');
|
|
210
|
+
/** Ends a prompt or text the projector's cap or this read's budget cut short. */
|
|
211
|
+
const CUT_MARK = '…(cut)';
|
|
212
|
+
const indent = (value) => printable(value).split('\n').map((line) => ` ${line}`).join('\n');
|
|
213
|
+
const renderRead = (result) => {
|
|
214
|
+
const lines = [`${result.key} (${printable(result.agent)})`];
|
|
215
|
+
result.turns.forEach((turn, i) => {
|
|
216
|
+
lines.push('', `── turn ${i + 1}/${result.turns.length}${turn.at ? ` ${printable(turn.at)}` : ''}`);
|
|
217
|
+
if (turn.prompt !== null) {
|
|
218
|
+
const [first = '', ...rest] = (printable(turn.prompt) + (turn.promptCut ? CUT_MARK : '')).split('\n');
|
|
219
|
+
lines.push(`> ${first}`, ...rest.map((line) => ` ${line}`));
|
|
220
|
+
}
|
|
221
|
+
for (const step of turn.steps) {
|
|
222
|
+
if (step.kind === 'text')
|
|
223
|
+
lines.push(indent(step.text + (step.cut ? CUT_MARK : '')));
|
|
224
|
+
else
|
|
225
|
+
lines.push(` · ${printable(step.name)}${step.target ? ` ${printable(step.target)}${step.cut ? CUT_MARK : ''}` : ''}${step.ok === true ? ' ✓' : step.ok === false ? ' ✗' : ''}`);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
if (result.turns.length === 0)
|
|
229
|
+
lines.push('', '(no turns in the part of the transcript read)');
|
|
230
|
+
if (result.more)
|
|
231
|
+
lines.push('', '(older history not shown)');
|
|
232
|
+
if (result.masked > 0)
|
|
233
|
+
lines.push('', `(${result.masked} secret-looking spot${result.masked === 1 ? '' : 's'} hidden as ***; masking is best effort)`);
|
|
234
|
+
return lines.join('\n');
|
|
235
|
+
};
|
|
236
|
+
const runAgentRead = async (opts, deps) => {
|
|
237
|
+
const result = (0, exports.readLocalAgent)(deps, opts.session, opts.turns);
|
|
238
|
+
if (!result.ok) {
|
|
239
|
+
if (opts.json)
|
|
240
|
+
await deps.out(JSON.stringify({ v: 1, error: result.code, message: result.message }));
|
|
241
|
+
else
|
|
242
|
+
deps.err(`hwip agent read: ${printable(result.message)}`); // it lists other sessions' names, which their agents chose
|
|
243
|
+
return 1;
|
|
244
|
+
}
|
|
245
|
+
if (opts.json) {
|
|
246
|
+
const { ok: _ok, ...contract } = result;
|
|
247
|
+
await deps.out(JSON.stringify(contract));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
await deps.out(renderRead(result));
|
|
251
|
+
}
|
|
252
|
+
return 0;
|
|
253
|
+
};
|
|
254
|
+
exports.runAgentRead = runAgentRead;
|
|
255
|
+
const USAGE = 'Usage: hwip agent read <session> [--turns <n>] [--json]';
|
|
256
|
+
/** The agent kind in the session's pane, from the same tmux fields and detector the listener's sweep uses. */
|
|
257
|
+
const paneAgentKind = async () => {
|
|
258
|
+
const { detectRemoteAgent, tmuxArgs, tmuxTargetFor } = await import('./listener.js');
|
|
259
|
+
const SEP = '␟';
|
|
260
|
+
return (name) => {
|
|
261
|
+
const target = tmuxTargetFor(name);
|
|
262
|
+
if (target === null)
|
|
263
|
+
return null;
|
|
264
|
+
const probe = (0, node_child_process_1.spawnSync)('tmux', tmuxArgs(['display-message', '-p', '-t', target, ['#{pane_current_command}', '#{pane_start_command}', '#{pane_current_path}', '#{pane_pid}'].join(SEP)]), {
|
|
265
|
+
encoding: 'utf-8',
|
|
266
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
267
|
+
});
|
|
268
|
+
if (probe.status !== 0)
|
|
269
|
+
return null;
|
|
270
|
+
const [current, start, path, pid] = (probe.stdout ?? '').trim().split(SEP);
|
|
271
|
+
const panePid = Number(pid);
|
|
272
|
+
return detectRemoteAgent({ currentCommand: current || null, startCommand: start || null, currentPath: path || null, panePid: Number.isInteger(panePid) && panePid > 0 ? panePid : null })?.kind ?? null;
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
const liveAgentReadDeps = async () => {
|
|
276
|
+
const local = await (0, agent_list_js_1.liveLocalAgentDeps)(); // quiets the listener's log first
|
|
277
|
+
const { isSubagentSessionName, liveSessionNames, resolveWatchTranscript } = await import('./listener.js');
|
|
278
|
+
return {
|
|
279
|
+
device: local.deviceId,
|
|
280
|
+
liveNames: liveSessionNames,
|
|
281
|
+
isSubagent: isSubagentSessionName,
|
|
282
|
+
inventory: local.collect,
|
|
283
|
+
agentOf: await paneAgentKind(),
|
|
284
|
+
transcript: resolveWatchTranscript,
|
|
285
|
+
readTail: (source) => {
|
|
286
|
+
const read = (0, transcript_tail_js_1.readSource)(source, null);
|
|
287
|
+
return read ? { lines: read.lines, size: read.state.size } : null;
|
|
288
|
+
},
|
|
289
|
+
out: agent_list_js_1.writeStdout,
|
|
290
|
+
err: (line) => console.error(line),
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
exports.liveAgentReadDeps = liveAgentReadDeps;
|
|
294
|
+
const handleAgentRead = async (args) => {
|
|
295
|
+
const session = typeof args._arg2 === 'string' ? args._arg2.trim() : '';
|
|
296
|
+
// A leading `-` would read as a flag to anything that passes the word on.
|
|
297
|
+
if (!session || session.startsWith('-')) {
|
|
298
|
+
console.error(USAGE);
|
|
299
|
+
return 2;
|
|
300
|
+
}
|
|
301
|
+
const wanted = args.turns === undefined ? String(exports.DEFAULT_TURNS) : args.turns;
|
|
302
|
+
const turns = typeof wanted === 'string' && /^\d{1,3}$/.test(wanted) ? Number(wanted) : NaN;
|
|
303
|
+
if (!(turns >= 1 && turns <= exports.MAX_TURNS)) {
|
|
304
|
+
console.error(`hwip agent read: --turns takes a whole number from 1 to ${exports.MAX_TURNS}\n${USAGE}`);
|
|
305
|
+
return 2;
|
|
306
|
+
}
|
|
307
|
+
return (0, exports.runAgentRead)({ session, turns, json: (0, args_js_1.flagOn)(args, 'json') }, await (0, exports.liveAgentReadDeps)());
|
|
308
|
+
};
|
|
309
|
+
exports.handleAgentRead = handleAgentRead;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type DetectionManifest } from './agent-state.js';
|
|
2
|
+
export declare const RULES_CACHE_FILE: string;
|
|
3
|
+
export declare const RULES_REFRESH_INTERVAL_MS: number;
|
|
4
|
+
/**
|
|
5
|
+
* Structural validation of an untrusted manifest. Engine-version gate
|
|
6
|
+
* included: a manifest authored for a future engine is rejected whole —
|
|
7
|
+
* partial interpretation of unknown semantics is worse than falling
|
|
8
|
+
* back to bundled rules.
|
|
9
|
+
*/
|
|
10
|
+
export declare const validateManifest: (value: unknown) => DetectionManifest | null;
|
|
11
|
+
/**
|
|
12
|
+
* Segment-wise numeric compare of dotted date versions
|
|
13
|
+
* ("2026.07.04.3"). Plain string compare fails on ".10" vs ".9";
|
|
14
|
+
* non-numeric segments compare as equal so a malformed version can
|
|
15
|
+
* never crash the refresh path.
|
|
16
|
+
*/
|
|
17
|
+
export declare const compareManifestVersions: (a: string, b: string) => number;
|
|
18
|
+
export type ManifestSource = 'remote' | 'cache' | 'bundled';
|
|
19
|
+
export declare const getActiveManifest: () => DetectionManifest;
|
|
20
|
+
export declare const getActiveManifestSource: () => ManifestSource;
|
|
21
|
+
/** Test hook. */
|
|
22
|
+
export declare const resetActiveManifest: () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Startup path (synchronous): promote the disk cache if it validates,
|
|
25
|
+
* else stay on bundled. Never throws — detection must work offline.
|
|
26
|
+
*/
|
|
27
|
+
export declare const loadManifestFromCache: () => ManifestSource;
|
|
28
|
+
/**
|
|
29
|
+
* Worker-thread path. A `worker_threads` Worker gets its own module instance,
|
|
30
|
+
* so `loadManifestFromCache` on the main thread leaves the worker on
|
|
31
|
+
* `DEFAULT_MANIFEST`, which would miss a fetched manifest's newer rules and its
|
|
32
|
+
* `disabledRuleIds` kill-switch for the life of the daemon (2026-09-14: a pi
|
|
33
|
+
* session whose pane the same rules classified `working` in-thread, back when
|
|
34
|
+
* the bundle carried claude rules only). Call this before each sweep: it
|
|
35
|
+
* reloads the cache only when the file changed, so the main thread's refresh
|
|
36
|
+
* reaches the worker on its next sweep without any message-protocol change.
|
|
37
|
+
*/
|
|
38
|
+
export declare const syncManifestFromCache: () => ManifestSource;
|
|
39
|
+
export declare const rulesUrl: () => string;
|
|
40
|
+
export interface RefreshResult {
|
|
41
|
+
source: ManifestSource;
|
|
42
|
+
/** 'updated' | 'not-modified' | 'stale-ignored' | 'invalid' | 'error' — for verbose logs. */
|
|
43
|
+
outcome: string;
|
|
44
|
+
version?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One refresh attempt: conditional GET, validate, persist, activate.
|
|
48
|
+
* All failure modes degrade to the current active manifest.
|
|
49
|
+
*/
|
|
50
|
+
export declare const refreshManifest: (fetchImpl?: typeof fetch) => Promise<RefreshResult>;
|
|
51
|
+
//# sourceMappingURL=agent-rules-fetch.d.ts.map
|