@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.
- package/LICENSE +190 -0
- package/NOTICE +11 -0
- package/README.md +1983 -0
- package/dist/agent-list.d.ts +76 -0
- package/dist/agent-list.js +102 -0
- package/dist/agent-read.d.ts +105 -0
- package/dist/agent-read.js +309 -0
- package/dist/agent-rules-fetch.d.ts +51 -0
- package/dist/agent-rules-fetch.js +275 -0
- package/dist/agent-rules.default.d.ts +30 -0
- package/dist/agent-rules.default.js +305 -0
- package/dist/agent-session.d.ts +23 -0
- package/dist/agent-session.js +44 -0
- package/dist/agent-state.d.ts +84 -0
- package/dist/agent-state.js +188 -0
- package/dist/agent-target.d.ts +60 -0
- package/dist/agent-target.js +81 -0
- package/dist/agents.d.ts +31 -0
- package/dist/agents.js +75 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +76 -0
- package/dist/args.d.ts +74 -0
- package/dist/args.js +254 -0
- package/dist/ask-context-attach.d.ts +73 -0
- package/dist/ask-context-attach.js +151 -0
- package/dist/ask-context-screen.d.ts +21 -0
- package/dist/ask-context-screen.js +49 -0
- package/dist/ask-context-switch.d.ts +31 -0
- package/dist/ask-context-switch.js +74 -0
- package/dist/ask-context.d.ts +84 -0
- package/dist/ask-context.js +172 -0
- package/dist/ask-pending.d.ts +47 -0
- package/dist/ask-pending.js +221 -0
- package/dist/ask-seal.d.ts +120 -0
- package/dist/ask-seal.js +179 -0
- package/dist/ask.d.ts +107 -0
- package/dist/ask.js +551 -0
- package/dist/blocked-prompt-seal.d.ts +39 -0
- package/dist/blocked-prompt-seal.js +92 -0
- package/dist/blocked-prompt.d.ts +62 -0
- package/dist/blocked-prompt.js +518 -0
- package/dist/channel.d.ts +67 -0
- package/dist/channel.js +136 -0
- package/dist/check-update.d.ts +28 -0
- package/dist/check-update.js +153 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +813 -0
- package/dist/codex-config.d.ts +24 -0
- package/dist/codex-config.js +169 -0
- package/dist/codex-transcript.d.ts +59 -0
- package/dist/codex-transcript.js +373 -0
- package/dist/command-refusal.d.ts +56 -0
- package/dist/command-refusal.js +127 -0
- package/dist/command-scan.d.ts +46 -0
- package/dist/command-scan.js +154 -0
- package/dist/command-seal.d.ts +57 -0
- package/dist/command-seal.js +88 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +150 -0
- package/dist/config.d.ts +156 -0
- package/dist/config.js +392 -0
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.js +28 -0
- package/dist/crypto.d.ts +278 -0
- package/dist/crypto.js +698 -0
- package/dist/desktop-notify.d.ts +33 -0
- package/dist/desktop-notify.js +49 -0
- package/dist/device-directory.d.ts +36 -0
- package/dist/device-directory.js +97 -0
- package/dist/device-presence.d.ts +17 -0
- package/dist/device-presence.js +62 -0
- package/dist/downloads.d.ts +29 -0
- package/dist/downloads.js +106 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +28 -0
- package/dist/file-crypto.d.ts +94 -0
- package/dist/file-crypto.js +278 -0
- package/dist/file-view-budget.d.ts +15 -0
- package/dist/file-view-budget.js +34 -0
- package/dist/file-view-switch.d.ts +28 -0
- package/dist/file-view-switch.js +122 -0
- package/dist/file-view.d.ts +108 -0
- package/dist/file-view.js +327 -0
- package/dist/gate.d.ts +141 -0
- package/dist/gate.js +336 -0
- package/dist/gemini-transcript.d.ts +32 -0
- package/dist/gemini-transcript.js +189 -0
- package/dist/http-failure.d.ts +98 -0
- package/dist/http-failure.js +349 -0
- package/dist/hwip-core.generated.d.ts +7 -0
- package/dist/hwip-core.generated.js +14 -0
- package/dist/hwip-hook.d.ts +77 -0
- package/dist/hwip-hook.js +340 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/input-sequencer.d.ts +65 -0
- package/dist/input-sequencer.js +142 -0
- package/dist/insecure-url.d.ts +16 -0
- package/dist/insecure-url.js +76 -0
- package/dist/installer.d.ts +96 -0
- package/dist/installer.js +850 -0
- package/dist/inventory-offload.d.ts +49 -0
- package/dist/inventory-offload.js +209 -0
- package/dist/inventory-worker.d.ts +2 -0
- package/dist/inventory-worker.js +27 -0
- package/dist/jsonc-doc.d.ts +106 -0
- package/dist/jsonc-doc.js +354 -0
- package/dist/keep-awake.d.ts +32 -0
- package/dist/keep-awake.js +46 -0
- package/dist/key-changes.d.ts +48 -0
- package/dist/key-changes.js +140 -0
- package/dist/lan-auth.d.ts +140 -0
- package/dist/lan-auth.js +217 -0
- package/dist/lan-endpoint.d.ts +41 -0
- package/dist/lan-endpoint.js +115 -0
- package/dist/lan-receiver.d.ts +122 -0
- package/dist/lan-receiver.js +510 -0
- package/dist/lan-sender.d.ts +76 -0
- package/dist/lan-sender.js +184 -0
- package/dist/lan-transfer.d.ts +36 -0
- package/dist/lan-transfer.js +54 -0
- package/dist/listener-device-id.d.ts +20 -0
- package/dist/listener-device-id.js +133 -0
- package/dist/listener-log.d.ts +4 -0
- package/dist/listener-log.js +23 -0
- package/dist/listener-process.d.ts +200 -0
- package/dist/listener-process.js +470 -0
- package/dist/listener-service.d.ts +225 -0
- package/dist/listener-service.js +682 -0
- package/dist/listener-worktree-cleanup.d.ts +27 -0
- package/dist/listener-worktree-cleanup.js +62 -0
- package/dist/listener.d.ts +1325 -0
- package/dist/listener.js +5681 -0
- package/dist/login-address.d.ts +25 -0
- package/dist/login-address.js +97 -0
- package/dist/login-page.d.ts +29 -0
- package/dist/login-page.js +93 -0
- package/dist/login.d.ts +98 -0
- package/dist/login.js +388 -0
- package/dist/mcp-command.d.ts +45 -0
- package/dist/mcp-command.js +50 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +29 -0
- package/dist/node-support.d.ts +19 -0
- package/dist/node-support.js +38 -0
- package/dist/notify-failure.d.ts +57 -0
- package/dist/notify-failure.js +150 -0
- package/dist/opencode-transcript.d.ts +139 -0
- package/dist/opencode-transcript.js +390 -0
- package/dist/optional-features.d.ts +6 -0
- package/dist/optional-features.js +19 -0
- package/dist/pane-fit.d.ts +40 -0
- package/dist/pane-fit.js +119 -0
- package/dist/peer-key.d.ts +33 -0
- package/dist/peer-key.js +34 -0
- package/dist/pi-transcript.d.ts +26 -0
- package/dist/pi-transcript.js +255 -0
- package/dist/presence.d.ts +14 -0
- package/dist/presence.js +124 -0
- package/dist/private-files.d.ts +51 -0
- package/dist/private-files.js +218 -0
- package/dist/project-switch.d.ts +19 -0
- package/dist/project-switch.js +60 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.js +21 -0
- package/dist/remote-agents.d.ts +410 -0
- package/dist/remote-agents.js +1218 -0
- package/dist/remote-hook.d.ts +12 -0
- package/dist/remote-hook.js +196 -0
- package/dist/replace-file.d.ts +50 -0
- package/dist/replace-file.js +328 -0
- package/dist/schedule-command.d.ts +35 -0
- package/dist/schedule-command.js +260 -0
- package/dist/schedule-launch.d.ts +15 -0
- package/dist/schedule-launch.js +24 -0
- package/dist/schedule-ready.d.ts +44 -0
- package/dist/schedule-ready.js +71 -0
- package/dist/schedule-remote.d.ts +77 -0
- package/dist/schedule-remote.js +167 -0
- package/dist/schedule-runner.d.ts +89 -0
- package/dist/schedule-runner.js +452 -0
- package/dist/schedule.d.ts +149 -0
- package/dist/schedule.js +428 -0
- package/dist/secret-mask.d.ts +29 -0
- package/dist/secret-mask.js +245 -0
- package/dist/self-register.d.ts +45 -0
- package/dist/self-register.js +67 -0
- package/dist/send.d.ts +56 -0
- package/dist/send.js +104 -0
- package/dist/session-diff.d.ts +161 -0
- package/dist/session-diff.js +445 -0
- package/dist/session-registry.d.ts +143 -0
- package/dist/session-registry.js +216 -0
- package/dist/sessions-report.d.ts +59 -0
- package/dist/sessions-report.js +99 -0
- package/dist/shell-audit.d.ts +34 -0
- package/dist/shell-audit.js +47 -0
- package/dist/subagent-transcripts.d.ts +167 -0
- package/dist/subagent-transcripts.js +437 -0
- package/dist/subscriber-trust.d.ts +128 -0
- package/dist/subscriber-trust.js +397 -0
- package/dist/templates.d.ts +105 -0
- package/dist/templates.js +871 -0
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.js +65 -0
- package/dist/tmux-liveness.d.ts +45 -0
- package/dist/tmux-liveness.js +71 -0
- package/dist/tmux-pane.d.ts +55 -0
- package/dist/tmux-pane.js +126 -0
- package/dist/tmux-socket.d.ts +20 -0
- package/dist/tmux-socket.js +292 -0
- package/dist/todo-fold.d.ts +31 -0
- package/dist/todo-fold.js +102 -0
- package/dist/transcript-tail.d.ts +312 -0
- package/dist/transcript-tail.js +718 -0
- package/dist/trust-command.d.ts +49 -0
- package/dist/trust-command.js +200 -0
- package/dist/trust-lock.d.ts +26 -0
- package/dist/trust-lock.js +94 -0
- package/dist/trust-pins.d.ts +37 -0
- package/dist/trust-pins.js +82 -0
- package/dist/turn-recorder-wire.d.ts +70 -0
- package/dist/turn-recorder-wire.js +165 -0
- package/dist/turn-recorder.d.ts +190 -0
- package/dist/turn-recorder.js +695 -0
- package/dist/turn-ring.d.ts +93 -0
- package/dist/turn-ring.js +418 -0
- package/dist/turn-watch.d.ts +177 -0
- package/dist/turn-watch.js +630 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +55 -0
- package/dist/uninstall.js +507 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +421 -0
- package/dist/verify.d.ts +47 -0
- package/dist/verify.js +396 -0
- package/dist/worktree.d.ts +144 -0
- package/dist/worktree.js +310 -0
- package/dist/wrapper.d.ts +254 -0
- package/dist/wrapper.js +704 -0
- package/dist/ws-url.d.ts +109 -0
- package/dist/ws-url.js +202 -0
- package/package.json +78 -10
- 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
|
package/dist/presence.js
ADDED
|
@@ -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
|