@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.
- package/LICENSE +190 -0
- package/NOTICE +11 -0
- package/README.md +1979 -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 +292 -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 +35 -0
- package/dist/blocked-prompt-seal.js +89 -0
- package/dist/blocked-prompt.d.ts +57 -0
- package/dist/blocked-prompt.js +181 -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 +272 -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 +5680 -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
package/dist/gate.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.autoPushMode = exports.readPushMode = exports.NONREADONLY_COUNT_FLAG = exports.TOOL_COUNT_FLAG = exports.PUSHMODE_DEFAULT_FLAG = exports.PUSHMODE_DEFAULT = exports.isMuted = exports.clearRemoteActive = exports.touchRemoteActive = exports.isRemoteActive = exports.remoteStatePath = exports.REMOTE_TTL_SEC = exports.remoteDigest = exports.remoteMarkerPath = exports.projectHash = exports.posixCksum = exports.stateDir = exports.decidePush = exports.normalizePushMode = exports.normalizeMarker = exports.GATE_DEFAULTS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Push-gate decision — the portable half of the Hwip Stop-hook logic.
|
|
6
|
+
*
|
|
7
|
+
* `decidePush` is the TS twin of `plugin/hooks/gate.sh` (hwipit/plugin).
|
|
8
|
+
* Both implementations are locked to the same semantics by the shared
|
|
9
|
+
* vector file `src/fixtures/gate-vectors.json` (vendored from the plugin
|
|
10
|
+
* repo's canonical copy via `npm run sync:plugin`): the bash side and this
|
|
11
|
+
* side run the exact same cases in their CIs, so a semantic change to one
|
|
12
|
+
* that isn't mirrored in the other fails a build. Edit them together.
|
|
13
|
+
*
|
|
14
|
+
* Ordering is contractual (encoded as named vectors):
|
|
15
|
+
* 1. alreadyAsked wins over EVERYTHING — even loud (dedup beats the dial).
|
|
16
|
+
* 2. priority is high iff marker === 'high', decided BEFORE the mode
|
|
17
|
+
* switch, so quiet+high and loud+high both push at high priority.
|
|
18
|
+
* 3. quiet → a high marker or an away user pushes; loud → always push; normal →
|
|
19
|
+
* marker overrides the heuristic (skip → silent, push/high → push),
|
|
20
|
+
* no marker → push iff toolCount ≥ 2 AND nonReadonlyCount > 0
|
|
21
|
+
* (the B1 read-only floor).
|
|
22
|
+
*/
|
|
23
|
+
const crypto_1 = require("crypto");
|
|
24
|
+
const fs_1 = require("fs");
|
|
25
|
+
const os_1 = require("os");
|
|
26
|
+
const path_1 = require("path");
|
|
27
|
+
const private_files_js_1 = require("./private-files.js");
|
|
28
|
+
/**
|
|
29
|
+
* "Assume real work" defaults for hooks that can't supply turn facts
|
|
30
|
+
* (most non-Claude agents pass no counts): in normal mode the push still
|
|
31
|
+
* fires — preserving the historical always-push behavior of the dumb
|
|
32
|
+
* hooks — while quiet/loud now work everywhere.
|
|
33
|
+
*
|
|
34
|
+
* These defaults cannot rescue a quiet dial: quiet only lets a `high` marker
|
|
35
|
+
* or an away user (presence.ts) through, and a hook with no turn facts has no
|
|
36
|
+
* marker either. That is why the installed templates pass `--pushmode-default
|
|
37
|
+
* normal` (see templates.ts) — for them quiet is not a lower volume, it is
|
|
38
|
+
* silence while the user is at the terminal.
|
|
39
|
+
*/
|
|
40
|
+
exports.GATE_DEFAULTS = {
|
|
41
|
+
toolCount: 2,
|
|
42
|
+
nonReadonlyCount: 1,
|
|
43
|
+
alreadyAsked: false,
|
|
44
|
+
};
|
|
45
|
+
const normalizeMarker = (raw) => raw === 'skip' || raw === 'push' || raw === 'high' ? raw : 'none';
|
|
46
|
+
exports.normalizeMarker = normalizeMarker;
|
|
47
|
+
const normalizePushMode = (raw) => raw === 'quiet' || raw === 'loud' ? raw : 'normal';
|
|
48
|
+
exports.normalizePushMode = normalizePushMode;
|
|
49
|
+
const decidePush = (input) => {
|
|
50
|
+
if (input.alreadyAsked)
|
|
51
|
+
return { push: false, priority: 'normal' };
|
|
52
|
+
const priority = input.marker === 'high' ? 'high' : 'normal';
|
|
53
|
+
if (input.pushMode === 'quiet')
|
|
54
|
+
return { push: input.marker === 'high' || input.away === true, priority };
|
|
55
|
+
if (input.pushMode === 'loud')
|
|
56
|
+
return { push: true, priority };
|
|
57
|
+
if (input.marker === 'skip')
|
|
58
|
+
return { push: false, priority };
|
|
59
|
+
if (input.marker === 'push' || input.marker === 'high')
|
|
60
|
+
return { push: true, priority };
|
|
61
|
+
return { push: input.toolCount >= 2 && input.nonReadonlyCount > 0, priority };
|
|
62
|
+
};
|
|
63
|
+
exports.decidePush = decidePush;
|
|
64
|
+
// ── Per-project gate state (mute + push-mode dial) ───────────────
|
|
65
|
+
//
|
|
66
|
+
// State lives under ${XDG_STATE_HOME:-~/.local/state}/hwip — a per-user
|
|
67
|
+
// directory. It used to live at predictable names in world-writable /tmp,
|
|
68
|
+
// where any local user could pre-create a victim's mute file (and sticky
|
|
69
|
+
// /tmp makes that file un-deletable by the victim). Legacy /tmp files are
|
|
70
|
+
// still honored during the migration window, but only when owned by the
|
|
71
|
+
// current user, which neutralizes planted files.
|
|
72
|
+
//
|
|
73
|
+
// The plugin's bash hooks key these files off `cksum` of the project dir
|
|
74
|
+
// (`printf '%s' "$dir" | cksum`). `posixCksum` is that CRC computed in
|
|
75
|
+
// process — a notify used to spawn `cksum` up to ~8 times — and the vector
|
|
76
|
+
// test pins it to the real `cksum`, so every already-written file keeps its name.
|
|
77
|
+
const stateDir = () => (0, path_1.join)(process.env.XDG_STATE_HOME || (0, path_1.join)((0, os_1.homedir)(), '.local', 'state'), 'hwip');
|
|
78
|
+
exports.stateDir = stateDir;
|
|
79
|
+
const ownedByCurrentUser = (path) => {
|
|
80
|
+
try {
|
|
81
|
+
const st = (0, fs_1.statSync)(path);
|
|
82
|
+
return typeof process.getuid !== 'function' || st.uid === process.getuid();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a state file: current location first, then user-owned legacy /tmp,
|
|
90
|
+
* then — for `pushmode` only — the machine-wide default written by
|
|
91
|
+
* `/hwip-mode quiet --global`. Mirrors plugin/hooks/gate.sh hwip_state_present,
|
|
92
|
+
* including the deliberate absence of a global mute (see the comment there).
|
|
93
|
+
*/
|
|
94
|
+
const findStateFile = (kind, hash) => {
|
|
95
|
+
const current = (0, path_1.join)((0, exports.stateDir)(), `${kind}-${hash}`);
|
|
96
|
+
if ((0, fs_1.existsSync)(current))
|
|
97
|
+
return current;
|
|
98
|
+
const legacy = `/tmp/hwip-${kind}-${hash}`;
|
|
99
|
+
if (ownedByCurrentUser(legacy))
|
|
100
|
+
return legacy;
|
|
101
|
+
if (kind !== 'pushmode')
|
|
102
|
+
return null;
|
|
103
|
+
const globalDefault = (0, path_1.join)((0, exports.stateDir)(), 'pushmode-default');
|
|
104
|
+
return (0, fs_1.existsSync)(globalDefault) ? globalDefault : null;
|
|
105
|
+
};
|
|
106
|
+
/** CRC table for POSIX `cksum`: polynomial 0x04C11DB7, most significant bit first. */
|
|
107
|
+
const CKSUM_TABLE = (() => {
|
|
108
|
+
const table = new Uint32Array(256);
|
|
109
|
+
for (let i = 0; i < 256; i++) {
|
|
110
|
+
let c = i << 24;
|
|
111
|
+
for (let k = 0; k < 8; k++)
|
|
112
|
+
c = c & 0x80000000 ? (c << 1) ^ 0x04c11db7 : c << 1;
|
|
113
|
+
table[i] = c >>> 0;
|
|
114
|
+
}
|
|
115
|
+
return table;
|
|
116
|
+
})();
|
|
117
|
+
/**
|
|
118
|
+
* The first field of POSIX `cksum` over `data`: the CRC of the bytes, then of
|
|
119
|
+
* the length (low byte first, no trailing zero bytes), complemented.
|
|
120
|
+
*/
|
|
121
|
+
const posixCksum = (data) => {
|
|
122
|
+
let crc = 0;
|
|
123
|
+
const step = (byte) => {
|
|
124
|
+
crc = ((crc << 8) ^ CKSUM_TABLE[((crc >>> 24) ^ byte) & 0xff]) >>> 0;
|
|
125
|
+
};
|
|
126
|
+
for (const byte of data)
|
|
127
|
+
step(byte);
|
|
128
|
+
for (let n = data.length; n > 0; n = Math.floor(n / 256))
|
|
129
|
+
step(n & 0xff);
|
|
130
|
+
return ~crc >>> 0;
|
|
131
|
+
};
|
|
132
|
+
exports.posixCksum = posixCksum;
|
|
133
|
+
/** `cksum` of the project dir as the plugin computes it. */
|
|
134
|
+
const projectHash = (dir) => String((0, exports.posixCksum)(Buffer.from(dir, 'utf-8')));
|
|
135
|
+
exports.projectHash = projectHash;
|
|
136
|
+
// ── Remote-origin marker (ADR-0002) ──────────────────────────────
|
|
137
|
+
//
|
|
138
|
+
// The listener records every phone→pane text injection as a one-shot
|
|
139
|
+
// marker file; a prompt-submit hook (Claude Code plugin's hwip-remote.sh,
|
|
140
|
+
// or `hwip remote-hook` for Gemini/Codex) consumes it on an exact-text
|
|
141
|
+
// match and flags the prompt as remote-originated. Writer and TS reader
|
|
142
|
+
// share these two helpers so their bytes can never diverge; the bash
|
|
143
|
+
// reader is held to the same semantics by the plugin's test suite.
|
|
144
|
+
/** Marker path for a project hash: `<stateDir>/remote-<cksum(projectDir)>`. */
|
|
145
|
+
const remoteMarkerPath = (hash) => (0, path_1.join)((0, exports.stateDir)(), `remote-${hash}`);
|
|
146
|
+
exports.remoteMarkerPath = remoteMarkerPath;
|
|
147
|
+
/**
|
|
148
|
+
* sha256 over the ASCII-only-trimmed text — recorded at inject time,
|
|
149
|
+
* recomputed at prompt submit. Trim strips ONLY ' \t\r\n\f\v', NOT
|
|
150
|
+
* String.trim(): trim() also eats Unicode whitespace (U+00A0 etc.) that
|
|
151
|
+
* the bash reader keeps, and both sides must hash identical bytes.
|
|
152
|
+
*/
|
|
153
|
+
const remoteDigest = (text) => (0, crypto_1.createHash)('sha256')
|
|
154
|
+
.update(text.replace(/^[ \t\r\n\f\v]+|[ \t\r\n\f\v]+$/g, ''))
|
|
155
|
+
.digest('hex');
|
|
156
|
+
exports.remoteDigest = remoteDigest;
|
|
157
|
+
// ── Sticky REMOTE state ──────────────────────────────────────────
|
|
158
|
+
//
|
|
159
|
+
// The marker above is a ONE-SHOT entry signal, consumed the moment a prompt
|
|
160
|
+
// matches it. REMOTE outlives that turn — the user can answer from the phone
|
|
161
|
+
// and then type at the terminal — so the mode itself lives in
|
|
162
|
+
// `remote-active-<hash>`, holding the epoch second it was last confirmed.
|
|
163
|
+
// Keeping it in a file is also what lets it survive context compaction.
|
|
164
|
+
// Bash twin: plugin/hooks/gate.sh hwip_remote_active / hwip_remote_touch.
|
|
165
|
+
//
|
|
166
|
+
// Deliberately not routed through findStateFile. That helper also honors a
|
|
167
|
+
// legacy /tmp copy so files written by older versions keep working; this kind
|
|
168
|
+
// has never had a /tmp writer, so the branch could only ever match something
|
|
169
|
+
// stale — and since the refresh always writes the XDG path, such a file would
|
|
170
|
+
// never expire.
|
|
171
|
+
/**
|
|
172
|
+
* How long REMOTE stays live without a refresh. Generous on purpose: the state
|
|
173
|
+
* is refreshed on every phone prompt and every answered `hwip_ask`, so it only
|
|
174
|
+
* has to outlive a working session, never an idle user. It is the backstop and
|
|
175
|
+
* not the usual exit — a terminal-typed prompt, a Done-like answer and the
|
|
176
|
+
* model's `<!-- hwip: exit -->` all delete the file outright. What none of them
|
|
177
|
+
* covers is a session that simply died: there is no SessionEnd hook, so without
|
|
178
|
+
* a TTL a crash or Ctrl-C would latch REMOTE forever.
|
|
179
|
+
*/
|
|
180
|
+
exports.REMOTE_TTL_SEC = 14400;
|
|
181
|
+
/** State path for a project hash: `<stateDir>/remote-active-<cksum(dir)>`. */
|
|
182
|
+
const remoteStatePath = (hash) => (0, path_1.join)((0, exports.stateDir)(), `remote-active-${hash}`);
|
|
183
|
+
exports.remoteStatePath = remoteStatePath;
|
|
184
|
+
/**
|
|
185
|
+
* True while REMOTE is live for this project. A time ahead of the clock counts
|
|
186
|
+
* as now (and is rewritten as now). State that is expired or
|
|
187
|
+
* unparseable is deleted on sight — the same housekeeping a stale entry
|
|
188
|
+
* marker gets, since state that can never flag again is dead weight.
|
|
189
|
+
*/
|
|
190
|
+
const isRemoteActive = (dir, now = Date.now) => {
|
|
191
|
+
const hash = (0, exports.projectHash)(dir);
|
|
192
|
+
const file = (0, exports.remoteStatePath)(hash);
|
|
193
|
+
let raw;
|
|
194
|
+
try {
|
|
195
|
+
// The first line, without the spaces around it: what `read -r ts` hands the bash twin.
|
|
196
|
+
raw = ((0, fs_1.readFileSync)(file, 'utf-8').split('\n')[0] ?? '').replace(/^[ \t]+|[ \t]+$/g, '');
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
// A plain number of at most 12 digits, matching the bash twin (`hwip_age_sec`): digits only
|
|
202
|
+
// (Number() alone reads '1e10' as ten billion, so a file bash would sweep would live on here as a
|
|
203
|
+
// far-future timestamp), and nothing longer is a time (year 33658). A leading 0 is decimal.
|
|
204
|
+
const ageSec = /^\d{1,12}$/.test(raw) ? Math.floor(now() / 1000) - Number(raw) : null;
|
|
205
|
+
const sweep = () => {
|
|
206
|
+
try {
|
|
207
|
+
(0, fs_1.unlinkSync)(file);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
/* best-effort housekeeping */
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
if (ageSec === null || ageSec > exports.REMOTE_TTL_SEC) {
|
|
214
|
+
sweep();
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (ageSec < 0) {
|
|
218
|
+
// A time ahead of the clock (it was set back, or the file is damaged) reads as now, and now is written
|
|
219
|
+
// back: left alone the file would stay live until the clock caught up with it, however far ahead it is.
|
|
220
|
+
// If now cannot be written (a read-only state folder) nothing would ever end it: it is not REMOTE.
|
|
221
|
+
if (!writeRemoteNow(dir, now)) {
|
|
222
|
+
sweep();
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
};
|
|
228
|
+
exports.isRemoteActive = isRemoteActive;
|
|
229
|
+
/**
|
|
230
|
+
* Enter REMOTE, or push its expiry back. Best-effort by contract: the prompt
|
|
231
|
+
* hooks that call this must never fail a prompt over state IO.
|
|
232
|
+
*/
|
|
233
|
+
const touchRemoteActive = (dir, now = Date.now) => {
|
|
234
|
+
writeRemoteNow(dir, now);
|
|
235
|
+
};
|
|
236
|
+
exports.touchRemoteActive = touchRemoteActive;
|
|
237
|
+
/** The state file holds now; false when it could not be written. */
|
|
238
|
+
const writeRemoteNow = (dir, now) => {
|
|
239
|
+
const hash = (0, exports.projectHash)(dir);
|
|
240
|
+
try {
|
|
241
|
+
(0, private_files_js_1.mkdirHandedBack)((0, exports.stateDir)());
|
|
242
|
+
// Before the write, so a file root makes is the sudo user's from the start (and the write follows it).
|
|
243
|
+
(0, private_files_js_1.createdHandedBack)((0, exports.remoteStatePath)(hash));
|
|
244
|
+
(0, fs_1.writeFileSync)((0, exports.remoteStatePath)(hash), `${Math.floor(now() / 1000)}\n`);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
/* a hook must never fail on state IO */
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* Leave REMOTE. Best-effort like its twin: an absent file is success, and the
|
|
254
|
+
* caller is a hook with its own job to finish. Bash twin: gate.sh
|
|
255
|
+
* hwip_remote_clear.
|
|
256
|
+
*/
|
|
257
|
+
const clearRemoteActive = (dir) => {
|
|
258
|
+
const hash = (0, exports.projectHash)(dir);
|
|
259
|
+
try {
|
|
260
|
+
(0, fs_1.unlinkSync)((0, exports.remoteStatePath)(hash));
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
/* already gone, or unwritable — the prompt still goes through */
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
exports.clearRemoteActive = clearRemoteActive;
|
|
267
|
+
/** True when the user ran /hwip-mode mute for this project. */
|
|
268
|
+
const isMuted = (dir) => {
|
|
269
|
+
const hash = (0, exports.projectHash)(dir);
|
|
270
|
+
return findStateFile('muted', hash) !== null;
|
|
271
|
+
};
|
|
272
|
+
exports.isMuted = isMuted;
|
|
273
|
+
/**
|
|
274
|
+
* Push mode for an install that has never set a dial. The twin is
|
|
275
|
+
* plugin/hooks/gate.sh's missing-5th-argument default; the shared vectors
|
|
276
|
+
* never reach either one (every vector passes pushMode explicitly), so both
|
|
277
|
+
* sides pin it in their own tests.
|
|
278
|
+
*/
|
|
279
|
+
exports.PUSHMODE_DEFAULT = 'quiet';
|
|
280
|
+
/**
|
|
281
|
+
* `notify` flag naming the push mode to assume when the project has no dial.
|
|
282
|
+
* Written by templates.ts into every hook-driven agent's completion hook and
|
|
283
|
+
* read back in cli.ts — shared so the two can never drift apart.
|
|
284
|
+
*/
|
|
285
|
+
exports.PUSHMODE_DEFAULT_FLAG = 'pushmode-default';
|
|
286
|
+
/**
|
|
287
|
+
* `notify` flags carrying the turn's tool counts. Only the drop-in artifacts
|
|
288
|
+
* (pi extension, opencode plugin) can supply them — they are the only installed
|
|
289
|
+
* hooks that see per-tool events. Shared for the same reason as the flag above:
|
|
290
|
+
* templates.ts writes them, cli.ts reads them, and a rename on one side alone
|
|
291
|
+
* would send `gateCount` back to GATE_DEFAULTS with nothing to show for it.
|
|
292
|
+
*/
|
|
293
|
+
exports.TOOL_COUNT_FLAG = 'tools';
|
|
294
|
+
exports.NONREADONLY_COUNT_FLAG = 'nonreadonly';
|
|
295
|
+
/**
|
|
296
|
+
* The user's session push-mode dial (/hwip-mode quiet | /hwip-mode loud | /hwip-mode normal).
|
|
297
|
+
*
|
|
298
|
+
* Three failure shapes, three answers — "no dial" is the only one that gets
|
|
299
|
+
* the quiet default:
|
|
300
|
+
* - no dial file → `fallback` (PUSHMODE_DEFAULT unless the caller
|
|
301
|
+
* overrides it with --pushmode-default)
|
|
302
|
+
* - unusable dial file → normal. An unreadable
|
|
303
|
+
* file, or a garbled/empty value is a broken
|
|
304
|
+
* setting, and resolving breakage to silence
|
|
305
|
+
* leaves the user with no symptom to debug. The
|
|
306
|
+
* point of the new default is quiet, not hidden
|
|
307
|
+
* errors.
|
|
308
|
+
* - readable dial file → whatever it says.
|
|
309
|
+
*/
|
|
310
|
+
const readPushMode = (dir, fallback = exports.PUSHMODE_DEFAULT) => {
|
|
311
|
+
const hash = (0, exports.projectHash)(dir);
|
|
312
|
+
const file = findStateFile('pushmode', hash);
|
|
313
|
+
if (!file)
|
|
314
|
+
return fallback;
|
|
315
|
+
try {
|
|
316
|
+
return (0, exports.normalizePushMode)((0, fs_1.readFileSync)(file, 'utf-8').replace(/\s+/g, ''));
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return 'normal';
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
exports.readPushMode = readPushMode;
|
|
323
|
+
/**
|
|
324
|
+
* Push mode for a `--auto` notify: the user's dial if they set one, otherwise
|
|
325
|
+
* the mode named by `--pushmode-default`, otherwise the built-in quiet.
|
|
326
|
+
*
|
|
327
|
+
* The dial outranks the flag deliberately. The flag exists so an agent whose
|
|
328
|
+
* hook cannot participate in the heuristic still pushes out of the box; if it
|
|
329
|
+
* outranked the dial, `/hwip-mode quiet` would silently do nothing for that agent.
|
|
330
|
+
*
|
|
331
|
+
* A flag value that isn't one of the three modes resolves to `normal`, not to
|
|
332
|
+
* the quiet default — same rule as a garbled dial file. A caller that passes
|
|
333
|
+
* nonsense has a bug, and answering a bug with silence hides it.
|
|
334
|
+
*/
|
|
335
|
+
const autoPushMode = (dir, flag) => (0, exports.readPushMode)(dir, typeof flag === 'string' ? (0, exports.normalizePushMode)(flag) : exports.PUSHMODE_DEFAULT);
|
|
336
|
+
exports.autoPushMode = autoPushMode;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI's transcript format, projected into the events the timeline draws.
|
|
3
|
+
*
|
|
4
|
+
* Gemini's vocabulary belongs to this file alone — the formats share none, so
|
|
5
|
+
* nothing here is borrowed from the Claude, pi or Codex projectors beyond the
|
|
6
|
+
* `transcript-tail` helpers that are about `TurnEvent` itself.
|
|
7
|
+
*
|
|
8
|
+
* Shape measured 2026-10-08 against `~/.gemini/tmp/<project>/chats/session-*.jsonl`
|
|
9
|
+
* (8 sessions, structure only — no conversation text was opened) and the writer
|
|
10
|
+
* that produces it: `@google/gemini-cli` 0.46.0, `bundle/chunk-6ZHP2EJW.js`
|
|
11
|
+
* (line types at 281975-281983, `pushMessage`/`$set` handling at 282072-282074,
|
|
12
|
+
* append at 282289-282291). The version that wrote each of the 8 files is not
|
|
13
|
+
* knowable; 0.46.0's reader and writer agree on the shape below.
|
|
14
|
+
*
|
|
15
|
+
* Three kinds of line, one per `JSON.parse`:
|
|
16
|
+
*
|
|
17
|
+
* - a header `{sessionId, projectHash, startTime, …}` — no `id`;
|
|
18
|
+
* - `{ $set: {…} }` — a patch to the session record;
|
|
19
|
+
* - a message `{id, timestamp, type, content, …}`.
|
|
20
|
+
*
|
|
21
|
+
* The writer also emits `{ $rewindTo: id }` (282532) when the user rewinds. It
|
|
22
|
+
* has no `id`, so it is skipped with the header, and the messages it rewinds
|
|
23
|
+
* past still project: the timeline records what was said, not what the session
|
|
24
|
+
* later forgot.
|
|
25
|
+
*
|
|
26
|
+
* Nothing here refuses to read another version: an unknown record type is
|
|
27
|
+
* named through `onUnknown` and skipped, so the timeline is thinner by one row
|
|
28
|
+
* rather than stopped.
|
|
29
|
+
*/
|
|
30
|
+
import { type TranscriptProjector } from './transcript-tail.js';
|
|
31
|
+
export declare const projectGeminiEntries: TranscriptProjector;
|
|
32
|
+
//# sourceMappingURL=gemini-transcript.d.ts.map
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Gemini CLI's transcript format, projected into the events the timeline draws.
|
|
4
|
+
*
|
|
5
|
+
* Gemini's vocabulary belongs to this file alone — the formats share none, so
|
|
6
|
+
* nothing here is borrowed from the Claude, pi or Codex projectors beyond the
|
|
7
|
+
* `transcript-tail` helpers that are about `TurnEvent` itself.
|
|
8
|
+
*
|
|
9
|
+
* Shape measured 2026-10-08 against `~/.gemini/tmp/<project>/chats/session-*.jsonl`
|
|
10
|
+
* (8 sessions, structure only — no conversation text was opened) and the writer
|
|
11
|
+
* that produces it: `@google/gemini-cli` 0.46.0, `bundle/chunk-6ZHP2EJW.js`
|
|
12
|
+
* (line types at 281975-281983, `pushMessage`/`$set` handling at 282072-282074,
|
|
13
|
+
* append at 282289-282291). The version that wrote each of the 8 files is not
|
|
14
|
+
* knowable; 0.46.0's reader and writer agree on the shape below.
|
|
15
|
+
*
|
|
16
|
+
* Three kinds of line, one per `JSON.parse`:
|
|
17
|
+
*
|
|
18
|
+
* - a header `{sessionId, projectHash, startTime, …}` — no `id`;
|
|
19
|
+
* - `{ $set: {…} }` — a patch to the session record;
|
|
20
|
+
* - a message `{id, timestamp, type, content, …}`.
|
|
21
|
+
*
|
|
22
|
+
* The writer also emits `{ $rewindTo: id }` (282532) when the user rewinds. It
|
|
23
|
+
* has no `id`, so it is skipped with the header, and the messages it rewinds
|
|
24
|
+
* past still project: the timeline records what was said, not what the session
|
|
25
|
+
* later forgot.
|
|
26
|
+
*
|
|
27
|
+
* Nothing here refuses to read another version: an unknown record type is
|
|
28
|
+
* named through `onUnknown` and skipped, so the timeline is thinner by one row
|
|
29
|
+
* rather than stopped.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.projectGeminiEntries = void 0;
|
|
33
|
+
const transcript_tail_js_1 = require("./transcript-tail.js");
|
|
34
|
+
/**
|
|
35
|
+
* Which tool argument reads as "what this call is about", first match wins.
|
|
36
|
+
*
|
|
37
|
+
* The names are Gemini's built-in tool parameters, read out of the bundle
|
|
38
|
+
* (`PARAM_*` constants, 276613-276684) rather than guessed. `description` and
|
|
39
|
+
* `title` lead because they are the sentence the model wrote for a person
|
|
40
|
+
* (`run_shell_command` carries one); the rest are the target of the file and
|
|
41
|
+
* search tools, in the order of how directly they name a thing.
|
|
42
|
+
*
|
|
43
|
+
* `command` is absent for the reason it is absent from every other list — a
|
|
44
|
+
* command line is the argument most likely to carry a secret. `prompt`
|
|
45
|
+
* (`web_fetch`, which embeds URLs) and `instruction` (`replace`) are absent for
|
|
46
|
+
* the same reason of not knowing what a person pasted into them.
|
|
47
|
+
*/
|
|
48
|
+
const GEMINI_TARGET_KEYS = ['description', 'title', 'file_path', 'dir_path', 'path', 'pattern', 'query', 'name'];
|
|
49
|
+
/** Text out of a message's `content`: a string, or parts of which only `text` is prose. */
|
|
50
|
+
const textOf = (content) => {
|
|
51
|
+
if (typeof content === 'string')
|
|
52
|
+
return content;
|
|
53
|
+
if (!Array.isArray(content))
|
|
54
|
+
return '';
|
|
55
|
+
let text = '';
|
|
56
|
+
for (const part of content) {
|
|
57
|
+
if (part && typeof part === 'object' && typeof part.text === 'string') {
|
|
58
|
+
text += part.text;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return text;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* What the person typed, out of a `user` message.
|
|
65
|
+
*
|
|
66
|
+
* `content` is what went to the model, and for a prompt with `@path` references
|
|
67
|
+
* that includes the referenced files' bodies after a
|
|
68
|
+
* `--- Content from referenced files ---` line (`REFERENCE_CONTENT_START`,
|
|
69
|
+
* `bundle:250454`). The message may also carry `displayContent`, the version the
|
|
70
|
+
* UI shows (`bundle:330220-330232`: recorded only when it differs from
|
|
71
|
+
* `content`). Either way the file bodies must not reach the phone as a prompt:
|
|
72
|
+
* prefer `displayContent`, and cut `content` at the marker when it is all there
|
|
73
|
+
* is. Read from the vendor source; no message on this machine carried either.
|
|
74
|
+
*/
|
|
75
|
+
const REFERENCE_CONTENT_MARKER = '--- Content from referenced files ---';
|
|
76
|
+
const typedText = (entry) => {
|
|
77
|
+
const shown = textOf(entry.displayContent);
|
|
78
|
+
const text = shown.trim() ? shown : textOf(entry.content);
|
|
79
|
+
const cut = text.indexOf(REFERENCE_CONTENT_MARKER);
|
|
80
|
+
return cut === -1 ? text : text.slice(0, cut);
|
|
81
|
+
};
|
|
82
|
+
/** `toolCalls[].status` values that mean the call is over; the rest are still in flight. */
|
|
83
|
+
const SETTLED = new Set(['success', 'error', 'cancelled']);
|
|
84
|
+
const projectGeminiEntries = (lines, opts = {}) => {
|
|
85
|
+
const events = [];
|
|
86
|
+
for (const raw of lines) {
|
|
87
|
+
let entry;
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
if (!parsed || typeof parsed !== 'object')
|
|
91
|
+
continue;
|
|
92
|
+
entry = parsed;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// `$set.messages` replaces the whole message list, so reading it would
|
|
98
|
+
// repeat everything already written above it; and the one a session
|
|
99
|
+
// opens with is the context Gemini injects, not anything a person said.
|
|
100
|
+
// The other keys (`lastUpdated`, `summary`, …) are bookkeeping.
|
|
101
|
+
if ('$set' in entry)
|
|
102
|
+
continue;
|
|
103
|
+
// The header is the only line without an `id`.
|
|
104
|
+
if (typeof entry.id !== 'string')
|
|
105
|
+
continue;
|
|
106
|
+
const at = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
|
|
107
|
+
const stamp = at ? { at } : {};
|
|
108
|
+
if (entry.type === 'user') {
|
|
109
|
+
// A `user` message is either typed text or the answers to tool
|
|
110
|
+
// calls (`functionResponse` parts, no `text`) — the latter joins to
|
|
111
|
+
// nothing and is skipped. No wrapper filter on the text: of the
|
|
112
|
+
// typed messages measured, none opened with `<`, and a guess at an
|
|
113
|
+
// injection wrapper nobody has seen deletes real sentences.
|
|
114
|
+
const text = typedText(entry).trim();
|
|
115
|
+
if (text)
|
|
116
|
+
events.push({ kind: 'prompt', text: (0, transcript_tail_js_1.clamp)(text, transcript_tail_js_1.MAX_EVENT_TEXT_CHARS), ...stamp });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (entry.type === 'info' || entry.type === 'error')
|
|
120
|
+
continue;
|
|
121
|
+
if (entry.type !== 'gemini') {
|
|
122
|
+
opts.onUnknown?.(`gemini record type ${typeof entry.type === 'string' ? entry.type : '(none)'}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const tokens = entry.tokens;
|
|
126
|
+
if (tokens && typeof tokens === 'object') {
|
|
127
|
+
const t = tokens;
|
|
128
|
+
const thinking = Array.isArray(entry.thoughts) ? entry.thoughts.length : 0;
|
|
129
|
+
events.push({
|
|
130
|
+
kind: 'msg',
|
|
131
|
+
mid: (0, transcript_tail_js_1.clamp)(entry.id),
|
|
132
|
+
...(typeof entry.model === 'string' ? { model: (0, transcript_tail_js_1.clamp)(entry.model) } : {}),
|
|
133
|
+
out: (0, transcript_tail_js_1.finiteNumber)(t.output),
|
|
134
|
+
// `input` is the prompt token count and already includes the
|
|
135
|
+
// cached share (measured: cached <= input in 9 of 9), so adding
|
|
136
|
+
// `cached` would count it twice.
|
|
137
|
+
ctx: (0, transcript_tail_js_1.finiteNumber)(t.input),
|
|
138
|
+
...(thinking ? { thinking } : {}),
|
|
139
|
+
...stamp,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// Gemini rewrites a whole message as a new line whenever it changes, so
|
|
143
|
+
// a reply that called tools is on disk twice: first with its `content`,
|
|
144
|
+
// then again with the same `content` and `toolCalls`. The prose already
|
|
145
|
+
// went out with the first line, so a line with `toolCalls` sends none.
|
|
146
|
+
// That loses text only for a reply first written together with its
|
|
147
|
+
// `toolCalls`, which the writer does not do: a message it creates for a
|
|
148
|
+
// tool call starts with `content: ""` (`recordToolCalls`, 282401-282451).
|
|
149
|
+
// The opposite miss is accepted: `recordMessageTokens` (282374) rewrites
|
|
150
|
+
// a reply once its `tokens` arrive late, and that second line repeats
|
|
151
|
+
// the same text (measured 0 of 12) — a doubled row beats a lost one.
|
|
152
|
+
const toolCalls = Array.isArray(entry.toolCalls) ? entry.toolCalls : undefined;
|
|
153
|
+
if (!toolCalls) {
|
|
154
|
+
const text = textOf(entry.content);
|
|
155
|
+
if (text.trim())
|
|
156
|
+
events.push({ kind: 'text', text: (0, transcript_tail_js_1.clamp)(text, transcript_tail_js_1.MAX_EVENT_TEXT_CHARS), ...stamp });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
for (const item of toolCalls) {
|
|
160
|
+
if (!item || typeof item !== 'object')
|
|
161
|
+
continue;
|
|
162
|
+
const call = item;
|
|
163
|
+
if (typeof call.id !== 'string' || typeof call.name !== 'string')
|
|
164
|
+
continue;
|
|
165
|
+
const target = (0, transcript_tail_js_1.oneLine)((0, transcript_tail_js_1.targetFrom)(call.args, GEMINI_TARGET_KEYS));
|
|
166
|
+
events.push({
|
|
167
|
+
kind: 'tool',
|
|
168
|
+
id: (0, transcript_tail_js_1.clamp)(call.id),
|
|
169
|
+
name: (0, transcript_tail_js_1.clamp)(call.name),
|
|
170
|
+
...(target ? { target } : {}),
|
|
171
|
+
...stamp,
|
|
172
|
+
});
|
|
173
|
+
// Settled calls carry their outcome on the same line as the call.
|
|
174
|
+
// Only the line count of the display text leaves, never the text.
|
|
175
|
+
if (typeof call.status === 'string' && SETTLED.has(call.status)) {
|
|
176
|
+
const resultLines = typeof call.resultDisplay === 'string' ? (0, transcript_tail_js_1.resultLinesOf)(call.resultDisplay) : 0;
|
|
177
|
+
events.push({
|
|
178
|
+
kind: 'tool_result',
|
|
179
|
+
id: (0, transcript_tail_js_1.clamp)(call.id),
|
|
180
|
+
ok: call.status === 'success',
|
|
181
|
+
...(resultLines ? { lines: resultLines } : {}),
|
|
182
|
+
...stamp,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return (0, transcript_tail_js_1.sliceSinceLastPrompt)(events, opts);
|
|
188
|
+
};
|
|
189
|
+
exports.projectGeminiEntries = projectGeminiEntries;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type http from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Did `fetch` reject because the answer was a redirect (every call that carries
|
|
4
|
+
* the API key passes `redirect: 'error'`)? The reason sits in the error's cause
|
|
5
|
+
* chain, one or two levels down depending on who wrapped it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const isRedirectRefusal: (err: unknown) => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Did `fetch` reject before the request could be sent? Any other rejection — a socket reset, a body
|
|
10
|
+
* that stops halfway — is also a `TypeError`, and may be of a request the server took. The code sits
|
|
11
|
+
* in the cause chain, as for a refused redirect. The shapes are Node 24.21's (undici 7.29), read
|
|
12
|
+
* 2026-10-14: `{ code, syscall }` on the cause, and an `AggregateError` of them for a host with two addresses.
|
|
13
|
+
*/
|
|
14
|
+
export declare const neverConnected: (err: unknown) => boolean;
|
|
15
|
+
/** Did `fetch` reject because the address is not one? It will say so again however often it is asked. */
|
|
16
|
+
export declare const isInvalidUrl: (err: unknown) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Is this failure a permanent configuration problem (bad base URL, unsendable API key) rather than a
|
|
19
|
+
* network blip? Such errors say so in fixed words — `apiFetch` throws them before any `fetch`, and
|
|
20
|
+
* `resolveBaseUrl` throws them before any request — so swallowing them as "could not fetch" or
|
|
21
|
+
* retrying them with backoff only repeats them. Matched by message, as the throwers sanitize it — so
|
|
22
|
+
* never on an error that carries an HTTP status (`HttpError`, a `HwipError` from a reply): its message
|
|
23
|
+
* is the server's own text, and a 5xx that happens to say "is unknown" is still worth another try.
|
|
24
|
+
*/
|
|
25
|
+
export declare const isConfigFailure: (err: unknown) => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Did `fetch` reject for a header value it cannot send? It does so before it opens a connection, and the
|
|
28
|
+
* same way however often it is asked. Two shapes (Node 24.21, read 2026-10-14 — held to a real `fetch`
|
|
29
|
+
* in http-failure.test.ts): a `TypeError` with no cause that says which value — whole for a line break
|
|
30
|
+
* or a NUL, by place for a character past U+00FF — and, for any other control character, undici's
|
|
31
|
+
* `UND_ERR_INVALID_ARG` ("invalid X-API-Key header") under "fetch failed". Looked for down the cause
|
|
32
|
+
* chain, as the callers hand on what `fetch` threw inside an error in their own words.
|
|
33
|
+
*/
|
|
34
|
+
export declare const isUnsendableHeader: (err: unknown) => boolean;
|
|
35
|
+
/** "could not reach api.hwip.app (ENOTFOUND)" from a rejected `fetch`. */
|
|
36
|
+
export declare const networkFailureMessage: (err: unknown, url: string) => string;
|
|
37
|
+
/** The body as JSON when it is JSON, else null. A read that fails (an abort
|
|
38
|
+
* mid-body) still throws: that is a timeout or a dropped connection, not a
|
|
39
|
+
* page that is not ours. */
|
|
40
|
+
export declare const readJsonBody: (response: Response) => Promise<unknown>;
|
|
41
|
+
export declare const httpFailureMessage: (status: number, body: unknown) => string;
|
|
42
|
+
/** What to do about a rejected key, appended to its message. */
|
|
43
|
+
export declare const AUTH_HINT = "run `hwip login`, or check --key / HWIP_API_KEY";
|
|
44
|
+
/**
|
|
45
|
+
* The proxy variables, read the way undici's `EnvHttpProxyAgent` reads them
|
|
46
|
+
* (6.29): lower case before upper case, an https target falls back to the http
|
|
47
|
+
* proxy, and `NO_PROXY` is a list of hosts, `.suffix`es and `host:port`s. The
|
|
48
|
+
* listener's socket is not `fetch` and never reaches its dispatcher, so it asks
|
|
49
|
+
* the same question here and the two cannot disagree about what goes direct.
|
|
50
|
+
*/
|
|
51
|
+
export interface ProxyEnv {
|
|
52
|
+
http_proxy?: string;
|
|
53
|
+
HTTP_PROXY?: string;
|
|
54
|
+
https_proxy?: string;
|
|
55
|
+
HTTPS_PROXY?: string;
|
|
56
|
+
no_proxy?: string;
|
|
57
|
+
NO_PROXY?: string;
|
|
58
|
+
}
|
|
59
|
+
/** The proxy a request to `rawUrl` goes through, or null for a direct connection. `ws` / `wss` follow `http` / `https`. */
|
|
60
|
+
export declare const proxyForUrl: (rawUrl: string, env?: ProxyEnv) => string | null;
|
|
61
|
+
/** The proxy settings `~/.hwip/config.json` can hold: what a launchd service, which never sees the shell's variables, reads instead. */
|
|
62
|
+
export interface ProxyConfig {
|
|
63
|
+
httpProxy?: unknown;
|
|
64
|
+
httpsProxy?: unknown;
|
|
65
|
+
noProxy?: unknown;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Route `fetch` and the listener's socket through HTTPS_PROXY / HTTP_PROXY,
|
|
69
|
+
* honouring NO_PROXY, when the variables are set; a no-op otherwise.
|
|
70
|
+
*
|
|
71
|
+
* `config` (`httpProxy`, `httpsProxy`, `noProxy` of ~/.hwip/config.json) fills
|
|
72
|
+
* in what the environment leaves unset, one setting at a time: a launchd
|
|
73
|
+
* service starts with its own environment and the plist must not hold a proxy
|
|
74
|
+
* password, so the file is where it finds them. The environment wins when it
|
|
75
|
+
* sets a name at all, empty included.
|
|
76
|
+
*
|
|
77
|
+
* Not through Node's own `http.setGlobalProxyFromEnv`: it exists only from 24.14
|
|
78
|
+
* and 25.4, and the CLI runs on older Nodes. `fetch` gets undici's
|
|
79
|
+
* `EnvHttpProxyAgent` as the global dispatcher (undici 6, Node >= 18.17); `ws`
|
|
80
|
+
* opens its own connection, so it gets a CONNECT-tunnelling agent from
|
|
81
|
+
* `https-proxy-agent` (`webSocketProxyOptions`). Both libraries load only here,
|
|
82
|
+
* so a run with no proxy pays nothing for them.
|
|
83
|
+
*
|
|
84
|
+
* A proxy that cannot be set up is an error, not a direct connection: going
|
|
85
|
+
* around a proxy someone configured is the wrong way to fail. The error names
|
|
86
|
+
* the setting and never quotes it: the value may carry the proxy's password.
|
|
87
|
+
*/
|
|
88
|
+
export declare const useProxyFromEnv: (env?: ProxyEnv, config?: ProxyConfig) => Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Options for the listener's `new WebSocket`: `ws` opens its own connection
|
|
91
|
+
* and never reaches fetch's dispatcher, so with a proxy set it is handed an
|
|
92
|
+
* agent that tunnels through it (CONNECT, for `ws:` as well as `wss:`) — unless
|
|
93
|
+
* NO_PROXY says that host goes direct.
|
|
94
|
+
*/
|
|
95
|
+
export declare const webSocketProxyOptions: (url: string) => {
|
|
96
|
+
agent?: http.Agent;
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=http-failure.d.ts.map
|