@hwip/cli 0.1.2 → 0.1.4

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
@@ -39,6 +39,17 @@ Codex / …). Safe to re-run: a saved login is reused untouched (no
39
39
  prompts), so a re-run just refreshes the agent integrations. To switch
40
40
  account, `hwip install --relogin` forces a fresh sign-in.
41
41
 
42
+ **Auto session names.** Agents without a title of their own — Codex,
43
+ Cursor, Windsurf, Gemini and pi — name their sessions after what they were
44
+ asked, so the phone shows that instead of a tmux name. The registrations
45
+ `hwip install` writes turn it on (`HWIP_AUTONAME=1` in the MCP entry, the
46
+ `hwip_session_title` tool in the pi extension), so **after updating
47
+ `@hwip/cli`, run `hwip install` once more** to get it on a machine
48
+ installed before; `hwip verify` says which MCP agents still lack it. Claude
49
+ Code and opencode title sessions themselves and are left as they are. To
50
+ hide the names agents choose, turn off *Name sessions automatically* in
51
+ the app's settings — names you set yourself always show.
52
+
42
53
  **Why global, not `npx`?** `hwip cc` (drive a session from your phone)
43
54
  needs `hwip` on your `PATH`, and the agent hooks this installs are
44
55
  `$(command -v hwip || npx …)` — a global binary skips an npx cold-start
@@ -432,17 +443,28 @@ block here.
432
443
  hwip sh --detach # create it, print its name, exit
