@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/login.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleLogin = exports.runLoginFlow = exports.resolveTimeoutSec = exports.resolveAllowHosts = exports.resolveWebUrl = exports.openBrowser = exports.browserPlan = exports.MAX_CALLBACK_BODY_BYTES = exports.parseCallback = exports.MAX_STATE_MISMATCHES = exports.parseCallbackParams = exports.persistConfig = exports.buildBridgeUrl = exports.stripUndefined = void 0;
|
|
4
|
+
const node_http_1 = require("node:http");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_readline_1 = require("node:readline");
|
|
9
|
+
const config_js_1 = require("./config.js");
|
|
10
|
+
const login_address_js_1 = require("./login-address.js");
|
|
11
|
+
const login_page_js_1 = require("./login-page.js");
|
|
12
|
+
const redact_js_1 = require("./redact.js");
|
|
13
|
+
const DEFAULT_WEB_URL = 'https://console.hwip.app';
|
|
14
|
+
const DEFAULT_TIMEOUT_SEC = 300;
|
|
15
|
+
// ── Pure helpers (unit-tested) ───────────────────────────────────
|
|
16
|
+
const stripUndefined = (obj) => {
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
19
|
+
if (value !== undefined)
|
|
20
|
+
out[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
};
|
|
24
|
+
exports.stripUndefined = stripUndefined;
|
|
25
|
+
/**
|
|
26
|
+
* `post=1` tells the console this listener takes the callback as a form POST:
|
|
27
|
+
* the key then travels in the request body and never in a URL (browser history,
|
|
28
|
+
* autofill and session restore keep URLs). `/cb` takes no key any other way.
|
|
29
|
+
*/
|
|
30
|
+
const buildBridgeUrl = (webUrl, port, state, host) => {
|
|
31
|
+
const base = webUrl.replace(/\/+$/, '');
|
|
32
|
+
const params = new URLSearchParams({ port: String(port), state, host, post: '1' });
|
|
33
|
+
return `${base}/auth/cli-bridge?${params.toString()}`;
|
|
34
|
+
};
|
|
35
|
+
exports.buildBridgeUrl = buildBridgeUrl;
|
|
36
|
+
const persistConfig = (next) => {
|
|
37
|
+
const existing = (0, config_js_1.loadConfig)();
|
|
38
|
+
(0, config_js_1.saveConfig)({ ...existing, ...(0, exports.stripUndefined)(next) });
|
|
39
|
+
};
|
|
40
|
+
exports.persistConfig = persistConfig;
|
|
41
|
+
/**
|
|
42
|
+
* The callback's fields, from a form body (the only place the key may be). The `baseUrl` and `wsUrl` it names are
|
|
43
|
+
* where `config.json` will send the key, so one that is not https/wss on a host hwip knows (`login-address.ts`) is
|
|
44
|
+
* refused whole — nothing is saved — and ends the login saying which address and why (C131).
|
|
45
|
+
* `extraHosts`: hosts besides `hwip.app` the callback may name (the console's, those already in the config).
|
|
46
|
+
*/
|
|
47
|
+
const parseCallbackParams = (params, expectedState, extraHosts = []) => {
|
|
48
|
+
const state = params.get('state');
|
|
49
|
+
if (state !== expectedState)
|
|
50
|
+
return { ok: false, status: 403, reason: 'state mismatch' };
|
|
51
|
+
const apiKey = params.get('key') ?? undefined;
|
|
52
|
+
if (!apiKey)
|
|
53
|
+
return { ok: false, status: 400, reason: 'missing key' };
|
|
54
|
+
const baseUrl = params.get('baseUrl') ?? undefined;
|
|
55
|
+
const wsUrl = params.get('wsUrl') ?? undefined;
|
|
56
|
+
for (const [field, value] of [['baseUrl', baseUrl], ['wsUrl', wsUrl]]) {
|
|
57
|
+
if (value === undefined)
|
|
58
|
+
continue;
|
|
59
|
+
const problem = (0, login_address_js_1.callbackAddressProblem)(field, value, extraHosts);
|
|
60
|
+
if (problem) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
status: 400,
|
|
64
|
+
reason: 'the console sent an address hwip will not save',
|
|
65
|
+
endsLogin: `${problem}, so the sign-in was not saved. Check the console you signed in at (--web-url), or name a host you trust with --allow-host <host>, and run \`hwip login\` again`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, config: { apiKey, hookId: params.get('hook') ?? undefined, baseUrl, wsUrl } };
|
|
70
|
+
};
|
|
71
|
+
exports.parseCallbackParams = parseCallbackParams;
|
|
72
|
+
/** Requests with a wrong state one login answers 403 and waits through, before it gives up. */
|
|
73
|
+
exports.MAX_STATE_MISMATCHES = 5;
|
|
74
|
+
const KEY_IN_URL = 'the console sent the key in a URL, which hwip refuses, so it was not saved. That URL is in the browser\'s history: ' +
|
|
75
|
+
'revoke the key in the app (Settings > API Keys), update the console, and run `hwip login` again';
|
|
76
|
+
/**
|
|
77
|
+
* A GET callback. It carries no key: the console's "cancel" is one (`?error=denied&state=…`) and answers "missing
|
|
78
|
+
* key". A key in the query is refused unread (C102) and ends the login with `KEY_IN_URL`; its value is not in the
|
|
79
|
+
* result, the page or the log.
|
|
80
|
+
*/
|
|
81
|
+
const parseCallback = (reqUrl, expectedState) => {
|
|
82
|
+
const url = new URL(reqUrl, 'http://127.0.0.1');
|
|
83
|
+
if (url.pathname !== '/cb')
|
|
84
|
+
return { ok: false, status: 404, reason: 'not found' };
|
|
85
|
+
if (url.searchParams.get('state') !== expectedState)
|
|
86
|
+
return { ok: false, status: 403, reason: 'state mismatch' };
|
|
87
|
+
if (url.searchParams.has('key'))
|
|
88
|
+
return { ok: false, status: 400, reason: 'a key in the URL is refused', endsLogin: KEY_IN_URL };
|
|
89
|
+
return { ok: false, status: 400, reason: 'missing key' };
|
|
90
|
+
};
|
|
91
|
+
exports.parseCallback = parseCallback;
|
|
92
|
+
/** A form with five short fields is well under this; the cap is for whatever else can reach a loopback port. */
|
|
93
|
+
exports.MAX_CALLBACK_BODY_BYTES = 8 * 1024;
|
|
94
|
+
const FORM_TYPE = 'application/x-www-form-urlencoded';
|
|
95
|
+
/** The body of a callback POST, or the status to refuse it with. */
|
|
96
|
+
const readCallbackBody = (req) => {
|
|
97
|
+
const type = (req.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase();
|
|
98
|
+
if (type !== FORM_TYPE)
|
|
99
|
+
return Promise.resolve({ status: 415, reason: 'unsupported content type' });
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const chunks = [];
|
|
102
|
+
let size = 0;
|
|
103
|
+
req.on('data', (chunk) => {
|
|
104
|
+
size += chunk.length;
|
|
105
|
+
if (size > exports.MAX_CALLBACK_BODY_BYTES) {
|
|
106
|
+
resolve({ status: 413, reason: 'request too large' });
|
|
107
|
+
req.destroy();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
chunks.push(chunk);
|
|
111
|
+
});
|
|
112
|
+
req.on('end', () => resolve({ body: Buffer.concat(chunks).toString('utf-8') }));
|
|
113
|
+
// The browser went away before the body ended: nothing to answer, but the wait must not outlive the socket.
|
|
114
|
+
req.on('close', () => resolve({ status: 400, reason: 'unreadable request' }));
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
// ── Browser launch ───────────────────────────────────────────────
|
|
118
|
+
const browserCommand = () => {
|
|
119
|
+
if (process.platform === 'darwin')
|
|
120
|
+
return { cmd: 'open', prefixArgs: [] };
|
|
121
|
+
if (process.platform === 'win32')
|
|
122
|
+
return { cmd: 'cmd', prefixArgs: ['/c', 'start', ''] };
|
|
123
|
+
return { cmd: 'xdg-open', prefixArgs: [] };
|
|
124
|
+
};
|
|
125
|
+
const browserPlan = (env = process.env, platform = process.platform) => {
|
|
126
|
+
// A $BROWSER helper opens it where the user is — VS Code Remote-SSH sets one
|
|
127
|
+
// and forwards the loopback port — and WSL opens the Windows browser, which
|
|
128
|
+
// shares localhost.
|
|
129
|
+
if (env.BROWSER)
|
|
130
|
+
return { kind: 'open' };
|
|
131
|
+
if (env.SSH_CONNECTION || env.SSH_TTY)
|
|
132
|
+
return { kind: 'ssh' };
|
|
133
|
+
if (platform !== 'darwin' && platform !== 'win32' && !env.WSL_DISTRO_NAME && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
|
|
134
|
+
return { kind: 'none', reason: 'no display (DISPLAY and WAYLAND_DISPLAY are unset)' };
|
|
135
|
+
}
|
|
136
|
+
return { kind: 'open' };
|
|
137
|
+
};
|
|
138
|
+
exports.browserPlan = browserPlan;
|
|
139
|
+
/** How long an opener has to fail before it counts as started. */
|
|
140
|
+
const OPENER_GRACE_MS = 1500;
|
|
141
|
+
/**
|
|
142
|
+
* Opens `url`; resolves to why it could not, or undefined once the opener
|
|
143
|
+
* started. An opener that exits non-zero inside the grace window failed —
|
|
144
|
+
* `xdg-open` with no handler exits 3 — and one still running after it, or
|
|
145
|
+
* done with 0, handed the URL on.
|
|
146
|
+
*/
|
|
147
|
+
const openBrowser = (url, graceMs = OPENER_GRACE_MS) => {
|
|
148
|
+
const { cmd, prefixArgs } = browserCommand();
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
try {
|
|
151
|
+
const child = (0, node_child_process_1.spawn)(cmd, [...prefixArgs, url], { stdio: 'ignore', detached: true });
|
|
152
|
+
// A missing `xdg-open` fails here, after spawn() returned.
|
|
153
|
+
child.once('error', (err) => resolve(`${cmd} failed: ${err.message}`));
|
|
154
|
+
child.once('spawn', () => {
|
|
155
|
+
const grace = setTimeout(() => {
|
|
156
|
+
child.unref();
|
|
157
|
+
resolve(undefined);
|
|
158
|
+
}, graceMs);
|
|
159
|
+
child.once('exit', (code) => {
|
|
160
|
+
clearTimeout(grace);
|
|
161
|
+
resolve(code ? `${cmd} exited with code ${code}` : undefined);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
resolve(`${cmd} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
exports.openBrowser = openBrowser;
|
|
171
|
+
/**
|
|
172
|
+
* Inside `hwip install`, Enter skips the wait and goes to the paste prompt —
|
|
173
|
+
* and a key pasted right here, before Enter, is that answer. Resolves to the
|
|
174
|
+
* typed line. Null without a terminal to read it from.
|
|
175
|
+
*/
|
|
176
|
+
const enterToSkip = () => {
|
|
177
|
+
if (process.stdin.isTTY !== true)
|
|
178
|
+
return null;
|
|
179
|
+
const rl = (0, node_readline_1.createInterface)({ input: process.stdin });
|
|
180
|
+
const pressed = new Promise((resolve) => rl.once('line', (line) => resolve(line.trim())));
|
|
181
|
+
return { pressed, close: () => rl.close() };
|
|
182
|
+
};
|
|
183
|
+
// ── Loopback server ──────────────────────────────────────────────
|
|
184
|
+
const respond = (res, status, body) => {
|
|
185
|
+
res.writeHead(status, {
|
|
186
|
+
'content-type': 'text/html; charset=utf-8',
|
|
187
|
+
// The page is inline styles and an inline drawing, nothing else: let the
|
|
188
|
+
// browser refuse anything more. What got here carries the new key (in the
|
|
189
|
+
// body), so it is not cached and not sent on as a referrer.
|
|
190
|
+
'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'",
|
|
191
|
+
'cache-control': 'no-store',
|
|
192
|
+
'referrer-policy': 'no-referrer',
|
|
193
|
+
});
|
|
194
|
+
res.end(body);
|
|
195
|
+
};
|
|
196
|
+
const startLoopbackServer = (state, extraHosts) => {
|
|
197
|
+
let mismatches = 0;
|
|
198
|
+
let settle;
|
|
199
|
+
let fail;
|
|
200
|
+
const done = new Promise((res, rej) => {
|
|
201
|
+
settle = res;
|
|
202
|
+
fail = rej;
|
|
203
|
+
});
|
|
204
|
+
const server = (0, node_http_1.createServer)((req, res) => {
|
|
205
|
+
// A handler that rejects would end the process; whatever slipped past the checks inside ends the login instead.
|
|
206
|
+
handleCallback(req, res).catch((err) => {
|
|
207
|
+
if (!res.headersSent)
|
|
208
|
+
respond(res, 500, (0, login_page_js_1.errorPage)('The sign-in could not be completed. Return to your terminal for the reason.'));
|
|
209
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
const handleCallback = async (req, res) => {
|
|
213
|
+
let result;
|
|
214
|
+
if (req.method === 'POST') {
|
|
215
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
216
|
+
// The key is in the body: the query of a POST is not read.
|
|
217
|
+
if (url.pathname !== '/cb') {
|
|
218
|
+
result = { ok: false, status: 404, reason: 'not found' };
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const read = await readCallbackBody(req);
|
|
222
|
+
result = 'body' in read
|
|
223
|
+
? (0, exports.parseCallbackParams)(new URLSearchParams(read.body), state, extraHosts)
|
|
224
|
+
: { ok: false, status: read.status, reason: read.reason };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
result = (0, exports.parseCallback)(req.url ?? '/', state);
|
|
229
|
+
}
|
|
230
|
+
if (!result.ok) {
|
|
231
|
+
respond(res, result.status, (0, login_page_js_1.errorPage)(result.reason));
|
|
232
|
+
// A request that does not hold the state is not the console's: it is answered and the wait goes on, so one stray
|
|
233
|
+
// request cannot end the sign-in. A run of them can (the state is a secret only the console has).
|
|
234
|
+
if (result.status === 403 && ++mismatches >= exports.MAX_STATE_MISMATCHES) {
|
|
235
|
+
fail(new Error(`state mismatch — refused after ${exports.MAX_STATE_MISMATCHES} requests that did not hold the sign-in state`));
|
|
236
|
+
}
|
|
237
|
+
else if (result.endsLogin)
|
|
238
|
+
fail(new Error(result.endsLogin));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// A handler that throws takes the process down with it. The file can have gone bad since `handleLogin` looked.
|
|
242
|
+
try {
|
|
243
|
+
(0, exports.persistConfig)(result.config);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
respond(res, 500, (0, login_page_js_1.errorPage)('The key could not be saved. Return to your terminal for the reason.'));
|
|
247
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
respond(res, 200, (0, login_page_js_1.okPage)());
|
|
251
|
+
settle(result.config);
|
|
252
|
+
};
|
|
253
|
+
return new Promise((resolveHandle, rejectHandle) => {
|
|
254
|
+
server.once('error', rejectHandle);
|
|
255
|
+
server.listen(0, '127.0.0.1', () => {
|
|
256
|
+
const { port } = server.address();
|
|
257
|
+
resolveHandle({ port, done, close: () => server.close() });
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
// ── Command ──────────────────────────────────────────────────────
|
|
262
|
+
// The loopback server closes with the attempt, so the bridge URL is no use
|
|
263
|
+
// after this: the way forward is the manual flow — or, inside `hwip install`,
|
|
264
|
+
// the paste prompt that follows.
|
|
265
|
+
const headlessHint = (reason, pasteNext) => {
|
|
266
|
+
console.error(`\n Could not open a browser: ${reason}.`);
|
|
267
|
+
console.error(pasteNext ? ' Paste the API key below instead.\n' : ` Use the manual flow instead:\n${MANUAL_FLOW}\n`);
|
|
268
|
+
};
|
|
269
|
+
const MANUAL_FLOW = ' npx @hwip/cli install --key ak_… --hook hook_…';
|
|
270
|
+
const resolveWebUrl = (raw) => typeof raw === 'string' ? raw : DEFAULT_WEB_URL;
|
|
271
|
+
exports.resolveWebUrl = resolveWebUrl;
|
|
272
|
+
/** The hosts `--allow-host` adds to the callback's trusted ones. A bad value stops the command — it is not read as "none". */
|
|
273
|
+
const resolveAllowHosts = (raw) => {
|
|
274
|
+
const parsed = (0, login_address_js_1.parseAllowHosts)(raw);
|
|
275
|
+
if ('error' in parsed)
|
|
276
|
+
throw new Error(parsed.error);
|
|
277
|
+
return parsed.hosts;
|
|
278
|
+
};
|
|
279
|
+
exports.resolveAllowHosts = resolveAllowHosts;
|
|
280
|
+
const resolveTimeoutSec = (raw) => {
|
|
281
|
+
if (typeof raw !== 'string')
|
|
282
|
+
return DEFAULT_TIMEOUT_SEC;
|
|
283
|
+
const n = Number(raw);
|
|
284
|
+
// Reject NaN / non-positive — a bad --timeout must not fire setTimeout instantly.
|
|
285
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_TIMEOUT_SEC;
|
|
286
|
+
};
|
|
287
|
+
exports.resolveTimeoutSec = resolveTimeoutSec;
|
|
288
|
+
/**
|
|
289
|
+
* Core login flow shared by `handleLogin` (CLI command) and `handleInstall`
|
|
290
|
+
* (auto-trigger on missing credentials). Returns the issued credentials, or
|
|
291
|
+
* null when the browser can't open (headless) or the callback never arrives.
|
|
292
|
+
* `deps.open` is injectable for tests; defaults to the real browser launcher.
|
|
293
|
+
*/
|
|
294
|
+
const runLoginFlow = async (
|
|
295
|
+
/** `pasteNext`: the caller (`hwip install`) prompts for the key when this returns null. */
|
|
296
|
+
opts, deps = {}) => {
|
|
297
|
+
const open = deps.open ?? exports.openBrowser;
|
|
298
|
+
const plan = deps.plan ?? (0, exports.browserPlan)();
|
|
299
|
+
const pasteNext = opts.pasteNext === true;
|
|
300
|
+
if (plan.kind === 'none') {
|
|
301
|
+
headlessHint(plan.reason, pasteNext);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const state = (0, node_crypto_1.randomBytes)(16).toString('hex');
|
|
305
|
+
let handle;
|
|
306
|
+
try {
|
|
307
|
+
handle = await startLoopbackServer(state, [...(0, login_address_js_1.trustedCallbackHosts)(opts.webUrl, (0, config_js_1.loadConfig)()), ...(opts.allowHosts ?? [])]);
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
console.error(` Error: could not start local server (${err instanceof Error ? err.message : 'unknown'})`);
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const bridgeUrl = (0, exports.buildBridgeUrl)(opts.webUrl, handle.port, state, (0, node_os_1.hostname)());
|
|
314
|
+
if (plan.kind === 'ssh') {
|
|
315
|
+
// No browser here, but one on the user's machine can still finish the
|
|
316
|
+
// login once the callback port reaches this box.
|
|
317
|
+
console.log(`\n This is an SSH session, so no browser opens here. To sign in from your own machine,\n` +
|
|
318
|
+
` forward the callback port and open the URL there:\n` +
|
|
319
|
+
` ssh -L ${handle.port}:127.0.0.1:${handle.port} <this host>\n` +
|
|
320
|
+
` ${bridgeUrl}\n` +
|
|
321
|
+
(pasteNext ? '' : ` Otherwise press Ctrl+C and use the manual flow:\n${MANUAL_FLOW}\n`));
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const failed = await open(bridgeUrl);
|
|
325
|
+
if (failed) {
|
|
326
|
+
headlessHint(failed, pasteNext);
|
|
327
|
+
handle.close();
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
console.log(`\n Opened the browser to sign in. If it did not appear, open:\n ${bridgeUrl}\n`);
|
|
331
|
+
}
|
|
332
|
+
// Inside install Ctrl+C would end the whole install; Enter goes to the paste prompt.
|
|
333
|
+
const skip = pasteNext ? (deps.skip ?? enterToSkip)() : null;
|
|
334
|
+
const hint = !pasteNext ? ', Ctrl+C to cancel' : skip ? ', or press Enter to paste the API key instead' : '';
|
|
335
|
+
console.log(` Waiting for sign-in in the browser (up to ${opts.timeoutSec}s${hint})...`);
|
|
336
|
+
let timer;
|
|
337
|
+
const timeout = new Promise((_, reject) => {
|
|
338
|
+
timer = setTimeout(() => reject(new Error(`no sign-in within ${opts.timeoutSec}s`)), opts.timeoutSec * 1000);
|
|
339
|
+
timer.unref();
|
|
340
|
+
});
|
|
341
|
+
try {
|
|
342
|
+
const skipped = skip
|
|
343
|
+
? skip.pressed.then((line) => (line ? { pastedKey: line } : null))
|
|
344
|
+
: new Promise(() => { });
|
|
345
|
+
return await Promise.race([handle.done, timeout, skipped]);
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
console.error(`\n Login failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
if (timer)
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
skip?.close();
|
|
355
|
+
handle.close();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
exports.runLoginFlow = runLoginFlow;
|
|
359
|
+
const handleLogin = async (args, deps = {}) => {
|
|
360
|
+
const webUrl = (0, exports.resolveWebUrl)(args['web-url']);
|
|
361
|
+
const timeoutSec = (0, exports.resolveTimeoutSec)(args.timeout);
|
|
362
|
+
let allowHosts;
|
|
363
|
+
try {
|
|
364
|
+
allowHosts = (0, exports.resolveAllowHosts)(args['allow-host']);
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
console.error(`\n Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
// Before a browser opens: a sign-in whose key cannot be saved is a sign-in for nothing.
|
|
371
|
+
const blocked = (0, config_js_1.configWriteProblem)();
|
|
372
|
+
if (blocked) {
|
|
373
|
+
console.error(`\n Error: ${blocked}\n`);
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
const result = await (0, exports.runLoginFlow)({ webUrl, timeoutSec, allowHosts }, deps);
|
|
377
|
+
// Without `pasteNext` there is no Enter-to-paste, so no pasted key either.
|
|
378
|
+
if (!result || !('apiKey' in result)) {
|
|
379
|
+
console.error('\n Error: login did not complete\n');
|
|
380
|
+
return 1;
|
|
381
|
+
}
|
|
382
|
+
console.log(` + Config saved. API key: ${(0, redact_js_1.maskKey)(result.apiKey)}`);
|
|
383
|
+
if (result.hookId)
|
|
384
|
+
console.log(` + Hook: ${result.hookId}`);
|
|
385
|
+
console.log('\n Done! Restart your agents.\n');
|
|
386
|
+
return 0;
|
|
387
|
+
};
|
|
388
|
+
exports.handleLogin = handleLogin;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How every agent launches the Hwip MCP server — one source, three schemas.
|
|
3
|
+
*
|
|
4
|
+
* Registrations used to say `npx -y @hwip/mcp-server`, which left an
|
|
5
|
+
* `npm exec` launcher resident beside the server for the life of the session.
|
|
6
|
+
* `hwip mcp` (see mcp.ts) runs the server in the CLI's own process instead.
|
|
7
|
+
*
|
|
8
|
+
* Cursor/Windsurf, Gemini and OpenCode each want that launch in a different
|
|
9
|
+
* shape, and the shapes drifted apart when each site spelled the command out
|
|
10
|
+
* itself. Everything below is derived from MCP_LAUNCH_ARGV.
|
|
11
|
+
*/
|
|
12
|
+
/** What the agents run. `hwip` resolves out of the same global bin dir as `npx`. */
|
|
13
|
+
export declare const MCP_LAUNCH_ARGV: readonly ["hwip", "mcp"];
|
|
14
|
+
/**
|
|
15
|
+
* `mcpServers.hwip` — what the installer writes for Cursor and Windsurf.
|
|
16
|
+
* The env block is deliberate: both spawn the MCP server from a graphical
|
|
17
|
+
* context that may not inherit shell env, so the key is passed through
|
|
18
|
+
* explicitly rather than left to inheritance.
|
|
19
|
+
*
|
|
20
|
+
* `hwipit/plugin`'s `.mcp.json` holds the same shape but is a HAND-KEPT COPY —
|
|
21
|
+
* it lives in another repo and cannot import this. Change one, change both.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MCP_SERVERS_ENTRY: {
|
|
24
|
+
command: "hwip";
|
|
25
|
+
args: "mcp"[];
|
|
26
|
+
env: {
|
|
27
|
+
HWIP_API_KEY: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* opencode.json's own MCP schema — top-level `mcp` key (not `mcpServers`),
|
|
32
|
+
* `command` as ARRAY (binary + args), `type: "local"`. No `environment` —
|
|
33
|
+
* opencode is a terminal app that inherits shell env, and the MCP server
|
|
34
|
+
* falls back to ~/.hwip/config.json anyway (an unexpanded "${HWIP_API_KEY}"
|
|
35
|
+
* literal would shadow that fallback with junk).
|
|
36
|
+
*/
|
|
37
|
+
export declare const OPENCODE_MCP_ENTRY: {
|
|
38
|
+
type: string;
|
|
39
|
+
command: ("mcp" | "hwip")[];
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
};
|
|
42
|
+
export declare const GEMINI_MCP_ADD: string;
|
|
43
|
+
/** Older gemini CLIs only accept `mcp add <name> -- <command…>`, with no scope flag. */
|
|
44
|
+
export declare const GEMINI_MCP_ADD_LEGACY: string;
|
|
45
|
+
//# sourceMappingURL=mcp-command.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* How every agent launches the Hwip MCP server — one source, three schemas.
|
|
4
|
+
*
|
|
5
|
+
* Registrations used to say `npx -y @hwip/mcp-server`, which left an
|
|
6
|
+
* `npm exec` launcher resident beside the server for the life of the session.
|
|
7
|
+
* `hwip mcp` (see mcp.ts) runs the server in the CLI's own process instead.
|
|
8
|
+
*
|
|
9
|
+
* Cursor/Windsurf, Gemini and OpenCode each want that launch in a different
|
|
10
|
+
* shape, and the shapes drifted apart when each site spelled the command out
|
|
11
|
+
* itself. Everything below is derived from MCP_LAUNCH_ARGV.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.GEMINI_MCP_ADD_LEGACY = exports.GEMINI_MCP_ADD = exports.OPENCODE_MCP_ENTRY = exports.MCP_SERVERS_ENTRY = exports.MCP_LAUNCH_ARGV = void 0;
|
|
15
|
+
/** What the agents run. `hwip` resolves out of the same global bin dir as `npx`. */
|
|
16
|
+
exports.MCP_LAUNCH_ARGV = ['hwip', 'mcp'];
|
|
17
|
+
const [MCP_BIN, ...MCP_SUBCOMMAND] = exports.MCP_LAUNCH_ARGV;
|
|
18
|
+
/**
|
|
19
|
+
* `mcpServers.hwip` — what the installer writes for Cursor and Windsurf.
|
|
20
|
+
* The env block is deliberate: both spawn the MCP server from a graphical
|
|
21
|
+
* context that may not inherit shell env, so the key is passed through
|
|
22
|
+
* explicitly rather than left to inheritance.
|
|
23
|
+
*
|
|
24
|
+
* `hwipit/plugin`'s `.mcp.json` holds the same shape but is a HAND-KEPT COPY —
|
|
25
|
+
* it lives in another repo and cannot import this. Change one, change both.
|
|
26
|
+
*/
|
|
27
|
+
exports.MCP_SERVERS_ENTRY = {
|
|
28
|
+
command: MCP_BIN,
|
|
29
|
+
args: [...MCP_SUBCOMMAND],
|
|
30
|
+
env: { HWIP_API_KEY: '${HWIP_API_KEY}' },
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* opencode.json's own MCP schema — top-level `mcp` key (not `mcpServers`),
|
|
34
|
+
* `command` as ARRAY (binary + args), `type: "local"`. No `environment` —
|
|
35
|
+
* opencode is a terminal app that inherits shell env, and the MCP server
|
|
36
|
+
* falls back to ~/.hwip/config.json anyway (an unexpanded "${HWIP_API_KEY}"
|
|
37
|
+
* literal would shadow that fallback with junk).
|
|
38
|
+
*/
|
|
39
|
+
exports.OPENCODE_MCP_ENTRY = {
|
|
40
|
+
type: 'local',
|
|
41
|
+
command: [...exports.MCP_LAUNCH_ARGV],
|
|
42
|
+
enabled: true,
|
|
43
|
+
};
|
|
44
|
+
// gemini ≥0.26 syntax: `mcp add [-s scope] <name> <command> [args…]`. The
|
|
45
|
+
// server NAME and the BINARY are both "hwip" here, so the doubled word below
|
|
46
|
+
// is correct, not a copy-paste slip. Default scope is "project" (cwd-local
|
|
47
|
+
// .gemini/), so user scope is pinned for a machine-wide install.
|
|
48
|
+
exports.GEMINI_MCP_ADD = `gemini mcp add -s user hwip ${MCP_BIN} -- ${MCP_SUBCOMMAND.join(' ')}`;
|
|
49
|
+
/** Older gemini CLIs only accept `mcp add <name> -- <command…>`, with no scope flag. */
|
|
50
|
+
exports.GEMINI_MCP_ADD_LEGACY = `gemini mcp add hwip -- ${exports.MCP_LAUNCH_ARGV.join(' ')}`;
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hwip mcp` — run the Hwip MCP server inside THIS process.
|
|
3
|
+
*
|
|
4
|
+
* Every registration used to say `npx -y @hwip/mcp-server`, which left an
|
|
5
|
+
* `npm exec` launcher resident for the whole session on top of the server it
|
|
6
|
+
* spawned — two node processes where one does the work. `@hwip/mcp-server`
|
|
7
|
+
* is a dependency of this package now, so agents register `hwip mcp` and get
|
|
8
|
+
* exactly one process.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Importing `@hwip/mcp-server` IS starting it: the package is a bin entry
|
|
12
|
+
* whose module body wires an McpServer to a StdioServerTransport and exports
|
|
13
|
+
* nothing. Verified against 2.3.0 (`dist/index.js` ends in `main().catch(…)`).
|
|
14
|
+
* Injectable so tests don't need the real package resolvable.
|
|
15
|
+
*/
|
|
16
|
+
type McpServerLoader = () => Promise<unknown>;
|
|
17
|
+
export declare const handleMcp: (load?: McpServerLoader) => Promise<number>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=mcp.d.ts.map
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `hwip mcp` — run the Hwip MCP server inside THIS process.
|
|
4
|
+
*
|
|
5
|
+
* Every registration used to say `npx -y @hwip/mcp-server`, which left an
|
|
6
|
+
* `npm exec` launcher resident for the whole session on top of the server it
|
|
7
|
+
* spawned — two node processes where one does the work. `@hwip/mcp-server`
|
|
8
|
+
* is a dependency of this package now, so agents register `hwip mcp` and get
|
|
9
|
+
* exactly one process.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.handleMcp = void 0;
|
|
13
|
+
const importMcpServer = () => import('@hwip/mcp-server');
|
|
14
|
+
const handleMcp = async (load = importMcpServer) => {
|
|
15
|
+
try {
|
|
16
|
+
await load();
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
20
|
+
console.error(`hwip: could not start the MCP server (@hwip/mcp-server): ${reason}`);
|
|
21
|
+
console.error('hwip: reinstall the CLI with `npm i -g @hwip/cli`.');
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
// The server owns this process from here: it holds stdio open and decides
|
|
25
|
+
// when to exit. Resolving would hand cli.ts a code for `process.exit()`,
|
|
26
|
+
// killing the server the line above just started.
|
|
27
|
+
return new Promise(() => { });
|
|
28
|
+
};
|
|
29
|
+
exports.handleMcp = handleMcp;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node versions this CLI runs on: the intersection of what its runtime
|
|
3
|
+
* dependencies declare in `engines` (read from `package-lock.json`, non-dev
|
|
4
|
+
* packages, 2026-10-02): `@inquirer/*` `>=23.5.0 || ^22.13.0 || ^20.17.0`,
|
|
5
|
+
* `mute-stream` `^20.17.0 || >=22.9.0`, `undici` 6 `>=18.17`, the MCP server
|
|
6
|
+
* `>=18.17.0`, the rest lower. `package.json` `engines.node` is this string,
|
|
7
|
+
* and `node-support.test.ts` recomputes the intersection from the lockfile, so
|
|
8
|
+
* a dependency that raises its floor fails a test instead of leaving `engines`
|
|
9
|
+
* untrue again (C80: it said `>=18` for a while after its dependencies needed 20.17).
|
|
10
|
+
*/
|
|
11
|
+
export declare const SUPPORTED_NODE = "^20.17.0 || ^22.13.0 || >=23.5.0";
|
|
12
|
+
/**
|
|
13
|
+
* Whether a version (`v24.15.0` or `24.15.0`) is in `SUPPORTED_NODE`. Anything
|
|
14
|
+
* that is not `X.Y.Z` (with an optional `-prerelease`) is not. A prerelease sorts
|
|
15
|
+
* below its release, as in npm: `20.17.0-rc.1` is under the floor, `20.17.1-rc.1`
|
|
16
|
+
* is over it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const isSupportedNode: (version: string) => boolean;
|
|
19
|
+
//# sourceMappingURL=node-support.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSupportedNode = exports.SUPPORTED_NODE = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The Node versions this CLI runs on: the intersection of what its runtime
|
|
6
|
+
* dependencies declare in `engines` (read from `package-lock.json`, non-dev
|
|
7
|
+
* packages, 2026-10-02): `@inquirer/*` `>=23.5.0 || ^22.13.0 || ^20.17.0`,
|
|
8
|
+
* `mute-stream` `^20.17.0 || >=22.9.0`, `undici` 6 `>=18.17`, the MCP server
|
|
9
|
+
* `>=18.17.0`, the rest lower. `package.json` `engines.node` is this string,
|
|
10
|
+
* and `node-support.test.ts` recomputes the intersection from the lockfile, so
|
|
11
|
+
* a dependency that raises its floor fails a test instead of leaving `engines`
|
|
12
|
+
* untrue again (C80: it said `>=18` for a while after its dependencies needed 20.17).
|
|
13
|
+
*/
|
|
14
|
+
exports.SUPPORTED_NODE = '^20.17.0 || ^22.13.0 || >=23.5.0';
|
|
15
|
+
/** The lowest supported minor of each line whose floor is not its `.0`. */
|
|
16
|
+
const FLOOR_MINOR = { 20: 17, 22: 13, 23: 5 };
|
|
17
|
+
/**
|
|
18
|
+
* Whether a version (`v24.15.0` or `24.15.0`) is in `SUPPORTED_NODE`. Anything
|
|
19
|
+
* that is not `X.Y.Z` (with an optional `-prerelease`) is not. A prerelease sorts
|
|
20
|
+
* below its release, as in npm: `20.17.0-rc.1` is under the floor, `20.17.1-rc.1`
|
|
21
|
+
* is over it.
|
|
22
|
+
*/
|
|
23
|
+
const isSupportedNode = (version) => {
|
|
24
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/.exec(version.trim());
|
|
25
|
+
if (!m)
|
|
26
|
+
return false;
|
|
27
|
+
const [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
28
|
+
const prerelease = m[4] !== undefined;
|
|
29
|
+
if (major >= 24)
|
|
30
|
+
return true;
|
|
31
|
+
const floor = FLOOR_MINOR[major];
|
|
32
|
+
if (floor === undefined)
|
|
33
|
+
return false;
|
|
34
|
+
if (minor !== floor)
|
|
35
|
+
return minor > floor;
|
|
36
|
+
return !prerelease || patch > 0;
|
|
37
|
+
};
|
|
38
|
+
exports.isSupportedNode = isSupportedNode;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The last failed `hwip notify`, kept so a failure is visible after the fact.
|
|
3
|
+
*
|
|
4
|
+
* Every agent hook runs `hwip notify … 2>/dev/null || true`: a hook must never
|
|
5
|
+
* break the session, so the error output is thrown away. A revoked key, a used
|
|
6
|
+
* up quota or a dead network then looked exactly like "quiet mode, nothing to
|
|
7
|
+
* send". `hwip notify` itself writes this file on failure and deletes it on
|
|
8
|
+
* success — the one writer — so the record survives whatever the hook does
|
|
9
|
+
* with stderr. Concurrent hooks race last-writer-wins: a failure that finishes
|
|
10
|
+
* after a newer success stays until the next good push clears it. `hwip verify` and the Claude Code plugin (/hwip-mode, the
|
|
11
|
+
* SessionStart hook) only read it.
|
|
12
|
+
*
|
|
13
|
+
* THE CONTRACT (the plugin reads it with jq and node, so changing it is a
|
|
14
|
+
* cross-repo change — notify-failure.test.ts pins it):
|
|
15
|
+
*
|
|
16
|
+
* path ${XDG_STATE_HOME:-~/.local/state}/hwip/last-notify-failure.json
|
|
17
|
+
* — the same state dir as the mute and push-mode dials (gate.ts
|
|
18
|
+
* stateDir, plugin hooks/gate.sh HWIP_STATE_DIR)
|
|
19
|
+
* body {"v":1,"at":"<ISO 8601 UTC>","kind":"<kind>","status":<int|null>,"code":"<string|null>"}
|
|
20
|
+
* kind no-key | auth | quota | rate-limit | client | server | timeout | network | no-app | unsealed
|
|
21
|
+
*
|
|
22
|
+
* `no-app` (NO_APP_DEVICE) and `unsealed` (E2E_REQUIRED) are refusals made here, before the push went out: they
|
|
23
|
+
* carry no HTTP status, so a reader never shows one as the server's answer.
|
|
24
|
+
*
|
|
25
|
+
* A device whose key changed is not recorded here: the pin that replaced it writes
|
|
26
|
+
* key-changes.json (key-changes.ts), which another process's push cannot clear.
|
|
27
|
+
*
|
|
28
|
+
* No message text and nothing from the request: `status` is the HTTP status
|
|
29
|
+
* and `code` the server's error code (e.g. QUOTA_EXCEEDED), so the file can
|
|
30
|
+
* never carry a key, a title or a body.
|
|
31
|
+
*/
|
|
32
|
+
export declare const NOTIFY_FAILURE_KINDS: readonly ["no-key", "auth", "quota", "rate-limit", "client", "server", "timeout", "network", "no-app", "unsealed"];
|
|
33
|
+
export type NotifyFailureKind = typeof NOTIFY_FAILURE_KINDS[number];
|
|
34
|
+
export interface NotifyFailure {
|
|
35
|
+
readonly v: 1;
|
|
36
|
+
readonly at: string;
|
|
37
|
+
readonly kind: NotifyFailureKind;
|
|
38
|
+
readonly status: number | null;
|
|
39
|
+
readonly code: string | null;
|
|
40
|
+
}
|
|
41
|
+
export declare const notifyFailurePath: () => string;
|
|
42
|
+
/** What went wrong, in the contract's terms. Anything that is not an HwipError
|
|
43
|
+
* never got an HTTP answer — fetch rejects on DNS, refused or reset. The code is read before the
|
|
44
|
+
* status: a refusal made here carries a status (400) the server never sent. */
|
|
45
|
+
export declare const classifyNotifyFailure: (err: unknown) => Pick<NotifyFailure, "kind" | "status" | "code">;
|
|
46
|
+
/** Best effort: a hook must never fail because its failure could not be noted. */
|
|
47
|
+
export declare const recordNotifyFailure: (failure: Pick<NotifyFailure, "kind" | "status" | "code">, now?: Date) => void;
|
|
48
|
+
export declare const clearNotifyFailure: () => void;
|
|
49
|
+
/** Run one push attempt and leave the record matching its outcome. */
|
|
50
|
+
export declare const withNotifyFailureRecord: <T>(send: () => Promise<T>) => Promise<T>;
|
|
51
|
+
/** null when there is no record, or one this version cannot read. */
|
|
52
|
+
export declare const readNotifyFailure: () => NotifyFailure | null;
|
|
53
|
+
/** One line for a person: what, when, and the HTTP status when there was one. */
|
|
54
|
+
export declare const describeNotifyFailure: (f: NotifyFailure) => string;
|
|
55
|
+
/** Transient kinds clear themselves on the next good push; the rest need the user. */
|
|
56
|
+
export declare const isTransientNotifyFailure: (f: NotifyFailure) => boolean;
|
|
57
|
+
//# sourceMappingURL=notify-failure.d.ts.map
|