@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 +33 -11
- package/dist/agents.d.ts +10 -0
- package/dist/agents.js +12 -1
- package/dist/codex-config.d.ts +8 -5
- package/dist/codex-config.js +121 -12
- package/dist/command-scan.d.ts +7 -3
- package/dist/command-scan.js +59 -15
- package/dist/installer.js +7 -1
- package/dist/listener.d.ts +19 -0
- package/dist/listener.js +78 -17
- package/dist/mcp-command.d.ts +13 -1
- package/dist/mcp-command.js +18 -5
- package/dist/pane-fit.d.ts +39 -11
- package/dist/pane-fit.js +162 -29
- package/dist/remote-agents.d.ts +64 -12
- package/dist/remote-agents.js +141 -18
- package/dist/session-registry.d.ts +22 -0
- package/dist/session-registry.js +50 -3
- package/dist/session-title.d.ts +46 -0
- package/dist/session-title.js +103 -0
- package/dist/templates.d.ts +1 -0
- package/dist/templates.js +41 -1
- package/dist/verify.d.ts +3 -0
- package/dist/verify.js +33 -8
- package/package.json +1 -1
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
|
|
436
|
-
|
|
437
|
-
right-aligned clocks fold in two on a phone.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
phone's width and
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
a
|
|
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)();
|
package/dist/codex-config.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/** The table hwip writes into a config that has none.
|
|
2
|
-
* the Cursor/Windsurf JSON entry: codex is a terminal
|
|
3
|
-
* inherits the shell's `HWIP_API_KEY`, and an unexpanded
|
|
4
|
-
* literal would shadow the ~/.hwip/config.json fallback
|
|
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). */
|
package/dist/codex-config.js
CHANGED
|
@@ -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 `
|
|
22
|
-
* `cwd`, `startup_timeout_sec`, a comment
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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)
|
|
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
|
-
...
|
|
212
|
+
...after.slice(0, start),
|
|
116
213
|
'[mcp_servers.hwip]',
|
|
117
214
|
commandLine,
|
|
118
215
|
argsLine,
|
|
216
|
+
...ours,
|
|
119
217
|
...kept,
|
|
120
|
-
...
|
|
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). */
|
package/dist/command-scan.d.ts
CHANGED
|
@@ -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
|
|
8
|
-
* registers the skill under (`AGENT_SKILL_PREFIX`, plugin names for Claude
|
|
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
|
-
/**
|
|
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. */
|
package/dist/command-scan.js
CHANGED
|
@@ -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
|
-
/**
|
|
21
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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)(
|
|
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(
|
|
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
|
-
|
|
148
|
+
for (const install of installs) {
|
|
149
|
+
const installPath = isObject(install) ? install.installPath : undefined;
|
|
150
|
+
if (typeof installPath !== 'string' || !installPath)
|
|
111
151
|
continue;
|
|
112
|
-
|
|
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
|
-
|
|
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 {
|
package/dist/listener.d.ts
CHANGED
|
@@ -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
|
/**
|