@hwip/cli 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/NOTICE +11 -0
- package/README.md +1979 -0
- package/dist/agent-list.d.ts +76 -0
- package/dist/agent-list.js +102 -0
- package/dist/agent-read.d.ts +105 -0
- package/dist/agent-read.js +309 -0
- package/dist/agent-rules-fetch.d.ts +51 -0
- package/dist/agent-rules-fetch.js +275 -0
- package/dist/agent-rules.default.d.ts +30 -0
- package/dist/agent-rules.default.js +292 -0
- package/dist/agent-session.d.ts +23 -0
- package/dist/agent-session.js +44 -0
- package/dist/agent-state.d.ts +84 -0
- package/dist/agent-state.js +188 -0
- package/dist/agent-target.d.ts +60 -0
- package/dist/agent-target.js +81 -0
- package/dist/agents.d.ts +31 -0
- package/dist/agents.js +75 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +76 -0
- package/dist/args.d.ts +74 -0
- package/dist/args.js +254 -0
- package/dist/ask-context-attach.d.ts +73 -0
- package/dist/ask-context-attach.js +151 -0
- package/dist/ask-context-screen.d.ts +21 -0
- package/dist/ask-context-screen.js +49 -0
- package/dist/ask-context-switch.d.ts +31 -0
- package/dist/ask-context-switch.js +74 -0
- package/dist/ask-context.d.ts +84 -0
- package/dist/ask-context.js +172 -0
- package/dist/ask-pending.d.ts +47 -0
- package/dist/ask-pending.js +221 -0
- package/dist/ask-seal.d.ts +120 -0
- package/dist/ask-seal.js +179 -0
- package/dist/ask.d.ts +107 -0
- package/dist/ask.js +551 -0
- package/dist/blocked-prompt-seal.d.ts +35 -0
- package/dist/blocked-prompt-seal.js +89 -0
- package/dist/blocked-prompt.d.ts +57 -0
- package/dist/blocked-prompt.js +181 -0
- package/dist/channel.d.ts +67 -0
- package/dist/channel.js +136 -0
- package/dist/check-update.d.ts +28 -0
- package/dist/check-update.js +153 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +813 -0
- package/dist/codex-config.d.ts +24 -0
- package/dist/codex-config.js +169 -0
- package/dist/codex-transcript.d.ts +59 -0
- package/dist/codex-transcript.js +373 -0
- package/dist/command-refusal.d.ts +56 -0
- package/dist/command-refusal.js +127 -0
- package/dist/command-scan.d.ts +46 -0
- package/dist/command-scan.js +154 -0
- package/dist/command-seal.d.ts +57 -0
- package/dist/command-seal.js +88 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +150 -0
- package/dist/config.d.ts +156 -0
- package/dist/config.js +392 -0
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.js +28 -0
- package/dist/crypto.d.ts +278 -0
- package/dist/crypto.js +698 -0
- package/dist/desktop-notify.d.ts +33 -0
- package/dist/desktop-notify.js +49 -0
- package/dist/device-directory.d.ts +36 -0
- package/dist/device-directory.js +97 -0
- package/dist/device-presence.d.ts +17 -0
- package/dist/device-presence.js +62 -0
- package/dist/downloads.d.ts +29 -0
- package/dist/downloads.js +106 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +28 -0
- package/dist/file-crypto.d.ts +94 -0
- package/dist/file-crypto.js +272 -0
- package/dist/file-view-budget.d.ts +15 -0
- package/dist/file-view-budget.js +34 -0
- package/dist/file-view-switch.d.ts +28 -0
- package/dist/file-view-switch.js +122 -0
- package/dist/file-view.d.ts +108 -0
- package/dist/file-view.js +327 -0
- package/dist/gate.d.ts +141 -0
- package/dist/gate.js +336 -0
- package/dist/gemini-transcript.d.ts +32 -0
- package/dist/gemini-transcript.js +189 -0
- package/dist/http-failure.d.ts +98 -0
- package/dist/http-failure.js +349 -0
- package/dist/hwip-core.generated.d.ts +7 -0
- package/dist/hwip-core.generated.js +14 -0
- package/dist/hwip-hook.d.ts +77 -0
- package/dist/hwip-hook.js +340 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/input-sequencer.d.ts +65 -0
- package/dist/input-sequencer.js +142 -0
- package/dist/insecure-url.d.ts +16 -0
- package/dist/insecure-url.js +76 -0
- package/dist/installer.d.ts +96 -0
- package/dist/installer.js +850 -0
- package/dist/inventory-offload.d.ts +49 -0
- package/dist/inventory-offload.js +209 -0
- package/dist/inventory-worker.d.ts +2 -0
- package/dist/inventory-worker.js +27 -0
- package/dist/jsonc-doc.d.ts +106 -0
- package/dist/jsonc-doc.js +354 -0
- package/dist/keep-awake.d.ts +32 -0
- package/dist/keep-awake.js +46 -0
- package/dist/key-changes.d.ts +48 -0
- package/dist/key-changes.js +140 -0
- package/dist/lan-auth.d.ts +140 -0
- package/dist/lan-auth.js +217 -0
- package/dist/lan-endpoint.d.ts +41 -0
- package/dist/lan-endpoint.js +115 -0
- package/dist/lan-receiver.d.ts +122 -0
- package/dist/lan-receiver.js +510 -0
- package/dist/lan-sender.d.ts +76 -0
- package/dist/lan-sender.js +184 -0
- package/dist/lan-transfer.d.ts +36 -0
- package/dist/lan-transfer.js +54 -0
- package/dist/listener-device-id.d.ts +20 -0
- package/dist/listener-device-id.js +133 -0
- package/dist/listener-log.d.ts +4 -0
- package/dist/listener-log.js +23 -0
- package/dist/listener-process.d.ts +200 -0
- package/dist/listener-process.js +470 -0
- package/dist/listener-service.d.ts +225 -0
- package/dist/listener-service.js +682 -0
- package/dist/listener-worktree-cleanup.d.ts +27 -0
- package/dist/listener-worktree-cleanup.js +62 -0
- package/dist/listener.d.ts +1325 -0
- package/dist/listener.js +5680 -0
- package/dist/login-address.d.ts +25 -0
- package/dist/login-address.js +97 -0
- package/dist/login-page.d.ts +29 -0
- package/dist/login-page.js +93 -0
- package/dist/login.d.ts +98 -0
- package/dist/login.js +388 -0
- package/dist/mcp-command.d.ts +45 -0
- package/dist/mcp-command.js +50 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +29 -0
- package/dist/node-support.d.ts +19 -0
- package/dist/node-support.js +38 -0
- package/dist/notify-failure.d.ts +57 -0
- package/dist/notify-failure.js +150 -0
- package/dist/opencode-transcript.d.ts +139 -0
- package/dist/opencode-transcript.js +390 -0
- package/dist/optional-features.d.ts +6 -0
- package/dist/optional-features.js +19 -0
- package/dist/pane-fit.d.ts +40 -0
- package/dist/pane-fit.js +119 -0
- package/dist/peer-key.d.ts +33 -0
- package/dist/peer-key.js +34 -0
- package/dist/pi-transcript.d.ts +26 -0
- package/dist/pi-transcript.js +255 -0
- package/dist/presence.d.ts +14 -0
- package/dist/presence.js +124 -0
- package/dist/private-files.d.ts +51 -0
- package/dist/private-files.js +218 -0
- package/dist/project-switch.d.ts +19 -0
- package/dist/project-switch.js +60 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.js +21 -0
- package/dist/remote-agents.d.ts +410 -0
- package/dist/remote-agents.js +1218 -0
- package/dist/remote-hook.d.ts +12 -0
- package/dist/remote-hook.js +196 -0
- package/dist/replace-file.d.ts +50 -0
- package/dist/replace-file.js +328 -0
- package/dist/schedule-command.d.ts +35 -0
- package/dist/schedule-command.js +260 -0
- package/dist/schedule-launch.d.ts +15 -0
- package/dist/schedule-launch.js +24 -0
- package/dist/schedule-ready.d.ts +44 -0
- package/dist/schedule-ready.js +71 -0
- package/dist/schedule-remote.d.ts +77 -0
- package/dist/schedule-remote.js +167 -0
- package/dist/schedule-runner.d.ts +89 -0
- package/dist/schedule-runner.js +452 -0
- package/dist/schedule.d.ts +149 -0
- package/dist/schedule.js +428 -0
- package/dist/secret-mask.d.ts +29 -0
- package/dist/secret-mask.js +245 -0
- package/dist/self-register.d.ts +45 -0
- package/dist/self-register.js +67 -0
- package/dist/send.d.ts +56 -0
- package/dist/send.js +104 -0
- package/dist/session-diff.d.ts +161 -0
- package/dist/session-diff.js +445 -0
- package/dist/session-registry.d.ts +143 -0
- package/dist/session-registry.js +216 -0
- package/dist/sessions-report.d.ts +59 -0
- package/dist/sessions-report.js +99 -0
- package/dist/shell-audit.d.ts +34 -0
- package/dist/shell-audit.js +47 -0
- package/dist/subagent-transcripts.d.ts +167 -0
- package/dist/subagent-transcripts.js +437 -0
- package/dist/subscriber-trust.d.ts +128 -0
- package/dist/subscriber-trust.js +397 -0
- package/dist/templates.d.ts +105 -0
- package/dist/templates.js +871 -0
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.js +65 -0
- package/dist/tmux-liveness.d.ts +45 -0
- package/dist/tmux-liveness.js +71 -0
- package/dist/tmux-pane.d.ts +55 -0
- package/dist/tmux-pane.js +126 -0
- package/dist/tmux-socket.d.ts +20 -0
- package/dist/tmux-socket.js +292 -0
- package/dist/todo-fold.d.ts +31 -0
- package/dist/todo-fold.js +102 -0
- package/dist/transcript-tail.d.ts +312 -0
- package/dist/transcript-tail.js +718 -0
- package/dist/trust-command.d.ts +49 -0
- package/dist/trust-command.js +200 -0
- package/dist/trust-lock.d.ts +26 -0
- package/dist/trust-lock.js +94 -0
- package/dist/trust-pins.d.ts +37 -0
- package/dist/trust-pins.js +82 -0
- package/dist/turn-recorder-wire.d.ts +70 -0
- package/dist/turn-recorder-wire.js +165 -0
- package/dist/turn-recorder.d.ts +190 -0
- package/dist/turn-recorder.js +695 -0
- package/dist/turn-ring.d.ts +93 -0
- package/dist/turn-ring.js +418 -0
- package/dist/turn-watch.d.ts +177 -0
- package/dist/turn-watch.js +630 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +55 -0
- package/dist/uninstall.js +507 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +421 -0
- package/dist/verify.d.ts +47 -0
- package/dist/verify.js +396 -0
- package/dist/worktree.d.ts +144 -0
- package/dist/worktree.js +310 -0
- package/dist/wrapper.d.ts +254 -0
- package/dist/wrapper.js +704 -0
- package/dist/ws-url.d.ts +109 -0
- package/dist/ws-url.js +202 -0
- package/package.json +78 -10
- package/bin/hwip.js +0 -3
package/dist/config.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VERSION = exports.saveConfig = exports.configWriteProblem = exports.resolveBaseUrl = exports.checkBaseUrl = exports.fileKeyBaseRefusal = exports.keyIsFromFile = exports.configIgnoredBecause = exports.loadConfig = exports.configModeProblem = exports.detectProjectDir = exports.PROJECT_DIR_ENV_VARS = exports.groupOf = exports.checkoutOf = exports.tmuxName = exports.SESSION_SHELL_OPTION = exports.HERDR_SESSION_OPTIONS = exports.SESSION_LABEL_OPTION = exports.SESSION_PROJECT_OPTION = exports.resolveHookId = exports.resolvedEnv = exports.unreadableConfigDefaults = exports.TRUST_FILE = exports.CONFIG_FILE = exports.CONFIG_DIR = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const os_1 = require("os");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const private_files_js_1 = require("./private-files.js");
|
|
9
|
+
const replace_file_js_1 = require("./replace-file.js");
|
|
10
|
+
const ws_url_js_1 = require("./ws-url.js");
|
|
11
|
+
exports.CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.hwip');
|
|
12
|
+
exports.CONFIG_FILE = (0, path_1.join)(exports.CONFIG_DIR, 'config.json');
|
|
13
|
+
/** The listener's subscriber trust store (subscriber-trust.ts), shared with `hwip trust`. */
|
|
14
|
+
exports.TRUST_FILE = (0, path_1.join)(exports.CONFIG_DIR, 'trusted-devices.json');
|
|
15
|
+
exports.unreadableConfigDefaults = { keepAwake: false, turnHistory: false };
|
|
16
|
+
const resolvedEnv = (key, env = process.env) => {
|
|
17
|
+
const val = env[key];
|
|
18
|
+
return val && !val.startsWith('${') ? val : undefined;
|
|
19
|
+
};
|
|
20
|
+
exports.resolvedEnv = resolvedEnv;
|
|
21
|
+
/**
|
|
22
|
+
* The two-way hook id: `HWIP_HOOK_ID` when it carries a real value, else
|
|
23
|
+
* `hookId` from ~/.hwip/config.json — what `hwip setup` writes. The plugin's
|
|
24
|
+
* gate.sh `hwip_hook_id` and the MCP server resolve it in the same order.
|
|
25
|
+
*/
|
|
26
|
+
const resolveHookId = (env = process.env) => (0, exports.resolvedEnv)('HWIP_HOOK_ID', env) || (0, exports.loadConfig)().hookId;
|
|
27
|
+
exports.resolveHookId = resolveHookId;
|
|
28
|
+
/** tmux session options the listener reads before falling back to parsing the name (listener `parseSessionName`). */
|
|
29
|
+
exports.SESSION_PROJECT_OPTION = '@hwip_project';
|
|
30
|
+
exports.SESSION_LABEL_OPTION = '@hwip_session_label';
|
|
31
|
+
/**
|
|
32
|
+
* The herdr pane a session is attached from, as `<herdr env var> → <session
|
|
33
|
+
* option>`. herdr finds agents by a pane's foreground process, which for a
|
|
34
|
+
* wrapped pane is the tmux client, so the plugin's `hwip-herdr.sh` hook and
|
|
35
|
+
* the pi extension (templates `PI_EXTENSION`) report the agent's state
|
|
36
|
+
* themselves, to the pane these options name. They are session options rather
|
|
37
|
+
* than the agent's own env because a `hwip` reattach from another herdr pane
|
|
38
|
+
* (or from outside herdr) must retarget a running agent.
|
|
39
|
+
*/
|
|
40
|
+
exports.HERDR_SESSION_OPTIONS = {
|
|
41
|
+
HERDR_PANE_ID: '@hwip_herdr_pane',
|
|
42
|
+
HERDR_SOCKET_PATH: '@hwip_herdr_socket',
|
|
43
|
+
HERDR_BIN_PATH: '@hwip_herdr_bin',
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Set by `hwip sh` on the session it opens, to that session's own name. The
|
|
47
|
+
* one exception to the listener's shell-pane refusal. Only a process on this
|
|
48
|
+
* machine can set a tmux option, so it says the user opened the shell — but it
|
|
49
|
+
* is not a boundary: the phone can set it through `!` in a Claude Code session,
|
|
50
|
+
* or from a shell that is already marked (README § Defense).
|
|
51
|
+
*/
|
|
52
|
+
exports.SESSION_SHELL_OPTION = '@hwip_shell';
|
|
53
|
+
/** `name` as tmux holds it: tmux turns `.`/`:` into `_`, and `;` would end a tmux command (wrapper `tmuxSessionName`). */
|
|
54
|
+
const tmuxName = (name) => name.replace(/[.:;]/g, '_');
|
|
55
|
+
exports.tmuxName = tmuxName;
|
|
56
|
+
/** `git rev-parse` for `checkoutOf`, swappable in tests. */
|
|
57
|
+
const revParse = (dir) => (0, child_process_1.execFileSync)('git', ['-C', dir, 'rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir', '--git-dir'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 });
|
|
58
|
+
/**
|
|
59
|
+
* Which checkout `dir` is in, from one `git rev-parse`; null outside git.
|
|
60
|
+
*
|
|
61
|
+
* Inside a linked worktree `--show-toplevel` is the worktree itself, so its
|
|
62
|
+
* name is not the repo's. The common dir is `<main checkout>/.git` from every
|
|
63
|
+
* worktree, and differs from the git dir only in a linked one. A common dir not
|
|
64
|
+
* named `.git` (bare-repo layout, submodule, `--separate-git-dir`) has no
|
|
65
|
+
* checkout to be named for, so the toplevel's own name stands and nothing is
|
|
66
|
+
* called linked — those worktrees keep reading as their own projects.
|
|
67
|
+
*
|
|
68
|
+
* Anything but three absolute paths is "outside git": a git too old for
|
|
69
|
+
* `--path-format` echoes it back as an unknown flag and exits 0, which would
|
|
70
|
+
* otherwise shift every answer by one and name the repo `--path-format=absolute`.
|
|
71
|
+
*
|
|
72
|
+
* The timeout is for the listener's sweep worker, which calls this for every
|
|
73
|
+
* new pane cwd: a repo on a hung network mount must cost one sweep a moment,
|
|
74
|
+
* not stop inventory. A timeout reads as "outside git" — nothing is regrouped.
|
|
75
|
+
*/
|
|
76
|
+
const checkoutOf = (dir, run = revParse) => {
|
|
77
|
+
let lines;
|
|
78
|
+
try {
|
|
79
|
+
lines = run(dir).trim().split('\n');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (lines.length !== 3 || !lines.every(path_1.isAbsolute))
|
|
85
|
+
return null;
|
|
86
|
+
const [top, common, gitDir] = lines;
|
|
87
|
+
if ((0, path_1.basename)(common) !== '.git')
|
|
88
|
+
return { key: (0, path_1.basename)(top), linked: false, root: null };
|
|
89
|
+
const root = (0, path_1.dirname)(common);
|
|
90
|
+
const key = (0, path_1.basename)(root);
|
|
91
|
+
return key ? { key, linked: common !== gitDir, root } : null;
|
|
92
|
+
};
|
|
93
|
+
exports.checkoutOf = checkoutOf;
|
|
94
|
+
/**
|
|
95
|
+
* The project and label a `hwip-<tail>` session groups under, when its name
|
|
96
|
+
* does not say so — null when it does (the whole tail is the project).
|
|
97
|
+
*
|
|
98
|
+
* The one rule behind both the wrapper's session options and the listener's
|
|
99
|
+
* correction of sessions started without them, so the two never disagree.
|
|
100
|
+
* A tail starting `<repo>-` splits there, unless the rest is the wrapper's
|
|
101
|
+
* `-2`/`-3` family counter. Any session in a linked worktree belongs to its
|
|
102
|
+
* repo, whatever the worktree is called.
|
|
103
|
+
*/
|
|
104
|
+
const groupOf = (tail, checkout) => {
|
|
105
|
+
if (!checkout)
|
|
106
|
+
return null;
|
|
107
|
+
const { key, linked } = checkout;
|
|
108
|
+
// The tail is a tmux name, the key a directory name.
|
|
109
|
+
const named = (0, exports.tmuxName)(key);
|
|
110
|
+
const rest = tail.startsWith(`${named}-`) ? tail.slice(named.length + 1) : '';
|
|
111
|
+
if (rest && !/^\d+$/.test(rest))
|
|
112
|
+
return { project: key, label: rest };
|
|
113
|
+
return linked ? { project: key, label: tail } : null;
|
|
114
|
+
};
|
|
115
|
+
exports.groupOf = groupOf;
|
|
116
|
+
// Per-agent project-dir env vars, in precedence order. Deliberately NOT part
|
|
117
|
+
// of the remote-agent registry: Cursor/Windsurf carry project-dir envs but
|
|
118
|
+
// are not remote-controllable via tmux — the two tables have different
|
|
119
|
+
// membership.
|
|
120
|
+
exports.PROJECT_DIR_ENV_VARS = ['CLAUDE_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'WINDSURF_PROJECT_DIR'];
|
|
121
|
+
/** First set project-dir env (unresolved `${VAR}` placeholders ignored), else cwd. */
|
|
122
|
+
const detectProjectDir = () => {
|
|
123
|
+
for (const key of exports.PROJECT_DIR_ENV_VARS) {
|
|
124
|
+
const val = (0, exports.resolvedEnv)(key);
|
|
125
|
+
if (val)
|
|
126
|
+
return val;
|
|
127
|
+
}
|
|
128
|
+
return process.cwd();
|
|
129
|
+
};
|
|
130
|
+
exports.detectProjectDir = detectProjectDir;
|
|
131
|
+
let warnedAboutBrokenConfig = false;
|
|
132
|
+
let warnedAboutConfigFile = false;
|
|
133
|
+
/**
|
|
134
|
+
* An absent config is normal — plenty of commands run on `--key` alone, so it
|
|
135
|
+
* stays silent. A config that exists but does not parse is not normal, and used
|
|
136
|
+
* to be indistinguishable from absent: every value silently became undefined,
|
|
137
|
+
* so `hwip` behaved as if the file had never been written. That is precisely
|
|
138
|
+
* how "it isn't reading my config" looks from the outside, with nothing
|
|
139
|
+
* anywhere to say otherwise. Say it, on stderr, and carry on with defaults.
|
|
140
|
+
*/
|
|
141
|
+
/** What a reader of a refused config.json can still do; a writer (install, login) is not helped by it. */
|
|
142
|
+
const ENV_STILL_WORKS = 'HWIP_API_KEY and --key still work';
|
|
143
|
+
/**
|
|
144
|
+
* Why this machine should not trust `~/.hwip/config.json` as it stands, or null.
|
|
145
|
+
* The file holds the API key AND says where the key is sent (`baseUrl`, `wsUrl`),
|
|
146
|
+
* so one that another user owns, or that anyone can write, is a way to redirect
|
|
147
|
+
* the key. `refuse`: ignore the file (and never write over it); otherwise only
|
|
148
|
+
* warn and restrict it. Group access to a file the user owns is not refused: a
|
|
149
|
+
* user-private group (umask 002, a dotfiles checkout) makes every file 664, and
|
|
150
|
+
* ignoring those would stop every command. Root may read what a user owns (sudo).
|
|
151
|
+
*/
|
|
152
|
+
const configModeProblem = (st, myUid, file = exports.CONFIG_FILE) => {
|
|
153
|
+
const mode = (st.mode & 0o777).toString(8);
|
|
154
|
+
const envStillWorks = ENV_STILL_WORKS;
|
|
155
|
+
if (st.uid !== myUid && myUid !== 0) {
|
|
156
|
+
return { refuse: true, why: `${file} belongs to another user (uid ${st.uid}), so it is being ignored (${envStillWorks})` };
|
|
157
|
+
}
|
|
158
|
+
if (st.mode & 0o002) {
|
|
159
|
+
return { refuse: true, why: `${file} can be written by anyone (mode ${mode}), so it is being ignored — run \`chmod 600 ${file}\` (${envStillWorks})` };
|
|
160
|
+
}
|
|
161
|
+
if (st.mode & 0o064) {
|
|
162
|
+
return { refuse: false, why: `${file} can be reached by other users (mode ${mode}) and holds your API key — it was restricted to 0600` };
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
};
|
|
166
|
+
exports.configModeProblem = configModeProblem;
|
|
167
|
+
/** `configModeProblem` for the file as it is now. A symlinked file (dotfiles) is judged by what it points at. Windows has no owner or mode to read. */
|
|
168
|
+
const configFileProblem = () => {
|
|
169
|
+
if (process.platform === 'win32' || typeof process.getuid !== 'function')
|
|
170
|
+
return null;
|
|
171
|
+
try {
|
|
172
|
+
return (0, exports.configModeProblem)((0, fs_1.statSync)(exports.CONFIG_FILE), process.getuid());
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const FIX_OR_MOVE = 'fix it, or move it aside and run the command again';
|
|
179
|
+
/**
|
|
180
|
+
* Where a parse error is, as " at line 3 column 3", or nothing when V8 does not say. Never its message:
|
|
181
|
+
* that quotes the text around the error ("Unexpected token 'a', …"apiKey": ak_live_ab"…"), and this file
|
|
182
|
+
* holds the API key. The line and column are worked out from the position here, since the message only
|
|
183
|
+
* has the position on some Node versions.
|
|
184
|
+
*/
|
|
185
|
+
// Anchored to V8's own ending: its quoted snippet of the file comes before it and could hold "position 9" too.
|
|
186
|
+
const jsonErrorPlace = (err, text) => {
|
|
187
|
+
const position = err instanceof Error ? / in JSON at position (\d+)(?: \(line \d+ column \d+\))?$/.exec(err.message)?.[1] : undefined;
|
|
188
|
+
if (position === undefined)
|
|
189
|
+
return '';
|
|
190
|
+
const before = text.slice(0, Number(position));
|
|
191
|
+
const line = before.split('\n').length;
|
|
192
|
+
const column = Number(position) - (before.lastIndexOf('\n') + 1) + 1;
|
|
193
|
+
return ` at line ${line} column ${column}`;
|
|
194
|
+
};
|
|
195
|
+
const inspectConfigFile = () => {
|
|
196
|
+
let text;
|
|
197
|
+
try {
|
|
198
|
+
text = (0, replace_file_js_1.readTextIfExists)(exports.CONFIG_FILE);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
if (err instanceof replace_file_js_1.NotUtf8Error)
|
|
202
|
+
return { kind: 'broken', why: `${exports.CONFIG_FILE} is not valid UTF-8 text` };
|
|
203
|
+
// The code, not the message: it is for a person to find the file, and says nothing of what is in it.
|
|
204
|
+
const code = err instanceof Error && 'code' in err && typeof err.code === 'string' ? ` (${err.code})` : '';
|
|
205
|
+
return { kind: 'broken', why: `${exports.CONFIG_FILE} cannot be read${code}` };
|
|
206
|
+
}
|
|
207
|
+
if (text === null)
|
|
208
|
+
return { kind: 'absent' };
|
|
209
|
+
// Nothing in it to lose, and what a crash in the old plain write left behind.
|
|
210
|
+
if (text.trim() === '')
|
|
211
|
+
return { kind: 'ok', text, config: {} };
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(text);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
if (text.charCodeAt(0) === 0xfeff)
|
|
218
|
+
return { kind: 'broken', why: `${exports.CONFIG_FILE} starts with a byte order mark (save it as UTF-8 without BOM)` };
|
|
219
|
+
return { kind: 'broken', why: `${exports.CONFIG_FILE} is not valid JSON${jsonErrorPlace(err, text)}` };
|
|
220
|
+
}
|
|
221
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
222
|
+
return { kind: 'broken', why: `${exports.CONFIG_FILE} is not a JSON object` };
|
|
223
|
+
}
|
|
224
|
+
return { kind: 'ok', text, config: parsed };
|
|
225
|
+
};
|
|
226
|
+
const loadConfig = () => {
|
|
227
|
+
// Absent is normal and silent. One that exists but cannot be read is not, and
|
|
228
|
+
// used to be indistinguishable from absent — "it isn't reading my config",
|
|
229
|
+
// with nothing to say otherwise: say it, on stderr, and carry on with defaults.
|
|
230
|
+
// Nothing writes over it meanwhile (`saveConfig`).
|
|
231
|
+
const file = inspectConfigFile();
|
|
232
|
+
if (file.kind === 'absent')
|
|
233
|
+
return {};
|
|
234
|
+
const problem = configFileProblem();
|
|
235
|
+
if (problem) {
|
|
236
|
+
if (!warnedAboutConfigFile) {
|
|
237
|
+
warnedAboutConfigFile = true;
|
|
238
|
+
console.error(`hwip: ${problem.why}.`);
|
|
239
|
+
}
|
|
240
|
+
if (problem.refuse)
|
|
241
|
+
return { ...exports.unreadableConfigDefaults };
|
|
242
|
+
try {
|
|
243
|
+
(0, fs_1.chmodSync)(exports.CONFIG_FILE, 0o600);
|
|
244
|
+
}
|
|
245
|
+
catch { /* not ours to change — the warning stands */ }
|
|
246
|
+
}
|
|
247
|
+
if (file.kind === 'broken') {
|
|
248
|
+
// Once per process: a single command can call loadConfig several times
|
|
249
|
+
// (handleVerify and resolveHookId both do), and repeating the same two
|
|
250
|
+
// lines four times reads like four separate problems.
|
|
251
|
+
if (!warnedAboutBrokenConfig) {
|
|
252
|
+
warnedAboutBrokenConfig = true;
|
|
253
|
+
console.error(`hwip: ${file.why}, so it is being ignored.`);
|
|
254
|
+
console.error('hwip: fix it, or move it aside — hwip will not write over it.');
|
|
255
|
+
}
|
|
256
|
+
return { ...exports.unreadableConfigDefaults };
|
|
257
|
+
}
|
|
258
|
+
return file.config;
|
|
259
|
+
};
|
|
260
|
+
exports.loadConfig = loadConfig;
|
|
261
|
+
/** Why `config.json` is being ignored right now (it does not parse, or its mode refuses it), or null. For a log line or `hwip verify`. */
|
|
262
|
+
const configIgnoredBecause = () => {
|
|
263
|
+
const problem = configFileProblem();
|
|
264
|
+
if (problem?.refuse)
|
|
265
|
+
return problem.why.replace(` (${ENV_STILL_WORKS})`, '');
|
|
266
|
+
const file = inspectConfigFile();
|
|
267
|
+
return file.kind === 'broken' ? file.why : null;
|
|
268
|
+
};
|
|
269
|
+
exports.configIgnoredBecause = configIgnoredBecause;
|
|
270
|
+
/** The key about to be used is the config file's: none came by flag (`flagKey`) or `HWIP_API_KEY`. */
|
|
271
|
+
const keyIsFromFile = (flagKey, config) => !(typeof flagKey === 'string' && flagKey) && !(0, exports.resolvedEnv)('HWIP_API_KEY') && Boolean(config.apiKey);
|
|
272
|
+
exports.keyIsFromFile = keyIsFromFile;
|
|
273
|
+
/**
|
|
274
|
+
* Why the config file's key must not go to `envBase`, an address from `HWIP_BASE_URL`, or null. The key belongs to
|
|
275
|
+
* the file's `baseUrl`, else the default API; only the same origin may carry it (C129).
|
|
276
|
+
*/
|
|
277
|
+
const fileKeyBaseRefusal = (envBase, config) => {
|
|
278
|
+
// `"baseUrl": true` is no URL: where the key belongs is unknown, so it is not sent anywhere.
|
|
279
|
+
if (config.baseUrl !== undefined && typeof config.baseUrl !== 'string') {
|
|
280
|
+
return `${exports.CONFIG_FILE} has a "baseUrl" that is not text, so where the key goes is unknown — fix it, or pass --base-url`;
|
|
281
|
+
}
|
|
282
|
+
const home = config.baseUrl || ws_url_js_1.DEFAULT_API_BASE;
|
|
283
|
+
const target = (0, ws_url_js_1.originOf)(envBase);
|
|
284
|
+
// An address with no origin (not a URL, `data:`) is not the same as anything, another such one included.
|
|
285
|
+
if (target !== null && target === (0, ws_url_js_1.originOf)(home))
|
|
286
|
+
return null;
|
|
287
|
+
return `HWIP_BASE_URL points at ${target ?? 'something that is not a URL'}, but the API key in ${exports.CONFIG_FILE} belongs to ` +
|
|
288
|
+
`${(0, ws_url_js_1.originOf)(home) ?? 'an address it cannot read'}, so it is not sent there — set HWIP_API_KEY as well, pass --base-url, or unset HWIP_BASE_URL`;
|
|
289
|
+
};
|
|
290
|
+
exports.fileKeyBaseRefusal = fileKeyBaseRefusal;
|
|
291
|
+
/**
|
|
292
|
+
* `url`, unless it has a user name or password in it: `fetch` refuses such an address on every call, and quotes
|
|
293
|
+
* it whole when it does (C152). Said here once, by where the address came from (`from`) and never by value.
|
|
294
|
+
*/
|
|
295
|
+
const withoutCredentials = (url, from) => {
|
|
296
|
+
if ((0, ws_url_js_1.carriesCredentials)(url))
|
|
297
|
+
throw new Error(`${from} has a user name or password in it, and no request can be made to such an address — take it out`);
|
|
298
|
+
return url;
|
|
299
|
+
};
|
|
300
|
+
/**
|
|
301
|
+
* `url` as a base URL from `from`, before it is saved or sent anywhere: the same checks
|
|
302
|
+
* `resolveBaseUrl` applies (no user name or password in it, and an address at all).
|
|
303
|
+
*/
|
|
304
|
+
const checkBaseUrl = (url, from) => {
|
|
305
|
+
try {
|
|
306
|
+
void new URL(url);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
throw new Error(`${from} is not a valid address — fix it, or pass --base-url (or set HWIP_BASE_URL)`);
|
|
310
|
+
}
|
|
311
|
+
return withoutCredentials(url, from);
|
|
312
|
+
};
|
|
313
|
+
exports.checkBaseUrl = checkBaseUrl;
|
|
314
|
+
/**
|
|
315
|
+
* Where the key goes: the flag, `HWIP_BASE_URL`, the file, else the default API. A file that
|
|
316
|
+
* cannot be read may hold a base URL the key belongs to (staging, self-hosted); falling to the
|
|
317
|
+
* default would send the key to another server, so that stops here instead. A file ignored for
|
|
318
|
+
* its mode is not trusted for a base URL on purpose, and the default stands.
|
|
319
|
+
*
|
|
320
|
+
* `keyFromFile` (see `keyIsFromFile`): the config file's key belongs to the addresses the file says (its
|
|
321
|
+
* `baseUrl`, else the default API), so an address from `HWIP_BASE_URL` on another origin stops here instead of
|
|
322
|
+
* receiving it (C129). A flag is the user's own word on this command and is followed; so is the environment's
|
|
323
|
+
* address when the key came by flag or `HWIP_API_KEY` too.
|
|
324
|
+
*/
|
|
325
|
+
const resolveBaseUrl = (flagValue, config, keyFromFile = false) => {
|
|
326
|
+
// A bare flag parses as `true`, which is no URL.
|
|
327
|
+
const fromFlag = typeof flagValue === 'string' ? flagValue : '';
|
|
328
|
+
const fromEnv = fromFlag ? undefined : (0, exports.resolvedEnv)('HWIP_BASE_URL');
|
|
329
|
+
if (fromEnv && keyFromFile) {
|
|
330
|
+
const refusal = (0, exports.fileKeyBaseRefusal)(fromEnv, config);
|
|
331
|
+
if (refusal)
|
|
332
|
+
throw new Error(refusal);
|
|
333
|
+
}
|
|
334
|
+
if (fromFlag)
|
|
335
|
+
return withoutCredentials(fromFlag, '--base-url');
|
|
336
|
+
if (fromEnv)
|
|
337
|
+
return withoutCredentials(fromEnv, 'HWIP_BASE_URL');
|
|
338
|
+
// `"baseUrl": true` is no URL either, and `.replace` on it would crash the caller.
|
|
339
|
+
if (config.baseUrl !== undefined && typeof config.baseUrl !== 'string') {
|
|
340
|
+
throw new Error(`${exports.CONFIG_FILE} has a "baseUrl" that is not text, so where the key goes is unknown — fix it, or pass --base-url (or set HWIP_BASE_URL)`);
|
|
341
|
+
}
|
|
342
|
+
if (config.baseUrl)
|
|
343
|
+
return withoutCredentials(config.baseUrl, `"baseUrl" in ${exports.CONFIG_FILE}`);
|
|
344
|
+
const file = inspectConfigFile();
|
|
345
|
+
if (file.kind === 'broken' && !configFileProblem()?.refuse) {
|
|
346
|
+
throw new Error(`${file.why}, so the API base URL it may hold is unknown — fix it, or move it aside, or pass --base-url (or set HWIP_BASE_URL)`);
|
|
347
|
+
}
|
|
348
|
+
return ws_url_js_1.DEFAULT_API_BASE;
|
|
349
|
+
};
|
|
350
|
+
exports.resolveBaseUrl = resolveBaseUrl;
|
|
351
|
+
/**
|
|
352
|
+
* Why `config.json` must not be written over as it stands, or null. A file that
|
|
353
|
+
* is ignored, or that cannot be read as a JSON object, holds values a rewrite
|
|
354
|
+
* built from `{}` would drop (baseUrl, wsUrl, hookId, keepAwake, turnHistory).
|
|
355
|
+
*/
|
|
356
|
+
const writeBlock = (file) => {
|
|
357
|
+
const problem = configFileProblem();
|
|
358
|
+
if (problem?.refuse)
|
|
359
|
+
return `not saving: ${problem.why.replace(` (${ENV_STILL_WORKS})`, '')}`;
|
|
360
|
+
// `replaceFile` refuses too, but only once a login or install has already started: say it first.
|
|
361
|
+
if (file.kind === 'ok') {
|
|
362
|
+
try {
|
|
363
|
+
(0, fs_1.accessSync)(exports.CONFIG_FILE, fs_1.constants.W_OK);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return `not saving: ${exports.CONFIG_FILE} is read-only, so it is left as it is — chmod u+w it to let hwip save`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return file.kind === 'broken' ? `not saving: ${file.why}, so it is not being overwritten — ${FIX_OR_MOVE}` : null;
|
|
370
|
+
};
|
|
371
|
+
/** `writeBlock` for the file as it is now. `install` and `login` ask first, so nothing is signed in to or verified for a save that cannot happen. */
|
|
372
|
+
const configWriteProblem = () => writeBlock(inspectConfigFile());
|
|
373
|
+
exports.configWriteProblem = configWriteProblem;
|
|
374
|
+
const saveConfig = (config) => {
|
|
375
|
+
const file = inspectConfigFile();
|
|
376
|
+
const blocked = writeBlock(file);
|
|
377
|
+
if (blocked)
|
|
378
|
+
throw new Error(blocked);
|
|
379
|
+
(0, private_files_js_1.ensurePrivateDir)(exports.CONFIG_DIR);
|
|
380
|
+
// The file is replaced, not edited: `expected` turns a change by another process since the read above into an error, not a lost update.
|
|
381
|
+
(0, replace_file_js_1.replaceFile)(exports.CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', file.kind === 'ok' ? file.text : null, 0o600);
|
|
382
|
+
};
|
|
383
|
+
exports.saveConfig = saveConfig;
|
|
384
|
+
exports.VERSION = (() => {
|
|
385
|
+
try {
|
|
386
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'package.json'), 'utf-8'));
|
|
387
|
+
return pkg.version;
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return '0.0.0';
|
|
391
|
+
}
|
|
392
|
+
})();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice one `### ` section out of a generated rule core (hwip-core.generated.ts).
|
|
3
|
+
*
|
|
4
|
+
* The core is one string per audience, assembled upstream from
|
|
5
|
+
* plugin/docs/CORE_RULES.md. Two consumers need single sections of it:
|
|
6
|
+
* templates.ts, which builds the NORMAL-only rule file for agents whose
|
|
7
|
+
* prompt-submit hook can announce REMOTE, and remote-hook.ts, which injects
|
|
8
|
+
* the REMOTE sections on the turn that enters it — the twin of the plugin's
|
|
9
|
+
* hwip-remote.sh reading CORE_RULES.md on entry.
|
|
10
|
+
*
|
|
11
|
+
* Headings are matched by title prefix (`Sticky REMOTE mode`), not by the
|
|
12
|
+
* `(Rule N)` suffix: the extractor renumbers per audience, so the number is
|
|
13
|
+
* not stable across cores. A heading the core does not carry throws — every
|
|
14
|
+
* caller passes a literal, so the throw surfaces at import time in the tests
|
|
15
|
+
* rather than as a silently empty rule file.
|
|
16
|
+
*/
|
|
17
|
+
export declare const coreSection: (core: string, heading: string) => string;
|
|
18
|
+
//# sourceMappingURL=core-sections.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.coreSection = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Slice one `### ` section out of a generated rule core (hwip-core.generated.ts).
|
|
6
|
+
*
|
|
7
|
+
* The core is one string per audience, assembled upstream from
|
|
8
|
+
* plugin/docs/CORE_RULES.md. Two consumers need single sections of it:
|
|
9
|
+
* templates.ts, which builds the NORMAL-only rule file for agents whose
|
|
10
|
+
* prompt-submit hook can announce REMOTE, and remote-hook.ts, which injects
|
|
11
|
+
* the REMOTE sections on the turn that enters it — the twin of the plugin's
|
|
12
|
+
* hwip-remote.sh reading CORE_RULES.md on entry.
|
|
13
|
+
*
|
|
14
|
+
* Headings are matched by title prefix (`Sticky REMOTE mode`), not by the
|
|
15
|
+
* `(Rule N)` suffix: the extractor renumbers per audience, so the number is
|
|
16
|
+
* not stable across cores. A heading the core does not carry throws — every
|
|
17
|
+
* caller passes a literal, so the throw surfaces at import time in the tests
|
|
18
|
+
* rather than as a silently empty rule file.
|
|
19
|
+
*/
|
|
20
|
+
const coreSection = (core, heading) => {
|
|
21
|
+
const sections = core.split(/^(?=### )/m).map((s) => s.trim());
|
|
22
|
+
const found = sections.filter((s) => s.startsWith(`### ${heading}`));
|
|
23
|
+
if (found.length !== 1) {
|
|
24
|
+
throw new Error(`core section "${heading}" ${found.length ? 'is ambiguous' : 'not found'} in the generated core`);
|
|
25
|
+
}
|
|
26
|
+
return found[0];
|
|
27
|
+
};
|
|
28
|
+
exports.coreSection = coreSection;
|