@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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const hwip_hook_js_1 = require("./hwip-hook.js");
|
|
6
|
+
const errors_js_1 = require("./errors.js");
|
|
7
|
+
const args_js_1 = require("./args.js");
|
|
8
|
+
const config_js_1 = require("./config.js");
|
|
9
|
+
const gate_js_1 = require("./gate.js");
|
|
10
|
+
const presence_js_1 = require("./presence.js");
|
|
11
|
+
const key_changes_js_1 = require("./key-changes.js");
|
|
12
|
+
const notify_failure_js_1 = require("./notify-failure.js");
|
|
13
|
+
const http_failure_js_1 = require("./http-failure.js");
|
|
14
|
+
const redact_js_1 = require("./redact.js");
|
|
15
|
+
const usage_js_1 = require("./usage.js");
|
|
16
|
+
const remote_agents_js_1 = require("./remote-agents.js");
|
|
17
|
+
const remote_hook_js_1 = require("./remote-hook.js");
|
|
18
|
+
// Every command module below is loaded by the case that runs it, not up
|
|
19
|
+
// front. The build is CommonJS, so a static import is a require at startup —
|
|
20
|
+
// and `notify` (every agent turn) and `remote-hook` (every prompt) would pay
|
|
21
|
+
// for the listener daemon (ws, LAN transfer, turn watching) they never use.
|
|
22
|
+
// Only what those two hot paths need stays static above; cli-lazy-load.test.ts
|
|
23
|
+
// holds the line.
|
|
24
|
+
const detectBranchAndProject = () => {
|
|
25
|
+
const dir = (0, config_js_1.detectProjectDir)();
|
|
26
|
+
const project = dir.split('/').filter(Boolean).pop() ?? 'project';
|
|
27
|
+
let branch;
|
|
28
|
+
try {
|
|
29
|
+
branch = (0, child_process_1.execFileSync)('git', ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
30
|
+
encoding: 'utf-8',
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
32
|
+
}).trim();
|
|
33
|
+
if (!branch || branch === 'HEAD')
|
|
34
|
+
branch = undefined;
|
|
35
|
+
}
|
|
36
|
+
catch { /* not a git repo */ }
|
|
37
|
+
return { branch, project };
|
|
38
|
+
};
|
|
39
|
+
// ── Output ──────────────────────────────────────────────────────
|
|
40
|
+
const printUsage = (command) => {
|
|
41
|
+
console.log((command ? (0, usage_js_1.commandUsage)(command) : null) ?? (0, usage_js_1.usageText)());
|
|
42
|
+
};
|
|
43
|
+
const printError = (message, isJson) => {
|
|
44
|
+
if (isJson) {
|
|
45
|
+
console.error(JSON.stringify({ error: message, status: 'error' }));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.error(`Error: ${message}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const printJson = (data) => {
|
|
52
|
+
console.log(JSON.stringify(data, null, 2));
|
|
53
|
+
};
|
|
54
|
+
// ── Commands ────────────────────────────────────────────────────
|
|
55
|
+
/**
|
|
56
|
+
* Per-request limit when a hook runs notify (`--auto`). The agent's turn waits
|
|
57
|
+
* on the hook, and its calls run in a row: the device list, then for a long
|
|
58
|
+
* body an upload request and the S3 upload, then the send. Four calls at 6 s
|
|
59
|
+
* stay under the 60 s the installed Gemini, Codex and Copilot hooks allow (templates.ts).
|
|
60
|
+
*/
|
|
61
|
+
const HOOK_REQUEST_TIMEOUT_MS = 6_000;
|
|
62
|
+
const createHook = (args, timeoutMs) => {
|
|
63
|
+
const config = (0, config_js_1.loadConfig)();
|
|
64
|
+
const apiKey = args.key || (0, config_js_1.resolvedEnv)('HWIP_API_KEY') || config.apiKey;
|
|
65
|
+
const isJson = (0, args_js_1.flagOn)(args, 'json');
|
|
66
|
+
if (!apiKey) {
|
|
67
|
+
printError('API key required. Run "hwip install" or set HWIP_API_KEY', isJson);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
let baseUrl;
|
|
71
|
+
try {
|
|
72
|
+
baseUrl = (0, config_js_1.resolveBaseUrl)(args['base-url'], config, (0, config_js_1.keyIsFromFile)(args.key, config));
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
printError(err instanceof Error ? err.message : String(err), isJson);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return new hwip_hook_js_1.HwipHook({
|
|
79
|
+
apiKey,
|
|
80
|
+
baseUrl,
|
|
81
|
+
...(timeoutMs && { timeout: timeoutMs }),
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* `hwip trust [deviceId] [--reset] [--yes]` — fingerprints, and which device
|
|
86
|
+
* keys the listener seals for (trust-command.ts).
|
|
87
|
+
*/
|
|
88
|
+
const handleTrust = async (args) => {
|
|
89
|
+
const [{ initDeviceCrypto }, { createDeviceDirectory }, { flagText, parseTrustArgs, runTrust }] = await Promise.all([import('./crypto.js'), import('./device-directory.js'), import('./trust-command.js')]);
|
|
90
|
+
const config = (0, config_js_1.loadConfig)();
|
|
91
|
+
const apiKey = flagText(args.key) ?? (0, config_js_1.resolvedEnv)('HWIP_API_KEY') ?? config.apiKey ?? '';
|
|
92
|
+
// The device list is asked for only when `trust` has something to say about it (a device to pin, or the listing).
|
|
93
|
+
let directory;
|
|
94
|
+
const directoryFor = (report) => {
|
|
95
|
+
try {
|
|
96
|
+
directory ??= createDeviceDirectory({ apiKey, baseUrl: (0, config_js_1.resolveBaseUrl)(flagText(args['base-url']), config, (0, config_js_1.keyIsFromFile)(flagText(args.key), config)), log: (line) => console.error(line) });
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
if (report)
|
|
100
|
+
console.error(`hwip trust: ${err instanceof Error ? err.message : String(err)}`);
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return directory;
|
|
104
|
+
};
|
|
105
|
+
const registeredKey = async (deviceId) => directoryFor(true)?.publicKeyOf(deviceId) ?? null;
|
|
106
|
+
// The listing is an extra: with no API key or no answer, `hwip trust` shows what it did before.
|
|
107
|
+
// A broken configuration is said once instead: an empty list would read as "no devices".
|
|
108
|
+
let trustConfigSaid = false;
|
|
109
|
+
const devices = async () => {
|
|
110
|
+
if (!apiKey)
|
|
111
|
+
return [];
|
|
112
|
+
const directory = directoryFor(false);
|
|
113
|
+
if (directory) {
|
|
114
|
+
try {
|
|
115
|
+
return await directory.devices();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!trustConfigSaid) {
|
|
122
|
+
trustConfigSaid = true;
|
|
123
|
+
try {
|
|
124
|
+
(0, config_js_1.resolveBaseUrl)(flagText(args['base-url']), config, (0, config_js_1.keyIsFromFile)(flagText(args.key), config));
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error(`hwip trust: ${err instanceof Error ? err.message : String(err)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
};
|
|
132
|
+
const parsed = parseTrustArgs(args);
|
|
133
|
+
if ('error' in parsed) {
|
|
134
|
+
console.error(parsed.error);
|
|
135
|
+
return 2;
|
|
136
|
+
}
|
|
137
|
+
return runTrust(parsed, {
|
|
138
|
+
path: config_js_1.TRUST_FILE,
|
|
139
|
+
devicePublicKey: initDeviceCrypto,
|
|
140
|
+
registeredKey,
|
|
141
|
+
devices,
|
|
142
|
+
confirm: async (question) => {
|
|
143
|
+
// Nobody to ask: an unanswered question is not a yes, and says why.
|
|
144
|
+
if (process.stdin.isTTY !== true) {
|
|
145
|
+
console.error('hwip trust: no terminal to ask on — compare the fingerprints, then run it again with --yes');
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
150
|
+
return await confirm({ message: question, default: false });
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
// Ctrl-C lands here too; anything else is worth a line.
|
|
154
|
+
if (!(err instanceof Error && err.name === 'ExitPromptError')) {
|
|
155
|
+
console.error(`hwip trust: the question could not be asked (${err instanceof Error ? err.message : String(err)})`);
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
print: (line) => console.log(line),
|
|
161
|
+
printError: (line) => console.error(line),
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
/** Current tmux session name (the rename key), or null when not inside tmux. */
|
|
165
|
+
const detectCurrentTmuxSession = () => {
|
|
166
|
+
if (!process.env.TMUX)
|
|
167
|
+
return null;
|
|
168
|
+
try {
|
|
169
|
+
const name = (0, child_process_1.execFileSync)('tmux', ['display-message', '-p', '#S'], { encoding: 'utf-8' }).trim();
|
|
170
|
+
return name || null;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* `hwip rename "<name>"` — set THIS agent session's display name in the app.
|
|
178
|
+
* Auto-detects the current tmux session + this machine's listener device id
|
|
179
|
+
* (`computeListenerDeviceId`, the same id the listener registers, so the
|
|
180
|
+
* rename lands on the right device). `--clear` resets to the computed label.
|
|
181
|
+
*/
|
|
182
|
+
const handleRename = async (args) => {
|
|
183
|
+
const isJson = (0, args_js_1.flagOn)(args, 'json');
|
|
184
|
+
const clearing = (0, args_js_1.flagOn)(args, 'clear');
|
|
185
|
+
const alias = args._arg1 || '';
|
|
186
|
+
if (!alias && !clearing) {
|
|
187
|
+
printError('Usage: hwip rename "New name" (or --clear to reset to default)', isJson);
|
|
188
|
+
return 2;
|
|
189
|
+
}
|
|
190
|
+
const sessionName = args.session || detectCurrentTmuxSession();
|
|
191
|
+
if (!sessionName) {
|
|
192
|
+
printError('No tmux session detected. Run inside a `hwip cc` session, or pass --session <name>.', isJson);
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
const deviceId = args.device || (await import('./listener.js')).computeListenerDeviceId();
|
|
196
|
+
const hook = createHook(args);
|
|
197
|
+
if (!hook)
|
|
198
|
+
return 3;
|
|
199
|
+
try {
|
|
200
|
+
await hook.renameAgentSession(deviceId, sessionName, clearing ? '' : alias);
|
|
201
|
+
if (isJson)
|
|
202
|
+
printJson({ session: sessionName, alias: clearing ? null : alias, status: 'ok' });
|
|
203
|
+
else
|
|
204
|
+
console.log(clearing ? `Reset name for ${sessionName}` : `Renamed ${sessionName} → ${alias}`);
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
return handleError(err, isJson);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* `hwip forget <session>` — drop an ended session from this machine's record,
|
|
213
|
+
* which is what takes it out of the app's past list (and out of the resume
|
|
214
|
+
* whitelist, the same pair the phone's row-delete means).
|
|
215
|
+
*
|
|
216
|
+
* `--ended` does it for every remembered name tmux no longer has: a machine
|
|
217
|
+
* that launches agents under a fresh name per task collects rows nothing ever
|
|
218
|
+
* deletes, and naming each one by hand is not the answer to fifty of them.
|
|
219
|
+
*
|
|
220
|
+
* The order mirrors the listener's own delete handler (`listener.ts:1701-1721`)
|
|
221
|
+
* so the two ways to forget a session cannot disagree about what is allowed.
|
|
222
|
+
*/
|
|
223
|
+
const handleForget = async (args) => {
|
|
224
|
+
const isJson = (0, args_js_1.flagOn)(args, 'json');
|
|
225
|
+
const { isSubagentSessionName, sessionLiveness, silenceListenerLog } = await import('./listener.js');
|
|
226
|
+
const { forgetSession } = await import('./session-registry.js');
|
|
227
|
+
// The tmux socket discovery under `sessionLiveness` logs for a daemon with a
|
|
228
|
+
// redirected stdout, not for this command. A person reading the output wants
|
|
229
|
+
// those lines — a failed probe is the only explanation for an "unknown" — but
|
|
230
|
+
// in a --json line they break the parse.
|
|
231
|
+
if (isJson)
|
|
232
|
+
silenceListenerLog();
|
|
233
|
+
const name = typeof args._arg1 === 'string' ? args._arg1 : '';
|
|
234
|
+
const sweeping = (0, args_js_1.flagOn)(args, 'ended');
|
|
235
|
+
if (name && sweeping) {
|
|
236
|
+
printError('Usage: hwip forget <session> (or --ended, not both)', isJson);
|
|
237
|
+
return 2;
|
|
238
|
+
}
|
|
239
|
+
if (sweeping)
|
|
240
|
+
return forgetEveryEndedSession(isJson, isSubagentSessionName);
|
|
241
|
+
if (!name) {
|
|
242
|
+
printError('Usage: hwip forget <session> (or --ended to drop every session tmux no longer has)', isJson);
|
|
243
|
+
return 2;
|
|
244
|
+
}
|
|
245
|
+
// A subagent pane has no registry row — the sweep never remembers panes — so
|
|
246
|
+
// "never seen" would be a true answer to the wrong question.
|
|
247
|
+
if (isSubagentSessionName(name)) {
|
|
248
|
+
printError(`${name} is a subagent pane — those have no record to forget`, isJson);
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
251
|
+
const state = sessionLiveness(name);
|
|
252
|
+
if (state === 'running') {
|
|
253
|
+
printError(`${name} is still running — 'tmux kill-session -t ${name}' first`, isJson);
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
// Not the same as "it ended": tmux answered nothing. Deleting on that would
|
|
257
|
+
// take a live session's record and chat history on the strength of a failed
|
|
258
|
+
// question.
|
|
259
|
+
if (state === 'unknown') {
|
|
260
|
+
printError(`could not ask tmux whether ${name} is running — nothing was forgotten`, isJson);
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
const outcome = forgetSession(name);
|
|
264
|
+
if (outcome === 'unknown') {
|
|
265
|
+
printError(`${name}: never seen on this machine`, isJson);
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
if (isJson)
|
|
269
|
+
printJson({ session: name, status: outcome });
|
|
270
|
+
else if (outcome === 'scrollback_kept') {
|
|
271
|
+
console.log(`Forgot ${name} — dropped from the record, but its chat scrollback would not delete`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.log(`Forgot ${name}`);
|
|
275
|
+
}
|
|
276
|
+
return 0;
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* `hwip worktree` lists the worktrees `--wt` made and what keeps each;
|
|
280
|
+
* `hwip worktree clean` removes the ones nothing keeps (`--dry-run`: only says).
|
|
281
|
+
* The same judgement the listener makes when a session ends (`worktree.ts`),
|
|
282
|
+
* run on demand for the ones it left, or never saw end.
|
|
283
|
+
*/
|
|
284
|
+
const handleWorktree = async (args) => {
|
|
285
|
+
const sub = typeof args._arg1 === 'string' ? args._arg1 : '';
|
|
286
|
+
if (sub && sub !== 'list' && sub !== 'clean') {
|
|
287
|
+
console.error(`hwip worktree: unknown subcommand '${sub}' (list, clean)`);
|
|
288
|
+
return 2;
|
|
289
|
+
}
|
|
290
|
+
const dryRun = (0, args_js_1.flagOn)(args, 'dry-run');
|
|
291
|
+
if (dryRun && sub !== 'clean') {
|
|
292
|
+
console.error("hwip worktree: --dry-run belongs to 'hwip worktree clean'");
|
|
293
|
+
return 2;
|
|
294
|
+
}
|
|
295
|
+
const { readPaneCwds, reconcileWorktrees, tmuxRun, worktreeStatuses } = await import('./worktree.js');
|
|
296
|
+
// The daemon's tmux socket, not only the default one: a pane read from the wrong server is a session read as over.
|
|
297
|
+
const { tmuxArgs } = await import('./listener.js');
|
|
298
|
+
const panes = await readPaneCwds((tmuxCmd, timeout) => tmuxRun(tmuxArgs(tmuxCmd), timeout));
|
|
299
|
+
if (sub !== 'clean') {
|
|
300
|
+
// The list only reads: without tmux it still says what holds each worktree, just not which are in use.
|
|
301
|
+
if (!panes)
|
|
302
|
+
console.error('hwip worktree: could not ask tmux which sessions are running — "in use" is not shown');
|
|
303
|
+
const statuses = await worktreeStatuses(panes ?? []);
|
|
304
|
+
if (statuses.length === 0)
|
|
305
|
+
console.log('No worktrees made by hwip --wt. (`git worktree list` shows every worktree.)');
|
|
306
|
+
for (const { row, state } of statuses)
|
|
307
|
+
console.log(`${row.path} (${row.branch}) ${state}`);
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
if (!panes) {
|
|
311
|
+
console.error('hwip worktree: could not ask tmux which sessions are running — nothing was judged');
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
const report = await reconcileWorktrees({ paneCwds: panes, manual: true, dryRun });
|
|
315
|
+
for (const r of report.removed) {
|
|
316
|
+
console.log(`${dryRun ? 'Would remove' : 'Removed'} ${r.path}${r.branchDeleted ? ` (branch ${r.branch} deleted)` : ''}`);
|
|
317
|
+
}
|
|
318
|
+
for (const path of report.gone)
|
|
319
|
+
console.log(`${dryRun ? 'Would forget' : 'Forgot'} ${path} (directory already gone)`);
|
|
320
|
+
for (const k of report.kept)
|
|
321
|
+
console.log(`Kept ${k.path} — ${k.reasons.join(', ')}`);
|
|
322
|
+
for (const path of report.inUse)
|
|
323
|
+
console.log(`In use ${path}`);
|
|
324
|
+
const total = report.removed.length + report.kept.length + report.inUse.length + report.gone.length;
|
|
325
|
+
if (total === 0)
|
|
326
|
+
console.log('No worktrees made by hwip --wt.');
|
|
327
|
+
// A refusal is the judgement working; a removal git turned down after the check passed is not.
|
|
328
|
+
return report.kept.some((k) => k.removeFailed) ? 1 : 0;
|
|
329
|
+
};
|
|
330
|
+
const forgetEveryEndedSession = async (isJson, isSubagentSessionName) => {
|
|
331
|
+
const { liveSessionNames } = await import('./listener.js');
|
|
332
|
+
const { forgetEnded } = await import('./session-registry.js');
|
|
333
|
+
// One question for the whole sweep, asked before anything is deleted: a
|
|
334
|
+
// per-name probe would let tmux go away halfway through and turn the rest of
|
|
335
|
+
// the registry into "ended".
|
|
336
|
+
const running = liveSessionNames();
|
|
337
|
+
if (running === null) {
|
|
338
|
+
printError('no tmux server answered — nothing was forgotten', isJson);
|
|
339
|
+
return 1;
|
|
340
|
+
}
|
|
341
|
+
const live = new Set(running);
|
|
342
|
+
// A subagent pane is not in that list and never will be: panes are mapped by
|
|
343
|
+
// the daemon's sweep, and this process has no such map. Unknown is the honest
|
|
344
|
+
// answer, and it is the one that leaves the row alone — a guard against a
|
|
345
|
+
// pane name reaching the registry, not something today's registry contains
|
|
346
|
+
// (`listener.ts`: the sweep never remembers panes), so the lines below that
|
|
347
|
+
// report it stay unprinted until that changes.
|
|
348
|
+
const liveness = (name) => {
|
|
349
|
+
if (isSubagentSessionName(name))
|
|
350
|
+
return 'unknown';
|
|
351
|
+
return live.has(name) ? 'running' : 'ended';
|
|
352
|
+
};
|
|
353
|
+
const sweep = forgetEnded(liveness);
|
|
354
|
+
if (isJson) {
|
|
355
|
+
printJson(sweep);
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
const { forgotten, scrollbackKept, kept, unreachable } = sweep;
|
|
359
|
+
for (const name of forgotten) {
|
|
360
|
+
const note = scrollbackKept.includes(name) ? ' — chat scrollback would not delete' : '';
|
|
361
|
+
console.log(`Forgot ${name}${note}`);
|
|
362
|
+
}
|
|
363
|
+
for (const name of unreachable)
|
|
364
|
+
console.log(`Left ${name} alone — could not tell whether it is running`);
|
|
365
|
+
const tail = unreachable.length > 0 ? `, left ${unreachable.length} undecided` : '';
|
|
366
|
+
if (forgotten.length === 0)
|
|
367
|
+
console.log(`Nothing to forget — ${kept.length} running${tail}`);
|
|
368
|
+
else
|
|
369
|
+
console.log(`Forgot ${forgotten.length} ended session(s), kept ${kept.length} running${tail}`);
|
|
370
|
+
return 0;
|
|
371
|
+
};
|
|
372
|
+
/** Parse a gate count flag; garbage input falls back to the default (never accidentally silences). */
|
|
373
|
+
const gateCount = (raw, fallback) => {
|
|
374
|
+
const n = typeof raw === 'string' ? Number(raw) : NaN;
|
|
375
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
376
|
+
};
|
|
377
|
+
const readStdin = async () => {
|
|
378
|
+
let stdin = '';
|
|
379
|
+
for await (const chunk of process.stdin)
|
|
380
|
+
stdin += chunk;
|
|
381
|
+
return stdin;
|
|
382
|
+
};
|
|
383
|
+
/**
|
|
384
|
+
* Internal — invoked by the Gemini/Codex prompt-submit hooks that
|
|
385
|
+
* `hwip setup` registers, never typed by users (hence absent from help).
|
|
386
|
+
* Reads the hook JSON on stdin; prints additionalContext JSON on a match
|
|
387
|
+
* (remote-origin detection, ADR-0002). Always exits 0: the hook only adds
|
|
388
|
+
* context and must never block a prompt.
|
|
389
|
+
*/
|
|
390
|
+
const handleRemoteHook = async (args) => {
|
|
391
|
+
const agent = args._arg1;
|
|
392
|
+
if (!(0, remote_hook_js_1.isRemoteHookAgent)(agent))
|
|
393
|
+
return 0;
|
|
394
|
+
try {
|
|
395
|
+
const out = (0, remote_hook_js_1.runRemoteHook)(agent, await readStdin());
|
|
396
|
+
if (out)
|
|
397
|
+
console.log(out);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
/* never block a prompt */
|
|
401
|
+
}
|
|
402
|
+
return 0;
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* `hwip notify --channel <channelId|name> …` — push to a channel this account owns.
|
|
406
|
+
*
|
|
407
|
+
* Not the agent hook's path: it skips the project mute and the push gate (a person
|
|
408
|
+
* typed this), sends plain text, and never fills a
|
|
409
|
+
* title or body from the branch name. The server decides ownership; a name is
|
|
410
|
+
* resolved against `GET /channels` first (channel.ts).
|
|
411
|
+
*/
|
|
412
|
+
const handleChannelNotify = async (args) => {
|
|
413
|
+
const isJson = (0, args_js_1.flagOn)(args, 'json');
|
|
414
|
+
const { parseChannelNotify, PLAINTEXT_NOTE, resolveChannel } = await import('./channel.js');
|
|
415
|
+
const parsed = parseChannelNotify(args);
|
|
416
|
+
if ('error' in parsed) {
|
|
417
|
+
printError(parsed.error, isJson);
|
|
418
|
+
return 2;
|
|
419
|
+
}
|
|
420
|
+
const hook = createHook(args);
|
|
421
|
+
if (!hook)
|
|
422
|
+
return 3;
|
|
423
|
+
try {
|
|
424
|
+
const target = resolveChannel(await hook.listChannels(), parsed.ref);
|
|
425
|
+
if ('error' in target) {
|
|
426
|
+
printError(target.error, isJson);
|
|
427
|
+
return 2;
|
|
428
|
+
}
|
|
429
|
+
console.error(PLAINTEXT_NOTE);
|
|
430
|
+
let result;
|
|
431
|
+
try {
|
|
432
|
+
result = await hook.notify({ ...parsed.payload, channelId: target.channelId });
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
if (err instanceof errors_js_1.HwipError && err.status === 403 && !(err instanceof errors_js_1.QuotaExceededError)) {
|
|
436
|
+
printError(`${err.message} — only the channel's owner can send to it`, isJson);
|
|
437
|
+
return 1;
|
|
438
|
+
}
|
|
439
|
+
throw err;
|
|
440
|
+
}
|
|
441
|
+
if (isJson) {
|
|
442
|
+
printJson({ pushId: result.pushId, channelId: target.channelId, encrypted: false, status: 'ok' });
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
console.log(`Push sent to ${target.name ? `"${target.name}" ` : ''}(${target.channelId}): ${result.pushId}`);
|
|
446
|
+
}
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
return handleError(err, isJson);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
/** `hwip channel list` — the channels this account owns and follows. */
|
|
454
|
+
const handleChannel = async (args) => {
|
|
455
|
+
const isJson = (0, args_js_1.flagOn)(args, 'json');
|
|
456
|
+
if (args._arg1 !== 'list') {
|
|
457
|
+
printError('Usage: hwip channel list', isJson);
|
|
458
|
+
return 2;
|
|
459
|
+
}
|
|
460
|
+
const hook = createHook(args);
|
|
461
|
+
if (!hook)
|
|
462
|
+
return 3;
|
|
463
|
+
try {
|
|
464
|
+
const { channelLines } = await import('./channel.js');
|
|
465
|
+
const channels = await hook.listChannels();
|
|
466
|
+
if (isJson)
|
|
467
|
+
printJson(channels);
|
|
468
|
+
else
|
|
469
|
+
for (const line of channelLines(channels))
|
|
470
|
+
console.log(line);
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
return handleError(err, isJson);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const handleNotify = async (args) => {
|
|
478
|
+
const isJson = (0, args_js_1.flagOn)(args, 'json');
|
|
479
|
+
if (args.channel !== undefined)
|
|
480
|
+
return handleChannelNotify(args);
|
|
481
|
+
const projectDir = (0, config_js_1.detectProjectDir)();
|
|
482
|
+
if ((0, gate_js_1.isMuted)(projectDir))
|
|
483
|
+
return 0;
|
|
484
|
+
// --auto: apply the shared push-gate before sending. Inputs default to
|
|
485
|
+
// GATE_DEFAULTS ("assume real work") so dumb hooks keep their historical
|
|
486
|
+
// always-push behavior in normal mode, while the /hwip-mode quiet | /hwip-mode loud
|
|
487
|
+
// dial now works for every hook-driven agent. Gated-out → silent success.
|
|
488
|
+
// With no dial the mode falls back to --pushmode-default, then to quiet.
|
|
489
|
+
// Quiet still pushes when the user has left the terminal (presence.ts).
|
|
490
|
+
if ((0, args_js_1.flagOn)(args, 'auto')) {
|
|
491
|
+
const marker = (0, gate_js_1.normalizeMarker)(typeof args.marker === 'string' ? args.marker : undefined);
|
|
492
|
+
const pushMode = (0, gate_js_1.autoPushMode)(projectDir, args[gate_js_1.PUSHMODE_DEFAULT_FLAG]);
|
|
493
|
+
const verdict = (0, gate_js_1.decidePush)({
|
|
494
|
+
toolCount: gateCount(args[gate_js_1.TOOL_COUNT_FLAG], gate_js_1.GATE_DEFAULTS.toolCount),
|
|
495
|
+
nonReadonlyCount: gateCount(args[gate_js_1.NONREADONLY_COUNT_FLAG], gate_js_1.GATE_DEFAULTS.nonReadonlyCount),
|
|
496
|
+
alreadyAsked: gate_js_1.GATE_DEFAULTS.alreadyAsked,
|
|
497
|
+
marker,
|
|
498
|
+
pushMode,
|
|
499
|
+
away: (0, presence_js_1.awayForGate)(pushMode, marker),
|
|
500
|
+
});
|
|
501
|
+
if (!verdict.push)
|
|
502
|
+
return 0;
|
|
503
|
+
if (verdict.priority === 'high' && !args.priority)
|
|
504
|
+
args.priority = 'high';
|
|
505
|
+
}
|
|
506
|
+
// Past the gates, this IS a push attempt — its outcome is recorded for
|
|
507
|
+
// `hwip verify` and the plugin (notify-failure.ts), since hooks discard stderr.
|
|
508
|
+
const hook = createHook(args, (0, args_js_1.flagOn)(args, 'auto') ? HOOK_REQUEST_TIMEOUT_MS : undefined);
|
|
509
|
+
if (!hook) {
|
|
510
|
+
(0, notify_failure_js_1.recordNotifyFailure)({ kind: 'no-key', status: null, code: null });
|
|
511
|
+
return 3;
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const sessionId = args.session || (0, config_js_1.resolvedEnv)('HWIP_SESSION_ID') || undefined;
|
|
515
|
+
// When body isn't supplied (common case for hook-driven invocations like
|
|
516
|
+
// `hwip notify --title "Task done"`), auto-fill with branch + project so
|
|
517
|
+
// the user can tell which session finished without opening the app.
|
|
518
|
+
let title = args.title;
|
|
519
|
+
let body = args.body;
|
|
520
|
+
if (!body) {
|
|
521
|
+
const { branch, project } = detectBranchAndProject();
|
|
522
|
+
body = branch ? `${project} · ${branch}` : project;
|
|
523
|
+
}
|
|
524
|
+
if (!title)
|
|
525
|
+
title = 'Task done';
|
|
526
|
+
const result = await (0, notify_failure_js_1.withNotifyFailureRecord)(() => hook.notify({
|
|
527
|
+
title,
|
|
528
|
+
body,
|
|
529
|
+
url: args.url,
|
|
530
|
+
type: args.type || 'hook',
|
|
531
|
+
priority: args.priority || undefined,
|
|
532
|
+
targetDeviceId: args.device,
|
|
533
|
+
sessionId,
|
|
534
|
+
}));
|
|
535
|
+
if (isJson) {
|
|
536
|
+
printJson({ pushId: result.pushId, status: 'ok' });
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
console.log(`Push sent: ${result.pushId}`);
|
|
540
|
+
}
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
return handleError(err, isJson);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const handleList = async (args) => {
|
|
548
|
+
const isJson = (0, args_js_1.flagOn)(args, 'json');
|
|
549
|
+
const hook = createHook(args);
|
|
550
|
+
if (!hook)
|
|
551
|
+
return 3;
|
|
552
|
+
try {
|
|
553
|
+
// Clamp to the documented 1-20 range; a non-numeric value falls back to
|
|
554
|
+
// the server default instead of sending limit=NaN in the query string.
|
|
555
|
+
const rawLimit = args.limit ? Number(args.limit) : NaN;
|
|
556
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(20, Math.max(1, Math.floor(rawLimit))) : undefined;
|
|
557
|
+
const result = await hook.list({
|
|
558
|
+
limit,
|
|
559
|
+
type: args.type,
|
|
560
|
+
});
|
|
561
|
+
if (isJson) {
|
|
562
|
+
printJson(result);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
if (result.pushes.length === 0) {
|
|
566
|
+
console.log('No pushes found.');
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
for (const p of result.pushes) {
|
|
570
|
+
// Sealed for the phone: this host holds no key to open it.
|
|
571
|
+
const title = p.isEncrypted ? '[encrypted]' : (p.title ?? '(no title)');
|
|
572
|
+
const time = new Date(p.createdAt).toLocaleString();
|
|
573
|
+
console.log(` ${p.pushId} [${p.type}] ${title} (${time})`);
|
|
574
|
+
}
|
|
575
|
+
if (result.hasMore)
|
|
576
|
+
console.log(` ... more available (use --limit to increase)`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return 0;
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
return handleError(err, isJson);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
const handleDismiss = async (args) => {
|
|
586
|
+
const isJson = (0, args_js_1.flagOn)(args, 'json');
|
|
587
|
+
const hook = createHook(args);
|
|
588
|
+
if (!hook)
|
|
589
|
+
return 3;
|
|
590
|
+
try {
|
|
591
|
+
if ((0, args_js_1.flagOn)(args, 'all')) {
|
|
592
|
+
const result = await hook.dismissAll();
|
|
593
|
+
if (isJson) {
|
|
594
|
+
printJson({ dismissed: result.dismissed, status: 'ok' });
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
console.log(`Dismissed ${result.dismissed} pushes.`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
const pushId = args._arg1;
|
|
602
|
+
if (!pushId) {
|
|
603
|
+
printError('Push ID required. Usage: hwip dismiss <push-id> or hwip dismiss --all', isJson);
|
|
604
|
+
return 1;
|
|
605
|
+
}
|
|
606
|
+
await hook.dismiss(pushId);
|
|
607
|
+
if (isJson) {
|
|
608
|
+
printJson({ dismissed: true, pushId, status: 'ok' });
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
console.log(`Dismissed: ${pushId}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
return handleError(err, isJson);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
const handleTest = async (args) => {
|
|
621
|
+
const isJson = (0, args_js_1.flagOn)(args, 'json');
|
|
622
|
+
const hook = createHook(args);
|
|
623
|
+
if (!hook) {
|
|
624
|
+
(0, notify_failure_js_1.recordNotifyFailure)({ kind: 'no-key', status: null, code: null });
|
|
625
|
+
return 3;
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
const result = await (0, notify_failure_js_1.withNotifyFailureRecord)(() => hook.notify({
|
|
629
|
+
title: 'Hwip Test',
|
|
630
|
+
body: `CLI connected successfully (v${config_js_1.VERSION})`,
|
|
631
|
+
}));
|
|
632
|
+
if (isJson) {
|
|
633
|
+
printJson({ pushId: result.pushId, status: 'ok', message: 'Test notification sent' });
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
console.log(`Test notification sent: ${result.pushId}`);
|
|
637
|
+
}
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
return handleError(err, isJson);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
// ── Error Handler ───────────────────────────────────────────────
|
|
645
|
+
const handleError = (err, isJson) => {
|
|
646
|
+
if (err instanceof errors_js_1.QuotaExceededError) {
|
|
647
|
+
printError(err.message, isJson);
|
|
648
|
+
return 2;
|
|
649
|
+
}
|
|
650
|
+
if (err instanceof errors_js_1.AuthenticationError) {
|
|
651
|
+
printError(err.message, isJson);
|
|
652
|
+
return 3;
|
|
653
|
+
}
|
|
654
|
+
if (err instanceof errors_js_1.HwipError) {
|
|
655
|
+
printError(err.message, isJson);
|
|
656
|
+
return 1;
|
|
657
|
+
}
|
|
658
|
+
printError(err instanceof Error ? err.message : 'Unknown error', isJson);
|
|
659
|
+
return 1;
|
|
660
|
+
};
|
|
661
|
+
// ── Passthrough ─────────────────────────────────────────────────
|
|
662
|
+
/**
|
|
663
|
+
* Collect raw argv after the given subcommand token so flags like
|
|
664
|
+
* `--resume` reach the wrapped agent verbatim instead of being swallowed
|
|
665
|
+
* by `parseArgs`. Returns [] when the command isn't found.
|
|
666
|
+
*/
|
|
667
|
+
const collectPassthrough = (argv, cmd) => {
|
|
668
|
+
const idx = argv.indexOf(cmd, 2);
|
|
669
|
+
return idx >= 0 ? argv.slice(idx + 1) : [];
|
|
670
|
+
};
|
|
671
|
+
// ── Main ────────────────────────────────────────────────────────
|
|
672
|
+
/** The commands that show, and clear, the recorded key changes. */
|
|
673
|
+
const KEY_CHANGE_SURFACES = new Set(['verify', 'trust', 'test', 'notify', 'ask']);
|
|
674
|
+
const main = async () => {
|
|
675
|
+
const { args, sendArgv } = (0, args_js_1.parseCliArgv)(process.argv);
|
|
676
|
+
const command = args._command;
|
|
677
|
+
if ((0, args_js_1.flagOn)(args, 'version', true)) {
|
|
678
|
+
console.log(config_js_1.VERSION);
|
|
679
|
+
return 0;
|
|
680
|
+
}
|
|
681
|
+
// Remote-control subcommands (`hwip cc` / `hwip codex` / …) come from the
|
|
682
|
+
// registry — one table row per agent, no hardcoded cases. Pass the typed
|
|
683
|
+
// command token to collectPassthrough (aliases map to the same agent).
|
|
684
|
+
const remote = (0, remote_agents_js_1.findAgentBySubcommand)(command);
|
|
685
|
+
// Before any command runs.
|
|
686
|
+
if ((0, args_js_1.asksForUsage)(process.argv, args, remote !== undefined)) {
|
|
687
|
+
// `hwip notify --help` and `hwip help notify` print that command's usage;
|
|
688
|
+
// no command, or a word that is not one, prints all of them.
|
|
689
|
+
printUsage((0, usage_js_1.usageTarget)(args));
|
|
690
|
+
return 0;
|
|
691
|
+
}
|
|
692
|
+
// Before any request, the listener's socket included — but after `--version`
|
|
693
|
+
// and usage, which must answer whatever the environment holds.
|
|
694
|
+
try {
|
|
695
|
+
await (0, http_failure_js_1.useProxyFromEnv)(process.env, (0, config_js_1.loadConfig)());
|
|
696
|
+
}
|
|
697
|
+
catch (err) {
|
|
698
|
+
// A hook's stderr is thrown away; leave the reason where `hwip-mode` reads
|
|
699
|
+
// it. `network`: nothing was sent, and that is why.
|
|
700
|
+
if (command === 'notify')
|
|
701
|
+
(0, notify_failure_js_1.recordNotifyFailure)({ kind: 'network', status: null, code: 'PROXY_CONFIG' });
|
|
702
|
+
throw err;
|
|
703
|
+
}
|
|
704
|
+
if (remote) {
|
|
705
|
+
const { handleAgentSession, splitAgentOptions } = await import('./wrapper.js');
|
|
706
|
+
const { opts, rest, error } = splitAgentOptions(collectPassthrough(process.argv, command));
|
|
707
|
+
if (error) {
|
|
708
|
+
console.error(`hwip: ${error}`);
|
|
709
|
+
return 2;
|
|
710
|
+
}
|
|
711
|
+
return handleAgentSession(remote, rest, opts);
|
|
712
|
+
}
|
|
713
|
+
for (const flag of (0, args_js_1.unknownFlags)(args))
|
|
714
|
+
console.error(`hwip ${command}: unknown flag --${flag} (ignored)`);
|
|
715
|
+
// A yes/no flag with a value that is neither (`--dry-run=maybe`) stops the command before it does anything.
|
|
716
|
+
const badValue = (0, args_js_1.badFlagValue)(args);
|
|
717
|
+
if (badValue) {
|
|
718
|
+
// The value is whatever was typed (a key pasted after a yes/no flag is possible): cleaned like the error path below.
|
|
719
|
+
console.error((0, redact_js_1.redactSecrets)(`hwip ${command}: ${badValue}`));
|
|
720
|
+
return 2;
|
|
721
|
+
}
|
|
722
|
+
// A device whose key changed waits beside the pins until a person is shown it (key-changes.ts): these are the commands
|
|
723
|
+
// a person types. At a terminal only — a hook's `hwip notify … 2>/dev/null` would take it unseen.
|
|
724
|
+
if (KEY_CHANGE_SURFACES.has(command))
|
|
725
|
+
(0, key_changes_js_1.showKeyChanges)(config_js_1.TRUST_FILE, process.stderr);
|
|
726
|
+
switch (command) {
|
|
727
|
+
case 'install':
|
|
728
|
+
case 'setup':
|
|
729
|
+
return (await import('./installer.js')).handleInstall(args);
|
|
730
|
+
case 'login':
|
|
731
|
+
return (await import('./login.js')).handleLogin(args);
|
|
732
|
+
case 'uninstall':
|
|
733
|
+
return (await import('./uninstall.js')).handleUninstall(args);
|
|
734
|
+
case 'verify':
|
|
735
|
+
return (await import('./verify.js')).handleVerify(args);
|
|
736
|
+
case 'check-update':
|
|
737
|
+
return (await import('./check-update.js')).handleCheckUpdate(args);
|
|
738
|
+
case 'notify':
|
|
739
|
+
return handleNotify(args);
|
|
740
|
+
case 'channel':
|
|
741
|
+
return handleChannel(args);
|
|
742
|
+
case 'ask':
|
|
743
|
+
return (await import('./ask.js')).handleAsk(args);
|
|
744
|
+
case 'ask-context':
|
|
745
|
+
return (await import('./ask-context-switch.js')).handleAskContext(args);
|
|
746
|
+
case 'file-view':
|
|
747
|
+
return (await import('./file-view-switch.js')).handleFileView(args);
|
|
748
|
+
// `hwip agent send` is the name (C82); `hwip send` is the older one for the same command.
|
|
749
|
+
case 'agent':
|
|
750
|
+
case 'send': {
|
|
751
|
+
// Read-only and local: before `createHook`, so no API key is needed (no exit 3).
|
|
752
|
+
if (command === 'agent' && args._arg1 === 'list')
|
|
753
|
+
return (await import('./agent-list.js')).handleAgentList(args);
|
|
754
|
+
if (command === 'agent' && args._arg1 === 'read')
|
|
755
|
+
return (await import('./agent-read.js')).handleAgentRead(args);
|
|
756
|
+
const { agentSubcommandError, liveSendDeps, parseSendArgs, runSend, sendReadsStdin } = await import('./send.js');
|
|
757
|
+
const named = command === 'agent' ? 'agent send' : 'send';
|
|
758
|
+
const unknown = command === 'agent' ? agentSubcommandError(String(args._arg1)) : null;
|
|
759
|
+
if (unknown) {
|
|
760
|
+
console.error(unknown);
|
|
761
|
+
return 2;
|
|
762
|
+
}
|
|
763
|
+
const send = parseSendArgs(sendArgv, sendReadsStdin(sendArgv) ? await readStdin() : undefined, named);
|
|
764
|
+
if ('error' in send) {
|
|
765
|
+
console.error(send.error);
|
|
766
|
+
return 2;
|
|
767
|
+
}
|
|
768
|
+
const hook = createHook(args);
|
|
769
|
+
if (!hook)
|
|
770
|
+
return 3;
|
|
771
|
+
return runSend(send, liveSendDeps(hook), named);
|
|
772
|
+
}
|
|
773
|
+
case 'list':
|
|
774
|
+
return handleList(args);
|
|
775
|
+
case 'dismiss':
|
|
776
|
+
return handleDismiss(args);
|
|
777
|
+
case 'rename':
|
|
778
|
+
return handleRename(args);
|
|
779
|
+
case 'forget':
|
|
780
|
+
return handleForget(args);
|
|
781
|
+
case 'worktree':
|
|
782
|
+
return handleWorktree(args);
|
|
783
|
+
case 'test':
|
|
784
|
+
return handleTest(args);
|
|
785
|
+
case 'trust':
|
|
786
|
+
return handleTrust(args);
|
|
787
|
+
case 'schedule':
|
|
788
|
+
return (await import('./schedule-command.js')).handleSchedule(collectPassthrough(process.argv, command));
|
|
789
|
+
case 'sh': {
|
|
790
|
+
const { handleShellSession, splitAgentOptions } = await import('./wrapper.js');
|
|
791
|
+
const { opts, rest, error } = splitAgentOptions(collectPassthrough(process.argv, command));
|
|
792
|
+
if (error || rest.length || opts.worktree) {
|
|
793
|
+
console.error(`hwip: ${error ?? "'hwip sh' takes only --label <x> and --detach"}`);
|
|
794
|
+
return 2;
|
|
795
|
+
}
|
|
796
|
+
return handleShellSession(opts);
|
|
797
|
+
}
|
|
798
|
+
case 'mcp':
|
|
799
|
+
return (await import('./mcp.js')).handleMcp();
|
|
800
|
+
case 'listener':
|
|
801
|
+
return (await import('./listener.js')).handleListener(args);
|
|
802
|
+
case 'remote-hook':
|
|
803
|
+
return handleRemoteHook(args);
|
|
804
|
+
default:
|
|
805
|
+
printError(`Unknown command: ${command}`, (0, args_js_1.flagOn)(args, 'json', true));
|
|
806
|
+
printUsage();
|
|
807
|
+
return 1;
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
main().then((code) => process.exit(code), (err) => {
|
|
811
|
+
console.error((0, redact_js_1.redactSecrets)(err instanceof Error ? err.message : String(err)));
|
|
812
|
+
process.exit(1);
|
|
813
|
+
});
|