@hwip/cli 0.1.13 → 0.1.15
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/README.md +1 -1
- package/dist/agent-list.d.ts +23 -0
- package/dist/agent-list.js +63 -4
- package/dist/agent-read.js +6 -1
- package/dist/agent-target.d.ts +36 -2
- package/dist/agent-target.js +38 -5
- package/dist/args.js +1 -1
- package/dist/cli.js +17 -3
- package/dist/listener.d.ts +7 -0
- package/dist/listener.js +5 -1
- package/dist/remote-agents.d.ts +12 -0
- package/dist/remote-agents.js +8 -1
- package/dist/usage.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1321,7 +1321,7 @@ hwip notify --title "Hello" --json
|
|
|
1321
1321
|
| `dismiss <id>` | Dismiss a push (or `--all`) |
|
|
1322
1322
|
| `rename <name>` | Set the current agent session's display name in the app — run inside a `hwip cc` session, or name one with `--session <name>` (`--clear` resets). Auto-detects the tmux session + this machine's listener device id (`--device <id>` picks another machine), so the alias lands on the right device |
|
|
1323
1323
|
| `agent send <target> <message…>` | Type a message into another agent session — on this machine or another — as if the user sent it from the phone; it shows in that session's chat and puts it in remote mode. `target` is the `<deviceId>:<session>` key or a name/alias that names exactly one session (a name two machines share is refused, with both keys listed, and so is a session on a machine that is offline — its listener would never type it). The receiver reads `[from <your alias or tmux name>@<machine> · reply: <your key>]` first; outside tmux it is `[from cli@<host>]`, and from a tmux session the listener does not report, or from a subagent, it is `[from <session name>@<host>]` — neither carries a reply key. The CLI twin of the MCP `hwip_agent_send`, for agents without MCP (pi). Global flags (`--key`, `--base-url`) go before `agent` (or between `agent` and `send`); everything after the target is the message, verbatim, and a `--key` or `--base-url` placed there is refused (exit 2) rather than typed into the other session. A `-` as the message reads it from stdin: write it on a quoted heredoc (`<<'EOF'`), since inside double quotes the shell would run backticks or `$(…)` in it. The message is always sealed, for the target computer and the devices the account lists with a key (see [Sealed `hwip agent send`](#sealed-hwip-agent-send)); the target is pinned on the way, no pairing step. A target the account does not list with a key, or an unreadable command counter, is refused, never sent in the clear; an unreadable trust file only leaves the pins unchecked, and the send goes on. Exit 0 means the server stored the message, and the line it prints says so (`sent → <key> (stored; not confirmed typed)`): the target's listener can still refuse it (rate limit, a shell pane, a session gone since) and this command does not wait to hear of it. Exit 2 usage, 1 failed or refused, 3 no API key |
|
|
1324
|
-
| `agent list [--json]` | List the agent sessions running on THIS machine: key (`<deviceId>:<session>`, what `agent send` and `agent read` take), agent, state (`working`, `blocked`, `idle`, `unknown` — the screen matches no rule; `-` / `null` only when the sweep gave none) and name. `--json` prints one object, `{"v":1,"device":…,"sessions":[…]}`. It looks at tmux and the screens on this machine only — no server, no relay, no API key (it never exits 3) — and writes nothing, not even the listener's `known-sessions.json` (`$XDG_STATE_HOME/hwip/`), which only the listener refreshes. `--turns` is a usage error here (exit 2). Exit 0 listed (an empty list is not an error), 1 no tmux server answered. The CLI twin of the MCP `hwip_agent_list`. |
|
|
1324
|
+
| `agent list [--all] [--json]` | List the agent sessions running on THIS machine: key (`<deviceId>:<session>`, what `agent send` and `agent read` take), agent, state (`working`, `blocked`, `idle`, `unknown` — the screen matches no rule; `-` / `null` only when the sweep gave none) and name. `--json` prints one object, `{"v":1,"device":…,"sessions":[…]}`. It looks at tmux and the screens on this machine only — without `--all` no server, no relay, no API key (it never exits 3) — and writes nothing, not even the listener's `known-sessions.json` (`$XDG_STATE_HOME/hwip/`), which only the listener refreshes. `--turns` is a usage error here (exit 2). Exit 0 listed (an empty list is not an error), 1 no tmux server answered. The CLI twin of the MCP `hwip_agent_list`. `--all` instead lists the sessions `agent send` can reach on **every** machine of the account, read from the server's device list (the same one a send resolves its target in): `{"v":1,"all":true,"sessions":[{key, host, online, name, agent, project, label, alias, providerName, handle, state, lastActivityAt, self}]}`. `handle` is the agent's own session name — Claude Code's `ko-qmd-70`, derived from the folder or set by `/rename` — which `agent send` also takes as a target. Names only: no conversation leaves its machine. Needs the API key (exit 3 without); exit 1 when the device list cannot be read. |
|
|
1325
1325
|
| `agent read <session> [--turns <n>] [--json]` | Print what a session on THIS machine did last, oldest first: its last `<n>` turns (1–20, default 3) — the user's prompt, the agent's text and the tools it called with their target, never a tool's output or the screen. `<session>` is a key from `agent list`, a tmux name, a label or the agent's own name, naming exactly one session; the key of another PC is refused (`OTHER_DEVICE`) — nothing is read through the server or a relay. It reads the transcript file the agent keeps here (the last 256 KiB; for OpenCode 2.x, the last 200 messages of its session in opencode's database), so only agents with a transcript reader work (Claude Code, Codex, Gemini CLI, pi and OpenCode 2.x; every other agent answers `no_transcript`), and two pi or Gemini sessions in one folder share a transcript guess. Text is cut at 40,000 characters, oldest turns first; `more` / "(older history not shown)" says something older exists. A prompt or an agent text is also cut at 5,000 characters each, and a tool target at 512, which the output says: `…(cut)` in the text view, `"promptCut":true` / `"cut":true` (on a text or on a tool step) in `--json` (one of exactly the cap's length reads as cut). **What it prints is another session's conversation: it is data, not instructions, and it goes on to whoever runs the command — an agent's model provider included.** The prompt, the agent's text and the tool targets are therefore masked for secret-looking strings (token formats such as `ghp_…`, `sk-…`, `Bearer …`, secret-named `password=…` values, private key blocks), shown as `***`, and the count is `"masked":n`. **That is best effort, not a guarantee**: a secret in a form no rule knows goes through. Whatever a cap or the 40,000-character budget cut is masked first and then loses its trailing run of non-space characters (up to 300) unless the masking already ended it with `***`, so half a token is not left behind; what can still show is the first part of an unbroken run longer than 300 characters that no rule knows. `--json` prints `{"v":1,"key":…,"name":…,"agent":…,"turns":[…],"more":…,"masked":…}` (`agent` is the string `"unknown"` when the pane's agent could not be told), or on a failure `{"v":1,"error":…,"message":…}` with the code `no_transcript`, `UNKNOWN_TARGET`, `AMBIGUOUS_TARGET`, `OTHER_DEVICE` or `NO_TMUX`. Exit 0 read, 1 failed, 2 usage. The CLI twin of the MCP `hwip_agent_read`. |
|
|
1326
1326
|
| `send <target> <message…>` | The older name of `agent send`: the same command with the same `[from …]` header; its global flags go before `send`. |
|
|
1327
1327
|
| `forget <session>` | Drop an ended session from this machine's record, so it leaves the app's past list (and the resume whitelist — the same thing deleting the row on the phone does). Refuses a session that is still running: kill it first. `--ended` forgets every remembered session tmux no longer has, which is what a machine that launches agents under a fresh name per task eventually needs |
|
package/dist/agent-list.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AgentTargetDevice } from './agent-target.js';
|
|
1
2
|
import type { AgentState } from './agent-state.js';
|
|
2
3
|
/**
|
|
3
4
|
* `hwip agent list` — the agent sessions running on THIS machine. It looks at tmux
|
|
@@ -20,6 +21,7 @@ export interface ListedSession {
|
|
|
20
21
|
project: string;
|
|
21
22
|
label?: string | null;
|
|
22
23
|
providerSessionName?: string | null;
|
|
24
|
+
providerSessionHandle?: string | null;
|
|
23
25
|
state?: AgentState;
|
|
24
26
|
lastActivityAt?: string;
|
|
25
27
|
}
|
|
@@ -43,6 +45,8 @@ export interface AgentListEntry {
|
|
|
43
45
|
project: string;
|
|
44
46
|
label: string | null;
|
|
45
47
|
providerName: string | null;
|
|
48
|
+
/** The agent's own name for the session, shown or not (Claude Code's `ko-qmd-70`) — a name `agent send` takes. */
|
|
49
|
+
handle: string | null;
|
|
46
50
|
state: AgentState | null;
|
|
47
51
|
attached: boolean;
|
|
48
52
|
lastActivityAt: string | null;
|
|
@@ -63,6 +67,22 @@ export declare const listLocalAgents: (deps: LocalAgentDeps) => AgentListResult;
|
|
|
63
67
|
export declare const runAgentList: (opts: {
|
|
64
68
|
json: boolean;
|
|
65
69
|
}, deps: LocalAgentDeps) => Promise<number>;
|
|
70
|
+
/** What `hwip agent list --all` needs: the account's device list, and the key of the session it runs in. */
|
|
71
|
+
export interface AllAgentDeps {
|
|
72
|
+
/** `GET /devices`; throws when the list cannot be fetched. */
|
|
73
|
+
listDevices: () => Promise<AgentTargetDevice[]>;
|
|
74
|
+
selfKey: () => string | null;
|
|
75
|
+
out: (text: string) => void | Promise<void>;
|
|
76
|
+
err: (line: string) => void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* `hwip agent list --all` — every session `hwip agent send` could reach, on every
|
|
80
|
+
* machine the account lists, read from the server's device list (the same one a
|
|
81
|
+
* send resolves its target in). Names only: no conversation leaves its machine.
|
|
82
|
+
*/
|
|
83
|
+
export declare const runAgentListAll: (opts: {
|
|
84
|
+
json: boolean;
|
|
85
|
+
}, deps: AllAgentDeps) => Promise<number>;
|
|
66
86
|
/** Write to stdout and wait for it: `main` calls `process.exit` right after, which cuts off a pipe's tail. */
|
|
67
87
|
export declare const writeStdout: (text: string) => Promise<void>;
|
|
68
88
|
/**
|
|
@@ -73,4 +93,7 @@ export declare const writeStdout: (text: string) => Promise<void>;
|
|
|
73
93
|
*/
|
|
74
94
|
export declare const liveLocalAgentDeps: () => Promise<LocalAgentDeps>;
|
|
75
95
|
export declare const handleAgentList: (args: Record<string, string | boolean>) => Promise<number>;
|
|
96
|
+
export declare const handleAgentListAll: (args: Record<string, string | boolean>, hook: {
|
|
97
|
+
listDevices: () => Promise<AgentTargetDevice[]>;
|
|
98
|
+
}) => Promise<number>;
|
|
76
99
|
//# sourceMappingURL=agent-list.d.ts.map
|
package/dist/agent-list.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.handleAgentList = exports.liveLocalAgentDeps = exports.writeStdout = exports.runAgentList = exports.listLocalAgents = exports.NO_TMUX_MESSAGE = void 0;
|
|
3
|
+
exports.handleAgentListAll = exports.handleAgentList = exports.liveLocalAgentDeps = exports.writeStdout = exports.runAgentListAll = exports.runAgentList = exports.listLocalAgents = exports.NO_TMUX_MESSAGE = void 0;
|
|
4
4
|
const args_js_1 = require("./args.js");
|
|
5
5
|
const agent_session_js_1 = require("./agent-session.js");
|
|
6
6
|
const agent_target_js_1 = require("./agent-target.js");
|
|
@@ -19,6 +19,7 @@ const listLocalAgents = (deps) => {
|
|
|
19
19
|
project: s.project,
|
|
20
20
|
label: s.label ?? null,
|
|
21
21
|
providerName: s.providerSessionName ?? null,
|
|
22
|
+
handle: s.providerSessionHandle ?? null,
|
|
22
23
|
state: s.state ?? null,
|
|
23
24
|
attached: s.attached,
|
|
24
25
|
lastActivityAt: s.lastActivityAt ?? null,
|
|
@@ -31,9 +32,12 @@ const listLocalAgents = (deps) => {
|
|
|
31
32
|
return { ok: true, v: 1, device, sessions };
|
|
32
33
|
};
|
|
33
34
|
exports.listLocalAgents = listLocalAgents;
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* The agent names its session itself (`/rename`), so the text may hold escape sequences a terminal would act on, or bidi
|
|
37
|
+
* overrides and zero-width characters that make one name read as another — the set the server's `oneLine` strips.
|
|
38
|
+
*/
|
|
35
39
|
// eslint-disable-next-line no-control-regex -- stripping them is the point
|
|
36
|
-
const printable = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ');
|
|
40
|
+
const printable = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f---]/g, ' ');
|
|
37
41
|
const renderList = (sessions) => {
|
|
38
42
|
if (sessions.length === 0)
|
|
39
43
|
return 'no agent sessions on this machine';
|
|
@@ -58,6 +62,45 @@ const runAgentList = async (opts, deps) => {
|
|
|
58
62
|
return 0;
|
|
59
63
|
};
|
|
60
64
|
exports.runAgentList = runAgentList;
|
|
65
|
+
const renderAll = (rows) => {
|
|
66
|
+
if (rows.length === 0)
|
|
67
|
+
return 'no agent sessions on any machine';
|
|
68
|
+
const nameOf = (r) => {
|
|
69
|
+
const shown = r.alias ?? r.providerName ?? r.label ?? '-';
|
|
70
|
+
const handle = r.handle && r.handle !== shown ? ` [${r.handle}]` : '';
|
|
71
|
+
return printable(`${shown}${handle}${r.online ? '' : ' (offline)'}${r.self ? ' (this session)' : ''}`);
|
|
72
|
+
};
|
|
73
|
+
// Every cell is another machine's report, the key and agent kind included.
|
|
74
|
+
const table = [['KEY', 'HOST', 'AGENT', 'STATE', 'NAME'], ...rows.map((r) => [printable(r.key), printable(r.host), printable(r.agent ?? '-'), printable(r.state ?? '-'), nameOf(r)])];
|
|
75
|
+
const widths = [0, 1, 2, 3].map((col) => Math.max(...table.map((r) => r[col].length)));
|
|
76
|
+
return table.map((r) => [...widths.map((w, col) => r[col].padEnd(w)), r[4]].join(' ')).join('\n');
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* `hwip agent list --all` — every session `hwip agent send` could reach, on every
|
|
80
|
+
* machine the account lists, read from the server's device list (the same one a
|
|
81
|
+
* send resolves its target in). Names only: no conversation leaves its machine.
|
|
82
|
+
*/
|
|
83
|
+
const runAgentListAll = async (opts, deps) => {
|
|
84
|
+
let rows;
|
|
85
|
+
try {
|
|
86
|
+
// No session key (outside tmux): there is no own session, so no row is `self` — as in the local list.
|
|
87
|
+
rows = (0, agent_target_js_1.listAgentTargets)(await deps.listDevices(), deps.selfKey() ?? undefined);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
const message = `could not read the device list: ${e instanceof Error ? e.message : String(e)}`;
|
|
91
|
+
if (opts.json)
|
|
92
|
+
await deps.out(JSON.stringify({ v: 1, error: 'LIST_FAILED', message }));
|
|
93
|
+
else
|
|
94
|
+
deps.err(`hwip agent list --all: ${message}`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
if (opts.json)
|
|
98
|
+
await deps.out(JSON.stringify({ v: 1, all: true, sessions: rows }));
|
|
99
|
+
else
|
|
100
|
+
await deps.out(renderAll(rows));
|
|
101
|
+
return 0;
|
|
102
|
+
};
|
|
103
|
+
exports.runAgentListAll = runAgentListAll;
|
|
61
104
|
/** Write to stdout and wait for it: `main` calls `process.exit` right after, which cuts off a pipe's tail. */
|
|
62
105
|
const writeStdout = (text) => new Promise((resolve) => {
|
|
63
106
|
process.stdout.write(`${text}\n`, () => resolve());
|
|
@@ -94,9 +137,25 @@ exports.liveLocalAgentDeps = liveLocalAgentDeps;
|
|
|
94
137
|
const handleAgentList = async (args) => {
|
|
95
138
|
// `--turns` belongs to `agent read`; the flag table is shared by the `agent` subcommands, so it is refused here.
|
|
96
139
|
if (args._arg2 || args.turns !== undefined) {
|
|
97
|
-
console.error('Usage: hwip agent list [--json]');
|
|
140
|
+
console.error('Usage: hwip agent list [--all] [--json]');
|
|
98
141
|
return 2;
|
|
99
142
|
}
|
|
100
143
|
return (0, exports.runAgentList)({ json: (0, args_js_1.flagOn)(args, 'json') }, await (0, exports.liveLocalAgentDeps)());
|
|
101
144
|
};
|
|
102
145
|
exports.handleAgentList = handleAgentList;
|
|
146
|
+
const handleAgentListAll = async (args, hook) => {
|
|
147
|
+
if (args._arg2 || args.turns !== undefined) {
|
|
148
|
+
console.error('Usage: hwip agent list [--all] [--json]');
|
|
149
|
+
return 2;
|
|
150
|
+
}
|
|
151
|
+
return (0, exports.runAgentListAll)({ json: (0, args_js_1.flagOn)(args, 'json') }, {
|
|
152
|
+
listDevices: () => hook.listDevices(),
|
|
153
|
+
selfKey: () => {
|
|
154
|
+
const context = (0, agent_session_js_1.agentSessionContext)();
|
|
155
|
+
return context ? `${context.agentDeviceId}:${context.agentSessionName}` : null;
|
|
156
|
+
},
|
|
157
|
+
out: exports.writeStdout,
|
|
158
|
+
err: (line) => console.error(line),
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
exports.handleAgentListAll = handleAgentListAll;
|
package/dist/agent-read.js
CHANGED
|
@@ -171,7 +171,7 @@ const readLocalAgent = (deps, target, turnsWanted) => {
|
|
|
171
171
|
if (rows.length === 0 && deps.liveNames() === null)
|
|
172
172
|
return fail('NO_TMUX', agent_list_js_1.NO_TMUX_MESSAGE);
|
|
173
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);
|
|
174
|
+
const resolved = (0, agent_target_js_1.resolveAgentTarget)([{ deviceId: device, agentSessions: rows.map((r) => ({ name: r.name, label: r.label, providerSessionName: r.providerSessionName, providerSessionHandle: r.providerSessionHandle, state: r.state, parentName: r.parentName, agentKind: r.agentKind })) }], target);
|
|
175
175
|
name = resolved.name;
|
|
176
176
|
agent = rows.find((r) => r.name === name && (0, agent_target_js_1.isAgentTarget)(r))?.agentKind ?? null;
|
|
177
177
|
}
|
|
@@ -298,6 +298,11 @@ const handleAgentRead = async (args) => {
|
|
|
298
298
|
console.error(USAGE);
|
|
299
299
|
return 2;
|
|
300
300
|
}
|
|
301
|
+
// `--all` is `agent list`'s: the `agent` flag table is shared, and another machine's conversation stays there.
|
|
302
|
+
if ((0, args_js_1.flagOn)(args, 'all')) {
|
|
303
|
+
console.error(`hwip agent read: --all is for agent list; agent read reads this machine only\n${USAGE}`);
|
|
304
|
+
return 2;
|
|
305
|
+
}
|
|
301
306
|
const wanted = args.turns === undefined ? String(exports.DEFAULT_TURNS) : args.turns;
|
|
302
307
|
const turns = typeof wanted === 'string' && /^\d{1,3}$/.test(wanted) ? Number(wanted) : NaN;
|
|
303
308
|
if (!(turns >= 1 && turns <= exports.MAX_TURNS)) {
|
package/dist/agent-target.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export interface AgentTargetDevice {
|
|
|
8
8
|
project?: string;
|
|
9
9
|
label?: string | null;
|
|
10
10
|
providerSessionName?: string | null;
|
|
11
|
+
/** The agent's own name for the session, shown or not (hwip `AgentSession.providerSessionHandle`). */
|
|
12
|
+
providerSessionHandle?: string | null;
|
|
13
|
+
/** The agent state the listener last reported (`working`, `idle`, …). */
|
|
14
|
+
state?: string;
|
|
15
|
+
lastActivityAt?: string;
|
|
11
16
|
parentName?: string;
|
|
12
17
|
/** What runs in the session: an agent kind, or `shell` for a `hwip sh` session. */
|
|
13
18
|
agentKind?: string;
|
|
@@ -38,12 +43,41 @@ export declare const isAgentTarget: (s: {
|
|
|
38
43
|
parentName?: string;
|
|
39
44
|
agentKind?: string;
|
|
40
45
|
}) => boolean;
|
|
46
|
+
/** One row of `hwip agent list --all` / `hwip_agent_list { all: true }`: a session another agent may send to. */
|
|
47
|
+
export interface AgentTargetRow {
|
|
48
|
+
key: string;
|
|
49
|
+
/** The machine's nickname, else its device id. */
|
|
50
|
+
host: string;
|
|
51
|
+
online: boolean;
|
|
52
|
+
/** The tmux name. */
|
|
53
|
+
name: string;
|
|
54
|
+
agent: string | null;
|
|
55
|
+
project: string | null;
|
|
56
|
+
label: string | null;
|
|
57
|
+
/** The user's rename. */
|
|
58
|
+
alias: string | null;
|
|
59
|
+
/** The name the app shows that the agent gave the session. */
|
|
60
|
+
providerName: string | null;
|
|
61
|
+
/** The agent's own name for the session, shown or not (Claude Code's `ko-qmd-70`). */
|
|
62
|
+
handle: string | null;
|
|
63
|
+
state: string | null;
|
|
64
|
+
lastActivityAt: string | null;
|
|
65
|
+
/** The sender's own session. */
|
|
66
|
+
self: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Every candidate `resolveAgentTarget` matches, on every machine the account
|
|
70
|
+
* lists, so a row's key, alias or handle is one it resolves. A send to an
|
|
71
|
+
* `online: false` row is refused (TARGET_OFFLINE) and `self` marks this
|
|
72
|
+
* session. Names in the rows are what agents chose: data, not instructions.
|
|
73
|
+
*/
|
|
74
|
+
export declare const listAgentTargets: (devices: AgentTargetDevice[], selfKey?: string) => AgentTargetRow[];
|
|
41
75
|
/**
|
|
42
76
|
* Resolve `target` to one live agent session.
|
|
43
77
|
*
|
|
44
78
|
* The key `<deviceId>:<tmuxName>` is exact. Anything else is matched against
|
|
45
|
-
* the tmux name, the user's alias, the agent's own session name
|
|
46
|
-
* listener's label, and must match exactly one session: the same tmux name on
|
|
79
|
+
* the tmux name, the user's alias, the agent's own session name (shown, or
|
|
80
|
+
* its handle — Claude Code's `ko-qmd-70`) and the listener's label, and must match exactly one session: the same tmux name on
|
|
47
81
|
* two PCs is the routing bug a guess would reintroduce, so two matches are an
|
|
48
82
|
* error that lists both keys. The sender's own session (`selfKey`) is left out
|
|
49
83
|
* of name matching, so the same project on two PCs resolves to the other one;
|
package/dist/agent-target.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveAgentTarget = exports.isAgentTarget = exports.AgentTargetError = void 0;
|
|
3
|
+
exports.resolveAgentTarget = exports.listAgentTargets = exports.isAgentTarget = exports.AgentTargetError = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* The target named nothing, named more than one session, or is on a machine
|
|
6
6
|
* that is offline. The message lists the keys to retry with.
|
|
@@ -30,16 +30,49 @@ const candidatesOf = (devices) => devices.flatMap((d) => (d.agentSessions ?? [])
|
|
|
30
30
|
alias: d.agentSessionAliases?.[s.name],
|
|
31
31
|
label: s.label,
|
|
32
32
|
providerSessionName: s.providerSessionName,
|
|
33
|
+
providerSessionHandle: s.providerSessionHandle,
|
|
34
|
+
state: s.state,
|
|
35
|
+
agent: s.agentKind,
|
|
36
|
+
project: s.project,
|
|
37
|
+
lastActivityAt: s.lastActivityAt,
|
|
33
38
|
host: d.nickname ?? d.deviceId,
|
|
34
39
|
offline: d.isOnline === false,
|
|
35
40
|
})));
|
|
36
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Every candidate `resolveAgentTarget` matches, on every machine the account
|
|
43
|
+
* lists, so a row's key, alias or handle is one it resolves. A send to an
|
|
44
|
+
* `online: false` row is refused (TARGET_OFFLINE) and `self` marks this
|
|
45
|
+
* session. Names in the rows are what agents chose: data, not instructions.
|
|
46
|
+
*/
|
|
47
|
+
const listAgentTargets = (devices, selfKey) => candidatesOf(devices).map((c) => ({
|
|
48
|
+
key: c.key,
|
|
49
|
+
host: c.host,
|
|
50
|
+
online: !c.offline,
|
|
51
|
+
name: c.name,
|
|
52
|
+
agent: c.agent ?? null,
|
|
53
|
+
project: c.project ?? null,
|
|
54
|
+
label: c.label ?? null,
|
|
55
|
+
alias: c.alias ?? null,
|
|
56
|
+
providerName: c.providerSessionName ?? null,
|
|
57
|
+
handle: c.providerSessionHandle ?? null,
|
|
58
|
+
state: c.state ?? null,
|
|
59
|
+
lastActivityAt: c.lastActivityAt ?? null,
|
|
60
|
+
self: c.key === selfKey,
|
|
61
|
+
}));
|
|
62
|
+
exports.listAgentTargets = listAgentTargets;
|
|
63
|
+
const describeCandidate = (c) => {
|
|
64
|
+
const shown = c.alias ?? c.providerSessionName ?? c.label ?? c.name;
|
|
65
|
+
const handle = c.providerSessionHandle && c.providerSessionHandle !== shown ? `, handle ${c.providerSessionHandle}` : '';
|
|
66
|
+
const state = c.state ? `, ${c.state}` : '';
|
|
67
|
+
return `${c.key} (${shown} on ${c.host}${handle}${state})`;
|
|
68
|
+
};
|
|
69
|
+
const listCandidates = (list) => list.map(describeCandidate).join(', ') || 'none';
|
|
37
70
|
/**
|
|
38
71
|
* Resolve `target` to one live agent session.
|
|
39
72
|
*
|
|
40
73
|
* The key `<deviceId>:<tmuxName>` is exact. Anything else is matched against
|
|
41
|
-
* the tmux name, the user's alias, the agent's own session name
|
|
42
|
-
* listener's label, and must match exactly one session: the same tmux name on
|
|
74
|
+
* the tmux name, the user's alias, the agent's own session name (shown, or
|
|
75
|
+
* its handle — Claude Code's `ko-qmd-70`) and the listener's label, and must match exactly one session: the same tmux name on
|
|
43
76
|
* two PCs is the routing bug a guess would reintroduce, so two matches are an
|
|
44
77
|
* error that lists both keys. The sender's own session (`selfKey`) is left out
|
|
45
78
|
* of name matching, so the same project on two PCs resolves to the other one;
|
|
@@ -63,7 +96,7 @@ const resolveAgentTarget = (devices, target, selfKey) => {
|
|
|
63
96
|
const exact = all.find((c) => c.key === target);
|
|
64
97
|
if (exact)
|
|
65
98
|
return toTarget(exact);
|
|
66
|
-
const named = (c) => [c.name, c.alias, c.providerSessionName, c.label].some((n) => n === target);
|
|
99
|
+
const named = (c) => [c.name, c.alias, c.providerSessionName, c.providerSessionHandle, c.label].some((n) => n === target);
|
|
67
100
|
const others = all.filter((c) => c.key !== selfKey);
|
|
68
101
|
const matches = others.filter(named);
|
|
69
102
|
if (matches.length === 1)
|
package/dist/args.js
CHANGED
|
@@ -188,7 +188,7 @@ exports.COMMAND_FLAGS = {
|
|
|
188
188
|
ask: ['title', 'body', 'actions', 'timeout', 'hook', 'accepts-exit', 'api-key'],
|
|
189
189
|
'ask-context': ['global'],
|
|
190
190
|
'file-view': ['global'],
|
|
191
|
-
agent: ['json', 'turns'],
|
|
191
|
+
agent: ['json', 'turns', 'all'],
|
|
192
192
|
send: [],
|
|
193
193
|
list: ['limit', 'type'],
|
|
194
194
|
dismiss: ['all'],
|
package/dist/cli.js
CHANGED
|
@@ -487,7 +487,8 @@ const handleNotify = async (args) => {
|
|
|
487
487
|
// dial now works for every hook-driven agent. Gated-out → silent success.
|
|
488
488
|
// With no dial the mode falls back to --pushmode-default, then to quiet.
|
|
489
489
|
// Quiet still pushes when the user has left the terminal (presence.ts).
|
|
490
|
-
|
|
490
|
+
const isAuto = (0, args_js_1.flagOn)(args, 'auto');
|
|
491
|
+
if (isAuto) {
|
|
491
492
|
const marker = (0, gate_js_1.normalizeMarker)(typeof args.marker === 'string' ? args.marker : undefined);
|
|
492
493
|
const pushMode = (0, gate_js_1.autoPushMode)(projectDir, args[gate_js_1.PUSHMODE_DEFAULT_FLAG]);
|
|
493
494
|
const verdict = (0, gate_js_1.decidePush)({
|
|
@@ -505,7 +506,7 @@ const handleNotify = async (args) => {
|
|
|
505
506
|
}
|
|
506
507
|
// Past the gates, this IS a push attempt — its outcome is recorded for
|
|
507
508
|
// `hwip verify` and the plugin (notify-failure.ts), since hooks discard stderr.
|
|
508
|
-
const hook = createHook(args,
|
|
509
|
+
const hook = createHook(args, isAuto ? HOOK_REQUEST_TIMEOUT_MS : undefined);
|
|
509
510
|
if (!hook) {
|
|
510
511
|
(0, notify_failure_js_1.recordNotifyFailure)({ kind: 'no-key', status: null, code: null });
|
|
511
512
|
return 3;
|
|
@@ -532,10 +533,11 @@ const handleNotify = async (args) => {
|
|
|
532
533
|
targetDeviceId: args.device,
|
|
533
534
|
sessionId,
|
|
534
535
|
}));
|
|
536
|
+
// Hook mode: stdout is the hook's response (Codex parses it as JSON), so stay silent.
|
|
535
537
|
if (isJson) {
|
|
536
538
|
printJson({ pushId: result.pushId, status: 'ok' });
|
|
537
539
|
}
|
|
538
|
-
else {
|
|
540
|
+
else if (!isAuto) {
|
|
539
541
|
console.log(`Push sent: ${result.pushId}`);
|
|
540
542
|
}
|
|
541
543
|
return 0;
|
|
@@ -748,6 +750,13 @@ const main = async () => {
|
|
|
748
750
|
// `hwip agent send` is the name (C82); `hwip send` is the older one for the same command.
|
|
749
751
|
case 'agent':
|
|
750
752
|
case 'send': {
|
|
753
|
+
if (command === 'agent' && args._arg1 === 'list' && (0, args_js_1.flagOn)(args, 'all')) {
|
|
754
|
+
// Every machine is the server's device list: this one needs the API key.
|
|
755
|
+
const hook = createHook(args);
|
|
756
|
+
if (!hook)
|
|
757
|
+
return 3;
|
|
758
|
+
return (await import('./agent-list.js')).handleAgentListAll(args, hook);
|
|
759
|
+
}
|
|
751
760
|
// Read-only and local: before `createHook`, so no API key is needed (no exit 3).
|
|
752
761
|
if (command === 'agent' && args._arg1 === 'list')
|
|
753
762
|
return (await import('./agent-list.js')).handleAgentList(args);
|
|
@@ -760,6 +769,11 @@ const main = async () => {
|
|
|
760
769
|
console.error(unknown);
|
|
761
770
|
return 2;
|
|
762
771
|
}
|
|
772
|
+
// `--all` and `--turns` are agent list's and agent read's: the `agent` flag table is shared, so send refuses them rather than ignore them.
|
|
773
|
+
if (command === 'agent' && ((0, args_js_1.flagOn)(args, 'all') || args.turns !== undefined)) {
|
|
774
|
+
console.error(`hwip ${named}: --all and --turns are for agent list / agent read`);
|
|
775
|
+
return 2;
|
|
776
|
+
}
|
|
763
777
|
const send = parseSendArgs(sendArgv, sendReadsStdin(sendArgv) ? await readStdin() : undefined, named);
|
|
764
778
|
if ('error' in send) {
|
|
765
779
|
console.error(send.error);
|
package/dist/listener.d.ts
CHANGED
|
@@ -128,6 +128,13 @@ interface AgentSession {
|
|
|
128
128
|
* account setting that hides agent-made names hides `'auto'` only.
|
|
129
129
|
*/
|
|
130
130
|
providerSessionNameSource?: ProviderNameSource;
|
|
131
|
+
/**
|
|
132
|
+
* The name the agent itself addresses this session by, whoever chose it
|
|
133
|
+
* (`RemoteAgent.resolveSessionHandle`) — Claude Code's `ko-qmd-70` even
|
|
134
|
+
* when `providerSessionName` withholds it. Not shown in the app; another agent
|
|
135
|
+
* sends to the session by it. Null where the agent has none.
|
|
136
|
+
*/
|
|
137
|
+
providerSessionHandle?: string | null;
|
|
131
138
|
createdAt?: string;
|
|
132
139
|
/**
|
|
133
140
|
* Which run of the name this is (hwip docs/session-runs.md): tmux
|
package/dist/listener.js
CHANGED
|
@@ -333,7 +333,7 @@ exports.knownSessionsFingerprint = knownSessionsFingerprint;
|
|
|
333
333
|
const sessionsFingerprint = (sessions) => sessions
|
|
334
334
|
.map((s) => JSON.stringify([
|
|
335
335
|
s.name, s.attached, s.agentKind, s.agentSessionId ?? null,
|
|
336
|
-
s.project, s.label ?? null, s.providerSessionName ?? null, s.providerSessionNameSource ?? null, s.createdAt ?? null, s.runId ?? null, s.firstSeq ?? null,
|
|
336
|
+
s.project, s.label ?? null, s.providerSessionName ?? null, s.providerSessionNameSource ?? null, s.providerSessionHandle ?? null, s.createdAt ?? null, s.runId ?? null, s.firstSeq ?? null,
|
|
337
337
|
s.lastActivityAt ?? null, s.state ?? null,
|
|
338
338
|
s.stateChangedAt ?? null, s.stateRuleId ?? null,
|
|
339
339
|
s.blockedPrompt?.id ?? null, s.blockedPrompt?.at ?? null,
|
|
@@ -3634,6 +3634,9 @@ const collectSessionsVerbose = () => {
|
|
|
3634
3634
|
const providerName = info.currentPath
|
|
3635
3635
|
? (main.agent.resolveSessionName?.(info.currentPath, info.panePid ?? undefined) ?? null)
|
|
3636
3636
|
: null;
|
|
3637
|
+
const handle = info.currentPath
|
|
3638
|
+
? (main.agent.resolveSessionHandle?.(info.currentPath, info.panePid ?? undefined) ?? null)
|
|
3639
|
+
: null;
|
|
3637
3640
|
if (info.currentPath)
|
|
3638
3641
|
paneCwdOf.set(name, info.currentPath);
|
|
3639
3642
|
const resumeId = info.currentPath
|
|
@@ -3651,6 +3654,7 @@ const collectSessionsVerbose = () => {
|
|
|
3651
3654
|
label: parsed.label,
|
|
3652
3655
|
providerSessionName: providerName?.name ?? null,
|
|
3653
3656
|
...(providerName?.source ? { providerSessionNameSource: providerName.source } : {}),
|
|
3657
|
+
providerSessionHandle: handle,
|
|
3654
3658
|
createdAt: epochToIso(group.created),
|
|
3655
3659
|
runId: (0, exports.runIdOfEpoch)(group.created),
|
|
3656
3660
|
lastActivityAt: epochToIso(group.activity),
|
package/dist/remote-agents.d.ts
CHANGED
|
@@ -63,6 +63,15 @@ export interface RemoteAgent {
|
|
|
63
63
|
* the user setting that hides agent-made names hides `'auto'` only.
|
|
64
64
|
*/
|
|
65
65
|
resolveSessionName?: (paneCwd: string, panePid?: number) => ProviderName | null;
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the name the agent itself addresses this session by, whoever
|
|
68
|
+
* chose it — Claude Code's session name, cwd-derived (`ko-qmd-70`) or
|
|
69
|
+
* renamed. Not shown in the app: `resolveSessionName` withholds a derived name as
|
|
70
|
+
* saying no more than the tmux name, but it is the name the user knows the
|
|
71
|
+
* session by in the agent, so another agent may be told to send to it
|
|
72
|
+
* (`providerSessionHandle` on the wire, matched by `resolveAgentTarget`).
|
|
73
|
+
*/
|
|
74
|
+
resolveSessionHandle?: (paneCwd: string, panePid?: number) => string | null;
|
|
66
75
|
/**
|
|
67
76
|
* Resolve the file this agent is writing its session transcript to, for the
|
|
68
77
|
* live chat timeline (`turn-watch`).
|
|
@@ -232,6 +241,8 @@ export declare const detectClaudeSessionIdByPid: (panePid: number, paneCwd: stri
|
|
|
232
241
|
export declare const detectClaudeProviderName: (panePid: number, paneCwd: string | null, deps?: PidResolveDeps & {
|
|
233
242
|
transcriptOf?: (cwd: string, sessionId: string) => string | null;
|
|
234
243
|
}) => ProviderName | null;
|
|
244
|
+
/** Claude Code's own name for the session in this pane, whoever chose it — see `RemoteAgent.resolveSessionHandle`. */
|
|
245
|
+
export declare const detectClaudeSessionHandle: (panePid: number, paneCwd: string | null, deps?: PidResolveDeps) => string | null;
|
|
235
246
|
/**
|
|
236
247
|
* The transcript file for the Claude Code session running in a tmux pane.
|
|
237
248
|
*
|
|
@@ -367,6 +378,7 @@ declare const REMOTE_AGENT_TABLE: readonly [{
|
|
|
367
378
|
readonly quitCommand: "/exit";
|
|
368
379
|
readonly resolveSessionId: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
369
380
|
readonly resolveSessionName: (paneCwd: string, panePid: number | undefined) => ProviderName | null;
|
|
381
|
+
readonly resolveSessionHandle: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
370
382
|
readonly resolveTranscript: (paneCwd: string, panePid: number | undefined) => string | null;
|
|
371
383
|
readonly projectTranscript: TranscriptProjector;
|
|
372
384
|
readonly resolveResumeId: (paneCwd: string, panePid: number | undefined) => string | null;
|
package/dist/remote-agents.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.matchAgentByPaneCommand = exports.findAgentBySubcommand = exports.REMOTE_AGENTS = exports.opencodeTranscriptPath = exports.detectOpencodeSession = exports.geminiTranscriptPath = exports.codexTranscriptPath = exports.detectCodexSessionName = exports.newestVersionedDb = exports.detectHermesResumeId = exports.detectHermesSessionName = exports.sqliteJson = exports.transcriptIdSince = exports.paneSince = exports.pickRowByProcStart = exports.detectPiSessionName = exports.piTranscriptPath = exports.claudeTranscriptPath = exports.detectClaudeProviderName = exports.detectClaudeSessionIdByPid = exports.detectClaudeSessionByPid = exports.pidSessionRecordsExist = exports.collectDescendantPids = exports.psStartTimes = exports.foregroundAgentFor = exports.readProcTable = exports.parseProcTable = exports.detectClaudeSessionId = exports.claudeProjectDirName = void 0;
|
|
3
|
+
exports.matchAgentByPaneCommand = exports.findAgentBySubcommand = exports.REMOTE_AGENTS = exports.opencodeTranscriptPath = exports.detectOpencodeSession = exports.geminiTranscriptPath = exports.codexTranscriptPath = exports.detectCodexSessionName = exports.newestVersionedDb = exports.detectHermesResumeId = exports.detectHermesSessionName = exports.sqliteJson = exports.transcriptIdSince = exports.paneSince = exports.pickRowByProcStart = exports.detectPiSessionName = exports.piTranscriptPath = exports.claudeTranscriptPath = exports.detectClaudeSessionHandle = exports.detectClaudeProviderName = exports.detectClaudeSessionIdByPid = exports.detectClaudeSessionByPid = exports.pidSessionRecordsExist = exports.collectDescendantPids = exports.psStartTimes = exports.foregroundAgentFor = exports.readProcTable = exports.parseProcTable = exports.detectClaudeSessionId = exports.claudeProjectDirName = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Remote-control agent registry — the single table behind every
|
|
6
6
|
* `hwip <agent>` subcommand, the listener's pane matching, and the
|
|
@@ -426,6 +426,12 @@ const detectClaudeProviderName = (panePid, paneCwd, deps = {}) => {
|
|
|
426
426
|
return title ? { name: title, source: 'auto' } : null;
|
|
427
427
|
};
|
|
428
428
|
exports.detectClaudeProviderName = detectClaudeProviderName;
|
|
429
|
+
/** Claude Code's own name for the session in this pane, whoever chose it — see `RemoteAgent.resolveSessionHandle`. */
|
|
430
|
+
const detectClaudeSessionHandle = (panePid, paneCwd, deps = {}) => {
|
|
431
|
+
const record = (0, exports.detectClaudeSessionByPid)(panePid, paneCwd, deps);
|
|
432
|
+
return record ? nonBlank(record.name) : null;
|
|
433
|
+
};
|
|
434
|
+
exports.detectClaudeSessionHandle = detectClaudeSessionHandle;
|
|
429
435
|
/**
|
|
430
436
|
* The transcript file for the Claude Code session running in a tmux pane.
|
|
431
437
|
*
|
|
@@ -1211,6 +1217,7 @@ const REMOTE_AGENT_TABLE = [
|
|
|
1211
1217
|
resolveSessionId: (paneCwd, panePid) => (panePid !== undefined ? (0, exports.detectClaudeSessionIdByPid)(panePid, paneCwd) : null)
|
|
1212
1218
|
?? (0, exports.detectClaudeSessionId)(paneCwd),
|
|
1213
1219
|
resolveSessionName: (paneCwd, panePid) => panePid !== undefined ? (0, exports.detectClaudeProviderName)(panePid, paneCwd) : null,
|
|
1220
|
+
resolveSessionHandle: (paneCwd, panePid) => panePid !== undefined ? (0, exports.detectClaudeSessionHandle)(panePid, paneCwd) : null,
|
|
1214
1221
|
resolveTranscript: (paneCwd, panePid) => (0, exports.claudeTranscriptPath)(paneCwd, panePid),
|
|
1215
1222
|
projectTranscript: transcript_tail_js_1.projectTranscriptEntries,
|
|
1216
1223
|
// The pid match alone: the mtime fallback above can name another
|
package/dist/usage.js
CHANGED
|
@@ -72,7 +72,7 @@ Commands:
|
|
|
72
72
|
Exit 0 = the server stored it; whether the session
|
|
73
73
|
typed it is not confirmed. 2 usage, 1 failed (3 no
|
|
74
74
|
API key)
|
|
75
|
-
agent list [--json]
|
|
75
|
+
agent list [--all] [--json]
|
|
76
76
|
List the agent sessions running on THIS machine: key
|
|
77
77
|
(<deviceId>:<session> — what 'agent send' and 'agent
|
|
78
78
|
read' take), agent, state (working|blocked|idle|unknown,
|
|
@@ -81,6 +81,14 @@ Commands:
|
|
|
81
81
|
and the screens here only: no server, no API key; it
|
|
82
82
|
writes nothing.
|
|
83
83
|
Exit 0 listed, 1 no tmux server answered
|
|
84
|
+
--all lists the sessions 'agent send' can reach on EVERY
|
|
85
|
+
machine of the account, from the server's device list:
|
|
86
|
+
key, host, agent, state and name, with the agent's own
|
|
87
|
+
session name in [ ] (Claude Code's, e.g. ko-qmd-70 —
|
|
88
|
+
'agent send' takes it). Offline machines are marked.
|
|
89
|
+
--json prints {"v":1,"all":true,"sessions":[…]}. Needs
|
|
90
|
+
the API key (exit 3 without). Exit 0 listed, 1 the list
|
|
91
|
+
could not be read
|
|
84
92
|
agent read <session> [--turns <n>] [--json]
|
|
85
93
|
Print what a session on THIS machine did last, oldest
|
|
86
94
|
first: its last <n> turns (1-20, default 3) — the
|