@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,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.refreshManifest = exports.rulesUrl = exports.syncManifestFromCache = exports.loadManifestFromCache = exports.resetActiveManifest = exports.getActiveManifestSource = exports.getActiveManifest = exports.compareManifestVersions = exports.validateManifest = exports.RULES_REFRESH_INTERVAL_MS = exports.RULES_CACHE_FILE = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* OTA delivery for agent detection rules (SPEC-AGENT-AWARENESS §S7).
|
|
6
|
+
*
|
|
7
|
+
* Agent UIs change on their release cadence, not ours: when Claude
|
|
8
|
+
* Code reshapes its status line, detection must be fixable by shipping
|
|
9
|
+
* DATA, not a new daemon. The listener therefore resolves its manifest
|
|
10
|
+
* as: valid remote fetch → valid disk cache → bundled defaults. Every
|
|
11
|
+
* tier fails closed to the next — a dead endpoint, corrupt cache, or
|
|
12
|
+
* hostile payload can never leave the listener without rules, and
|
|
13
|
+
* `disabledRuleIds` in a fetched manifest acts as a same-day
|
|
14
|
+
* kill-switch for a misfiring rule (no release, no restart).
|
|
15
|
+
*
|
|
16
|
+
* Version precedence: a fetched/cached manifest only activates when
|
|
17
|
+
* its version is >= the bundled one. npm publishes land in minutes
|
|
18
|
+
* while the server manifest ships with a full deploy, so a fresh cli
|
|
19
|
+
* routinely carries NEWER rules than the endpoint — without the gate,
|
|
20
|
+
* the older remote manifest would shadow every bundled rule fix until
|
|
21
|
+
* the server caught up. Server-side rollback still works: the server
|
|
22
|
+
* bumps the version even when reverting rule content.
|
|
23
|
+
*/
|
|
24
|
+
const fs_1 = require("fs");
|
|
25
|
+
const path_1 = require("path");
|
|
26
|
+
const config_js_1 = require("./config.js");
|
|
27
|
+
const agent_state_js_1 = require("./agent-state.js");
|
|
28
|
+
const agent_rules_default_js_1 = require("./agent-rules.default.js");
|
|
29
|
+
const private_files_js_1 = require("./private-files.js");
|
|
30
|
+
exports.RULES_CACHE_FILE = (0, path_1.join)(config_js_1.CONFIG_DIR, 'agent-rules.json');
|
|
31
|
+
exports.RULES_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
32
|
+
// Matches the server-side serving cap; anything bigger is not a manifest.
|
|
33
|
+
const MAX_MANIFEST_BYTES = 256 * 1024;
|
|
34
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
35
|
+
const VALID_STATES = new Set(['working', 'blocked', 'idle', 'unknown']);
|
|
36
|
+
const isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string');
|
|
37
|
+
const isCondition = (v) => {
|
|
38
|
+
if (typeof v !== 'object' || v === null)
|
|
39
|
+
return false;
|
|
40
|
+
const c = v;
|
|
41
|
+
if (c.contains !== undefined && !isStringArray(c.contains))
|
|
42
|
+
return false;
|
|
43
|
+
if (c.regex !== undefined && !isStringArray(c.regex))
|
|
44
|
+
return false;
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
const isRule = (v) => {
|
|
48
|
+
if (typeof v !== 'object' || v === null)
|
|
49
|
+
return false;
|
|
50
|
+
const r = v;
|
|
51
|
+
if (typeof r.id !== 'string' || r.id.length === 0)
|
|
52
|
+
return false;
|
|
53
|
+
if (typeof r.state !== 'string' || !VALID_STATES.has(r.state))
|
|
54
|
+
return false;
|
|
55
|
+
// Safe integers, not merely finite. These numbers arrive from the server
|
|
56
|
+
// and decide behavior: a fractional priority orders rules by values no
|
|
57
|
+
// author can reason about, 1e21 outranks every rule ever written including
|
|
58
|
+
// the kill-switch, and a fractional tailLines slices a capture at a
|
|
59
|
+
// fraction of a line.
|
|
60
|
+
if (!Number.isSafeInteger(r.priority))
|
|
61
|
+
return false;
|
|
62
|
+
if (r.region !== undefined && r.region !== 'tail' && r.region !== 'whole')
|
|
63
|
+
return false;
|
|
64
|
+
if (r.tailLines !== undefined && (!Number.isSafeInteger(r.tailLines) || r.tailLines < 1 || r.tailLines > 200))
|
|
65
|
+
return false;
|
|
66
|
+
if (r.contains !== undefined && !isStringArray(r.contains))
|
|
67
|
+
return false;
|
|
68
|
+
if (r.regex !== undefined && !isStringArray(r.regex))
|
|
69
|
+
return false;
|
|
70
|
+
if (r.any !== undefined && (!Array.isArray(r.any) || !r.any.every(isCondition)))
|
|
71
|
+
return false;
|
|
72
|
+
if (r.not !== undefined && (!Array.isArray(r.not) || !r.not.every(isCondition)))
|
|
73
|
+
return false;
|
|
74
|
+
if (r.skipStateUpdate !== undefined && typeof r.skipStateUpdate !== 'boolean')
|
|
75
|
+
return false;
|
|
76
|
+
return true;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Structural validation of an untrusted manifest. Engine-version gate
|
|
80
|
+
* included: a manifest authored for a future engine is rejected whole —
|
|
81
|
+
* partial interpretation of unknown semantics is worse than falling
|
|
82
|
+
* back to bundled rules.
|
|
83
|
+
*/
|
|
84
|
+
const validateManifest = (value) => {
|
|
85
|
+
if (typeof value !== 'object' || value === null)
|
|
86
|
+
return null;
|
|
87
|
+
const m = value;
|
|
88
|
+
if (m.engineVersion !== agent_state_js_1.ENGINE_VERSION)
|
|
89
|
+
return null;
|
|
90
|
+
if (typeof m.version !== 'string' || m.version.length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
if (m.disabledRuleIds !== undefined && !isStringArray(m.disabledRuleIds))
|
|
93
|
+
return null;
|
|
94
|
+
if (typeof m.agents !== 'object' || m.agents === null)
|
|
95
|
+
return null;
|
|
96
|
+
for (const rules of Object.values(m.agents)) {
|
|
97
|
+
if (!Array.isArray(rules) || !rules.every(isRule))
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
};
|
|
102
|
+
exports.validateManifest = validateManifest;
|
|
103
|
+
/**
|
|
104
|
+
* Segment-wise numeric compare of dotted date versions
|
|
105
|
+
* ("2026.07.04.3"). Plain string compare fails on ".10" vs ".9";
|
|
106
|
+
* non-numeric segments compare as equal so a malformed version can
|
|
107
|
+
* never crash the refresh path.
|
|
108
|
+
*/
|
|
109
|
+
const compareManifestVersions = (a, b) => {
|
|
110
|
+
const as = a.split('.').map(Number);
|
|
111
|
+
const bs = b.split('.').map(Number);
|
|
112
|
+
const len = Math.max(as.length, bs.length);
|
|
113
|
+
for (let i = 0; i < len; i++) {
|
|
114
|
+
const d = (as[i] ?? 0) - (bs[i] ?? 0);
|
|
115
|
+
if (Number.isNaN(d))
|
|
116
|
+
continue;
|
|
117
|
+
if (d !== 0)
|
|
118
|
+
return d < 0 ? -1 : 1;
|
|
119
|
+
}
|
|
120
|
+
return 0;
|
|
121
|
+
};
|
|
122
|
+
exports.compareManifestVersions = compareManifestVersions;
|
|
123
|
+
let activeManifest = agent_rules_default_js_1.DEFAULT_MANIFEST;
|
|
124
|
+
let activeSource = 'bundled';
|
|
125
|
+
const getActiveManifest = () => activeManifest;
|
|
126
|
+
exports.getActiveManifest = getActiveManifest;
|
|
127
|
+
const getActiveManifestSource = () => activeSource;
|
|
128
|
+
exports.getActiveManifestSource = getActiveManifestSource;
|
|
129
|
+
const activateManifest = (manifest, source) => {
|
|
130
|
+
activeManifest = manifest;
|
|
131
|
+
activeSource = source;
|
|
132
|
+
// Old manifest's compiled patterns must not pin memory forever.
|
|
133
|
+
(0, agent_state_js_1.clearRegexCache)();
|
|
134
|
+
};
|
|
135
|
+
/** Test hook. */
|
|
136
|
+
const resetActiveManifest = () => {
|
|
137
|
+
activateManifest(agent_rules_default_js_1.DEFAULT_MANIFEST, 'bundled');
|
|
138
|
+
};
|
|
139
|
+
exports.resetActiveManifest = resetActiveManifest;
|
|
140
|
+
/**
|
|
141
|
+
* Startup path (synchronous): promote the disk cache if it validates,
|
|
142
|
+
* else stay on bundled. Never throws — detection must work offline.
|
|
143
|
+
*/
|
|
144
|
+
const loadManifestFromCache = () => {
|
|
145
|
+
try {
|
|
146
|
+
const cache = JSON.parse((0, fs_1.readFileSync)(exports.RULES_CACHE_FILE, 'utf-8'));
|
|
147
|
+
const manifest = (0, exports.validateManifest)(cache.manifest);
|
|
148
|
+
if (manifest && (0, exports.compareManifestVersions)(manifest.version, agent_rules_default_js_1.DEFAULT_MANIFEST.version) >= 0) {
|
|
149
|
+
activateManifest(manifest, 'cache');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Missing/corrupt cache — bundled rules carry the session.
|
|
154
|
+
}
|
|
155
|
+
return activeSource;
|
|
156
|
+
};
|
|
157
|
+
exports.loadManifestFromCache = loadManifestFromCache;
|
|
158
|
+
/** mtime of the cache file the last time `syncManifestFromCache` looked; null = no file. */
|
|
159
|
+
let cacheSeenMtime;
|
|
160
|
+
/**
|
|
161
|
+
* Worker-thread path. A `worker_threads` Worker gets its own module instance,
|
|
162
|
+
* so `loadManifestFromCache` on the main thread leaves the worker on
|
|
163
|
+
* `DEFAULT_MANIFEST`, which would miss a fetched manifest's newer rules and its
|
|
164
|
+
* `disabledRuleIds` kill-switch for the life of the daemon (2026-09-14: a pi
|
|
165
|
+
* session whose pane the same rules classified `working` in-thread, back when
|
|
166
|
+
* the bundle carried claude rules only). Call this before each sweep: it
|
|
167
|
+
* reloads the cache only when the file changed, so the main thread's refresh
|
|
168
|
+
* reaches the worker on its next sweep without any message-protocol change.
|
|
169
|
+
*/
|
|
170
|
+
const syncManifestFromCache = () => {
|
|
171
|
+
let mtime;
|
|
172
|
+
try {
|
|
173
|
+
mtime = (0, fs_1.statSync)(exports.RULES_CACHE_FILE).mtimeMs;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
mtime = null;
|
|
177
|
+
}
|
|
178
|
+
if (mtime === cacheSeenMtime)
|
|
179
|
+
return activeSource;
|
|
180
|
+
if (mtime === null) {
|
|
181
|
+
cacheSeenMtime = null;
|
|
182
|
+
return activeSource;
|
|
183
|
+
}
|
|
184
|
+
const source = (0, exports.loadManifestFromCache)();
|
|
185
|
+
// Latch only a read that landed. The cache is written without tmp+rename,
|
|
186
|
+
// so a stat/read from this thread can catch it half-written; a 304 refresh
|
|
187
|
+
// never rewrites the file, so latching that torn read would leave this
|
|
188
|
+
// thread on bundled rules for the daemon's life.
|
|
189
|
+
if (source === 'cache')
|
|
190
|
+
cacheSeenMtime = mtime;
|
|
191
|
+
return source;
|
|
192
|
+
};
|
|
193
|
+
exports.syncManifestFromCache = syncManifestFromCache;
|
|
194
|
+
const readCachedEtag = () => {
|
|
195
|
+
try {
|
|
196
|
+
const cache = JSON.parse((0, fs_1.readFileSync)(exports.RULES_CACHE_FILE, 'utf-8'));
|
|
197
|
+
return typeof cache.etag === 'string' ? cache.etag : undefined;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const rulesUrl = () => {
|
|
204
|
+
const override = (0, config_js_1.resolvedEnv)('HWIP_AGENT_RULES_URL');
|
|
205
|
+
if (override)
|
|
206
|
+
return override;
|
|
207
|
+
// The base the key goes to: a config file that cannot be read stops this (the refresh then keeps the
|
|
208
|
+
// current manifest) rather than fetching from the default stage while the account is on another.
|
|
209
|
+
const base = (0, config_js_1.resolveBaseUrl)(undefined, (0, config_js_1.loadConfig)());
|
|
210
|
+
return `${base.replace(/\/$/, '')}/agent-detection/manifest`;
|
|
211
|
+
};
|
|
212
|
+
exports.rulesUrl = rulesUrl;
|
|
213
|
+
/**
|
|
214
|
+
* One refresh attempt: conditional GET, validate, persist, activate.
|
|
215
|
+
* All failure modes degrade to the current active manifest.
|
|
216
|
+
*/
|
|
217
|
+
const refreshManifest = async (fetchImpl = fetch) => {
|
|
218
|
+
let url;
|
|
219
|
+
try {
|
|
220
|
+
url = (0, exports.rulesUrl)();
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
// The address itself is broken: saying so once beats an 'error' that reads as offline,
|
|
224
|
+
// and retrying a broken address on every tick would only repeat it.
|
|
225
|
+
console.error(`agent rules: ${err instanceof Error ? err.message : String(err)} — keeping the current manifest`);
|
|
226
|
+
return { source: activeSource, outcome: 'config-error' };
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const etag = readCachedEtag();
|
|
230
|
+
const res = await fetchImpl(url, {
|
|
231
|
+
headers: etag ? { 'If-None-Match': etag } : {},
|
|
232
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
233
|
+
});
|
|
234
|
+
if (res.status === 304) {
|
|
235
|
+
return { source: activeSource, outcome: 'not-modified', version: activeManifest.version };
|
|
236
|
+
}
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
return { source: activeSource, outcome: 'error' };
|
|
239
|
+
}
|
|
240
|
+
const body = await res.text();
|
|
241
|
+
if (Buffer.byteLength(body, 'utf-8') > MAX_MANIFEST_BYTES) {
|
|
242
|
+
return { source: activeSource, outcome: 'invalid' };
|
|
243
|
+
}
|
|
244
|
+
const manifest = (0, exports.validateManifest)(JSON.parse(body));
|
|
245
|
+
if (!manifest) {
|
|
246
|
+
return { source: activeSource, outcome: 'invalid' };
|
|
247
|
+
}
|
|
248
|
+
const cache = {
|
|
249
|
+
etag: res.headers.get('etag') ?? undefined,
|
|
250
|
+
fetchedAt: new Date().toISOString(),
|
|
251
|
+
manifest,
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
(0, private_files_js_1.mkdirHandedBack)(config_js_1.CONFIG_DIR);
|
|
255
|
+
(0, fs_1.writeFileSync)(exports.RULES_CACHE_FILE, JSON.stringify(cache, null, 2) + '\n');
|
|
256
|
+
(0, private_files_js_1.handBackToSudoUser)(exports.RULES_CACHE_FILE);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Cache write failure is non-fatal: the manifest still
|
|
260
|
+
// activates for this process lifetime.
|
|
261
|
+
}
|
|
262
|
+
if ((0, exports.compareManifestVersions)(manifest.version, agent_rules_default_js_1.DEFAULT_MANIFEST.version) < 0) {
|
|
263
|
+
// Endpoint hasn't caught up to this build's bundled rules.
|
|
264
|
+
// Cache is still written above (the ETag stops refetch churn)
|
|
265
|
+
// but startup applies the same gate, so bundled stays active.
|
|
266
|
+
return { source: activeSource, outcome: 'stale-ignored', version: manifest.version };
|
|
267
|
+
}
|
|
268
|
+
activateManifest(manifest, 'remote');
|
|
269
|
+
return { source: 'remote', outcome: 'updated', version: manifest.version };
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return { source: activeSource, outcome: 'error' };
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
exports.refreshManifest = refreshManifest;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled default detection rules — the fallback when no OTA manifest
|
|
3
|
+
* has been fetched (or the fetched one fails validation, §S7).
|
|
4
|
+
*
|
|
5
|
+
* Rule content is authored HERE, from observation of each agent's UI.
|
|
6
|
+
* Do not import or transcribe rules from third-party projects: the
|
|
7
|
+
* bundled set is original work under this repo's license.
|
|
8
|
+
*
|
|
9
|
+
* Claude Code observations behind these rules (UI as of mid-2026):
|
|
10
|
+
* - While running, the status line shows an "esc to interrupt" hint
|
|
11
|
+
* next to the spinner/progress text.
|
|
12
|
+
* - Blocking dialogs (permission requests, plan approval, question
|
|
13
|
+
* forms) render a numbered/arrow-key option list with "esc" as the
|
|
14
|
+
* cancel affordance and Enter as the confirm affordance.
|
|
15
|
+
* - At rest, the input box renders a `❯` prompt and the footer offers
|
|
16
|
+
* "? for shortcuts".
|
|
17
|
+
* - The transcript viewer (ctrl+o) overlays the pane; the agent may
|
|
18
|
+
* still be working underneath, so it must not change the state.
|
|
19
|
+
*
|
|
20
|
+
* Every agent here carries the same rules the server manifest publishes
|
|
21
|
+
* (hwip server/src/agent-rules/manifest.json, version 2026.10.18.1 — hwip#532, merged and deployed before this cli ships),
|
|
22
|
+
* so a listener that never reaches the endpoint classifies them too. A fetched
|
|
23
|
+
* manifest replaces this bundle whole for state reports; schedules read only
|
|
24
|
+
* this bundle. An agent without rules here
|
|
25
|
+
* reports `unknown`: honest ignorance beats guessed state, and
|
|
26
|
+
* harness-bench.test.ts fails the moment a rule set is half-filled.
|
|
27
|
+
*/
|
|
28
|
+
import { type DetectionManifest } from './agent-state.js';
|
|
29
|
+
export declare const DEFAULT_MANIFEST: DetectionManifest;
|
|
30
|
+
//# sourceMappingURL=agent-rules.default.d.ts.map
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_MANIFEST = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Bundled default detection rules — the fallback when no OTA manifest
|
|
6
|
+
* has been fetched (or the fetched one fails validation, §S7).
|
|
7
|
+
*
|
|
8
|
+
* Rule content is authored HERE, from observation of each agent's UI.
|
|
9
|
+
* Do not import or transcribe rules from third-party projects: the
|
|
10
|
+
* bundled set is original work under this repo's license.
|
|
11
|
+
*
|
|
12
|
+
* Claude Code observations behind these rules (UI as of mid-2026):
|
|
13
|
+
* - While running, the status line shows an "esc to interrupt" hint
|
|
14
|
+
* next to the spinner/progress text.
|
|
15
|
+
* - Blocking dialogs (permission requests, plan approval, question
|
|
16
|
+
* forms) render a numbered/arrow-key option list with "esc" as the
|
|
17
|
+
* cancel affordance and Enter as the confirm affordance.
|
|
18
|
+
* - At rest, the input box renders a `❯` prompt and the footer offers
|
|
19
|
+
* "? for shortcuts".
|
|
20
|
+
* - The transcript viewer (ctrl+o) overlays the pane; the agent may
|
|
21
|
+
* still be working underneath, so it must not change the state.
|
|
22
|
+
*
|
|
23
|
+
* Every agent here carries the same rules the server manifest publishes
|
|
24
|
+
* (hwip server/src/agent-rules/manifest.json, version 2026.10.18.1 — hwip#532, merged and deployed before this cli ships),
|
|
25
|
+
* so a listener that never reaches the endpoint classifies them too. A fetched
|
|
26
|
+
* manifest replaces this bundle whole for state reports; schedules read only
|
|
27
|
+
* this bundle. An agent without rules here
|
|
28
|
+
* reports `unknown`: honest ignorance beats guessed state, and
|
|
29
|
+
* harness-bench.test.ts fails the moment a rule set is half-filled.
|
|
30
|
+
*/
|
|
31
|
+
const agent_state_js_1 = require("./agent-state.js");
|
|
32
|
+
exports.DEFAULT_MANIFEST = {
|
|
33
|
+
engineVersion: agent_state_js_1.ENGINE_VERSION,
|
|
34
|
+
version: '2026.10.18.1',
|
|
35
|
+
agents: {
|
|
36
|
+
claude: [
|
|
37
|
+
{
|
|
38
|
+
// Transcript / verbose-output overlay: freeze state.
|
|
39
|
+
id: 'claude-transcript-overlay',
|
|
40
|
+
state: 'unknown',
|
|
41
|
+
priority: 1000,
|
|
42
|
+
skipStateUpdate: true,
|
|
43
|
+
any: [
|
|
44
|
+
{ contains: ['showing detailed transcript'] },
|
|
45
|
+
{ contains: ['ctrl+o to toggle'] },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
// Blocking dialog: an option list waiting on the user.
|
|
50
|
+
// "esc" alone is ambiguous (working shows "esc to
|
|
51
|
+
// interrupt"), so require a selection affordance too.
|
|
52
|
+
id: 'claude-blocked-dialog',
|
|
53
|
+
state: 'blocked',
|
|
54
|
+
priority: 900,
|
|
55
|
+
contains: ['esc'],
|
|
56
|
+
any: [
|
|
57
|
+
{ contains: ['do you want'] },
|
|
58
|
+
{ contains: ['enter to select'] },
|
|
59
|
+
{ contains: ['enter to confirm'] },
|
|
60
|
+
{ contains: ['to navigate'] },
|
|
61
|
+
],
|
|
62
|
+
not: [{ contains: ['esc to interrupt'] }],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'claude-working-interrupt-hint',
|
|
66
|
+
state: 'working',
|
|
67
|
+
priority: 800,
|
|
68
|
+
contains: ['esc to interrupt'],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
// Newer CC skins drop the "esc to interrupt" hint; the
|
|
72
|
+
// live token counter on the spinner line
|
|
73
|
+
// ("✶ Churning… (1m 2s · ↓ 1.2k tokens)") only renders
|
|
74
|
+
// while the agent is actually running.
|
|
75
|
+
id: 'claude-working-token-counter',
|
|
76
|
+
state: 'working',
|
|
77
|
+
priority: 790,
|
|
78
|
+
regex: ['[↓↑]\\s?[\\d.,]+k? tokens'],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
// Elapsed-time spinner suffix ("(12s · ..." / "(1m 2s ·")
|
|
82
|
+
// — second fallback for skins that hide the token counter
|
|
83
|
+
// early in a turn.
|
|
84
|
+
id: 'claude-working-elapsed-spinner',
|
|
85
|
+
state: 'working',
|
|
86
|
+
priority: 780,
|
|
87
|
+
regex: ['…\\s*\\((?:\\d+m )?\\d+s ·'],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
// Compact pass ("✱ Compacting conversation… (51s)")
|
|
91
|
+
// renders neither the interrupt hint nor a token
|
|
92
|
+
// counter, but the session is busy and can't take
|
|
93
|
+
// input — without this it reads as idle.
|
|
94
|
+
id: 'claude-working-compacting',
|
|
95
|
+
state: 'working',
|
|
96
|
+
priority: 770,
|
|
97
|
+
contains: ['compacting conversation'],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
// Idle prompt: `❯` at line start in the tail, with no
|
|
101
|
+
// dialog affordances around it.
|
|
102
|
+
id: 'claude-idle-prompt',
|
|
103
|
+
state: 'idle',
|
|
104
|
+
priority: 600,
|
|
105
|
+
regex: ['^\\s*❯'],
|
|
106
|
+
not: [
|
|
107
|
+
{ contains: ['do you want'] },
|
|
108
|
+
{ contains: ['enter to select'] },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
// Fallback idle signal when the prompt glyph is themed
|
|
113
|
+
// away: the shortcuts footer only renders at rest.
|
|
114
|
+
id: 'claude-idle-shortcuts-footer',
|
|
115
|
+
state: 'idle',
|
|
116
|
+
priority: 500,
|
|
117
|
+
contains: ['? for shortcuts'],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
codex: [
|
|
121
|
+
// Codex CLI v0.160.1, captured 2026-10-08 and 2026-10-15 (see
|
|
122
|
+
// harness-bench.test.ts). Its dialogs replace the composer and its
|
|
123
|
+
// footer with a `› 1.` option list over an `enter <verb> · esc …`
|
|
124
|
+
// line. At rest the composer sits over a `? for shortcuts` footer;
|
|
125
|
+
// while a turn runs both stay drawn under a `• Working (… • esc to
|
|
126
|
+
// interrupt)` line, so working must outrank idle.
|
|
127
|
+
//
|
|
128
|
+
// KNOWN GAP — no tool ever ran (the workspace was out of credits),
|
|
129
|
+
// so the tool-approval dialog was never drawn and has no rule. The
|
|
130
|
+
// idle rule refuses a tail holding the markers those dialogs and
|
|
131
|
+
// older Codex approval prompts draw — a `› 1.` option list, a
|
|
132
|
+
// `(y)`/`(n)` key hint, a `· esc …` footer, `enter to confirm` or
|
|
133
|
+
// `esc to cancel` — so an approval dialog that kept the footer
|
|
134
|
+
// reads `unknown` as long as it draws one of them. One that draws
|
|
135
|
+
// none would read `idle`; capture it before trusting this rule
|
|
136
|
+
// further.
|
|
137
|
+
{
|
|
138
|
+
id: 'codex-blocked-hooks-review',
|
|
139
|
+
state: 'blocked',
|
|
140
|
+
priority: 900,
|
|
141
|
+
contains: ['hooks need review', 'enter confirm'],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'codex-blocked-usage-limit',
|
|
145
|
+
state: 'blocked',
|
|
146
|
+
priority: 900,
|
|
147
|
+
contains: ['usage limit reached', 'enter select'],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'codex-blocked-update',
|
|
151
|
+
state: 'blocked',
|
|
152
|
+
priority: 900,
|
|
153
|
+
contains: ['update available', 'enter continue'],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'codex-blocked-trust-dialog',
|
|
157
|
+
state: 'blocked',
|
|
158
|
+
priority: 900,
|
|
159
|
+
contains: ['trust this folder', 'enter continue'],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'codex-working-interrupt-hint',
|
|
163
|
+
state: 'working',
|
|
164
|
+
priority: 800,
|
|
165
|
+
contains: ['esc to interrupt'],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
// The footer is gone while a draft is typed, so a pane with
|
|
169
|
+
// unsent text reads `unknown` — the safe side for a schedule.
|
|
170
|
+
// 30 lines, not the default 10: a dialog marker drawn high
|
|
171
|
+
// above a kept footer must still be in view of `not`.
|
|
172
|
+
id: 'codex-idle-shortcuts-footer',
|
|
173
|
+
state: 'idle',
|
|
174
|
+
priority: 600,
|
|
175
|
+
tailLines: 30,
|
|
176
|
+
contains: ['? for shortcuts'],
|
|
177
|
+
not: [
|
|
178
|
+
{ regex: ['^\\s*›\\s*\\d+\\.'] },
|
|
179
|
+
{ regex: ['·\\s*esc '] },
|
|
180
|
+
{ regex: ['enter (?:to )?(?:confirm|select|continue)'] },
|
|
181
|
+
{ regex: ['esc to (?:cancel|deny)'] },
|
|
182
|
+
{ regex: ['\\([yn]\\)\\s*$'] },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
gemini: [
|
|
187
|
+
{
|
|
188
|
+
// Tool approval (Shell, Edit, WriteFile), captured 2026-10-18 on
|
|
189
|
+
// Gemini CLI v0.46.0: a rounded box whose answers start with
|
|
190
|
+
// `│ ● 1. Allow once` under a boxed question line. Keyed on
|
|
191
|
+
// both, not the bare phrase, so agent text or a tool output box
|
|
192
|
+
// printing the answer row is not a dialog. The
|
|
193
|
+
// dialog draws no footer and hides the input box; 30 lines
|
|
194
|
+
// because a long diff sits between the header and the answers
|
|
195
|
+
// and a later notice box can be drawn below it.
|
|
196
|
+
id: 'gemini-blocked-tool-approval',
|
|
197
|
+
state: 'blocked',
|
|
198
|
+
priority: 900,
|
|
199
|
+
tailLines: 30,
|
|
200
|
+
regex: ['^\\s*│\\s+(?:●\\s+)?1\\.\\s+allow once'],
|
|
201
|
+
any: [
|
|
202
|
+
{ regex: ['^\\s*│ allow execution of .*\\?\\s*│$'] },
|
|
203
|
+
{ regex: ['^\\s*│ apply this change\\?\\s*│$'] },
|
|
204
|
+
],
|
|
205
|
+
// The input box is hidden while Gemini asks; with it on screen the box is the agent's own text.
|
|
206
|
+
not: [{ contains: ['type your message'] }],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
// Observed on Gemini CLI v0.46.0.
|
|
210
|
+
id: 'gemini-blocked-trust-dialog',
|
|
211
|
+
state: 'blocked',
|
|
212
|
+
priority: 900,
|
|
213
|
+
tailLines: 30,
|
|
214
|
+
contains: ['do you trust the files in this folder?'],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'gemini-working-cancel-hint',
|
|
218
|
+
state: 'working',
|
|
219
|
+
priority: 800,
|
|
220
|
+
contains: ['esc to cancel'],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'gemini-idle-input-placeholder',
|
|
224
|
+
state: 'idle',
|
|
225
|
+
priority: 600,
|
|
226
|
+
contains: ['type your message'],
|
|
227
|
+
not: [{ contains: ['esc to cancel'] }],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
opencode: [
|
|
231
|
+
// Written from the screen strings recorded in the C136 backlog
|
|
232
|
+
// row (opencode 1.18.35) and a user's permission screen, then
|
|
233
|
+
// checked against panes captured 2026-10-18 on opencode 2.0.29
|
|
234
|
+
// (harness bench: bash and edit requests). The button wording
|
|
235
|
+
// differs between versions (`Allow always` vs `Always allow`),
|
|
236
|
+
// hence the rule keys on the `Permission required` title line,
|
|
237
|
+
// guarded by any button or footer string so the phrase in tool
|
|
238
|
+
// output or agent text alone is not a dialog. tailLines 30
|
|
239
|
+
// because an edit request draws its diff between that line and
|
|
240
|
+
// the buttons. Other prompts (the agent asking a question) are
|
|
241
|
+
// not captured and report `unknown`.
|
|
242
|
+
// No idle rule: at rest the footer is only `ctrl+p commands`,
|
|
243
|
+
// which says nothing the absence of `esc interrupt` does not.
|
|
244
|
+
{
|
|
245
|
+
id: 'opencode-blocked-permission',
|
|
246
|
+
state: 'blocked',
|
|
247
|
+
priority: 900,
|
|
248
|
+
tailLines: 30,
|
|
249
|
+
contains: ['permission required'],
|
|
250
|
+
any: [
|
|
251
|
+
{ contains: ['allow once'] },
|
|
252
|
+
{ contains: ['reject'] },
|
|
253
|
+
{ contains: ['enter confirm'] },
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: 'opencode-working-interrupt-hint',
|
|
258
|
+
state: 'working',
|
|
259
|
+
priority: 800,
|
|
260
|
+
contains: ['esc interrupt'],
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
pi: [
|
|
264
|
+
{
|
|
265
|
+
// Observed on pi v0.85.1. `blocked` was captured on a picker
|
|
266
|
+
// drawn through ctx.ui.custom by an ask_user_question
|
|
267
|
+
// extension (hint `↑↓ navigate • Enter select • Esc cancel`).
|
|
268
|
+
// pi core's own extension dialogs end their hint with
|
|
269
|
+
// `escape/ctrl+c cancel` under the default keybindings —
|
|
270
|
+
// matched by the same pattern but read from the vendor file,
|
|
271
|
+
// not captured. A user who rebinds tui.select.cancel changes
|
|
272
|
+
// that text and falls back to `working` or `idle`.
|
|
273
|
+
id: 'pi-blocked-dialog',
|
|
274
|
+
state: 'blocked',
|
|
275
|
+
priority: 900,
|
|
276
|
+
any: [
|
|
277
|
+
{ regex: ['enter\\b.*\\besc(?:ape)?(?:/ctrl\\+c)?\\s+cancel\\s*$'] },
|
|
278
|
+
{ regex: ['enter to \\w+ • esc to go back\\s*$'] },
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
// pi's default Loader frames at the start of a line, bare or
|
|
283
|
+
// inside the editor's top border — anchored so a braille glyph
|
|
284
|
+
// mid-line in tool output does not count. An extension that
|
|
285
|
+
// swaps the indicator frames reports `idle` while working.
|
|
286
|
+
id: 'pi-working-spinner',
|
|
287
|
+
state: 'working',
|
|
288
|
+
priority: 800,
|
|
289
|
+
regex: ['^\\s*(?:─+\\s*)?[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] +[a-z]'],
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
// Keys on the footer's context-usage figure (`5.0%/1.0M`, or
|
|
293
|
+
// `?/1.0M` right after a compaction), which renders from the
|
|
294
|
+
// first frame, before any token count exists. The footer
|
|
295
|
+
// truncates its stats line with `...` when wider than the
|
|
296
|
+
// pane and this figure is its last item, so a pane too narrow
|
|
297
|
+
// for the stats reports `unknown` rather than `idle`.
|
|
298
|
+
id: 'pi-idle-context-footer',
|
|
299
|
+
state: 'idle',
|
|
300
|
+
priority: 600,
|
|
301
|
+
regex: ['(?:^|\\s)(?:\\d+(?:\\.\\d+)?%|\\?)/\\d+(?:\\.\\d+)?[km]?\\b'],
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable agent-session grouping when running inside a tmux agent session, so a
|
|
3
|
+
* push or hook event files under the same session key as the listener's own
|
|
4
|
+
* pushes — surviving Claude session-UUID rotation, and letting the phone open
|
|
5
|
+
* the agent chat (with its live terminal and key row) instead of a bare push
|
|
6
|
+
* screen: `buildNotificationDeepLink` needs BOTH fields
|
|
7
|
+
* (apps/mobile/src/navigation/linking.ts).
|
|
8
|
+
*
|
|
9
|
+
* The device id MUST equal the listener's `computeListenerDeviceId`, so it's
|
|
10
|
+
* resolved the same way (machine-id hash → sticky file → hostname) via the
|
|
11
|
+
* shared read-only helper — NOT a bare hostname hash, which drifts from the
|
|
12
|
+
* listener's id when a machine id is readable and files the push under a
|
|
13
|
+
* non-matching session key.
|
|
14
|
+
*
|
|
15
|
+
* One definition for every sender in this package: `hwip notify` (hwip-hook.ts)
|
|
16
|
+
* and `hwip ask` (ask.ts). The MCP server has its own copy of the same rule
|
|
17
|
+
* (mcp-server/src/config.ts) because it is a separate package.
|
|
18
|
+
*/
|
|
19
|
+
export declare const agentSessionContext: () => {
|
|
20
|
+
agentDeviceId: string;
|
|
21
|
+
agentSessionName: string;
|
|
22
|
+
} | null;
|
|
23
|
+
//# sourceMappingURL=agent-session.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.agentSessionContext = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const listener_device_id_js_1 = require("./listener-device-id.js");
|
|
6
|
+
/**
|
|
7
|
+
* Stable agent-session grouping when running inside a tmux agent session, so a
|
|
8
|
+
* push or hook event files under the same session key as the listener's own
|
|
9
|
+
* pushes — surviving Claude session-UUID rotation, and letting the phone open
|
|
10
|
+
* the agent chat (with its live terminal and key row) instead of a bare push
|
|
11
|
+
* screen: `buildNotificationDeepLink` needs BOTH fields
|
|
12
|
+
* (apps/mobile/src/navigation/linking.ts).
|
|
13
|
+
*
|
|
14
|
+
* The device id MUST equal the listener's `computeListenerDeviceId`, so it's
|
|
15
|
+
* resolved the same way (machine-id hash → sticky file → hostname) via the
|
|
16
|
+
* shared read-only helper — NOT a bare hostname hash, which drifts from the
|
|
17
|
+
* listener's id when a machine id is readable and files the push under a
|
|
18
|
+
* non-matching session key.
|
|
19
|
+
*
|
|
20
|
+
* One definition for every sender in this package: `hwip notify` (hwip-hook.ts)
|
|
21
|
+
* and `hwip ask` (ask.ts). The MCP server has its own copy of the same rule
|
|
22
|
+
* (mcp-server/src/config.ts) because it is a separate package.
|
|
23
|
+
*/
|
|
24
|
+
const agentSessionContext = () => {
|
|
25
|
+
if (!process.env.TMUX)
|
|
26
|
+
return null;
|
|
27
|
+
// A pi subagent pane carries its own wire name (<#S>.<pane> — the
|
|
28
|
+
// listener's naming) in this env, set by the extension for its notify
|
|
29
|
+
// children. It wins over #S, which would file the push under the parent.
|
|
30
|
+
const fromEnv = process.env.HWIP_AGENT_SESSION_NAME?.trim();
|
|
31
|
+
if (fromEnv)
|
|
32
|
+
return { agentDeviceId: (0, listener_device_id_js_1.listenerDeviceId)(), agentSessionName: fromEnv };
|
|
33
|
+
let name;
|
|
34
|
+
try {
|
|
35
|
+
name = (0, node_child_process_1.execFileSync)('tmux', ['display-message', '-p', '#S'], { encoding: 'utf-8' }).trim();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (!name)
|
|
41
|
+
return null;
|
|
42
|
+
return { agentDeviceId: (0, listener_device_id_js_1.listenerDeviceId)(), agentSessionName: name };
|
|
43
|
+
};
|
|
44
|
+
exports.agentSessionContext = agentSessionContext;
|