@hwip/cli 0.0.1 → 0.1.0

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.
Files changed (245) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +11 -0
  3. package/README.md +1979 -0
  4. package/dist/agent-list.d.ts +76 -0
  5. package/dist/agent-list.js +102 -0
  6. package/dist/agent-read.d.ts +105 -0
  7. package/dist/agent-read.js +309 -0
  8. package/dist/agent-rules-fetch.d.ts +51 -0
  9. package/dist/agent-rules-fetch.js +275 -0
  10. package/dist/agent-rules.default.d.ts +30 -0
  11. package/dist/agent-rules.default.js +292 -0
  12. package/dist/agent-session.d.ts +23 -0
  13. package/dist/agent-session.js +44 -0
  14. package/dist/agent-state.d.ts +84 -0
  15. package/dist/agent-state.js +188 -0
  16. package/dist/agent-target.d.ts +60 -0
  17. package/dist/agent-target.js +81 -0
  18. package/dist/agents.d.ts +31 -0
  19. package/dist/agents.js +75 -0
  20. package/dist/api.d.ts +28 -0
  21. package/dist/api.js +76 -0
  22. package/dist/args.d.ts +74 -0
  23. package/dist/args.js +254 -0
  24. package/dist/ask-context-attach.d.ts +73 -0
  25. package/dist/ask-context-attach.js +151 -0
  26. package/dist/ask-context-screen.d.ts +21 -0
  27. package/dist/ask-context-screen.js +49 -0
  28. package/dist/ask-context-switch.d.ts +31 -0
  29. package/dist/ask-context-switch.js +74 -0
  30. package/dist/ask-context.d.ts +84 -0
  31. package/dist/ask-context.js +172 -0
  32. package/dist/ask-pending.d.ts +47 -0
  33. package/dist/ask-pending.js +221 -0
  34. package/dist/ask-seal.d.ts +120 -0
  35. package/dist/ask-seal.js +179 -0
  36. package/dist/ask.d.ts +107 -0
  37. package/dist/ask.js +551 -0
  38. package/dist/blocked-prompt-seal.d.ts +35 -0
  39. package/dist/blocked-prompt-seal.js +89 -0
  40. package/dist/blocked-prompt.d.ts +57 -0
  41. package/dist/blocked-prompt.js +181 -0
  42. package/dist/channel.d.ts +67 -0
  43. package/dist/channel.js +136 -0
  44. package/dist/check-update.d.ts +28 -0
  45. package/dist/check-update.js +153 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.js +813 -0
  48. package/dist/codex-config.d.ts +24 -0
  49. package/dist/codex-config.js +169 -0
  50. package/dist/codex-transcript.d.ts +59 -0
  51. package/dist/codex-transcript.js +373 -0
  52. package/dist/command-refusal.d.ts +56 -0
  53. package/dist/command-refusal.js +127 -0
  54. package/dist/command-scan.d.ts +46 -0
  55. package/dist/command-scan.js +154 -0
  56. package/dist/command-seal.d.ts +57 -0
  57. package/dist/command-seal.js +88 -0
  58. package/dist/command-seq.d.ts +30 -0
  59. package/dist/command-seq.js +150 -0
  60. package/dist/config.d.ts +156 -0
  61. package/dist/config.js +392 -0
  62. package/dist/core-sections.d.ts +18 -0
  63. package/dist/core-sections.js +28 -0
  64. package/dist/crypto.d.ts +278 -0
  65. package/dist/crypto.js +698 -0
  66. package/dist/desktop-notify.d.ts +33 -0
  67. package/dist/desktop-notify.js +49 -0
  68. package/dist/device-directory.d.ts +36 -0
  69. package/dist/device-directory.js +97 -0
  70. package/dist/device-presence.d.ts +17 -0
  71. package/dist/device-presence.js +62 -0
  72. package/dist/downloads.d.ts +29 -0
  73. package/dist/downloads.js +106 -0
  74. package/dist/errors.d.ts +12 -0
  75. package/dist/errors.js +28 -0
  76. package/dist/file-crypto.d.ts +94 -0
  77. package/dist/file-crypto.js +272 -0
  78. package/dist/file-view-budget.d.ts +15 -0
  79. package/dist/file-view-budget.js +34 -0
  80. package/dist/file-view-switch.d.ts +28 -0
  81. package/dist/file-view-switch.js +122 -0
  82. package/dist/file-view.d.ts +108 -0
  83. package/dist/file-view.js +327 -0
  84. package/dist/gate.d.ts +141 -0
  85. package/dist/gate.js +336 -0
  86. package/dist/gemini-transcript.d.ts +32 -0
  87. package/dist/gemini-transcript.js +189 -0
  88. package/dist/http-failure.d.ts +98 -0
  89. package/dist/http-failure.js +349 -0
  90. package/dist/hwip-core.generated.d.ts +7 -0
  91. package/dist/hwip-core.generated.js +14 -0
  92. package/dist/hwip-hook.d.ts +77 -0
  93. package/dist/hwip-hook.js +340 -0
  94. package/dist/index.d.ts +7 -0
  95. package/dist/index.js +14 -0
  96. package/dist/input-sequencer.d.ts +65 -0
  97. package/dist/input-sequencer.js +142 -0
  98. package/dist/insecure-url.d.ts +16 -0
  99. package/dist/insecure-url.js +76 -0
  100. package/dist/installer.d.ts +96 -0
  101. package/dist/installer.js +850 -0
  102. package/dist/inventory-offload.d.ts +49 -0
  103. package/dist/inventory-offload.js +209 -0
  104. package/dist/inventory-worker.d.ts +2 -0
  105. package/dist/inventory-worker.js +27 -0
  106. package/dist/jsonc-doc.d.ts +106 -0
  107. package/dist/jsonc-doc.js +354 -0
  108. package/dist/keep-awake.d.ts +32 -0
  109. package/dist/keep-awake.js +46 -0
  110. package/dist/key-changes.d.ts +48 -0
  111. package/dist/key-changes.js +140 -0
  112. package/dist/lan-auth.d.ts +140 -0
  113. package/dist/lan-auth.js +217 -0
  114. package/dist/lan-endpoint.d.ts +41 -0
  115. package/dist/lan-endpoint.js +115 -0
  116. package/dist/lan-receiver.d.ts +122 -0
  117. package/dist/lan-receiver.js +510 -0
  118. package/dist/lan-sender.d.ts +76 -0
  119. package/dist/lan-sender.js +184 -0
  120. package/dist/lan-transfer.d.ts +36 -0
  121. package/dist/lan-transfer.js +54 -0
  122. package/dist/listener-device-id.d.ts +20 -0
  123. package/dist/listener-device-id.js +133 -0
  124. package/dist/listener-log.d.ts +4 -0
  125. package/dist/listener-log.js +23 -0
  126. package/dist/listener-process.d.ts +200 -0
  127. package/dist/listener-process.js +470 -0
  128. package/dist/listener-service.d.ts +225 -0
  129. package/dist/listener-service.js +682 -0
  130. package/dist/listener-worktree-cleanup.d.ts +27 -0
  131. package/dist/listener-worktree-cleanup.js +62 -0
  132. package/dist/listener.d.ts +1325 -0
  133. package/dist/listener.js +5680 -0
  134. package/dist/login-address.d.ts +25 -0
  135. package/dist/login-address.js +97 -0
  136. package/dist/login-page.d.ts +29 -0
  137. package/dist/login-page.js +93 -0
  138. package/dist/login.d.ts +98 -0
  139. package/dist/login.js +388 -0
  140. package/dist/mcp-command.d.ts +45 -0
  141. package/dist/mcp-command.js +50 -0
  142. package/dist/mcp.d.ts +19 -0
  143. package/dist/mcp.js +29 -0
  144. package/dist/node-support.d.ts +19 -0
  145. package/dist/node-support.js +38 -0
  146. package/dist/notify-failure.d.ts +57 -0
  147. package/dist/notify-failure.js +150 -0
  148. package/dist/opencode-transcript.d.ts +139 -0
  149. package/dist/opencode-transcript.js +390 -0
  150. package/dist/optional-features.d.ts +6 -0
  151. package/dist/optional-features.js +19 -0
  152. package/dist/pane-fit.d.ts +40 -0
  153. package/dist/pane-fit.js +119 -0
  154. package/dist/peer-key.d.ts +33 -0
  155. package/dist/peer-key.js +34 -0
  156. package/dist/pi-transcript.d.ts +26 -0
  157. package/dist/pi-transcript.js +255 -0
  158. package/dist/presence.d.ts +14 -0
  159. package/dist/presence.js +124 -0
  160. package/dist/private-files.d.ts +51 -0
  161. package/dist/private-files.js +218 -0
  162. package/dist/project-switch.d.ts +19 -0
  163. package/dist/project-switch.js +60 -0
  164. package/dist/redact.d.ts +10 -0
  165. package/dist/redact.js +21 -0
  166. package/dist/remote-agents.d.ts +410 -0
  167. package/dist/remote-agents.js +1218 -0
  168. package/dist/remote-hook.d.ts +12 -0
  169. package/dist/remote-hook.js +196 -0
  170. package/dist/replace-file.d.ts +50 -0
  171. package/dist/replace-file.js +328 -0
  172. package/dist/schedule-command.d.ts +35 -0
  173. package/dist/schedule-command.js +260 -0
  174. package/dist/schedule-launch.d.ts +15 -0
  175. package/dist/schedule-launch.js +24 -0
  176. package/dist/schedule-ready.d.ts +44 -0
  177. package/dist/schedule-ready.js +71 -0
  178. package/dist/schedule-remote.d.ts +77 -0
  179. package/dist/schedule-remote.js +167 -0
  180. package/dist/schedule-runner.d.ts +89 -0
  181. package/dist/schedule-runner.js +452 -0
  182. package/dist/schedule.d.ts +149 -0
  183. package/dist/schedule.js +428 -0
  184. package/dist/secret-mask.d.ts +29 -0
  185. package/dist/secret-mask.js +245 -0
  186. package/dist/self-register.d.ts +45 -0
  187. package/dist/self-register.js +67 -0
  188. package/dist/send.d.ts +56 -0
  189. package/dist/send.js +104 -0
  190. package/dist/session-diff.d.ts +161 -0
  191. package/dist/session-diff.js +445 -0
  192. package/dist/session-registry.d.ts +143 -0
  193. package/dist/session-registry.js +216 -0
  194. package/dist/sessions-report.d.ts +59 -0
  195. package/dist/sessions-report.js +99 -0
  196. package/dist/shell-audit.d.ts +34 -0
  197. package/dist/shell-audit.js +47 -0
  198. package/dist/subagent-transcripts.d.ts +167 -0
  199. package/dist/subagent-transcripts.js +437 -0
  200. package/dist/subscriber-trust.d.ts +128 -0
  201. package/dist/subscriber-trust.js +397 -0
  202. package/dist/templates.d.ts +105 -0
  203. package/dist/templates.js +871 -0
  204. package/dist/tmux-install.d.ts +44 -0
  205. package/dist/tmux-install.js +65 -0
  206. package/dist/tmux-liveness.d.ts +45 -0
  207. package/dist/tmux-liveness.js +71 -0
  208. package/dist/tmux-pane.d.ts +55 -0
  209. package/dist/tmux-pane.js +126 -0
  210. package/dist/tmux-socket.d.ts +20 -0
  211. package/dist/tmux-socket.js +292 -0
  212. package/dist/todo-fold.d.ts +31 -0
  213. package/dist/todo-fold.js +102 -0
  214. package/dist/transcript-tail.d.ts +312 -0
  215. package/dist/transcript-tail.js +718 -0
  216. package/dist/trust-command.d.ts +49 -0
  217. package/dist/trust-command.js +200 -0
  218. package/dist/trust-lock.d.ts +26 -0
  219. package/dist/trust-lock.js +94 -0
  220. package/dist/trust-pins.d.ts +37 -0
  221. package/dist/trust-pins.js +82 -0
  222. package/dist/turn-recorder-wire.d.ts +70 -0
  223. package/dist/turn-recorder-wire.js +165 -0
  224. package/dist/turn-recorder.d.ts +190 -0
  225. package/dist/turn-recorder.js +695 -0
  226. package/dist/turn-ring.d.ts +93 -0
  227. package/dist/turn-ring.js +418 -0
  228. package/dist/turn-watch.d.ts +177 -0
  229. package/dist/turn-watch.js +630 -0
  230. package/dist/types.d.ts +63 -0
  231. package/dist/types.js +2 -0
  232. package/dist/uninstall.d.ts +55 -0
  233. package/dist/uninstall.js +507 -0
  234. package/dist/usage.d.ts +17 -0
  235. package/dist/usage.js +421 -0
  236. package/dist/verify.d.ts +47 -0
  237. package/dist/verify.js +396 -0
  238. package/dist/worktree.d.ts +144 -0
  239. package/dist/worktree.js +310 -0
  240. package/dist/wrapper.d.ts +254 -0
  241. package/dist/wrapper.js +704 -0
  242. package/dist/ws-url.d.ts +109 -0
  243. package/dist/ws-url.js +202 -0
  244. package/package.json +78 -10
  245. package/bin/hwip.js +0 -3
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promptStillShown = exports.createPromptSealer = exports.PROMPT_SEAL_RETRY_MS = void 0;
4
+ /**
5
+ * Turns the dialog a blocked session shows into the sealed summary the session
6
+ * report carries (`blockedPrompt`, hwip SERVER-API.md).
7
+ *
8
+ * `observe` is called once per session per report cycle, synchronously, with the
9
+ * pane the state was read from. Sealing is async (key lookup, ECDH), so the first
10
+ * cycle that sees a prompt starts the seal and `sealing()` says when it is done:
11
+ * the listener holds that report for it a short while (`withSettledPrompts`), since
12
+ * the server pushes the moment a session turns blocked and the phone reads the
13
+ * prompt for the push's buttons right then. The sealed object is kept while the
14
+ * prompt id is unchanged, so an unchanged prompt makes an unchanged report.
15
+ *
16
+ * A prompt that cannot be parsed, has no one to seal for, or fails to seal is left
17
+ * out: the session reports the bare `blocked` state, as it did before. Nothing
18
+ * here logs the dialog text, the plaintext or the ciphertext.
19
+ */
20
+ const blocked_prompt_js_1 = require("./blocked-prompt.js");
21
+ /** After a failed seal or an empty recipient list, how long before the same prompt is tried again. */
22
+ exports.PROMPT_SEAL_RETRY_MS = 30_000;
23
+ const createPromptSealer = (deps) => {
24
+ const now = deps.now ?? Date.now;
25
+ const entries = new Map();
26
+ const start = (name, entry, plaintext) => {
27
+ entry.busy = true;
28
+ entry.sealing = (async () => {
29
+ try {
30
+ const recipients = await deps.recipients();
31
+ if (recipients.length === 0) {
32
+ entry.retryAt = now() + exports.PROMPT_SEAL_RETRY_MS;
33
+ return;
34
+ }
35
+ const sealed = await deps.seal(plaintext, recipients);
36
+ entry.wire = { id: entry.id, at: entry.at, sealed };
37
+ }
38
+ catch (err) {
39
+ // The message of a crypto error names no content; the dialog text is never in it.
40
+ deps.log?.(`blocked prompt for ${name}: seal failed (${err instanceof Error ? err.message : String(err)})`);
41
+ entry.retryAt = now() + exports.PROMPT_SEAL_RETRY_MS;
42
+ }
43
+ finally {
44
+ entry.busy = false;
45
+ entry.sealing = undefined;
46
+ }
47
+ })();
48
+ };
49
+ return {
50
+ observe: (name, state, paneText) => {
51
+ const parsed = state === 'blocked' && paneText !== null ? (0, blocked_prompt_js_1.parseBlockedDialog)(paneText) : undefined;
52
+ if (!parsed) {
53
+ entries.delete(name);
54
+ return undefined;
55
+ }
56
+ let entry = entries.get(name);
57
+ if (entry?.id !== parsed.id) {
58
+ entry = { id: parsed.id, at: new Date(now()).toISOString(), busy: false };
59
+ entries.set(name, entry);
60
+ }
61
+ if (entry.wire)
62
+ return entry.wire;
63
+ if (!entry.busy && (entry.retryAt === undefined || now() >= entry.retryAt)) {
64
+ entry.retryAt = undefined;
65
+ const plaintext = (0, blocked_prompt_js_1.promptPlaintext)(parsed, { sessionName: name, deviceId: deps.deviceId(), at: entry.at });
66
+ // A prompt too big for the cap is not retried: the screen would not shrink.
67
+ if (plaintext === undefined)
68
+ entry.retryAt = Number.POSITIVE_INFINITY;
69
+ else
70
+ start(name, entry, plaintext);
71
+ }
72
+ return undefined;
73
+ },
74
+ prune: (live) => {
75
+ for (const name of entries.keys()) {
76
+ if (!live.has(name))
77
+ entries.delete(name);
78
+ }
79
+ },
80
+ sealing: () => {
81
+ const running = [...entries.values()].flatMap((e) => (e.sealing ? [e.sealing] : []));
82
+ return running.length > 0 ? Promise.all(running).then(() => undefined) : undefined;
83
+ },
84
+ };
85
+ };
86
+ exports.createPromptSealer = createPromptSealer;
87
+ /** True when the pane shows the prompt `id` right now (a fresh capture, not the one a report was made from). */
88
+ const promptStillShown = (id, paneText) => paneText !== null && (0, blocked_prompt_js_1.parseBlockedDialog)(paneText)?.id === id;
89
+ exports.promptStillShown = promptStillShown;
@@ -0,0 +1,57 @@
1
+ /** Dialog titles whose layout was captured and is read. */
2
+ export declare const KNOWN_TITLES: readonly string[];
3
+ export declare const MAX_DETAIL_LINES = 12;
4
+ export declare const MAX_DETAIL_LINE_CHARS = 300;
5
+ export declare const MAX_LABEL_CHARS = 200;
6
+ export declare const MAX_OPTIONS = 9;
7
+ /** The plaintext a prompt is sealed from stays under this; the server's cap on the sealed body is 8 KiB. */
8
+ export declare const MAX_PROMPT_BYTES: number;
9
+ export interface PromptOption {
10
+ /** The number the dialog shows. */
11
+ n: number;
12
+ label: string;
13
+ /** Named keys (`ALLOWED_KEYS` in listener.ts) that pick this option from the highlight the pane shows now. */
14
+ keys: string[];
15
+ /** Choosing it grants more than this one request ("don't ask again", "always", "for this session"). */
16
+ persistent?: true;
17
+ }
18
+ export interface ParsedPrompt {
19
+ /** Hash of the dialog as shown, highlight included: moving the highlight is a different prompt. */
20
+ id: string;
21
+ title: string;
22
+ subtitle?: string;
23
+ detail: string[];
24
+ detailTruncated?: true;
25
+ note?: string;
26
+ question: string;
27
+ options: PromptOption[];
28
+ }
29
+ export declare const isPromptId: (value: unknown) => value is string;
30
+ /** The dialog at the bottom of `pane`, or undefined when it is not one this module reads. */
31
+ export declare const parseBlockedDialog: (pane: string) => ParsedPrompt | undefined;
32
+ /** What the app opens: the prompt plus which session and listener it belongs to, and when it was first seen. */
33
+ export interface PromptPlaintext {
34
+ v: 1;
35
+ sessionName: string;
36
+ deviceId: string;
37
+ at: string;
38
+ id: string;
39
+ kind: 'permission';
40
+ title: string;
41
+ subtitle?: string;
42
+ detail: string[];
43
+ detailTruncated?: true;
44
+ note?: string;
45
+ question: string;
46
+ options: PromptOption[];
47
+ }
48
+ /**
49
+ * The JSON to seal, within MAX_PROMPT_BYTES: detail lines go from the end until it fits.
50
+ * Undefined when even a prompt without detail does not fit.
51
+ */
52
+ export declare const promptPlaintext: (prompt: ParsedPrompt, ctx: {
53
+ sessionName: string;
54
+ deviceId: string;
55
+ at: string;
56
+ }) => string | undefined;
57
+ //# sourceMappingURL=blocked-prompt.d.ts.map
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promptPlaintext = exports.parseBlockedDialog = exports.isPromptId = exports.MAX_PROMPT_BYTES = exports.MAX_OPTIONS = exports.MAX_LABEL_CHARS = exports.MAX_DETAIL_LINE_CHARS = exports.MAX_DETAIL_LINES = exports.KNOWN_TITLES = void 0;
4
+ /**
5
+ * What a blocked Claude Code session is waiting for, read off its screen.
6
+ *
7
+ * The state detector (`agent-state.ts`) only says "blocked". The app wants to
8
+ * draw the question: which tool, which command, which answers. This module
9
+ * reads the approval dialog at the bottom of the pane into that summary. Pure:
10
+ * the listener owns tmux, sealing and timing (`blocked-prompt-seal.ts`).
11
+ *
12
+ * Layout read (Claude Code, captured from a live session; see the fixtures in
13
+ * `blocked-prompt.test.ts`):
14
+ *
15
+ * ───────────── full-width rule: the dialog starts below the last one
16
+ * Bash command title
17
+ * Run shell command subtitle (optional)
18
+ * ╌╌╌╌╌╌╌╌╌╌╌╌╌ dashed rule
19
+ * rtk ls -la . detail (the command, or the diff of an edit)
20
+ * ╌╌╌╌╌╌╌╌╌╌╌╌╌
21
+ * This command requires approval note (optional)
22
+ *
23
+ * Do you want to proceed? question
24
+ * ❯ 1. Yes options, `❯` marks the highlighted one
25
+ * 2. Yes, and don’t ask again for: rtk ls *
26
+ * 3. No
27
+ *
28
+ * Esc to cancel · Tab to amend footer
29
+ *
30
+ * Only the titles in `KNOWN_TITLES` are read. Another dialog (a plan, a
31
+ * question to the user, an MCP tool) has no captured layout, so it yields
32
+ * nothing and the session keeps reporting the bare `blocked` state.
33
+ */
34
+ const node_crypto_1 = require("node:crypto");
35
+ /** Dialog titles whose layout was captured and is read. */
36
+ exports.KNOWN_TITLES = ['Bash command', 'Edit file', 'Create file'];
37
+ exports.MAX_DETAIL_LINES = 12;
38
+ exports.MAX_DETAIL_LINE_CHARS = 300;
39
+ exports.MAX_LABEL_CHARS = 200;
40
+ exports.MAX_OPTIONS = 9;
41
+ /** The plaintext a prompt is sealed from stays under this; the server's cap on the sealed body is 8 KiB. */
42
+ exports.MAX_PROMPT_BYTES = 2 * 1024;
43
+ /** The shape of a prompt id on the wire: what `ifPrompt` may carry (the server checks the same). */
44
+ const PROMPT_ID = /^[A-Za-z0-9_-]{1,64}$/;
45
+ const isPromptId = (value) => typeof value === 'string' && PROMPT_ID.test(value);
46
+ exports.isPromptId = isPromptId;
47
+ const FULL_RULE = /^─{20,}$/;
48
+ const DASHED_RULE = /^╌{20,}$/;
49
+ const OPTION = /^\s*([❯>])?\s*(\d{1,2})\.\s+(\S.*)$/;
50
+ const FOOTER = /^\s*(esc to cancel|enter to select)/i;
51
+ // A choice that grants more than this one request: "don't ask again", "always allow", "allow all edits during this
52
+ // session", "… for this session". Wider on purpose — a miss lets the app skip its extra confirmation.
53
+ const PERSISTENT = /don.?t ask again|never ask|always|allow all|this session|from now on/i;
54
+ /** The character the highlight is drawn with. A pane without it is not a dialog this module can answer. */
55
+ const HIGHLIGHT = '❯';
56
+ const cut = (text, max) => {
57
+ if (text.length <= max)
58
+ return text;
59
+ const head = text.slice(0, max);
60
+ const last = head.charCodeAt(head.length - 1);
61
+ return last >= 0xd800 && last <= 0xdbff ? head.slice(0, -1) : head;
62
+ };
63
+ const idOf = (region) => (0, node_crypto_1.createHash)('sha256').update(region.join('\n')).digest('hex').slice(0, 16);
64
+ /** `down`×k or `up`×k to reach option `to` from option `from`, then `enter`. */
65
+ const keysFor = (from, to) => [...Array(Math.abs(to - from)).fill(to > from ? 'down' : 'up'), 'enter'];
66
+ /** The dialog at the bottom of `pane`, or undefined when it is not one this module reads. */
67
+ const parseBlockedDialog = (pane) => {
68
+ const lines = pane.split('\n').map((l) => l.trimEnd());
69
+ // The question is the last "…?" line whose next non-blank line is option 1.
70
+ let qAt = -1;
71
+ for (let i = lines.length - 2; i >= 0; i--) {
72
+ if (!lines[i].endsWith('?'))
73
+ continue;
74
+ const next = lines.slice(i + 1).find((l) => l !== '');
75
+ if (next !== undefined && OPTION.exec(next)?.[2] === '1') {
76
+ qAt = i;
77
+ break;
78
+ }
79
+ }
80
+ if (qAt < 0)
81
+ return undefined;
82
+ // The dialog starts below the last full-width rule above the question and ends at its footer;
83
+ // whatever the terminal draws below (a status line, its own rule) is not part of it.
84
+ let top = -1;
85
+ for (let i = qAt - 1; i >= 0; i--) {
86
+ if (FULL_RULE.test(lines[i])) {
87
+ top = i;
88
+ break;
89
+ }
90
+ }
91
+ if (top < 0)
92
+ return undefined;
93
+ let end = lines.length;
94
+ while (end > qAt + 1 && lines[end - 1] === '')
95
+ end--;
96
+ for (let i = qAt + 1; i < end; i++) {
97
+ if (FOOTER.test(lines[i])) {
98
+ end = i + 1;
99
+ break;
100
+ }
101
+ }
102
+ const region = lines.slice(top + 1, end);
103
+ const q = qAt - (top + 1);
104
+ const options = [];
105
+ for (const line of region.slice(q + 1)) {
106
+ if (line === '' || FOOTER.test(line))
107
+ continue;
108
+ const m = OPTION.exec(line);
109
+ if (m) {
110
+ options.push({ n: Number(m[2]), label: m[3].trim(), selected: m[1] === HIGHLIGHT });
111
+ }
112
+ else if (options.length > 0 && /^\s{3,}\S/.test(line)) {
113
+ // A long label wraps onto an indented line.
114
+ const last = options[options.length - 1];
115
+ last.label = `${last.label} ${line.trim()}`;
116
+ }
117
+ else {
118
+ return undefined;
119
+ }
120
+ }
121
+ if (options.length < 2 || options.length > exports.MAX_OPTIONS)
122
+ return undefined;
123
+ if (!options.every((o, i) => o.n === i + 1))
124
+ return undefined;
125
+ const selected = options.findIndex((o) => o.selected);
126
+ if (selected < 0 || options.filter((o) => o.selected).length !== 1)
127
+ return undefined;
128
+ // Above the question: title, subtitle, dashed rule, detail, dashed rule, note.
129
+ const head = region.slice(0, q);
130
+ const title = head.find((l) => l.trim() !== '')?.trim();
131
+ if (!title || !exports.KNOWN_TITLES.includes(title))
132
+ return undefined;
133
+ const body = head.slice(head.findIndex((l) => l.trim() !== '') + 1);
134
+ const firstDash = body.findIndex((l) => DASHED_RULE.test(l));
135
+ const before = (firstDash < 0 ? body : body.slice(0, firstDash)).map((l) => l.trim()).filter(Boolean);
136
+ const rest = firstDash < 0 ? [] : body.slice(firstDash + 1);
137
+ const secondDash = rest.findIndex((l) => DASHED_RULE.test(l));
138
+ const detailLines = (secondDash < 0 ? rest : rest.slice(0, secondDash)).map((l) => l.replace(/^ /, ''));
139
+ while (detailLines.length > 0 && detailLines[detailLines.length - 1].trim() === '')
140
+ detailLines.pop();
141
+ const after = secondDash < 0 ? [] : rest.slice(secondDash + 1).map((l) => l.trim()).filter(Boolean);
142
+ const prompt = {
143
+ id: idOf(region),
144
+ title,
145
+ detail: detailLines.slice(0, exports.MAX_DETAIL_LINES).map((l) => cut(l, exports.MAX_DETAIL_LINE_CHARS)),
146
+ question: cut(region[q].trim(), exports.MAX_LABEL_CHARS),
147
+ options: options.map((o, i) => ({
148
+ n: o.n,
149
+ label: cut(o.label, exports.MAX_LABEL_CHARS),
150
+ keys: keysFor(selected, i),
151
+ ...(PERSISTENT.test(o.label) ? { persistent: true } : {}),
152
+ })),
153
+ };
154
+ if (before.length > 0)
155
+ prompt.subtitle = cut(before.join(' '), exports.MAX_LABEL_CHARS);
156
+ if (after.length > 0)
157
+ prompt.note = cut(after.join(' '), exports.MAX_LABEL_CHARS);
158
+ if (detailLines.length > exports.MAX_DETAIL_LINES || detailLines.some((l) => l.length > exports.MAX_DETAIL_LINE_CHARS)) {
159
+ prompt.detailTruncated = true;
160
+ }
161
+ return prompt;
162
+ };
163
+ exports.parseBlockedDialog = parseBlockedDialog;
164
+ /**
165
+ * The JSON to seal, within MAX_PROMPT_BYTES: detail lines go from the end until it fits.
166
+ * Undefined when even a prompt without detail does not fit.
167
+ */
168
+ const promptPlaintext = (prompt, ctx) => {
169
+ const { id, ...fields } = prompt;
170
+ const base = { v: 1, ...ctx, id, kind: 'permission', ...fields };
171
+ let json = JSON.stringify(base);
172
+ while (Buffer.byteLength(json) > exports.MAX_PROMPT_BYTES) {
173
+ if (base.detail.length === 0)
174
+ return undefined;
175
+ base.detail = base.detail.slice(0, -1);
176
+ base.detailTruncated = true;
177
+ json = JSON.stringify(base);
178
+ }
179
+ return json;
180
+ };
181
+ exports.promptPlaintext = promptPlaintext;
@@ -0,0 +1,67 @@
1
+ import type { NotifyPayload } from './types.js';
2
+ /**
3
+ * The part of a channel `GET /channels` returns that this CLI shows. The record
4
+ * also carries fields that are not for a terminal or a `--json` pipe (a
5
+ * webhook secret, the index tag), so the list is rebuilt from these and
6
+ * nothing else is passed along.
7
+ */
8
+ export interface ChannelSummary {
9
+ channelId: string;
10
+ name: string;
11
+ description?: string;
12
+ isPublic: boolean;
13
+ subscriberCount: number;
14
+ ownerId: string;
15
+ /** Whether the account owns it, as the server says. Absent from a server that does not send it yet. */
16
+ isOwner?: boolean;
17
+ createdAt?: string;
18
+ }
19
+ /** The channels in a `GET /channels` answer; a row without an id or a name is dropped, not guessed. */
20
+ export declare const parseChannels: (data: unknown) => ChannelSummary[];
21
+ /**
22
+ * What `hwip channel list` prints. The server says per channel whether the
23
+ * account owns it (`isOwner`), and answers with the owned ones first. A server
24
+ * that does not send the field yet gives the rows no way to say so — an API key
25
+ * cannot learn its own user id — so such a row shows its owner and leaves the
26
+ * comparison to the person (the server refuses `notify --channel` for a channel
27
+ * the account does not own).
28
+ */
29
+ export declare const channelLines: (channels: readonly ChannelSummary[]) => string[];
30
+ export type ChannelRef = {
31
+ channelId: string;
32
+ name?: string;
33
+ } | {
34
+ error: string;
35
+ };
36
+ /**
37
+ * Which channel `--channel <ref>` means. An id in the list wins; otherwise the
38
+ * name must match exactly one channel *the account owns* — a channel it only
39
+ * follows with the same name does not make the name ambiguous, and is never
40
+ * the target. Two owned channels with one name are refused rather than picked:
41
+ * a push to the wrong channel reaches strangers. An id the list does not hold is
42
+ * passed on as it is — the server decides whether it exists and is the account's.
43
+ *
44
+ * A server that does not say who owns what (`isOwner` absent on any row) leaves
45
+ * every name match a candidate, as before: ambiguity is then refused, and a
46
+ * channel the account does not own is refused by the server.
47
+ */
48
+ export declare const resolveChannel: (channels: readonly ChannelSummary[], ref: string) => ChannelRef;
49
+ export interface ChannelNotify {
50
+ ref: string;
51
+ payload: Omit<NotifyPayload, 'channelId'>;
52
+ }
53
+ /**
54
+ * The flags of `hwip notify --channel`, or what is wrong with them.
55
+ *
56
+ * A channel push is not the agent hook's push: the gate (`--auto`), the device
57
+ * and the session flags mean "this agent session", and none of them go to a
58
+ * channel — refusing them is better than dropping them silently, and the
59
+ * default title and body a hook gets from the branch name would publish a
60
+ * project name to subscribers.
61
+ */
62
+ export declare const parseChannelNotify: (args: Readonly<Record<string, string | boolean>>) => ChannelNotify | {
63
+ error: string;
64
+ };
65
+ /** Printed to stderr on every channel push: whatever the account's end-to-end setting, this one is readable by the server. */
66
+ export declare const PLAINTEXT_NOTE = "Channel pushes are sent as plain text, not sealed \u2014 the server can read this one even with end-to-end encryption on.";
67
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLAINTEXT_NOTE = exports.parseChannelNotify = exports.resolveChannel = exports.channelLines = exports.parseChannels = void 0;
4
+ const text = (value) => (typeof value === 'string' && value ? value : undefined);
5
+ /** The channels in a `GET /channels` answer; a row without an id or a name is dropped, not guessed. */
6
+ const parseChannels = (data) => {
7
+ if (!Array.isArray(data))
8
+ return [];
9
+ const channels = [];
10
+ for (const raw of data) {
11
+ if (!raw || typeof raw !== 'object')
12
+ continue;
13
+ const row = raw;
14
+ const channelId = text(row.channelId);
15
+ const name = text(row.name);
16
+ if (!channelId || !name)
17
+ continue;
18
+ const description = text(row.description);
19
+ const createdAt = text(row.createdAt);
20
+ channels.push({
21
+ channelId,
22
+ name,
23
+ ...(description && { description }),
24
+ isPublic: row.isPublic === true,
25
+ subscriberCount: typeof row.subscriberCount === 'number' ? row.subscriberCount : 0,
26
+ ownerId: text(row.ownerId) ?? '',
27
+ ...(typeof row.isOwner === 'boolean' && { isOwner: row.isOwner }),
28
+ ...(createdAt && { createdAt }),
29
+ });
30
+ }
31
+ return channels;
32
+ };
33
+ exports.parseChannels = parseChannels;
34
+ /**
35
+ * What `hwip channel list` prints. The server says per channel whether the
36
+ * account owns it (`isOwner`), and answers with the owned ones first. A server
37
+ * that does not send the field yet gives the rows no way to say so — an API key
38
+ * cannot learn its own user id — so such a row shows its owner and leaves the
39
+ * comparison to the person (the server refuses `notify --channel` for a channel
40
+ * the account does not own).
41
+ */
42
+ const channelLines = (channels) => {
43
+ if (channels.length === 0)
44
+ return ['No channels.'];
45
+ const idWidth = Math.max(...channels.map((c) => c.channelId.length));
46
+ const nameWidth = Math.max(...channels.map((c) => c.name.length));
47
+ const lines = channels.map((c) => {
48
+ const subs = `${c.subscriberCount} ${c.subscriberCount === 1 ? 'subscriber' : 'subscribers'}`;
49
+ const role = c.isOwner === true ? 'yours' : c.isOwner === false ? 'following' : c.ownerId ? `owner ${c.ownerId.slice(0, 8)}` : 'owner unknown';
50
+ return ` ${c.channelId.padEnd(idWidth)} ${c.name.padEnd(nameWidth)} ${c.isPublic ? 'public ' : 'private'} ${subs} ${role}`;
51
+ });
52
+ lines.push('', 'Only an owner can send to a channel.');
53
+ return lines;
54
+ };
55
+ exports.channelLines = channelLines;
56
+ /**
57
+ * Which channel `--channel <ref>` means. An id in the list wins; otherwise the
58
+ * name must match exactly one channel *the account owns* — a channel it only
59
+ * follows with the same name does not make the name ambiguous, and is never
60
+ * the target. Two owned channels with one name are refused rather than picked:
61
+ * a push to the wrong channel reaches strangers. An id the list does not hold is
62
+ * passed on as it is — the server decides whether it exists and is the account's.
63
+ *
64
+ * A server that does not say who owns what (`isOwner` absent on any row) leaves
65
+ * every name match a candidate, as before: ambiguity is then refused, and a
66
+ * channel the account does not own is refused by the server.
67
+ */
68
+ const resolveChannel = (channels, ref) => {
69
+ const known = channels.every((c) => typeof c.isOwner === 'boolean');
70
+ const byId = channels.find((c) => c.channelId === ref);
71
+ if (byId) {
72
+ if (known && !byId.isOwner)
73
+ return { error: `you follow "${byId.name}" (${byId.channelId}) but do not own it — only the owner can send to a channel` };
74
+ return { channelId: byId.channelId, name: byId.name };
75
+ }
76
+ const named = channels.filter((c) => c.name === ref);
77
+ const byName = known ? named.filter((c) => c.isOwner) : named;
78
+ if (byName.length === 1)
79
+ return { channelId: byName[0].channelId, name: byName[0].name };
80
+ if (byName.length > 1) {
81
+ const ids = byName.map((c) => c.channelId).join(', ');
82
+ return { error: `${byName.length} channels are named "${ref}" (${ids}) — use the channelId` };
83
+ }
84
+ if (known && named.length > 0) {
85
+ return { error: `you follow "${ref}" (${named.map((c) => c.channelId).join(', ')}) but do not own a channel by that name — only the owner can send to a channel` };
86
+ }
87
+ if (/^ch_/.test(ref))
88
+ return { channelId: ref };
89
+ return { error: `no channel named "${ref}" — 'hwip channel list' shows the channels; use a channelId or an exact name` };
90
+ };
91
+ exports.resolveChannel = resolveChannel;
92
+ const PRIORITIES = ['low', 'normal', 'high', 'urgent'];
93
+ const TYPES = ['note', 'link', 'file', 'hook'];
94
+ /**
95
+ * The flags of `hwip notify --channel`, or what is wrong with them.
96
+ *
97
+ * A channel push is not the agent hook's push: the gate (`--auto`), the device
98
+ * and the session flags mean "this agent session", and none of them go to a
99
+ * channel — refusing them is better than dropping them silently, and the
100
+ * default title and body a hook gets from the branch name would publish a
101
+ * project name to subscribers.
102
+ */
103
+ const parseChannelNotify = (args) => {
104
+ const ref = args.channel;
105
+ if (typeof ref !== 'string' || !ref.trim())
106
+ return { error: '--channel needs a channelId or a channel name' };
107
+ for (const flag of ['auto', 'device', 'session']) {
108
+ if (args[flag] !== undefined)
109
+ return { error: `--${flag} is for agent sessions and cannot be combined with --channel` };
110
+ }
111
+ const title = typeof args.title === 'string' ? args.title : undefined;
112
+ const body = typeof args.body === 'string' ? args.body : undefined;
113
+ if (!title && !body)
114
+ return { error: 'a channel push needs --title or --body' };
115
+ const type = typeof args.type === 'string' ? args.type : 'hook';
116
+ if (!TYPES.includes(type))
117
+ return { error: `--type must be one of ${TYPES.join(', ')}` };
118
+ const priority = typeof args.priority === 'string' ? args.priority : undefined;
119
+ if (priority && !PRIORITIES.includes(priority)) {
120
+ return { error: `--priority must be one of ${PRIORITIES.join(', ')}` };
121
+ }
122
+ const url = typeof args.url === 'string' ? args.url : undefined;
123
+ return {
124
+ ref: ref.trim(),
125
+ payload: {
126
+ ...(title && { title }),
127
+ ...(body && { body }),
128
+ ...(url && { url }),
129
+ type: type,
130
+ ...(priority && { priority: priority }),
131
+ },
132
+ };
133
+ };
134
+ exports.parseChannelNotify = parseChannelNotify;
135
+ /** Printed to stderr on every channel push: whatever the account's end-to-end setting, this one is readable by the server. */
136
+ exports.PLAINTEXT_NOTE = 'Channel pushes are sent as plain text, not sealed — the server can read this one even with end-to-end encryption on.';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * How to update. A global install is what `hwip` on PATH runs — `npx @hwip/cli
3
+ * install` only re-ran setup from a fresh download and left it as it was —
4
+ * and the daemon keeps the old build until it restarts.
5
+ */
6
+ export declare const UPDATE_HINT = "npm i -g @hwip/cli \u2014 the next `hwip cc` restarts the listener on it (or run: hwip listener --restart)";
7
+ /** Semver-ish compare: returns true when `latest` is strictly newer than `current`. */
8
+ export declare const isNewer: (latest: string, current: string) => boolean;
9
+ /**
10
+ * The mcp-server version this CLI will actually run. It ships as a dependency
11
+ * (see mcp.ts), so it is pinned at install time — no longer "whatever npx
12
+ * fetched last". Returns null on a broken install rather than throwing; the
13
+ * report degrades to "latest is vX" the way it always did for this package.
14
+ */
15
+ export declare const installedMcpServerVersion: () => string | null;
16
+ /**
17
+ * One line for `hwip cc` when a newer CLI is out, or undefined. Asks npm at
18
+ * most once a day, briefly, and remembers the answer; an offline day keeps
19
+ * the last known version rather than asking again on every launch.
20
+ */
21
+ export declare const cachedUpdateNotice: (deps?: {
22
+ now?: () => number;
23
+ latest?: () => Promise<string | null>;
24
+ file?: string;
25
+ env?: NodeJS.ProcessEnv;
26
+ }) => Promise<string | undefined>;
27
+ export declare const handleCheckUpdate: (args: Record<string, string | boolean>) => Promise<number>;
28
+ //# sourceMappingURL=check-update.d.ts.map