433
444
  ```
434
445
 
435
- **The mirror fits a detached session to the phone.** A pane is as wide
436
- as the terminal that last drew it, so a desktop's prompt rules and
437
- right-aligned clocks fold in two on a phone. While the phone watches a
438
- session no terminal is attached to (a `hwip sh --detach` shell, or an
439
- agent whose terminal you closed), the listener resizes its window to the
440
- phone's width and gives the old width back when the phone leaves. An
441
- attached session keeps its size: shrinking it would shrink your desktop
442
- terminal, so the phone wraps those rows instead. A window split into
443
- panes is never resized, and one a terminal attaches to while the phone
444
- watches is handed back within a renew (5 s). A listener that died while
445
- a window was fitted restores it the next time it starts.
446
+ **The mirror fits a session to the phone.** A pane is as wide as the
447
+ terminal that last drew it, so a desktop's prompt rules and
448
+ right-aligned clocks fold in two on a phone. Whoever used the session
449
+ last decides its width, the way tmux's `window-size latest` does: typing
450
+ from the phone makes the phone the latest user, and the listener resizes
451
+ the window to the phone's width, and coming back to the mirror later
452
+ (a locked screen, a resubscribe) takes it again unless the desktop was
453
+ used since. Opening the mirror without typing does the same once
454
+ the desktop terminal has been idle for a minute, so a phone reconnecting
455
+ on its own never shrinks a terminal you are typing in. Pressing a key in
456
+ (or attaching) a desktop terminal makes it the latest user, and within a
457
+ renew (5 s) the window is handed back to tmux, which sizes it to that
458
+ terminal. Moving the mouse over a desktop pane or clicking another
459
+ pane is not using it: on macOS the listener reads when a key was last
460
+ pressed (the HID system clock, no permission needed) and counts a
461
+ desktop terminal as used no later than that key or its attach. Elsewhere
462
+ any tmux client activity counts. While the phone holds a session, a desktop terminal still
463
+ attached to it shows it narrower. When the phone leaves, a session no
464
+ terminal is attached to gets its old width back and an attached one is
465
+ sized to its terminal. A window split into panes is never resized. A
466
+ listener that died while a window was fitted restores it the next time
467
+ it starts.
446
468
 
447
469
  `hwip cursor` runs **`cursor-agent`**, Cursor's terminal agent — a
448
470
  separate install from the Cursor IDE (the bare `cursor` on your PATH
package/dist/agents.d.ts CHANGED
@@ -10,6 +10,16 @@
10
10
  * pi 0.85.1): global is `~/.pi/agent/skills/` and `~/.agents/skills/`, while
11
11
  * `.pi/skills/` is the PROJECT dir — scanning `~/.pi/skills` found only three
12
12
  * dangling symlinks and reported pi as having no skills at all.
13
+ *
14
+ * codex reads `$CODEX_HOME/skills` and `~/.agents/skills` (codex-rs
15
+ * `ext/skills/src/host_roots.rs`), and keeps its bundled skills in the hidden
16
+ * `.codex/skills/.system`, which the scanner's dot-skip would otherwise pass
17
+ * over. opencode reads the claude and agents dirs as well as its own
18
+ * (opencode `docs/skills.mdx § Discovery`, 2.0.29). Measured against the agents'
19
+ * own reports on 2026-10-20: codex 59 of `codex app-server` `skills/list` 59;
20
+ * opencode 87 of `opencode serve` `/api/skill` 91 — the rest are two built-ins
21
+ * and two skills it reads out of `~/.claude/skills/.trash/`, which the scanner's
22
+ * dot-skip leaves out on purpose.
13
23
  */
14
24
  export declare const AGENT_SKILL_DIRS: Record<string, string[]>;
15
25
  export interface Agent {
package/dist/agents.js CHANGED
@@ -20,10 +20,21 @@ const path_1 = require("path");
20
20
  * pi 0.85.1): global is `~/.pi/agent/skills/` and `~/.agents/skills/`, while
21
21
  * `.pi/skills/` is the PROJECT dir — scanning `~/.pi/skills` found only three
22
22
  * dangling symlinks and reported pi as having no skills at all.
23
+ *
24
+ * codex reads `$CODEX_HOME/skills` and `~/.agents/skills` (codex-rs
25
+ * `ext/skills/src/host_roots.rs`), and keeps its bundled skills in the hidden
26
+ * `.codex/skills/.system`, which the scanner's dot-skip would otherwise pass
27
+ * over. opencode reads the claude and agents dirs as well as its own
28
+ * (opencode `docs/skills.mdx § Discovery`, 2.0.29). Measured against the agents'
29
+ * own reports on 2026-10-20: codex 59 of `codex app-server` `skills/list` 59;
30
+ * opencode 87 of `opencode serve` `/api/skill` 91 — the rest are two built-ins
31
+ * and two skills it reads out of `~/.claude/skills/.trash/`, which the scanner's
32
+ * dot-skip leaves out on purpose.
23
33
  */
24
34
  exports.AGENT_SKILL_DIRS = {
25
35
  claude: ['.claude/skills'],
26
- codex: ['.codex/skills'],
36
+ codex: ['.codex/skills', '.codex/skills/.system', '.agents/skills'],
37
+ opencode: ['.claude/skills', '.agents/skills', '.config/opencode/skills'],
27
38
  pi: ['.pi/agent/skills', '.agents/skills'],
28
39
  };
29
40
  const HOME = (0, os_1.homedir)();
@@ -1,8 +1,8 @@
1
- /** The table hwip writes into a config that has none. No `env` block, unlike
2
- * the Cursor/Windsurf JSON entry: codex is a terminal app, so the server
3
- * inherits the shell's `HWIP_API_KEY`, and an unexpanded `"${HWIP_API_KEY}"`
4
- * literal would shadow the ~/.hwip/config.json fallback with junk (the same
5
- * reasoning as opencode's). */
1
+ /** The table hwip writes into a config that has none. Its `env` holds only the
2
+ * auto-name flag, unlike the Cursor/Windsurf JSON entry: codex is a terminal
3
+ * app, so the server inherits the shell's `HWIP_API_KEY`, and an unexpanded
4
+ * `"${HWIP_API_KEY}"` literal would shadow the ~/.hwip/config.json fallback
5
+ * with junk (the same reasoning as opencode's). */
6
6
  export declare const CODEX_MCP_TABLE: string;
7
7
  /**
8
8
  * The config with hwip's entry present and current, leaving every other byte
@@ -10,6 +10,9 @@ export declare const CODEX_MCP_TABLE: string;
10
10
  * it, so nothing above can capture the keys.
11
11
  */
12
12
  export declare const upsertHwipMcpTable: (config: string) => string;
13
+ /** Whether hwip's entry turns auto names on — what verify checks to tell an
14
+ * install from before the flag existed. */
15
+ export declare const hasHwipAutoName: (config: string) => boolean;
13
16
  /** The config with hwip's entry and its sub-tables gone, or null when it was
14
17
  * never there (so the caller can report "nothing to remove" rather than
15
18
  * rewriting an untouched file). */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.readHwipMcpArgv = exports.removeHwipMcpTable = exports.upsertHwipMcpTable = exports.CODEX_MCP_TABLE = void 0;
3
+ exports.readHwipMcpArgv = exports.removeHwipMcpTable = exports.hasHwipAutoName = exports.upsertHwipMcpTable = exports.CODEX_MCP_TABLE = void 0;
4
4
  /**
5
5
  * The hwip entry inside `~/.codex/config.toml`, which Codex CLI owns.
6
6
  *
@@ -18,8 +18,10 @@ exports.readHwipMcpArgv = exports.removeHwipMcpTable = exports.upsertHwipMcpTabl
18
18
  * `[mcp_servers.hwip.tools.hwip_ask]` when the user picks "always allow".
19
19
  * Removal takes them along (a child of a deleted server is dead config);
20
20
  * re-install keeps them, since they are the user's answer, not ours.
21
- * - **Only `command` and `args` are ours.** Any other key in the table —
22
- * `cwd`, `startup_timeout_sec`, a comment — survives a re-install.
21
+ * - **Only `command`, `args` and the `HWIP_AUTONAME` env pair are ours.** Any
22
+ * other key in the table — `cwd`, `startup_timeout_sec`, a comment, the
23
+ * user's own env pairs — survives a re-install; the flag joins whichever
24
+ * env the user already keeps.
23
25
  * - **Comments before the next table belong to that table**, not to ours, so
24
26
  * the region we replace or delete stops short of them.
25
27
  *
@@ -31,6 +33,8 @@ const [MCP_BIN, ...MCP_SUBCOMMAND] = mcp_command_js_1.MCP_LAUNCH_ARGV;
31
33
  const HWIP_TABLE_HEADER = /^\[mcp_servers\.(?:hwip|"hwip")\]$/;
32
34
  /** A sub-table of ours: `[mcp_servers.hwip.tools.hwip_ask]` and friends. */
33
35
  const HWIP_SUBTABLE_HEADER = /^\[mcp_servers\.(?:hwip|"hwip")\..+\]$/;
36
+ /** The user's own env table for hwip — where the flag goes when they keep one. */
37
+ const HWIP_ENV_SUBTABLE_HEADER = /^\[mcp_servers\.(?:hwip|"hwip")\.(?:env|"env")\]$/;
34
38
  /**
35
39
  * The dotted/inline form — `mcp_servers.hwip = { command = "npx", … }` — which
36
40
  * is valid TOML and which older hand-written configs use. It has to be found,
@@ -44,12 +48,87 @@ const isHwipDefinition = (line) => HWIP_TABLE_HEADER.test(line.trim()) || HWIP_I
44
48
  const OWNED_KEY = /^(?:command|args)\s*=/;
45
49
  const commandLine = `command = "${MCP_BIN}"`;
46
50
  const argsLine = `args = [${MCP_SUBCOMMAND.map((a) => `"${a}"`).join(', ')}]`;
47
- /** The table hwip writes into a config that has none. No `env` block, unlike
48
- * the Cursor/Windsurf JSON entry: codex is a terminal app, so the server
49
- * inherits the shell's `HWIP_API_KEY`, and an unexpanded `"${HWIP_API_KEY}"`
50
- * literal would shadow the ~/.hwip/config.json fallback with junk (the same
51
- * reasoning as opencode's). */
52
- exports.CODEX_MCP_TABLE = ['[mcp_servers.hwip]', commandLine, argsLine].join('\n');
51
+ const flagPair = `${mcp_command_js_1.AUTONAME_ENV} = "1"`;
52
+ const envLine = `env = { ${flagPair} }`;
53
+ /** `env = { … }` in hwip's table — known by its key, so a comment after it cannot hide it. */
54
+ const INLINE_ENV = /^env\s*=\s*\{/;
55
+ /** `env.KEY = …`, the dotted spelling of the same table. */
56
+ const DOTTED_ENV = /^env\./;
57
+ /** The flag's key, bare or quoted: TOML reads all three as one key. */
58
+ const FLAG = `(?:${mcp_command_js_1.AUTONAME_ENV}|"${mcp_command_js_1.AUTONAME_ENV}"|'${mcp_command_js_1.AUTONAME_ENV}')`;
59
+ const FLAG_KEY = new RegExp(`^${FLAG}\\s*=`);
60
+ const DOTTED_FLAG_KEY = new RegExp(`^env\\.${FLAG}\\s*=`);
61
+ const FLAG_ON = new RegExp(`(?:^|[\\s{,.])${FLAG}\\s*=\\s*["']1["']`);
62
+ /** The table hwip writes into a config that has none. Its `env` holds only the
63
+ * auto-name flag, unlike the Cursor/Windsurf JSON entry: codex is a terminal
64
+ * app, so the server inherits the shell's `HWIP_API_KEY`, and an unexpanded
65
+ * `"${HWIP_API_KEY}"` literal would shadow the ~/.hwip/config.json fallback
66
+ * with junk (the same reasoning as opencode's). */
67
+ exports.CODEX_MCP_TABLE = ['[mcp_servers.hwip]', commandLine, argsLine, envLine].join('\n');
68
+ /**
69
+ * Walks a line of TOML outside its strings: `at(i, depth)` sees every character that is not
70
+ * inside one, with the brace depth before it, and stops the walk by returning true.
71
+ */
72
+ const walkOutsideStrings = (text, from, at) => {
73
+ let depth = 0;
74
+ let quote = null;
75
+ for (let i = from; i < text.length; i += 1) {
76
+ const ch = text[i];
77
+ if (quote) {
78
+ if (ch === '\\' && quote === '"')
79
+ i += 1;
80
+ else if (ch === quote)
81
+ quote = null;
82
+ continue;
83
+ }
84
+ if (ch === '"' || ch === "'") {
85
+ quote = ch;
86
+ continue;
87
+ }
88
+ if (at(i, depth))
89
+ return;
90
+ if (ch === '{' || ch === '[')
91
+ depth += 1;
92
+ else if (ch === '}' || ch === ']')
93
+ depth -= 1;
94
+ }
95
+ };
96
+ /** The inline table's inside and what follows its closing brace (a comment), or null when it does
97
+ * not close on this line. */
98
+ const inlineParts = (line) => {
99
+ const open = line.indexOf('{');
100
+ let parts = null;
101
+ walkOutsideStrings(line, open, (i, depth) => {
102
+ if (line[i] !== '}' || depth !== 1)
103
+ return false;
104
+ parts = { inner: line.slice(open + 1, i), tail: line.slice(i + 1) };
105
+ return true;
106
+ });
107
+ return parts;
108
+ };
109
+ /** The pairs of an inline table, split at its own commas — never one inside a string or a nested value. */
110
+ const inlinePairs = (inner) => {
111
+ const pairs = [];
112
+ let start = 0;
113
+ walkOutsideStrings(inner, 0, (i, depth) => {
114
+ if (inner[i] === ',' && depth === 0) {
115
+ pairs.push(inner.slice(start, i));
116
+ start = i + 1;
117
+ }
118
+ return false;
119
+ });
120
+ pairs.push(inner.slice(start));
121
+ return pairs.map((pair) => pair.trim()).filter(Boolean);
122
+ };
123
+ /** An inline env table with the flag on and first, the user's pairs and comment kept. One that
124
+ * cannot be read is left alone: writing a second `env` beside it would break the whole file. */
125
+ const withFlag = (line) => {
126
+ const parts = inlineParts(line);
127
+ if (!parts)
128
+ return line;
129
+ const rest = inlinePairs(parts.inner).filter((pair) => !FLAG_KEY.test(pair));
130
+ return `env = { ${[flagPair, ...rest].join(', ')} }${parts.tail}`;
131
+ };
53
132
  /** CRLF in, CRLF out — a Windows config must not come back with mixed endings. */
54
133
  const eolOf = (config) => (config.includes('\r\n') ? '\r\n' : '\n');
55
134
  const splitLines = (config) => config.split(/\r?\n/);
@@ -109,18 +188,48 @@ const upsertHwipMcpTable = (config) => {
109
188
  const table = exports.CODEX_MCP_TABLE.split('\n');
110
189
  return rejoin([...lines.slice(0, start), ...table, ...lines.slice(start + 1)], eol);
111
190
  }
191
+ // A key defined twice makes codex refuse the whole file, so the flag goes
192
+ // into whichever env the user already keeps, and gets one only if none.
193
+ const envTable = lines.slice(bodyEnd(lines, start), entryEnd(lines, start))
194
+ .findIndex((line) => HWIP_ENV_SUBTABLE_HEADER.test(line.trim()));
195
+ const after = [...lines];
196
+ if (envTable !== -1) {
197
+ const header = bodyEnd(lines, start) + envTable;
198
+ const tableEnd = bodyEnd(lines, header);
199
+ const rows = lines.slice(header + 1, tableEnd).filter((line) => !FLAG_KEY.test(line.trim()));
200
+ after.splice(header, tableEnd - header, lines[header], flagPair, ...rows);
201
+ }
112
202
  const end = withoutTrailingPreamble(lines, start, bodyEnd(lines, start));
113
- const kept = lines.slice(start + 1, end).filter((line) => !OWNED_KEY.test(line.trim()));
203
+ const kept = lines.slice(start + 1, end)
204
+ .filter((line) => !OWNED_KEY.test(line.trim()) && !DOTTED_FLAG_KEY.test(line.trim()));
205
+ const inline = kept.findIndex((line) => INLINE_ENV.test(line.trim()));
206
+ if (inline !== -1)
207
+ kept[inline] = withFlag(kept[inline].trim());
208
+ const ours = envTable !== -1 || inline !== -1 ? []
209
+ : kept.some((line) => DOTTED_ENV.test(line.trim())) ? [`env.${flagPair}`]
210
+ : [envLine];
114
211
  return rejoin([
115
- ...lines.slice(0, start),
212
+ ...after.slice(0, start),
116
213
  '[mcp_servers.hwip]',
117
214
  commandLine,
118
215
  argsLine,
216
+ ...ours,
119
217
  ...kept,
120
- ...lines.slice(end),
218
+ ...after.slice(end),
121
219
  ], eol);
122
220
  };
123
221
  exports.upsertHwipMcpTable = upsertHwipMcpTable;
222
+ /** Whether hwip's entry turns auto names on — what verify checks to tell an
223
+ * install from before the flag existed. */
224
+ const hasHwipAutoName = (config) => {
225
+ const lines = splitLines(config);
226
+ const start = definitionIndex(lines);
227
+ if (start === -1)
228
+ return false;
229
+ const end = HWIP_INLINE_ENTRY.test(lines[start].trim()) ? start + 1 : entryEnd(lines, start);
230
+ return lines.slice(start, end).some((line) => !line.trim().startsWith('#') && FLAG_ON.test(line.trim()));
231
+ };
232
+ exports.hasHwipAutoName = hasHwipAutoName;
124
233
  /** The config with hwip's entry and its sub-tables gone, or null when it was
125
234
  * never there (so the caller can report "nothing to remove" rather than
126
235
  * rewriting an untouched file). */
@@ -4,8 +4,9 @@
4
4
  * Payload discipline (plan PLAN-13-16-18): paths never leave this machine, so
5
5
  * the output carries skill names and nothing else.
6
6
  *
7
- * An entry is the slash command itself, so it carries whatever prefix the agent
8
- * registers the skill under (`AGENT_SKILL_PREFIX`, plugin names for Claude Code).
7
+ * An entry is the invocation token itself, so it carries whatever prefix the
8
+ * agent registers the skill under (`AGENT_SKILL_PREFIX`, plugin names for Claude
9
+ * Code) — including the `$`/`@` mention sigil of agents that take no slash.
9
10
  *
10
11
  * Names come from the directory, not from the file — an ASSUMPTION, not a rule
11
12
  * the agents enforce: pi resolves a skill's name as `frontmatter.name` and only
@@ -23,7 +24,10 @@
23
24
  * existence until a divergent skill is actually measured.
24
25
  */
25
26
  export interface AgentCommandEntry {
26
- /** What the user types after `/` — see `AGENT_SKILL_PREFIX`. */
27
+ /**
28
+ * What the user types to run the skill: after a `/`, unless it starts with a
29
+ * mention sigil (`$`, `@`), which is typed as is — see `AGENT_SKILL_PREFIX`.
30
+ */
27
31
  name: string;
28
32
  }
29
33
  /** agentKind -> skill entries; agents with no skills are omitted. */
@@ -13,12 +13,24 @@ const agents_js_1 = require("./agents.js");
13
13
  * pi session was observed to abort rather than run the skill (2026-09-13).
14
14
  * Claude Code's user skills are `/<name>`; its PLUGIN skills are
15
15
  * `/<plugin>:<name>`, so those get their prefix from the install entry instead.
16
+ *
17
+ * codex and opencode do not run skills as slash commands at all. codex takes a
18
+ * `$name` mention (codex-rs `skills/src/selection.rs`; its `/skills` only opens a
19
+ * picker), and opencode 2.0 an `@name` mention (`docs/skills.mdx § Loading`; its
20
+ * `/api/command` lists no skills). Their entries carry that sigil, and a client
21
+ * inserts a name starting with `$` or `@` as typed, without the `/`.
16
22
  */
17
23
  const AGENT_SKILL_PREFIX = {
24
+ codex: '$',
25
+ opencode: '@',
18
26
  pi: 'skill:',
19
27
  };
20
- /** Agents that register skills filed under grouping folders — see `scanSkillDir`. */
21
- const AGENT_SKILL_RECURSIVE = new Set(['pi']);
28
+ /**
29
+ * Agents that register skills filed under grouping folders — see `scanSkillDir`.
30
+ * opencode walks them too: it lists `~/.claude/skills/synced/<id>/<name>`, which
31
+ * Claude Code itself does not register (`/api/skill`, 2026-10-20).
32
+ */
33
+ const AGENT_SKILL_RECURSIVE = new Set(['opencode', 'pi']);
22
34
  /** Serialized-catalog budget. Names-only measures ~2KB here; ws frames die at 128KB. */
23
35
  exports.PAYLOAD_LIMIT_BYTES = 64 * 1024;
24
36
  /**
@@ -88,28 +100,60 @@ const scanSkillDir = (dir, recursive = false, depth = 1) => {
88
100
  }
89
101
  return names;
90
102
  };
91
- /**
92
- * Plugin skills live at `<installPath>/skills/<name>/SKILL.md`. Only install paths
93
- * listed in `installed_plugins.json` are scanned — the plugin cache also holds
94
- * uninstalled marketplace entries (514 of 710 on this machine) that must not
95
- * appear.
96
- */
97
- const scanClaudePlugins = (home) => {
103
+ /** A plain JSON object, as opposed to an array, a scalar or null. */
104
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
105
+ /** The object under `key` in a JSON file, or an empty one when the file is missing, not JSON, or holds another shape there. */
106
+ const readJsonObject = (file, key) => {
98
107
  let parsed;
99
108
  try {
100
- parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(home, '.claude', 'plugins', 'installed_plugins.json'), 'utf-8'));
109
+ parsed = JSON.parse((0, fs_1.readFileSync)(file, 'utf-8'));
101
110
  }
102
111
  catch {
103
- return [];
112
+ return {};
104
113
  }
114
+ const value = isObject(parsed) ? parsed[key] : undefined;
115
+ return isObject(value) ? value : {};
116
+ };
117
+ /**
118
+ * Plugin keys Claude Code loads: `enabledPlugins[key] === true` in the user's
119
+ * `~/.claude/settings.json`. Installed is not enabled — a key set to `false`, or
120
+ * absent, registers nothing (`claude -p` init `slash_commands`, 2026-10-20: 71
121
+ * skills of two disabled plugins and one unlisted plugin were missing). A
122
+ * project's own settings can enable more, but the catalog is device-level.
123
+ */
124
+ const enabledClaudePlugins = (home) => {
125
+ const enabled = readJsonObject((0, path_1.join)(home, '.claude', 'settings.json'), 'enabledPlugins');
126
+ return new Set(Object.entries(enabled).filter(([, on]) => on === true).map(([key]) => key));
127
+ };
128
+ /** Command files of a plugin: `<installPath>/commands/<name>.md`. */
129
+ const scanCommandFiles = (dir) => entriesOf(dir)
130
+ .filter((entry) => entry.endsWith('.md') && !entry.startsWith('.'))
131
+ .map((entry) => entry.slice(0, -'.md'.length));
132
+ /**
133
+ * Plugin commands are `<installPath>/skills/<name>/SKILL.md` and
134
+ * `<installPath>/commands/<name>.md`, both registered as `/<plugin>:<name>`. Only
135
+ * install paths listed in `installed_plugins.json` are scanned — the plugin cache
136
+ * also holds uninstalled marketplace entries (514 of 710 on this machine) that
137
+ * must not appear — and of those only the enabled ones.
138
+ */
139
+ const scanClaudePlugins = (home) => {
140
+ const installed = readJsonObject((0, path_1.join)(home, '.claude', 'plugins', 'installed_plugins.json'), 'plugins');
141
+ const enabled = enabledClaudePlugins(home);
105
142
  const names = [];
106
- for (const [key, installs] of Object.entries(parsed.plugins ?? {})) {
143
+ for (const [key, installs] of Object.entries(installed)) {
144
+ if (!enabled.has(key) || !Array.isArray(installs))
145
+ continue;
107
146
  // Key is `<plugin>@<marketplace>`; the command prefix is the plugin half.
108
147
  const plugin = key.split('@')[0];
109
- for (const install of installs ?? []) {
110
- if (!install.installPath)
148
+ for (const install of installs) {
149
+ const installPath = isObject(install) ? install.installPath : undefined;
150
+ if (typeof installPath !== 'string' || !installPath)
111
151
  continue;
112
- names.push(...scanSkillDir((0, path_1.join)(install.installPath, 'skills')).map((name) => `${plugin}:${name}`));
152
+ const commands = [
153
+ ...scanSkillDir((0, path_1.join)(installPath, 'skills')),
154
+ ...scanCommandFiles((0, path_1.join)(installPath, 'commands')),
155
+ ];
156
+ names.push(...commands.map((name) => `${plugin}:${name}`));
113
157
  }
114
158
  }
115
159
  return names;
package/dist/installer.js CHANGED
@@ -334,7 +334,13 @@ const installGemini = () => {
334
334
  ok('MCP server added (legacy gemini CLI)');
335
335
  }
336
336
  catch {
337
- fail(`MCP add failed. Manual: ${mcp_command_js_1.GEMINI_MCP_ADD}`);
337
+ try {
338
+ (0, child_process_1.execSync)(mcp_command_js_1.GEMINI_MCP_ADD_BARE, { stdio: 'pipe' });
339
+ ok('MCP server added (legacy gemini CLI, without auto session names)');
340
+ }
341
+ catch {
342
+ fail(`MCP add failed. Manual: ${mcp_command_js_1.GEMINI_MCP_ADD}`);
343
+ }
338
344
  }
339
345
  }
340
346
  try {
@@ -22,7 +22,9 @@
22
22
  */
23
23
  import { type HwipConfig } from './config.js';
24
24
  import { type ShellAuditEntry } from './shell-audit.js';
25
+ import { type KnownSession } from './session-registry.js';
25
26
  import { type AgentKind } from './remote-agents.js';
27
+ import type { ProviderNameSource } from './session-title.js';
26
28
  import { type TranscriptSource } from './transcript-tail.js';
27
29
  import { type AgentState } from './agent-state.js';
28
30
  import { type SealedFileMeta, type SealedV2 } from './crypto.js';
@@ -120,6 +122,12 @@ interface AgentSession {
120
122
  * word for both would put the ambiguity in the type.
121
123
  */
122
124
  providerSessionName?: string | null;
125
+ /**
126
+ * Who chose `providerSessionName`: a person in the agent (`'user'`) or the
127
+ * agent itself (`'auto'`). Omitted when the agent's store cannot say. The
128
+ * account setting that hides agent-made names hides `'auto'` only.
129
+ */
130
+ providerSessionNameSource?: ProviderNameSource;
123
131
  createdAt?: string;
124
132
  /**
125
133
  * Which run of the name this is (hwip docs/session-runs.md): tmux
@@ -219,6 +227,8 @@ export interface ReportedKnownSession {
219
227
  /** The seq block the run's chunks took (Q24): the server reads its conversation from that key range alone. */
220
228
  firstSeq?: number;
221
229
  lastSeq?: number;
230
+ /** The later tmux sessions the run went on in (Q35): the server ends another run's push window at their start. */
231
+ tmuxRunIds?: string[];
222
232
  lastSeenAt: string;
223
233
  }
224
234
  /** Each live run's first seq, for the report (Q24). A run that has recorded nothing yet has none. */
@@ -271,6 +281,8 @@ export declare const resolveKeys: (keys: string[]) => string[] | null;
271
281
  export { invalidateTmuxSocketCache, silenceListenerLog, tmuxArgs };
272
282
  export { liveSessionNames, sessionExists, sessionLiveness };
273
283
  export { detectRemoteAgent, isSubagentSessionName, recordSubagentTranscripts, recordTargets, subagentTranscriptFor, tmuxTargetFor };
284
+ /** Test hook. */
285
+ export declare const resetPaneFitClaims: () => void;
274
286
  /**
275
287
  * Parse a `hwip-*` tmux session name into `{project, label}`. The wrapper
276
288
  * emits `hwip-<project>` and, with `--label <x>`, `hwip-<project>-<x>`; the
@@ -296,6 +308,13 @@ export declare const __checkoutCacheSize: () => number;
296
308
  * must agree: `epochToIso` keeps whole seconds and this floors them.
297
309
  */
298
310
  export declare const runIdOfEpoch: (epoch: string | undefined) => string | undefined;
311
+ /**
312
+ * Each live session's run once the sweep has read the conversations (hwip
313
+ * docs/session-runs.md §3): a session carrying on an earlier run's
314
+ * conversation reports — and its recorder tags — that run instead of its new
315
+ * tmux session. `sessions` and `recordable` are this sweep's, changed in place.
316
+ */
317
+ export declare const continueRuns: (sessions: AgentSession[], recordable: Record<string, RecordableSession>, conversationOf: (name: string) => string | null | undefined, known?: readonly KnownSession[]) => void;
299
318
  /** Test hook. */
300
319
  export declare const resetSessionStates: () => void;
301
320
  /**