@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,245 @@
1
+ "use strict";
2
+ /**
3
+ * Best-effort masking of secret-looking strings in a patch, before it is sealed
4
+ * for the phone. It keeps the patch's shape — the same lines, the same diff
5
+ * marker on each, the same headers — so line numbers and hunks still line up;
6
+ * only text after the marker changes.
7
+ *
8
+ * This is a list of well-known token formats and secret-named assignments, not
9
+ * a promise: a secret in a form no rule knows goes through, and so does key
10
+ * material whose BEGIN line is outside the patch (the rules see one line at a
11
+ * time, plus whether a key block opened above). The count of masked spots is
12
+ * part of the answer so the reader is told something was hidden, and is never
13
+ * told nothing was.
14
+ *
15
+ * Every pattern is bounded: the patch comes from a repository the agent may
16
+ * have cloned from anywhere, the masking runs on the listener's event loop, and
17
+ * an unbounded `x*` retried from every start of a 200 KB line is quadratic.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.maskSecrets = exports.maskText = void 0;
21
+ const redact_js_1 = require("./redact.js");
22
+ const MASK = '***';
23
+ /** Token formats with a recognisable prefix. Whole match is replaced. */
24
+ const TOKEN_FORMATS = [
25
+ /\bAKIA[0-9A-Z]{16}\b/g,
26
+ /\bgh[pousr]_[A-Za-z0-9]{20,255}/g,
27
+ /\bgithub_pat_[A-Za-z0-9_]{20,255}/g,
28
+ /\bsk-[A-Za-z0-9_-]{20,255}/g,
29
+ /\bxox[abprs]-[A-Za-z0-9-]{10,255}/g,
30
+ /\bBearer[ \t]+[A-Za-z0-9._~+/-]{16,512}=*/g,
31
+ // npm CLI tokens: npm_ plus exactly 36 word characters (env names such as
32
+ // npm_config_registry hold an underscore inside, so the boundary keeps them out).
33
+ /\bnpm_[A-Za-z0-9]{36}\b/g,
34
+ // GitLab personal access tokens.
35
+ /\bglpat-[A-Za-z0-9_]{20,64}/g,
36
+ // HTTP Basic credentials: the base64 blob needs a digit, sign or padding - a
37
+ // plain English word after Basic (Basic authentication is...) stays.
38
+ /\bBasic[ \t]+(?=[A-Za-z0-9+/=]*[0-9+/=])[A-Za-z0-9+/=]{16,512}/g,
39
+ /\beyJ[A-Za-z0-9_-]{5,256}\.[A-Za-z0-9_-]{5,1024}\.[A-Za-z0-9_-]{5,1024}/g,
40
+ ];
41
+ /** A line longer than this is not read by a person on a phone and is not
42
+ * scanned: it is masked whole. Keeps the scan linear in the patch. */
43
+ const MAX_SCANNED_LINE = 8 * 1024;
44
+ /**
45
+ * `name = value` / `name: value` / `"name": "value"` where the name says it is
46
+ * a secret and the value is at least eight characters. Only the value is
47
+ * replaced. A value that is a reference to somewhere the secret lives
48
+ * (`process.env.X`, `$VAR`) is not the secret.
49
+ */
50
+ const SECRET_ASSIGNMENT = /((?:secret|token|passw(?:or)?d|api[_-]?key|private[_-]?key|credential)[A-Za-z0-9_]{0,64}["']?[ \t]{0,16}[=:][ \t]{0,16}["'`]?)([^\s"'`,;)}\]]{8,2048})/gi;
51
+ /** The same secret-named assignments, with the value quoted: a quoted value may hold
52
+ * spaces (password = "hunter2 abc"), so the unquoted rule no-whitespace value
53
+ * misses it. Same eight-character floor as the unquoted rule; run before it, so a
54
+ * value is counted once even when its first word alone would match there. */
55
+ const SECRET_QUOTED_VALUE = /((?:secret|token|passw(?:or)?d|api[_-]?key|private[_-]?key|credential)[A-Za-z0-9_]{0,64}["']?[ \t]{0,16}[=:][ \t]{0,16}["'])([^"'`\n]{8,2048}?)(["'])/gi;
56
+ const ENV_REFERENCE = /^(?:process\.env|import\.meta\.env|os\.environ|os\.getenv|System\.getenv|ENV\[|\$\{?[A-Za-z_])/;
57
+ /** An unquoted member chain or call (`response.data.token`, `createToken(cfg)`) is code, not a literal.
58
+ * A digit in the first segment (`ya29.a0…`, `hunter2.0x`) says it is a value. */
59
+ const CODE_EXPRESSION = /^[A-Za-z_]+(?:\.\w+|\()/;
60
+ /** What may follow a BEGIN marker that opens a block: nothing, a closing quote, or a string's escaped newline. */
61
+ const PEM_CLOSING_TAIL = /^(?:\\(?:r\\)?n)?["'`\\\s,+]*$/;
62
+ const PEM_OPENING_TAIL = /^(?:["'`\\\s,+]*|\\(?:r\\)?n.*)$/;
63
+ const PEM_END = /-----END [A-Z0-9 ]{0,40}PRIVATE KEY-----/;
64
+ /** What a key block's body lines look like: base64, or a header such as `Proc-Type: 4,ENCRYPTED`. */
65
+ const PEM_BODY_LINE = /^\s{0,64}(?:#|\/\/|\*|;)?\s{0,64}["'`]?(?:[A-Za-z0-9+/=]{1,8192}|[A-Za-z][A-Za-z0-9-]{0,40}: .{0,200})(?:\\(?:r\\)?n)?["'`]?[,+\s]*$/;
66
+ /** Mask one line of content (marker already taken off). */
67
+ const maskBody = (body) => {
68
+ let masked = 0;
69
+ let out = body;
70
+ for (const format of TOKEN_FORMATS) {
71
+ out = out.replace(format, () => {
72
+ masked++;
73
+ return MASK;
74
+ });
75
+ }
76
+ out = out.replace(SECRET_QUOTED_VALUE, (_whole, head, _value, quote) => {
77
+ masked++;
78
+ return head + MASK + quote;
79
+ });
80
+ const cleaned = (0, redact_js_1.redactSecrets)(out);
81
+ if (cleaned !== out) {
82
+ masked++;
83
+ out = cleaned;
84
+ }
85
+ out = out.replace(SECRET_ASSIGNMENT, (whole, head, value) => {
86
+ const quoted = /["'`]$/.test(head);
87
+ if (ENV_REFERENCE.test(value) || (!quoted && CODE_EXPRESSION.test(value)))
88
+ return whole;
89
+ masked++;
90
+ return head + MASK;
91
+ });
92
+ return { body: out, masked };
93
+ };
94
+ /** The `@@ -a,b +c,d @@` part of a hunk header; what follows it is a line of the file's own text. */
95
+ const HUNK_RANGE = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
96
+ const PEM_MARKER = /-----(BEGIN|END) [A-Z0-9 ]{0,40}PRIVATE KEY-----/g;
97
+ /** A hunk header with the file's context line (`@@ -4,4 +4,4 @@ API_KEY=sk-…`) masked like any other text. */
98
+ const maskHunkHeader = (line) => {
99
+ const range = HUNK_RANGE.exec(line);
100
+ if (!range)
101
+ return { line, masked: 0 };
102
+ const tail = line.slice(range[0].length);
103
+ if (tail.length > MAX_SCANNED_LINE)
104
+ return { line: range[0] + ' ' + MASK, masked: 1 };
105
+ const result = maskBody(tail);
106
+ return { line: range[0] + result.body, masked: result.masked };
107
+ };
108
+ /**
109
+ * Mask one line's content (marker already taken off). `state.inPem` says a key
110
+ * block opened on an earlier line and carries a block opened on this one out.
111
+ * A line may hold several key blocks, the end of one and the start of the next,
112
+ * or a key block beside a token: every piece is read.
113
+ */
114
+ const maskContent = (body, state) => {
115
+ let masked = 0;
116
+ let out = '';
117
+ const plain = (text) => {
118
+ const result = maskBody(text);
119
+ masked += result.masked;
120
+ out += result.body;
121
+ };
122
+ let from = 0;
123
+ if (state.inPem) {
124
+ const end = PEM_END.exec(body);
125
+ if (!end) {
126
+ // Only lines that look like key material are hidden; anything else means
127
+ // the block was not one (or has ended out of view) and is read normally.
128
+ // A blank line stays inside: an encrypted key puts one between its headers and its body.
129
+ if (body.trim() === '')
130
+ return { body, masked: 0 };
131
+ if (PEM_BODY_LINE.test(body))
132
+ return { body: MASK, masked: 0 };
133
+ state.inPem = false;
134
+ }
135
+ else {
136
+ // The block ends on this line: what is before the end marker is its last text.
137
+ state.inPem = false;
138
+ const before = body.slice(0, end.index);
139
+ if (before === '' || PEM_BODY_LINE.test(before))
140
+ out += before === '' ? '' : MASK;
141
+ else
142
+ plain(before);
143
+ from = end.index;
144
+ }
145
+ }
146
+ // Every marker of the line, found once; `endAt[i]` is the first END among markers i.. — so a line
147
+ // thick with markers is read in one pass, not once per BEGIN.
148
+ const markers = [...body.matchAll(PEM_MARKER)];
149
+ const endAt = new Array(markers.length + 1).fill(-1);
150
+ for (let i = markers.length - 1; i >= 0; i--)
151
+ endAt[i] = markers[i][1] === 'END' ? i : endAt[i + 1];
152
+ let k = 0;
153
+ for (;;) {
154
+ while (k < markers.length && (markers[k].index < from || markers[k][1] !== 'BEGIN'))
155
+ k++;
156
+ const begin = markers[k];
157
+ if (!begin) {
158
+ plain(body.slice(from));
159
+ break;
160
+ }
161
+ const afterBegin = begin.index + begin[0].length;
162
+ plain(body.slice(from, begin.index));
163
+ out += begin[0];
164
+ const endIndex = endAt[k + 1];
165
+ if (endIndex >= 0) {
166
+ // Begin and end on one line (a key inside a string literal): the middle goes.
167
+ masked++;
168
+ out += MASK;
169
+ from = markers[endIndex].index;
170
+ k = endIndex;
171
+ continue;
172
+ }
173
+ // A marker that ends its line (after any quote) opens a block, counted once
174
+ // however many lines it has; so does one followed by a string's escaped
175
+ // newline, whose key text on the same line goes too. A marker inside code
176
+ // (`startsWith("-----BEGIN …") {`) does not.
177
+ const tail = body.slice(afterBegin);
178
+ if (PEM_OPENING_TAIL.test(tail)) {
179
+ masked++;
180
+ state.inPem = true;
181
+ out += PEM_CLOSING_TAIL.test(tail) ? tail : MASK;
182
+ break;
183
+ }
184
+ // Not a block: read on, a later marker on the line may be one.
185
+ from = afterBegin;
186
+ k++;
187
+ }
188
+ return { body: out, masked };
189
+ };
190
+ /**
191
+ * Mask secret-looking strings in prose or any other text that is not a patch: no marker is taken off a line, so
192
+ * a token at the head of a line is read like any other. For text that leaves the machine to a reader other than
193
+ * the user (what `hwip agent read` prints). Same rules, same limits as `maskSecrets`: best effort, not a promise.
194
+ */
195
+ const maskText = (text) => {
196
+ let masked = 0;
197
+ const state = { inPem: false };
198
+ const lines = text.split('\n').map((line) => {
199
+ if (line.length > MAX_SCANNED_LINE) {
200
+ masked++;
201
+ return MASK;
202
+ }
203
+ const result = maskContent(line, state);
204
+ masked += result.masked;
205
+ return result.body;
206
+ });
207
+ return { text: lines.join('\n'), masked };
208
+ };
209
+ exports.maskText = maskText;
210
+ const maskSecrets = (patch) => {
211
+ let masked = 0;
212
+ let inHeader = false;
213
+ const state = { inPem: false };
214
+ const lines = patch.split('\n').map((line) => {
215
+ // File headers run from `diff --git` to the first hunk: a line there that
216
+ // starts with `+++ ` is a path, not an added line.
217
+ if (line.startsWith('diff --git ')) {
218
+ inHeader = true;
219
+ state.inPem = false;
220
+ return line;
221
+ }
222
+ if (inHeader && !line.startsWith('@@'))
223
+ return line;
224
+ if (line.startsWith('@@')) {
225
+ inHeader = false;
226
+ state.inPem = false;
227
+ const header = maskHunkHeader(line);
228
+ masked += header.masked;
229
+ return header.line;
230
+ }
231
+ if (line === '' || line.startsWith('\\'))
232
+ return line;
233
+ const marker = line.slice(0, 1);
234
+ const body = line.slice(1);
235
+ if (body.length > MAX_SCANNED_LINE) {
236
+ masked++;
237
+ return marker + MASK;
238
+ }
239
+ const result = maskContent(body, state);
240
+ masked += result.masked;
241
+ return marker + result.body;
242
+ });
243
+ return { text: lines.join('\n'), masked };
244
+ };
245
+ exports.maskSecrets = maskSecrets;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * This computer's own key in the account's device list, put there when it is missing.
3
+ *
4
+ * Every push, question and command is sealed (F16), and the app seals its answer for the computer that asked by the key
5
+ * the device list holds for it. The listener puts that key there over its socket; a computer that only runs `hwip ask`,
6
+ * `hwip agent send` or `hwip notify` used to stay off the list, and every question it asked was refused. Each of
7
+ * those now checks the list it already read: listed under this key, nothing is sent; missing or under another key, one
8
+ * keyed `PUT /devices/{id}/public-key` (server SERVER-API.md), and the list it goes on with holds the new entry —
9
+ * no second `GET /devices`.
10
+ *
11
+ * The server lets only a `push:read` API key replace a key already listed; another key gets 403 and the old key stays.
12
+ */
13
+ export interface SelfKey {
14
+ deviceId: string;
15
+ publicKey: string;
16
+ }
17
+ /** Why the key is not on the list: the server's status, no answer, or 403 on replacing a key the list already holds. */
18
+ export type RegisterWhy = `http ${number}` | 'network' | 'timeout' | 'replace-forbidden';
19
+ /** Put this computer's key on the list: `null` once the server took it. */
20
+ export type Register = (self: SelfKey) => Promise<RegisterWhy | null>;
21
+ export type SelfListing<D> = {
22
+ ok: true;
23
+ devices: D[];
24
+ } | {
25
+ ok: false;
26
+ why: RegisterWhy;
27
+ };
28
+ /** The keyed `PUT`, never following a redirect (`apiFetch`). `fetchFn` is read per call, so a stub set later applies. */
29
+ export declare const registerOverApi: (account: {
30
+ apiKey: string;
31
+ baseUrl: string;
32
+ }, fetchFn?: typeof fetch) => Register;
33
+ /**
34
+ * `devices` with this computer listed under `self.publicKey`: as given when it already is, else after one `register`.
35
+ * The entry it adds is what the server made of it (`type: 'cli'`); one it replaces keeps every other field.
36
+ */
37
+ export declare const ensureSelfListed: <D extends {
38
+ deviceId: string;
39
+ publicKey?: string;
40
+ }>(devices: D[], self: SelfKey, register: Register) => Promise<SelfListing<D>>;
41
+ /** `why` for a person, inside parentheses. */
42
+ export declare const registerWhyText: (why: RegisterWhy) => string;
43
+ /** The refusal of a question or a command whose sender is not on the list: `what` was not sent. */
44
+ export declare const unregisteredText: (what: string, why?: RegisterWhy) => string;
45
+ //# sourceMappingURL=self-register.d.ts.map
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * This computer's own key in the account's device list, put there when it is missing.
4
+ *
5
+ * Every push, question and command is sealed (F16), and the app seals its answer for the computer that asked by the key
6
+ * the device list holds for it. The listener puts that key there over its socket; a computer that only runs `hwip ask`,
7
+ * `hwip agent send` or `hwip notify` used to stay off the list, and every question it asked was refused. Each of
8
+ * those now checks the list it already read: listed under this key, nothing is sent; missing or under another key, one
9
+ * keyed `PUT /devices/{id}/public-key` (server SERVER-API.md), and the list it goes on with holds the new entry —
10
+ * no second `GET /devices`.
11
+ *
12
+ * The server lets only a `push:read` API key replace a key already listed; another key gets 403 and the old key stays.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.unregisteredText = exports.registerWhyText = exports.ensureSelfListed = exports.registerOverApi = void 0;
16
+ const node_os_1 = require("node:os");
17
+ const api_js_1 = require("./api.js");
18
+ const crypto_js_1 = require("./crypto.js");
19
+ const REGISTER_TIMEOUT_MS = 10_000;
20
+ /** The name a new device record gets: the one the listener connects under. The server keeps an existing record's name. */
21
+ const nickname = () => (0, node_os_1.hostname)() || 'listener';
22
+ /** The keyed `PUT`, never following a redirect (`apiFetch`). `fetchFn` is read per call, so a stub set later applies. */
23
+ const registerOverApi = (account, fetchFn) => async (self) => {
24
+ let res;
25
+ try {
26
+ res = await (0, api_js_1.apiFetch)(fetchFn ?? fetch, (0, api_js_1.apiUrl)(account.baseUrl, `/devices/${encodeURIComponent(self.deviceId)}/public-key`), account.apiKey, REGISTER_TIMEOUT_MS, {
27
+ method: 'PUT',
28
+ body: JSON.stringify({ publicKey: self.publicKey, nickname: nickname() }),
29
+ });
30
+ }
31
+ catch (err) {
32
+ return err instanceof DOMException && err.name === 'TimeoutError' ? 'timeout' : 'network';
33
+ }
34
+ try {
35
+ await res.arrayBuffer();
36
+ }
37
+ catch { /* the answer's body is not needed */ }
38
+ return res.ok ? null : `http ${res.status}`;
39
+ };
40
+ exports.registerOverApi = registerOverApi;
41
+ /**
42
+ * `devices` with this computer listed under `self.publicKey`: as given when it already is, else after one `register`.
43
+ * The entry it adds is what the server made of it (`type: 'cli'`); one it replaces keeps every other field.
44
+ */
45
+ const ensureSelfListed = async (devices, self, register) => {
46
+ const listed = devices.find((d) => d.deviceId === self.deviceId);
47
+ if (listed?.publicKey && (0, crypto_js_1.samePublicKey)(listed.publicKey, self.publicKey))
48
+ return { ok: true, devices };
49
+ const why = await register(self);
50
+ if (why !== null)
51
+ return { ok: false, why: why === 'http 403' && listed?.publicKey ? 'replace-forbidden' : why };
52
+ if (listed)
53
+ return { ok: true, devices: devices.map((d) => (d === listed ? { ...d, publicKey: self.publicKey } : d)) };
54
+ // The server answers with the record; the list only needs what it is read for: an id, a key and a kind.
55
+ return { ok: true, devices: [...devices, { deviceId: self.deviceId, publicKey: self.publicKey, type: 'cli' }] };
56
+ };
57
+ exports.ensureSelfListed = ensureSelfListed;
58
+ /** `why` for a person, inside parentheses. */
59
+ const registerWhyText = (why) => why === 'replace-forbidden'
60
+ ? 'the account lists another key for it, and only an API key with push:read may replace it'
61
+ : why === 'network' ? 'could not reach the server'
62
+ : why === 'timeout' ? 'the server did not answer in time'
63
+ : `the server answered HTTP ${why.slice(5)}`;
64
+ exports.registerWhyText = registerWhyText;
65
+ /** The refusal of a question or a command whose sender is not on the list: `what` was not sent. */
66
+ const unregisteredText = (what, why) => `could not register this machine's key in the account's device list${why ? ` (${(0, exports.registerWhyText)(why)})` : ''}; the ${what} was not sent`;
67
+ exports.unregisteredText = unregisteredText;
package/dist/send.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { type AgentTargetDevice } from './agent-target.js';
2
+ import { type AgentCommandPayload, type CommandSealDeps } from './command-seal.js';
3
+ export interface SendDeps {
4
+ listDevices: () => Promise<AgentTargetDevice[]>;
5
+ sendAgentCommand: (payload: AgentCommandPayload) => Promise<{
6
+ pushId: string;
7
+ }>;
8
+ /** What `sealedCommand` needs; absent in tests that send plain. */
9
+ seal: CommandSealDeps;
10
+ /** This process's own agent session, or null outside tmux. */
11
+ context: () => {
12
+ agentDeviceId: string;
13
+ agentSessionName: string;
14
+ } | null;
15
+ hostname: () => string;
16
+ out: (line: string) => void;
17
+ err: (line: string) => void;
18
+ }
19
+ /** `hwip agent send` is the name; `hwip send` is the older one for the same command (C82). */
20
+ export type SendCommand = 'send' | 'agent send';
21
+ export declare const usageFor: (command: SendCommand) => string;
22
+ export declare const USAGE: string;
23
+ /** `hwip agent` takes `send`, `list` or `read`; null when that is what was typed. */
24
+ export declare const agentSubcommandError: (subcommand: string) => string | null;
25
+ /**
26
+ * `-` as the whole message: read it from stdin. An agent writes a summary on a
27
+ * quoted heredoc (`hwip agent send <key> - <<'EOF'`), where the shell expands nothing;
28
+ * inside double quotes its backticks or `$(…)` would run. Only on `-`: a bare
29
+ * target with an open, empty stdin would wait forever.
30
+ */
31
+ export declare const sendReadsStdin: (argv: string[]) => boolean;
32
+ /** The target and the message `hwip send` / `hwip agent send` got, or the usage error to exit 2 with. */
33
+ export declare const parseSendArgs: (argv: string[], stdin?: string, command?: SendCommand) => {
34
+ target: string;
35
+ message: string;
36
+ } | {
37
+ error: string;
38
+ };
39
+ /**
40
+ * `hwip agent send <target> <message…>` (and `hwip send`, the older name) — type a message into another agent session,
41
+ * the CLI twin of the MCP `hwip_agent_send` for agents without MCP (pi).
42
+ * Takes what `parseSendArgs` read. Exit 0 stored by the server · 1 anything else.
43
+ */
44
+ export declare const runSend: ({ target, message }: {
45
+ target: string;
46
+ message: string;
47
+ }, deps: SendDeps, command?: SendCommand) => Promise<number>;
48
+ export declare const liveSendDeps: (hook: {
49
+ listDevices: SendDeps["listDevices"];
50
+ sendAgentCommand: SendDeps["sendAgentCommand"];
51
+ account: {
52
+ apiKey: string;
53
+ baseUrl: string;
54
+ };
55
+ }) => SendDeps;
56
+ //# sourceMappingURL=send.d.ts.map
package/dist/send.js ADDED
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.liveSendDeps = exports.runSend = exports.parseSendArgs = exports.sendReadsStdin = exports.agentSubcommandError = exports.USAGE = exports.usageFor = void 0;
4
+ const node_os_1 = require("node:os");
5
+ const agent_session_js_1 = require("./agent-session.js");
6
+ const agent_target_js_1 = require("./agent-target.js");
7
+ const ask_js_1 = require("./ask.js");
8
+ const command_seal_js_1 = require("./command-seal.js");
9
+ const usageFor = (command) => `Usage: hwip [--key …] [--base-url …] ${command} <deviceId:session | name> <message… | - (read stdin)>`;
10
+ exports.usageFor = usageFor;
11
+ exports.USAGE = (0, exports.usageFor)('send');
12
+ /** `hwip agent` takes `send`, `list` or `read`; null when that is what was typed. */
13
+ const agentSubcommandError = (subcommand) => subcommand === 'send' || subcommand === 'list' || subcommand === 'read'
14
+ ? null
15
+ : `hwip agent: subcommands are send, list, read${subcommand ? `, not '${subcommand}'` : ''}\n${(0, exports.usageFor)('agent send')}`;
16
+ exports.agentSubcommandError = agentSubcommandError;
17
+ // A CLI flag after `send`. Everything there is message text, so `--key ak_…`
18
+ // would be typed into the other session in plaintext: refuse it instead. A
19
+ // message quoted as one argument that mentions `--key` is not a flag.
20
+ const MISPLACED_FLAG = /^--(key|base-url)(=|$)/;
21
+ /**
22
+ * `-` as the whole message: read it from stdin. An agent writes a summary on a
23
+ * quoted heredoc (`hwip agent send <key> - <<'EOF'`), where the shell expands nothing;
24
+ * inside double quotes its backticks or `$(…)` would run. Only on `-`: a bare
25
+ * target with an open, empty stdin would wait forever.
26
+ */
27
+ const sendReadsStdin = (argv) => argv.length === 2 && argv[1] === '-';
28
+ exports.sendReadsStdin = sendReadsStdin;
29
+ /** The target and the message `hwip send` / `hwip agent send` got, or the usage error to exit 2 with. */
30
+ const parseSendArgs = (argv, stdin, command = 'send') => {
31
+ const flag = argv.find((word) => MISPLACED_FLAG.test(word));
32
+ if (flag) {
33
+ return { error: `hwip ${command}: ${flag.split('=')[0]} goes before '${command.split(' ')[0]}' — after it, it is message text\n${(0, exports.usageFor)(command)}` };
34
+ }
35
+ const [target, ...words] = argv;
36
+ const message = ((0, exports.sendReadsStdin)(argv) ? (stdin ?? '') : words.join(' ')).trim();
37
+ return target && message ? { target, message } : { error: (0, exports.usageFor)(command) };
38
+ };
39
+ exports.parseSendArgs = parseSendArgs;
40
+ /**
41
+ * The header the receiver reads first. It matches the MCP tool's
42
+ * (mcp-server/src/tools/agent-send.ts `senderHeader`); the web app is to parse
43
+ * the same shape to caption the bubble. The reply key is offered only for a
44
+ * session the listener reports, and never for a subagent or a `hwip sh` shell:
45
+ * `resolveAgentTarget` takes none of them, so the reply would not resolve.
46
+ */
47
+ const senderHeader = (devices, deps) => {
48
+ const ctx = deps.context();
49
+ const own = ctx ? devices.find((d) => d.deviceId === ctx.agentDeviceId) : undefined;
50
+ const host = own?.nickname ?? deps.hostname();
51
+ const listed = !!ctx && !!own?.agentSessions?.some((s) => s.name === ctx.agentSessionName && (0, agent_target_js_1.isAgentTarget)(s));
52
+ if (!ctx || !listed)
53
+ return { header: `[from ${ctx?.agentSessionName ?? 'cli'}@${host}]` };
54
+ const ownKey = `${ctx.agentDeviceId}:${ctx.agentSessionName}`;
55
+ const label = own?.agentSessionAliases?.[ctx.agentSessionName] ?? ctx.agentSessionName;
56
+ return { header: `[from ${label}@${host} · reply: ${ownKey}]`, ownKey };
57
+ };
58
+ /**
59
+ * `hwip agent send <target> <message…>` (and `hwip send`, the older name) — type a message into another agent session,
60
+ * the CLI twin of the MCP `hwip_agent_send` for agents without MCP (pi).
61
+ * Takes what `parseSendArgs` read. Exit 0 stored by the server · 1 anything else.
62
+ */
63
+ const runSend = async ({ target, message }, deps, command = 'send') => {
64
+ try {
65
+ const devices = await deps.listDevices();
66
+ const { header, ownKey } = senderHeader(devices, deps);
67
+ const resolved = (0, agent_target_js_1.resolveAgentTarget)(devices, target, ownKey);
68
+ if (resolved.key === ownKey) {
69
+ deps.err(`hwip ${command}: the target is this session itself — pick another agent`);
70
+ return 1;
71
+ }
72
+ const text = `${header} ${message}`;
73
+ const prepared = await (0, command_seal_js_1.sealedCommand)({ targetDeviceId: resolved.deviceId, session: resolved.name, message: text }, deps.seal);
74
+ if (prepared.kind === 'refused') {
75
+ deps.err(`hwip ${command}: ${(0, command_seal_js_1.commandRefusalMessage)(prepared.cause, prepared.why)}`);
76
+ return 1;
77
+ }
78
+ await deps.sendAgentCommand({
79
+ targetDeviceId: resolved.deviceId,
80
+ agentSessionName: resolved.name,
81
+ isEncrypted: true,
82
+ ...prepared.fields,
83
+ });
84
+ // The server stored it. The listener may still refuse it (rate limit, a shell pane, a session gone since), and this
85
+ // command does not wait to hear of it.
86
+ deps.out(`sent → ${resolved.key} (stored; not confirmed typed)`);
87
+ return 0;
88
+ }
89
+ catch (err) {
90
+ deps.err(`hwip ${command}: ${err instanceof Error ? err.message : String(err)}`);
91
+ return 1;
92
+ }
93
+ };
94
+ exports.runSend = runSend;
95
+ const liveSendDeps = (hook) => ({
96
+ listDevices: () => hook.listDevices(),
97
+ sendAgentCommand: (payload) => hook.sendAgentCommand(payload),
98
+ seal: (0, ask_js_1.liveSealDeps)(hook.account.apiKey, hook.account.baseUrl),
99
+ context: agent_session_js_1.agentSessionContext,
100
+ hostname: node_os_1.hostname,
101
+ out: (line) => console.log(line),
102
+ err: (line) => console.error(line),
103
+ });
104
+ exports.liveSendDeps = liveSendDeps;