@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 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 |
@@ -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
@@ -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
- /** The agent names its session itself (`/rename`), so the text may hold escape sequences a terminal would act on. */
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;
@@ -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)) {
@@ -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 and the
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;
@@ -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
- const listCandidates = (list) => list.map((c) => `${c.key} (${c.alias ?? c.providerSessionName ?? c.label ?? c.name} on ${c.host})`).join(', ') || 'none';
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 and the
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
- if ((0, args_js_1.flagOn)(args, 'auto')) {
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, (0, args_js_1.flagOn)(args, 'auto') ? HOOK_REQUEST_TIMEOUT_MS : undefined);
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);
@@ -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),
@@ -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;
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hwip/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Hwip CLI + push notification SDK for AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",