@hwip/cli 0.0.1 → 0.1.1

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 +1983 -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 +305 -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 +39 -0
  39. package/dist/blocked-prompt-seal.js +92 -0
  40. package/dist/blocked-prompt.d.ts +62 -0
  41. package/dist/blocked-prompt.js +518 -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 +278 -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 +5681 -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,26 @@
1
+ /**
2
+ * pi's transcript format, projected into the events the timeline draws.
3
+ *
4
+ * The second projector, and the one that proves the seam: `turn-watch` reads
5
+ * this file's output through `TranscriptProjector` without knowing pi exists.
6
+ * Everything here is pi's vocabulary and belongs to pi alone — a third agent
7
+ * means a third file, not another branch in this one.
8
+ *
9
+ * Shape measured 2026-09-14 against `~/.pi/agent/sessions/**\/*.jsonl`
10
+ * (40 sessions, 494 lines in the deepest), and the writer that produces it:
11
+ * `@earendil-works/pi-coding-agent/dist/core/session-manager.js`, at its
12
+ * `CURRENT_SESSION_VERSION = 3`.
13
+ *
14
+ * Nothing here refuses to read another version, and that is deliberate: an
15
+ * unknown entry type falls out of the checks below and the timeline is thinner
16
+ * by one row. A projector that stopped on a version bump would take the rows it
17
+ * still understood down with it.
18
+ */
19
+ import { type TranscriptProjector } from './transcript-tail.js';
20
+ /**
21
+ * pi writes one line per message, not one per content block, so a message's
22
+ * `msg` event needs no merging across lines — the one place this projector is
23
+ * simpler than Claude's.
24
+ */
25
+ export declare const projectPiEntries: TranscriptProjector;
26
+ //# sourceMappingURL=pi-transcript.d.ts.map
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ /**
3
+ * pi's transcript format, projected into the events the timeline draws.
4
+ *
5
+ * The second projector, and the one that proves the seam: `turn-watch` reads
6
+ * this file's output through `TranscriptProjector` without knowing pi exists.
7
+ * Everything here is pi's vocabulary and belongs to pi alone — a third agent
8
+ * means a third file, not another branch in this one.
9
+ *
10
+ * Shape measured 2026-09-14 against `~/.pi/agent/sessions/**\/*.jsonl`
11
+ * (40 sessions, 494 lines in the deepest), and the writer that produces it:
12
+ * `@earendil-works/pi-coding-agent/dist/core/session-manager.js`, at its
13
+ * `CURRENT_SESSION_VERSION = 3`.
14
+ *
15
+ * Nothing here refuses to read another version, and that is deliberate: an
16
+ * unknown entry type falls out of the checks below and the timeline is thinner
17
+ * by one row. A projector that stopped on a version bump would take the rows it
18
+ * still understood down with it.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.projectPiEntries = void 0;
22
+ const transcript_tail_js_1 = require("./transcript-tail.js");
23
+ /**
24
+ * Which tool argument reads as "what this call is about", first match wins.
25
+ *
26
+ * pi's own list, deliberately not Claude's: pi has no `description` argument,
27
+ * so the sentence-a-human-wrote field Claude's list leads with does not exist
28
+ * here. `path` leads instead because it covers read/write/edit/grep, the calls
29
+ * where the target is the whole point of the row. `name` beats `task` because a
30
+ * `subagent` call carries both and its `task` is a briefing — measured at 300+
31
+ * characters with embedded newlines, which renders as a wall, not a label.
32
+ *
33
+ * `command` is absent for the reason it is absent from Claude's list — a
34
+ * command line is the argument most likely to carry a secret. `bashProgram`
35
+ * below is what stands in for it.
36
+ */
37
+ const PI_TARGET_KEYS = ['path', 'pattern', 'query', 'name', 'question', 'task', 'file_path'];
38
+ /**
39
+ * The program a shell call runs, and nothing else.
40
+ *
41
+ * Measured over 40 sessions: 1187 of ~1800 tool calls are `bash`/`guarded_bash`,
42
+ * whose only argument is `command`. Dropping it wholesale — the rule Claude's
43
+ * projector follows — would leave two thirds of a pi timeline as rows that say
44
+ * "bash" and nothing more, which is a list of nothing. Carrying `command` whole
45
+ * would put the field most likely to hold a token, an `Authorization` header or
46
+ * a signed URL onto the wire.
47
+ *
48
+ * So only a bare program name leaves: `git`, `npm`, `curl`. Arguments never do,
49
+ * which is where every one of those secrets lives.
50
+ *
51
+ * Three kinds of token are stepped over on the way to it, because stopping at
52
+ * any of them produces a row that says nothing:
53
+ *
54
+ * - `VAR=value` assignments, skipped rather than shown — the value is the
55
+ * secret-shaped half.
56
+ * - Flags. A leading `-l` is never the program.
57
+ * - A prefix builtin and everything up to the next `&&`, `||` or `;`. Measured
58
+ * over every pi session on this machine, `cd` is the first token of 786 of
59
+ * ~1400 shell calls — `cd <dir> && <the actual work>`. Reporting "cd" for
60
+ * the majority of a timeline's rows is the same as reporting nothing.
61
+ */
62
+ const SHELL_PREFIX_BUILTINS = new Set(['cd', 'set', 'export', 'source', 'exec', 'time', 'env', '.']);
63
+ /** `&&`, `||`, `;`, `|` — split on them rather than scanning for them, because
64
+ * they are written glued to their neighbours as often as spaced (`cd /x&&ls`). */
65
+ const SHELL_SEPARATOR = /&&|\|\||[;|]/;
66
+ /** The program a single command segment runs, or undefined when it names none. */
67
+ const programOfSegment = (segment) => {
68
+ for (const token of segment.trim().split(/\s+/)) {
69
+ if (!token)
70
+ continue;
71
+ // `FOO=bar cmd …` — the value is the secret-shaped half, so step over
72
+ // the assignment entirely rather than reporting either half of it.
73
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token))
74
+ continue;
75
+ // A flag where the program should be means this segment names no
76
+ // program. Skipping it would promote the following argument — a path,
77
+ // a URL — into the program slot, which is how `-l /some/path` would
78
+ // otherwise be reported as `/some/path`.
79
+ if (token.startsWith('-'))
80
+ return undefined;
81
+ return token;
82
+ }
83
+ return undefined;
84
+ };
85
+ const bashProgram = (input) => {
86
+ if (!input || typeof input !== 'object')
87
+ return undefined;
88
+ const command = input.command;
89
+ if (typeof command !== 'string')
90
+ return undefined;
91
+ // Split naively: a `|` or `;` inside quotes splits too, so a segment can
92
+ // begin mid-string and name no program. That is safe — the worst outcome is
93
+ // a missing label, never a wrong one that leaks an argument. The cap is a
94
+ // separate concern: a pathological `cd a && cd b && …` chain costs a fixed
95
+ // number of steps, not one per token of whatever the agent wrote.
96
+ const segments = command.split(SHELL_SEPARATOR, 64);
97
+ for (const segment of segments) {
98
+ const program = programOfSegment(segment);
99
+ if (program === undefined)
100
+ continue;
101
+ // `cd`, `set`, `env` … — real, but only ever a preamble to the work.
102
+ if (SHELL_PREFIX_BUILTINS.has(program))
103
+ continue;
104
+ return (0, transcript_tail_js_1.clamp)(program);
105
+ }
106
+ return undefined;
107
+ };
108
+ /**
109
+ * Wrappers pi uses when it injects context into the `user` role.
110
+ *
111
+ * pi stamps no provenance on an entry — measured across every session on this
112
+ * machine, a harness injection and a typed message carry byte-identical keys
113
+ * (`id,message,parentId,timestamp,type`), so the `origin.kind`/`promptSource`
114
+ * check Claude's projector makes has no counterpart here. The opening wrapper is
115
+ * the only signal there is.
116
+ *
117
+ * What they hold is why this matters: `<file name="/Users/…/artifacts/…">` and
118
+ * `<skill name="02-implement" location="/Users/…/SKILL.md">` carry a whole file
119
+ * or skill body plus absolute local paths, and without this they reach the phone
120
+ * as the user's own words.
121
+ *
122
+ * Measured over the 131 user-role messages in `~/.pi/agent/sessions` on this
123
+ * machine: 29 entries open with `<file`, 17 with `<skill`, and nothing else
124
+ * opens with a tag at all. The list is exactly what was observed, so a wrapper
125
+ * pi adds later needs adding here — and only after it is seen, because the cost
126
+ * of a wrong guess runs the other way: `<pm>` reads like one of these and is
127
+ * not, it is the plan template's package-manager placeholder appearing in prose
128
+ * a person wrote, and filtering on it would delete their message.
129
+ *
130
+ * These entries are dropped whole rather than stripped, unlike Claude's
131
+ * `<system-reminder>` blocks, because there is no human half to keep — the
132
+ * injection IS the entry.
133
+ */
134
+ const PI_INJECTED_PROMPT = /^\s*<(file|skill)[\s>]/;
135
+ /** Text out of pi's content blocks — every role builds its prose the same way. */
136
+ const textOf = (content) => {
137
+ if (typeof content === 'string')
138
+ return content;
139
+ if (!Array.isArray(content))
140
+ return '';
141
+ let text = '';
142
+ for (const block of content) {
143
+ if (!block || typeof block !== 'object')
144
+ continue;
145
+ const item = block;
146
+ if (item.type === 'text' && typeof item.text === 'string')
147
+ text += item.text;
148
+ }
149
+ return text;
150
+ };
151
+ /**
152
+ * pi writes one line per message, not one per content block, so a message's
153
+ * `msg` event needs no merging across lines — the one place this projector is
154
+ * simpler than Claude's.
155
+ */
156
+ const projectPiEntries = (lines, opts = {}) => {
157
+ const events = [];
158
+ for (const raw of lines) {
159
+ let entry;
160
+ try {
161
+ const parsed = JSON.parse(raw);
162
+ if (!parsed || typeof parsed !== 'object')
163
+ continue;
164
+ entry = parsed;
165
+ }
166
+ catch {
167
+ continue;
168
+ }
169
+ // `session`, `model_change`, `thinking_level_change`, `compaction`,
170
+ // `custom`, `custom_message` — pi's bookkeeping. None of it is
171
+ // something the agent did, and an entry type added in a later version
172
+ // lands here too rather than breaking the watch.
173
+ if (entry.type !== 'message')
174
+ continue;
175
+ const message = entry.message;
176
+ if (!message || typeof message !== 'object')
177
+ continue;
178
+ const msg = message;
179
+ const at = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
180
+ const stamp = at ? { at } : {};
181
+ if (msg.role === 'user') {
182
+ // What the person typed. Carried for the same reason Claude's
183
+ // prompts are: without it the timeline records no question — but
184
+ // only once the harness's own injections have been ruled out, since
185
+ // pi files those under this role too.
186
+ const text = textOf(msg.content).trim();
187
+ if (text && !PI_INJECTED_PROMPT.test(text)) {
188
+ events.push({ kind: 'prompt', text: (0, transcript_tail_js_1.clamp)(text, transcript_tail_js_1.MAX_EVENT_TEXT_CHARS), ...stamp });
189
+ }
190
+ continue;
191
+ }
192
+ if (msg.role === 'toolResult') {
193
+ if (typeof msg.toolCallId !== 'string')
194
+ continue;
195
+ const resultLines = (0, transcript_tail_js_1.resultLinesOf)(msg.content);
196
+ events.push({
197
+ kind: 'tool_result',
198
+ id: msg.toolCallId,
199
+ ok: msg.isError !== true,
200
+ ...(resultLines ? { lines: resultLines } : {}),
201
+ ...stamp,
202
+ });
203
+ continue;
204
+ }
205
+ if (msg.role !== 'assistant') {
206
+ opts.onUnknown?.(`pi message role ${typeof msg.role === 'string' ? msg.role : '(none)'}`);
207
+ continue;
208
+ }
209
+ const content = Array.isArray(msg.content) ? msg.content : [];
210
+ const usage = msg.usage;
211
+ if (usage && typeof usage === 'object' && typeof entry.id === 'string') {
212
+ const u = usage;
213
+ const thinking = content.filter((block) => block && typeof block === 'object' && block.type === 'thinking').length;
214
+ events.push({
215
+ kind: 'msg',
216
+ mid: (0, transcript_tail_js_1.clamp)(entry.id),
217
+ ...(typeof msg.model === 'string' ? { model: (0, transcript_tail_js_1.clamp)(msg.model) } : {}),
218
+ out: (0, transcript_tail_js_1.finiteNumber)(u.output),
219
+ ctx: (0, transcript_tail_js_1.finiteNumber)(u.input) + (0, transcript_tail_js_1.finiteNumber)(u.cacheRead) + (0, transcript_tail_js_1.finiteNumber)(u.cacheWrite),
220
+ ...(thinking ? { thinking } : {}),
221
+ ...stamp,
222
+ });
223
+ }
224
+ for (const block of content) {
225
+ if (!block || typeof block !== 'object')
226
+ continue;
227
+ const item = block;
228
+ if (item.type === 'toolCall' && typeof item.id === 'string' && typeof item.name === 'string') {
229
+ const target = (0, transcript_tail_js_1.oneLine)((0, transcript_tail_js_1.targetFrom)(item.arguments, PI_TARGET_KEYS)) ?? bashProgram(item.arguments);
230
+ events.push({
231
+ kind: 'tool',
232
+ id: item.id,
233
+ name: (0, transcript_tail_js_1.clamp)(item.name),
234
+ ...(target ? { target } : {}),
235
+ ...stamp,
236
+ });
237
+ continue;
238
+ }
239
+ if (item.type === 'text' && typeof item.text === 'string') {
240
+ if (item.text.trim())
241
+ events.push({ kind: 'text', text: (0, transcript_tail_js_1.clamp)(item.text, transcript_tail_js_1.MAX_EVENT_TEXT_CHARS), ...stamp });
242
+ continue;
243
+ }
244
+ // `thinking` falls through on purpose, exactly as it does for Claude:
245
+ // the timeline shows what the agent did, not what it considered.
246
+ // Only the count leaves, on `msg` above.
247
+ if (item.type === 'thinking')
248
+ continue;
249
+ // A block this file does not know, or a known one whose fields moved.
250
+ opts.onUnknown?.(`pi content block ${typeof item.type === 'string' ? item.type : '(no type)'}`);
251
+ }
252
+ }
253
+ return (0, transcript_tail_js_1.sliceSinceLastPrompt)(events, opts);
254
+ };
255
+ exports.projectPiEntries = projectPiEntries;
@@ -0,0 +1,14 @@
1
+ import type { GateMarker, GatePushMode } from './gate.js';
2
+ export interface PresenceDeps {
3
+ env: NodeJS.ProcessEnv;
4
+ /** stdout of a probe command, or null when it fails, is missing, or times out. */
5
+ run: (cmd: string, args: string[]) => string | null;
6
+ now: () => number;
7
+ }
8
+ export declare const isAway: (deps?: PresenceDeps) => boolean;
9
+ /**
10
+ * Presence for decidePush, probed only where it can change the verdict: quiet
11
+ * with no `high` marker is the one path that is otherwise silent.
12
+ */
13
+ export declare const awayForGate: (pushMode: GatePushMode, marker: GateMarker, probe?: () => boolean) => boolean;
14
+ //# sourceMappingURL=presence.d.ts.map
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.awayForGate = exports.isAway = void 0;
4
+ /**
5
+ * Presence — is the user away from the terminal?
6
+ *
7
+ * The TS twin of `plugin/hooks/gate.sh` hwip_is_away (hwipit/plugin): the
8
+ * Claude Stop hook asks there, every `notify --auto` hook asks here, and both
9
+ * must answer alike. The quiet dial silences routine pushes because the user is
10
+ * watching the pane; an away user is not, so the answer becomes decidePush's
11
+ * `away`. Keep the probe order and fallbacks in sync with the bash side — the
12
+ * cases in presence.test.ts mirror its `away:` block in test-hwip-stop.sh.
13
+ *
14
+ * Away is the machine verdict below, or this session's own tmux pane being
15
+ * hidden (`paneHidden`): a keyboard or a tmux client says the user is at the
16
+ * desk, not which tmux session they are looking at.
17
+ *
18
+ * Threshold: HWIP_AWAY_SEC seconds (default 300, `0` disables, anything that is
19
+ * not a whole number of at most 9 digits falls back to the default). Probes,
20
+ * first answer wins:
21
+ * 1. inside tmux and no client attached to the server at all → away. Any
22
+ * attached client means someone is at a tmux terminal, so switching the
23
+ * one client between sessions does not make the MACHINE read as away (the
24
+ * pane check above still asks about this session).
25
+ * 2. not over SSH and ioreg reports HIDIdleTime (macOS) → that decides, and
26
+ * nothing below runs: tmux activity only sees keys typed into tmux, so a
27
+ * user reading a browser would look idle to it. Inside tmux, "over SSH"
28
+ * comes from the session environment (`show-environment`), which tmux
29
+ * refreshes on every attach — the process's own SSH_CONNECTION is frozen
30
+ * at server start.
31
+ * 3. inside tmux → the newest client_activity (SSH and non-macOS hosts).
32
+ * Anything else is present: an unreadable signal must never add a push.
33
+ */
34
+ const child_process_1 = require("child_process");
35
+ const AWAY_SEC_DEFAULT = 300;
36
+ const PROBE_TIMEOUT_MS = 2000;
37
+ const runProbe = (cmd, args) => {
38
+ const r = (0, child_process_1.spawnSync)(cmd, args, {
39
+ encoding: 'utf-8',
40
+ stdio: ['ignore', 'pipe', 'ignore'],
41
+ timeout: PROBE_TIMEOUT_MS,
42
+ });
43
+ return r.status === 0 ? (r.stdout ?? '') : null;
44
+ };
45
+ const defaultDeps = () => ({ env: process.env, run: runProbe, now: Date.now });
46
+ const awayThresholdSec = (raw) => raw !== undefined && /^\d{1,9}$/.test(raw) ? Number(raw) : AWAY_SEC_DEFAULT;
47
+ const hidIdleSec = (ioregOut) => {
48
+ const match = ioregOut?.match(/"HIDIdleTime" = (\d+)/);
49
+ return match ? Math.floor(Number(match[1]) / 1e9) : null;
50
+ };
51
+ const newestActivitySec = (clientsOut) => {
52
+ const stamps = clientsOut.split('\n').filter((l) => /^\d+$/.test(l)).map(Number);
53
+ return stamps.length > 0 ? Math.max(...stamps) : null;
54
+ };
55
+ // null = tmux had no answer for the variable, so the process env decides.
56
+ const isOverSsh = (deps, inTmux) => {
57
+ const sessionVar = inTmux ? deps.run('tmux', ['show-environment', 'SSH_CONNECTION']) : null;
58
+ if (sessionVar !== null)
59
+ return sessionVar.startsWith('SSH_CONNECTION=');
60
+ return Boolean(deps.env.SSH_CONNECTION);
61
+ };
62
+ /**
63
+ * Is this session's tmux pane out of sight? Asks tmux about `$TMUX_PANE`:
64
+ * `#{session_attached}` (clients on its session), `#{window_active}` (its
65
+ * window is the session's current one), `#{pane_active}` and
66
+ * `#{window_zoomed_flag}` (a different pane zoomed over it hides it; a split
67
+ * that is merely not the active one is still on screen). A control-mode client
68
+ * (iTerm2 `tmux -CC`) makes a non-current window look hidden while it is on
69
+ * screen, so one attached anywhere reads as unknown; so does a window linked
70
+ * into several sessions, which tmux reports for just one of them (not
71
+ * detected). Anything short of a plain answer — not in tmux, no TMUX_PANE,
72
+ * tmux silent, output that is not four numbers — is not hidden.
73
+ */
74
+ const paneHidden = (deps) => {
75
+ const pane = deps.env.TMUX_PANE;
76
+ if (!deps.env.TMUX || !pane)
77
+ return false;
78
+ const out = deps.run('tmux', [
79
+ 'display-message', '-p', '-t', pane,
80
+ '#{session_attached} #{window_active} #{pane_active} #{window_zoomed_flag}',
81
+ ]);
82
+ if (out === null)
83
+ return false;
84
+ const fields = out.trim().split(/\s+/);
85
+ if (fields.length !== 4 || !fields.every((f) => /^\d+$/.test(f)))
86
+ return false;
87
+ const [attached, windowActive, paneActive, zoomed] = fields.map(Number);
88
+ const hidden = attached === 0 || windowActive === 0 || (zoomed !== 0 && paneActive === 0);
89
+ if (!hidden)
90
+ return false;
91
+ const control = deps.run('tmux', ['list-clients', '-F', '#{client_control_mode}']);
92
+ return !(control ?? '').split('\n').includes('1');
93
+ };
94
+ const machineAway = (deps, threshold) => {
95
+ // null = not in tmux, or no server answered; '' = a server with no clients.
96
+ const clients = deps.env.TMUX ? deps.run('tmux', ['list-clients', '-F', '#{client_activity}']) : null;
97
+ if (clients !== null && clients.trim() === '')
98
+ return true;
99
+ if (!isOverSsh(deps, clients !== null)) {
100
+ // -r -k -d 1: just the IOHIDSystem node (~4 KB), not its ~380 KB subtree.
101
+ const idle = hidIdleSec(deps.run('ioreg', ['-r', '-k', 'HIDIdleTime', '-d', '1', '-c', 'IOHIDSystem']));
102
+ if (idle !== null)
103
+ return idle >= threshold;
104
+ }
105
+ if (clients === null)
106
+ return false;
107
+ const newest = newestActivitySec(clients);
108
+ if (newest === null)
109
+ return false;
110
+ return Math.floor(deps.now() / 1000) - newest >= threshold;
111
+ };
112
+ const isAway = (deps = defaultDeps()) => {
113
+ const threshold = awayThresholdSec(deps.env.HWIP_AWAY_SEC);
114
+ if (threshold === 0)
115
+ return false;
116
+ return machineAway(deps, threshold) || paneHidden(deps);
117
+ };
118
+ exports.isAway = isAway;
119
+ /**
120
+ * Presence for decidePush, probed only where it can change the verdict: quiet
121
+ * with no `high` marker is the one path that is otherwise silent.
122
+ */
123
+ const awayForGate = (pushMode, marker, probe = exports.isAway) => pushMode === 'quiet' && marker !== 'high' && probe();
124
+ exports.awayForGate = awayForGate;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Who a file hwip creates under the home folder belongs to when root runs it by `sudo` with HOME
3
+ * kept (macOS does): the person who typed `sudo`, so the next plain `hwip` can read what this
4
+ * one wrote. Null unless every sign agrees: root, `SUDO_UID`/`SUDO_GID` set to real ids, the path
5
+ * under the home folder, and the home folder owned by that same `SUDO_UID`. The last one is what
6
+ * stops a root whose HOME is `/root` (sudo resets it there on Linux) from handing root's folder
7
+ * to whoever ran `sudo`.
8
+ */
9
+ export declare const sudoOwner: (path: string) => {
10
+ uid: number;
11
+ gid: number;
12
+ } | null;
13
+ /**
14
+ * Give a path root just created to the person who ran `sudo` (see `sudoOwner`). Best effort. The path
15
+ * itself, never what a link points at: whoever owns the folder can put a link where the new file was,
16
+ * and root following it would hand them a file of anyone's.
17
+ */
18
+ export declare const handBackToSudoUser: (path: string) => void;
19
+ /**
20
+ * `mkdir -p`: every folder it makes, not only the last, goes to the person who ran `sudo` when root
21
+ * makes it — a root-owned `~/.hwip` is one the next plain `hwip` cannot write in.
22
+ */
23
+ export declare const mkdirHandedBack: (dir: string, mode?: number) => void;
24
+ /**
25
+ * Make sure `path` exists before a plain write fills it, handing it back when root made it. For a
26
+ * caller whose write line is better left alone (it sits next to code others are changing); the
27
+ * write that follows keeps the owner, since it opens the same file.
28
+ */
29
+ export declare const createdHandedBack: (path: string) => void;
30
+ /** `mkdir` without `-p`: fails with EEXIST when it is there, so the caller can tell who made it first. */
31
+ export declare const mkdirExclusiveHandedBack: (dir: string) => void;
32
+ /**
33
+ * The directory exists and only its owner can enter it (0700). A symlink is left
34
+ * as it is: it points at a folder that may be shared on purpose (dotfiles), and
35
+ * what it points at is not ours to lock.
36
+ *
37
+ * Opened without following a link and changed through what was opened, so the folder looked at is the
38
+ * one changed: by name, a link put there after the look got its target locked instead. Where the
39
+ * platform has no such open (Windows, which has no such modes either), by name as before.
40
+ *
41
+ * A folder its owner cannot read (0377) cannot be opened, yet others may still write into it, so that one
42
+ * too is changed by name — when it is a folder of the caller's own, and unless root runs this (by either of
43
+ * its ids): root is who could lock a file of someone else's through a link put there after the look. A person
44
+ * can only lock their own.
45
+ */
46
+ export declare const ensurePrivateDir: (dir: string) => void;
47
+ /** The file exists and only its owner can read it (0600); its content is untouched. A link at the
48
+ * end is left as it is: creating its target and then fixing only the link would leave a file of
49
+ * someone's behind (C149 ②). An existing file is changed through an fd for the same reason. */
50
+ export declare const ensurePrivateFile: (path: string) => void;
51
+ //# sourceMappingURL=private-files.d.ts.map