@hwip/cli 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/NOTICE +11 -0
- package/README.md +1983 -0
- package/dist/agent-list.d.ts +76 -0
- package/dist/agent-list.js +102 -0
- package/dist/agent-read.d.ts +105 -0
- package/dist/agent-read.js +309 -0
- package/dist/agent-rules-fetch.d.ts +51 -0
- package/dist/agent-rules-fetch.js +275 -0
- package/dist/agent-rules.default.d.ts +30 -0
- package/dist/agent-rules.default.js +305 -0
- package/dist/agent-session.d.ts +23 -0
- package/dist/agent-session.js +44 -0
- package/dist/agent-state.d.ts +84 -0
- package/dist/agent-state.js +188 -0
- package/dist/agent-target.d.ts +60 -0
- package/dist/agent-target.js +81 -0
- package/dist/agents.d.ts +31 -0
- package/dist/agents.js +75 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +76 -0
- package/dist/args.d.ts +74 -0
- package/dist/args.js +254 -0
- package/dist/ask-context-attach.d.ts +73 -0
- package/dist/ask-context-attach.js +151 -0
- package/dist/ask-context-screen.d.ts +21 -0
- package/dist/ask-context-screen.js +49 -0
- package/dist/ask-context-switch.d.ts +31 -0
- package/dist/ask-context-switch.js +74 -0
- package/dist/ask-context.d.ts +84 -0
- package/dist/ask-context.js +172 -0
- package/dist/ask-pending.d.ts +47 -0
- package/dist/ask-pending.js +221 -0
- package/dist/ask-seal.d.ts +120 -0
- package/dist/ask-seal.js +179 -0
- package/dist/ask.d.ts +107 -0
- package/dist/ask.js +551 -0
- package/dist/blocked-prompt-seal.d.ts +39 -0
- package/dist/blocked-prompt-seal.js +92 -0
- package/dist/blocked-prompt.d.ts +62 -0
- package/dist/blocked-prompt.js +518 -0
- package/dist/channel.d.ts +67 -0
- package/dist/channel.js +136 -0
- package/dist/check-update.d.ts +28 -0
- package/dist/check-update.js +153 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +813 -0
- package/dist/codex-config.d.ts +24 -0
- package/dist/codex-config.js +169 -0
- package/dist/codex-transcript.d.ts +59 -0
- package/dist/codex-transcript.js +373 -0
- package/dist/command-refusal.d.ts +56 -0
- package/dist/command-refusal.js +127 -0
- package/dist/command-scan.d.ts +46 -0
- package/dist/command-scan.js +154 -0
- package/dist/command-seal.d.ts +57 -0
- package/dist/command-seal.js +88 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +150 -0
- package/dist/config.d.ts +156 -0
- package/dist/config.js +392 -0
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.js +28 -0
- package/dist/crypto.d.ts +278 -0
- package/dist/crypto.js +698 -0
- package/dist/desktop-notify.d.ts +33 -0
- package/dist/desktop-notify.js +49 -0
- package/dist/device-directory.d.ts +36 -0
- package/dist/device-directory.js +97 -0
- package/dist/device-presence.d.ts +17 -0
- package/dist/device-presence.js +62 -0
- package/dist/downloads.d.ts +29 -0
- package/dist/downloads.js +106 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +28 -0
- package/dist/file-crypto.d.ts +94 -0
- package/dist/file-crypto.js +278 -0
- package/dist/file-view-budget.d.ts +15 -0
- package/dist/file-view-budget.js +34 -0
- package/dist/file-view-switch.d.ts +28 -0
- package/dist/file-view-switch.js +122 -0
- package/dist/file-view.d.ts +108 -0
- package/dist/file-view.js +327 -0
- package/dist/gate.d.ts +141 -0
- package/dist/gate.js +336 -0
- package/dist/gemini-transcript.d.ts +32 -0
- package/dist/gemini-transcript.js +189 -0
- package/dist/http-failure.d.ts +98 -0
- package/dist/http-failure.js +349 -0
- package/dist/hwip-core.generated.d.ts +7 -0
- package/dist/hwip-core.generated.js +14 -0
- package/dist/hwip-hook.d.ts +77 -0
- package/dist/hwip-hook.js +340 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/input-sequencer.d.ts +65 -0
- package/dist/input-sequencer.js +142 -0
- package/dist/insecure-url.d.ts +16 -0
- package/dist/insecure-url.js +76 -0
- package/dist/installer.d.ts +96 -0
- package/dist/installer.js +850 -0
- package/dist/inventory-offload.d.ts +49 -0
- package/dist/inventory-offload.js +209 -0
- package/dist/inventory-worker.d.ts +2 -0
- package/dist/inventory-worker.js +27 -0
- package/dist/jsonc-doc.d.ts +106 -0
- package/dist/jsonc-doc.js +354 -0
- package/dist/keep-awake.d.ts +32 -0
- package/dist/keep-awake.js +46 -0
- package/dist/key-changes.d.ts +48 -0
- package/dist/key-changes.js +140 -0
- package/dist/lan-auth.d.ts +140 -0
- package/dist/lan-auth.js +217 -0
- package/dist/lan-endpoint.d.ts +41 -0
- package/dist/lan-endpoint.js +115 -0
- package/dist/lan-receiver.d.ts +122 -0
- package/dist/lan-receiver.js +510 -0
- package/dist/lan-sender.d.ts +76 -0
- package/dist/lan-sender.js +184 -0
- package/dist/lan-transfer.d.ts +36 -0
- package/dist/lan-transfer.js +54 -0
- package/dist/listener-device-id.d.ts +20 -0
- package/dist/listener-device-id.js +133 -0
- package/dist/listener-log.d.ts +4 -0
- package/dist/listener-log.js +23 -0
- package/dist/listener-process.d.ts +200 -0
- package/dist/listener-process.js +470 -0
- package/dist/listener-service.d.ts +225 -0
- package/dist/listener-service.js +682 -0
- package/dist/listener-worktree-cleanup.d.ts +27 -0
- package/dist/listener-worktree-cleanup.js +62 -0
- package/dist/listener.d.ts +1325 -0
- package/dist/listener.js +5681 -0
- package/dist/login-address.d.ts +25 -0
- package/dist/login-address.js +97 -0
- package/dist/login-page.d.ts +29 -0
- package/dist/login-page.js +93 -0
- package/dist/login.d.ts +98 -0
- package/dist/login.js +388 -0
- package/dist/mcp-command.d.ts +45 -0
- package/dist/mcp-command.js +50 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +29 -0
- package/dist/node-support.d.ts +19 -0
- package/dist/node-support.js +38 -0
- package/dist/notify-failure.d.ts +57 -0
- package/dist/notify-failure.js +150 -0
- package/dist/opencode-transcript.d.ts +139 -0
- package/dist/opencode-transcript.js +390 -0
- package/dist/optional-features.d.ts +6 -0
- package/dist/optional-features.js +19 -0
- package/dist/pane-fit.d.ts +40 -0
- package/dist/pane-fit.js +119 -0
- package/dist/peer-key.d.ts +33 -0
- package/dist/peer-key.js +34 -0
- package/dist/pi-transcript.d.ts +26 -0
- package/dist/pi-transcript.js +255 -0
- package/dist/presence.d.ts +14 -0
- package/dist/presence.js +124 -0
- package/dist/private-files.d.ts +51 -0
- package/dist/private-files.js +218 -0
- package/dist/project-switch.d.ts +19 -0
- package/dist/project-switch.js +60 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.js +21 -0
- package/dist/remote-agents.d.ts +410 -0
- package/dist/remote-agents.js +1218 -0
- package/dist/remote-hook.d.ts +12 -0
- package/dist/remote-hook.js +196 -0
- package/dist/replace-file.d.ts +50 -0
- package/dist/replace-file.js +328 -0
- package/dist/schedule-command.d.ts +35 -0
- package/dist/schedule-command.js +260 -0
- package/dist/schedule-launch.d.ts +15 -0
- package/dist/schedule-launch.js +24 -0
- package/dist/schedule-ready.d.ts +44 -0
- package/dist/schedule-ready.js +71 -0
- package/dist/schedule-remote.d.ts +77 -0
- package/dist/schedule-remote.js +167 -0
- package/dist/schedule-runner.d.ts +89 -0
- package/dist/schedule-runner.js +452 -0
- package/dist/schedule.d.ts +149 -0
- package/dist/schedule.js +428 -0
- package/dist/secret-mask.d.ts +29 -0
- package/dist/secret-mask.js +245 -0
- package/dist/self-register.d.ts +45 -0
- package/dist/self-register.js +67 -0
- package/dist/send.d.ts +56 -0
- package/dist/send.js +104 -0
- package/dist/session-diff.d.ts +161 -0
- package/dist/session-diff.js +445 -0
- package/dist/session-registry.d.ts +143 -0
- package/dist/session-registry.js +216 -0
- package/dist/sessions-report.d.ts +59 -0
- package/dist/sessions-report.js +99 -0
- package/dist/shell-audit.d.ts +34 -0
- package/dist/shell-audit.js +47 -0
- package/dist/subagent-transcripts.d.ts +167 -0
- package/dist/subagent-transcripts.js +437 -0
- package/dist/subscriber-trust.d.ts +128 -0
- package/dist/subscriber-trust.js +397 -0
- package/dist/templates.d.ts +105 -0
- package/dist/templates.js +871 -0
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.js +65 -0
- package/dist/tmux-liveness.d.ts +45 -0
- package/dist/tmux-liveness.js +71 -0
- package/dist/tmux-pane.d.ts +55 -0
- package/dist/tmux-pane.js +126 -0
- package/dist/tmux-socket.d.ts +20 -0
- package/dist/tmux-socket.js +292 -0
- package/dist/todo-fold.d.ts +31 -0
- package/dist/todo-fold.js +102 -0
- package/dist/transcript-tail.d.ts +312 -0
- package/dist/transcript-tail.js +718 -0
- package/dist/trust-command.d.ts +49 -0
- package/dist/trust-command.js +200 -0
- package/dist/trust-lock.d.ts +26 -0
- package/dist/trust-lock.js +94 -0
- package/dist/trust-pins.d.ts +37 -0
- package/dist/trust-pins.js +82 -0
- package/dist/turn-recorder-wire.d.ts +70 -0
- package/dist/turn-recorder-wire.js +165 -0
- package/dist/turn-recorder.d.ts +190 -0
- package/dist/turn-recorder.js +695 -0
- package/dist/turn-ring.d.ts +93 -0
- package/dist/turn-ring.js +418 -0
- package/dist/turn-watch.d.ts +177 -0
- package/dist/turn-watch.js +630 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +55 -0
- package/dist/uninstall.js +507 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +421 -0
- package/dist/verify.d.ts +47 -0
- package/dist/verify.js +396 -0
- package/dist/worktree.d.ts +144 -0
- package/dist/worktree.js +310 -0
- package/dist/wrapper.d.ts +254 -0
- package/dist/wrapper.js +704 -0
- package/dist/ws-url.d.ts +109 -0
- package/dist/ws-url.js +202 -0
- package/package.json +78 -10
- package/bin/hwip.js +0 -3
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reading a session's repository for the phone's file view (F5): the one place
|
|
4
|
+
* the working tree is opened for it. Design: hwip `docs/file-view.md` (V1~V14,
|
|
5
|
+
* D1~D13); this module is V6, V7, V9, V10, V11, D4 and D5.
|
|
6
|
+
*
|
|
7
|
+
* What a path may be and what the phone may see of the tree is decided here and
|
|
8
|
+
* nowhere else. Nothing here knows about sealing, the wire or who asked — the
|
|
9
|
+
* listener decides that; this decides what is read and what is never opened.
|
|
10
|
+
*
|
|
11
|
+
* git is run only through `gitIn`, with constant arguments. What this module adds
|
|
12
|
+
* is `ls-files` (twice) for the visible set and `check-attr` (through `filteredPaths`)
|
|
13
|
+
* for `filter=`; `resolveBase` and `filteredPaths` bring their own `rev-parse` and
|
|
14
|
+
* `hash-object`. None of them runs a filter. One caller path goes to git, after `--`.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.readBlob = exports.listTree = exports.visibleSet = exports.isViewablePath = exports.FILE_VIEW_MAX_BYTES = exports.TREE_PAGE_MAX_ENTRIES = void 0;
|
|
18
|
+
const buffer_1 = require("buffer");
|
|
19
|
+
const fs_1 = require("fs");
|
|
20
|
+
const path_1 = require("path");
|
|
21
|
+
const secret_mask_js_1 = require("./secret-mask.js");
|
|
22
|
+
const session_diff_js_1 = require("./session-diff.js");
|
|
23
|
+
/** The longest path, in UTF-8 bytes, the phone may name. */
|
|
24
|
+
const PATH_MAX_BYTES = 1024;
|
|
25
|
+
/** Most entries in one page of a directory listing. */
|
|
26
|
+
exports.TREE_PAGE_MAX_ENTRIES = 500;
|
|
27
|
+
/** A file past this is not read. Same value as an untracked file's line count limit in the diff. */
|
|
28
|
+
exports.FILE_VIEW_MAX_BYTES = session_diff_js_1.UNTRACKED_COUNT_MAX_BYTES;
|
|
29
|
+
/** A NUL in this many leading bytes makes a file binary — the length git itself looks at. */
|
|
30
|
+
const BINARY_PROBE_BYTES = 8000;
|
|
31
|
+
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
32
|
+
/**
|
|
33
|
+
* V7 ①, the string rules. A path is repo-relative and names one thing: not empty
|
|
34
|
+
* (a directory request's `""` for the root is the tree handler's to allow), no NUL
|
|
35
|
+
* or other control character, not absolute, at most 1,024 UTF-8 bytes, and no
|
|
36
|
+
* segment that is empty, `.`, `..` or — at any depth, in any case — `.git` (a
|
|
37
|
+
* case-insensitive file system opens `.GIT` as `.git`, D11). A leading `-` is fine:
|
|
38
|
+
* the path only ever follows `--`.
|
|
39
|
+
*/
|
|
40
|
+
const isViewablePath = (path) => {
|
|
41
|
+
if (path === '' || path.startsWith('/') || CONTROL_CHARACTER.test(path))
|
|
42
|
+
return false;
|
|
43
|
+
if (buffer_1.Buffer.byteLength(path, 'utf-8') > PATH_MAX_BYTES)
|
|
44
|
+
return false;
|
|
45
|
+
return path.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..' && segment.toLowerCase() !== '.git');
|
|
46
|
+
};
|
|
47
|
+
exports.isViewablePath = isViewablePath;
|
|
48
|
+
/** U+FFFD is what `gitIn` makes of a byte that is not UTF-8 (O1): such a name cannot be matched byte for byte. */
|
|
49
|
+
const REPLACEMENT_CHARACTER = '\uFFFD';
|
|
50
|
+
/**
|
|
51
|
+
* V6, the visible set: what git lists as tracked (`ls-files --stage`) plus what it lists as
|
|
52
|
+
* untracked and not ignored (`ls-files --others --exclude-standard`), under `scope` (the
|
|
53
|
+
* whole repository when absent). An ignored path is simply not there, so asking for it by
|
|
54
|
+
* name answers like a path that does not exist. The caller matches a path against the keys
|
|
55
|
+
* exactly; the pathspec only narrows what git has to print. A path the phone could never
|
|
56
|
+
* ask for (`isViewablePath`) or whose bytes are not UTF-8 is left out, so a listing never
|
|
57
|
+
* shows a name that cannot be opened.
|
|
58
|
+
*/
|
|
59
|
+
const visibleSet = (root, scope, deadline = (0, session_diff_js_1.newDeadline)()) => {
|
|
60
|
+
const pathspec = scope === undefined ? [] : ['--', scope];
|
|
61
|
+
const tracked = (0, session_diff_js_1.gitIn)(root, ['ls-files', '-z', '--stage', ...pathspec], deadline);
|
|
62
|
+
if (tracked.status !== 0)
|
|
63
|
+
return { ok: false, error: tracked.tooLarge ? 'too_large' : 'git_failed' };
|
|
64
|
+
const untracked = (0, session_diff_js_1.gitIn)(root, ['ls-files', '-z', '--others', '--exclude-standard', ...pathspec], deadline);
|
|
65
|
+
if (untracked.status !== 0)
|
|
66
|
+
return { ok: false, error: untracked.tooLarge ? 'too_large' : 'git_failed' };
|
|
67
|
+
const entries = new Map();
|
|
68
|
+
const admit = (path, entry) => {
|
|
69
|
+
if (path.includes(REPLACEMENT_CHARACTER) || !(0, exports.isViewablePath)(path))
|
|
70
|
+
return;
|
|
71
|
+
// A path in conflict is listed once per stage; the first row stands.
|
|
72
|
+
if (!entries.has(path))
|
|
73
|
+
entries.set(path, entry);
|
|
74
|
+
};
|
|
75
|
+
for (const record of tracked.stdout.split('\0')) {
|
|
76
|
+
// `<mode> <oid> <stage>\t<path>`
|
|
77
|
+
if (!record)
|
|
78
|
+
continue;
|
|
79
|
+
const tab = record.indexOf('\t');
|
|
80
|
+
// A row of another shape means git's output is not what this reads: say so, not "nothing is visible".
|
|
81
|
+
if (tab < 0)
|
|
82
|
+
return { ok: false, error: 'git_failed' };
|
|
83
|
+
const [mode, oid] = record.slice(0, tab).split(' ');
|
|
84
|
+
admit(record.slice(tab + 1), { mode, oid });
|
|
85
|
+
}
|
|
86
|
+
for (const record of untracked.stdout.split('\0')) {
|
|
87
|
+
if (!record)
|
|
88
|
+
continue;
|
|
89
|
+
if (record.endsWith('/'))
|
|
90
|
+
admit(record.slice(0, -1), { nested: true });
|
|
91
|
+
else
|
|
92
|
+
admit(record, {});
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, entries };
|
|
95
|
+
};
|
|
96
|
+
exports.visibleSet = visibleSet;
|
|
97
|
+
/** The errno of a file-system call that failed because nothing is there, or what is there cannot be followed. */
|
|
98
|
+
const isGone = (e) => e instanceof Error && 'code' in e && (e.code === 'ENOENT' || e.code === 'ENOTDIR' || e.code === 'ELOOP');
|
|
99
|
+
/**
|
|
100
|
+
* V7 ③: no directory of `dir` (a repo-relative path, `""` for none) is a symlink, and each is a directory. git does
|
|
101
|
+
* not track through a symlink, but a tracked directory can be replaced by one after the
|
|
102
|
+
* index was written. Walked a segment at a time with `lstat`, so a root that itself sits
|
|
103
|
+
* under a symlink (`/tmp` → `/private/tmp`) and a case-insensitive file system make no
|
|
104
|
+
* difference. A segment that does not exist ends the walk with a yes: there is nothing
|
|
105
|
+
* to follow there (a deleted file stays in the index until it is committed). null when the
|
|
106
|
+
* file system failed in some other way (permissions, I/O): that is not an answer about symlinks.
|
|
107
|
+
*/
|
|
108
|
+
const reachesWithoutSymlink = (root, dir) => {
|
|
109
|
+
let at = root;
|
|
110
|
+
for (const segment of dir === '' ? [] : dir.split('/')) {
|
|
111
|
+
at = (0, path_1.join)(at, segment);
|
|
112
|
+
try {
|
|
113
|
+
if (!(0, fs_1.lstatSync)(at).isDirectory())
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
return isGone(e) ? true : null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
};
|
|
122
|
+
/** `a/b/c` → `a/b`; `c` → `""`. */
|
|
123
|
+
const parentOf = (path) => path.slice(0, Math.max(0, path.lastIndexOf('/')));
|
|
124
|
+
const entryTypeOf = (entry) => {
|
|
125
|
+
if (entry.nested || entry.mode === '160000')
|
|
126
|
+
return 'submodule';
|
|
127
|
+
return entry.mode === '120000' ? 'symlink' : 'file';
|
|
128
|
+
};
|
|
129
|
+
const utf8 = (text) => buffer_1.Buffer.from(text, 'utf-8');
|
|
130
|
+
/**
|
|
131
|
+
* One page of the entries directly under `dir` (`""` is the repository root), by
|
|
132
|
+
* name in UTF-8 byte order. `cursor` is the last name of the page before; the page is
|
|
133
|
+
* at most `TREE_PAGE_MAX_ENTRIES` and `truncated` says there is more. A directory is
|
|
134
|
+
* only a name some visible path continues under, so an empty one, an ignored one and
|
|
135
|
+
* one that is not there are all `absent`.
|
|
136
|
+
*/
|
|
137
|
+
const listTree = (root, dir, cursor, deadline = (0, session_diff_js_1.newDeadline)()) => {
|
|
138
|
+
if (dir !== '' && !(0, exports.isViewablePath)(dir))
|
|
139
|
+
return { ok: false, error: 'bad_path' };
|
|
140
|
+
const set = (0, exports.visibleSet)(root, dir === '' ? undefined : dir, deadline);
|
|
141
|
+
if (!set.ok)
|
|
142
|
+
return set;
|
|
143
|
+
const prefix = dir === '' ? '' : `${dir}/`;
|
|
144
|
+
const found = new Map();
|
|
145
|
+
for (const [path, entry] of set.entries) {
|
|
146
|
+
if (!path.startsWith(prefix))
|
|
147
|
+
continue;
|
|
148
|
+
const rest = path.slice(prefix.length);
|
|
149
|
+
const slash = rest.indexOf('/');
|
|
150
|
+
if (slash < 0)
|
|
151
|
+
found.set(rest, entryTypeOf(entry));
|
|
152
|
+
else
|
|
153
|
+
found.set(rest.slice(0, slash), 'dir');
|
|
154
|
+
}
|
|
155
|
+
if (dir !== '' && found.size === 0)
|
|
156
|
+
return { ok: true, body: 'absent' };
|
|
157
|
+
const clear = reachesWithoutSymlink(root, dir);
|
|
158
|
+
if (clear === null)
|
|
159
|
+
return { ok: false, error: 'git_failed' };
|
|
160
|
+
if (!clear)
|
|
161
|
+
return { ok: true, body: 'absent' };
|
|
162
|
+
// Each name's bytes are made once: the sort would otherwise make two buffers per comparison.
|
|
163
|
+
const cursorBytes = cursor === undefined ? undefined : utf8(cursor);
|
|
164
|
+
const after = [...found]
|
|
165
|
+
.map(([name, type]) => ({ name, type, bytes: utf8(name) }))
|
|
166
|
+
.sort((a, b) => buffer_1.Buffer.compare(a.bytes, b.bytes))
|
|
167
|
+
.filter((entry) => cursorBytes === undefined || buffer_1.Buffer.compare(entry.bytes, cursorBytes) > 0);
|
|
168
|
+
const page = after.slice(0, exports.TREE_PAGE_MAX_ENTRIES).map(({ name, type }) => ({ name, type }));
|
|
169
|
+
const truncated = after.length > page.length;
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
body: 'entries',
|
|
173
|
+
entries: page,
|
|
174
|
+
truncated,
|
|
175
|
+
...(truncated ? { cursor: page[page.length - 1].name } : {}),
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
exports.listTree = listTree;
|
|
179
|
+
/**
|
|
180
|
+
* D4: names that stay unread even when a repository tracks them. The list is code, not
|
|
181
|
+
* configuration. Compared on the last segment, ignoring case (the file systems that
|
|
182
|
+
* open `.GIT` as `.git` open `.ENV` as `.env`).
|
|
183
|
+
*/
|
|
184
|
+
const BLOCKED_NAME = /^(?:\.env(?:\..*)?|.*\.(?:pem|key|p12|pfx|keystore)|id_rsa.*|id_ed25519.*|credentials.*)$/;
|
|
185
|
+
const ALLOWED_ENV_NAMES = new Set(['.env.example', '.env.sample']);
|
|
186
|
+
const isBlockedName = (path) => {
|
|
187
|
+
const name = (path.split('/').pop() ?? '').toLowerCase();
|
|
188
|
+
return !ALLOWED_ENV_NAMES.has(name) && BLOCKED_NAME.test(name);
|
|
189
|
+
};
|
|
190
|
+
const ABSENT = { ok: true, body: 'absent' };
|
|
191
|
+
const FILTERED = { ok: true, body: 'filtered' };
|
|
192
|
+
/** Is `path` still, right now, a regular file with the identity of `opened`? */
|
|
193
|
+
const sameFile = (path, opened) => {
|
|
194
|
+
try {
|
|
195
|
+
const now = (0, fs_1.lstatSync)(path, { bigint: true });
|
|
196
|
+
return now.isFile() && now.dev === opened.dev && now.ino === opened.ino;
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
// Gone is a swap (false); anything else is the file system failing, which the caller's catch reports.
|
|
200
|
+
if (isGone(e))
|
|
201
|
+
return false;
|
|
202
|
+
throw e;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
/** Read up to `limit` bytes from the start of `fd`; fewer when the file ends first. */
|
|
206
|
+
const readUpTo = (fd, limit) => {
|
|
207
|
+
const buffer = buffer_1.Buffer.alloc(limit);
|
|
208
|
+
let filled = 0;
|
|
209
|
+
while (filled < limit) {
|
|
210
|
+
const n = (0, fs_1.readSync)(fd, buffer, filled, limit - filled, filled);
|
|
211
|
+
if (n === 0)
|
|
212
|
+
break;
|
|
213
|
+
filled += n;
|
|
214
|
+
}
|
|
215
|
+
return buffer.subarray(0, filled);
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* What a file looks like to the phone. The order is the design's V7: ① the string rules
|
|
219
|
+
* (`bad_path`) · ② the visible set, exact match · ③ no symlink on the way · ④ `lstat`: a
|
|
220
|
+
* symlink is shown as its target string and never followed, a gitlink or nested repository
|
|
221
|
+
* is shown as a submodule and never entered, anything but a regular file is `absent` ·
|
|
222
|
+
* D4 and V9 (`filter=`) say `filtered` · ⑤ open with `O_NOFOLLOW | O_NONBLOCK`, `fstat`
|
|
223
|
+
* the descriptor and compare it with ④ — a different file is `absent`, a hard link is
|
|
224
|
+
* `filtered` (D5) · V11: past 1 MiB is `too_large`, a NUL in the first 8,000 bytes is
|
|
225
|
+
* `binary` · V10: the whole text is masked before the caller pages it. A path outside the
|
|
226
|
+
* set, ignored, deleted or swapped is `absent`, never an error: the phone cannot tell them apart.
|
|
227
|
+
*/
|
|
228
|
+
const readBlob = (root, path, options = {}) => {
|
|
229
|
+
if (!(0, exports.isViewablePath)(path))
|
|
230
|
+
return { ok: false, error: 'bad_path' };
|
|
231
|
+
const deadline = options.deadline ?? (0, session_diff_js_1.newDeadline)();
|
|
232
|
+
const set = (0, exports.visibleSet)(root, path, deadline);
|
|
233
|
+
if (!set.ok)
|
|
234
|
+
return set;
|
|
235
|
+
const entry = set.entries.get(path);
|
|
236
|
+
if (!entry)
|
|
237
|
+
return ABSENT;
|
|
238
|
+
const parent = parentOf(path);
|
|
239
|
+
const clear = reachesWithoutSymlink(root, parent);
|
|
240
|
+
if (clear === null)
|
|
241
|
+
return { ok: false, error: 'git_failed' };
|
|
242
|
+
if (!clear)
|
|
243
|
+
return ABSENT;
|
|
244
|
+
if (entry.nested)
|
|
245
|
+
return { ok: true, body: 'submodule' };
|
|
246
|
+
if (entry.mode === '160000')
|
|
247
|
+
return { ok: true, body: 'submodule', commit: entry.oid };
|
|
248
|
+
const full = (0, path_1.join)(root, path);
|
|
249
|
+
options.beforeLstat?.();
|
|
250
|
+
let seen;
|
|
251
|
+
try {
|
|
252
|
+
// bigint: an inode past 2^53 (FUSE, NFS) would otherwise compare equal to its neighbour.
|
|
253
|
+
seen = (0, fs_1.lstatSync)(full, { bigint: true });
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
return isGone(e) ? ABSENT : { ok: false, error: 'git_failed' };
|
|
257
|
+
}
|
|
258
|
+
if (seen.isSymbolicLink()) {
|
|
259
|
+
try {
|
|
260
|
+
return { ok: true, body: 'symlink', target: (0, fs_1.readlinkSync)(full) };
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
// EINVAL: it stopped being a symlink since the lstat — a swap, like a vanished file.
|
|
264
|
+
const swapped = isGone(e) || (e instanceof Error && 'code' in e && e.code === 'EINVAL');
|
|
265
|
+
return swapped ? ABSENT : { ok: false, error: 'git_failed' };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!seen.isFile())
|
|
269
|
+
return ABSENT;
|
|
270
|
+
if (isBlockedName(path))
|
|
271
|
+
return FILTERED;
|
|
272
|
+
const base = (0, session_diff_js_1.resolveBase)(root, 'head', deadline);
|
|
273
|
+
if (!base.ok)
|
|
274
|
+
return base;
|
|
275
|
+
const filtered = (0, session_diff_js_1.filteredPaths)(root, [path], base.rev, deadline);
|
|
276
|
+
if (!filtered)
|
|
277
|
+
return { ok: false, error: 'git_failed' };
|
|
278
|
+
if (filtered.size > 0)
|
|
279
|
+
return FILTERED;
|
|
280
|
+
options.beforeOpen?.();
|
|
281
|
+
let fd;
|
|
282
|
+
try {
|
|
283
|
+
fd = (0, fs_1.openSync)(full, fs_1.constants.O_RDONLY | fs_1.constants.O_NOFOLLOW | fs_1.constants.O_NONBLOCK);
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
// ELOOP is `O_NOFOLLOW` catching a symlink put there since the lstat; running out of descriptors is not "absent".
|
|
287
|
+
return isGone(e) ? ABSENT : { ok: false, error: 'git_failed' };
|
|
288
|
+
}
|
|
289
|
+
let bytes;
|
|
290
|
+
try {
|
|
291
|
+
const opened = (0, fs_1.fstatSync)(fd, { bigint: true });
|
|
292
|
+
if (!opened.isFile() || opened.dev !== seen.dev || opened.ino !== seen.ino)
|
|
293
|
+
return ABSENT;
|
|
294
|
+
// The walk and the `lstat` ran before the open; a directory swapped for a symlink in between would
|
|
295
|
+
// have made `seen` the outside file's. The descriptor is the one fixed thing: ask again, and the
|
|
296
|
+
// swap has to be undone and redone inside this window to pass.
|
|
297
|
+
const stillClear = reachesWithoutSymlink(root, parent);
|
|
298
|
+
if (stillClear === null)
|
|
299
|
+
return { ok: false, error: 'git_failed' };
|
|
300
|
+
if (!stillClear || !sameFile(full, opened))
|
|
301
|
+
return ABSENT;
|
|
302
|
+
if (opened.nlink !== 1n)
|
|
303
|
+
return FILTERED;
|
|
304
|
+
if (opened.size > BigInt(exports.FILE_VIEW_MAX_BYTES))
|
|
305
|
+
return { ok: false, error: 'too_large' };
|
|
306
|
+
// One byte past the limit: a file that grew after `fstat` is still caught.
|
|
307
|
+
bytes = readUpTo(fd, exports.FILE_VIEW_MAX_BYTES + 1);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// The file system failed under an open descriptor (EIO and the like): nothing was read.
|
|
311
|
+
return { ok: false, error: 'git_failed' };
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
try {
|
|
315
|
+
(0, fs_1.closeSync)(fd);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// intentionally ignored: the bytes (or the answer) are already in hand; a failed close of a read-only descriptor loses nothing.
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (bytes.length > exports.FILE_VIEW_MAX_BYTES)
|
|
322
|
+
return { ok: false, error: 'too_large' };
|
|
323
|
+
if (bytes.subarray(0, BINARY_PROBE_BYTES).includes(0))
|
|
324
|
+
return { ok: true, body: 'binary', size: bytes.length };
|
|
325
|
+
return { ok: true, body: 'text', ...(0, secret_mask_js_1.maskText)(bytes.toString('utf-8')) };
|
|
326
|
+
};
|
|
327
|
+
exports.readBlob = readBlob;
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export type GateMarker = 'skip' | 'push' | 'high' | 'none';
|
|
2
|
+
export type GatePushMode = 'quiet' | 'loud' | 'normal';
|
|
3
|
+
export interface GateInput {
|
|
4
|
+
/** Total tool_use blocks this turn. */
|
|
5
|
+
toolCount: number;
|
|
6
|
+
/** Tools that are NOT read-only (Read/Grep/Glob). */
|
|
7
|
+
nonReadonlyCount: number;
|
|
8
|
+
/** A hwip_ask already notified this turn. */
|
|
9
|
+
alreadyAsked: boolean;
|
|
10
|
+
marker: GateMarker;
|
|
11
|
+
pushMode: GatePushMode;
|
|
12
|
+
/** The user is away from the terminal (presence.ts awayForGate) — lets quiet
|
|
13
|
+
* push. Optional so existing callers keep compiling; absent = present,
|
|
14
|
+
* matching the bash twin's missing 6th argument. */
|
|
15
|
+
away?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface GateVerdict {
|
|
18
|
+
push: boolean;
|
|
19
|
+
priority: 'high' | 'normal';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* "Assume real work" defaults for hooks that can't supply turn facts
|
|
23
|
+
* (most non-Claude agents pass no counts): in normal mode the push still
|
|
24
|
+
* fires — preserving the historical always-push behavior of the dumb
|
|
25
|
+
* hooks — while quiet/loud now work everywhere.
|
|
26
|
+
*
|
|
27
|
+
* These defaults cannot rescue a quiet dial: quiet only lets a `high` marker
|
|
28
|
+
* or an away user (presence.ts) through, and a hook with no turn facts has no
|
|
29
|
+
* marker either. That is why the installed templates pass `--pushmode-default
|
|
30
|
+
* normal` (see templates.ts) — for them quiet is not a lower volume, it is
|
|
31
|
+
* silence while the user is at the terminal.
|
|
32
|
+
*/
|
|
33
|
+
export declare const GATE_DEFAULTS: {
|
|
34
|
+
readonly toolCount: 2;
|
|
35
|
+
readonly nonReadonlyCount: 1;
|
|
36
|
+
readonly alreadyAsked: false;
|
|
37
|
+
};
|
|
38
|
+
export declare const normalizeMarker: (raw: string | undefined) => GateMarker;
|
|
39
|
+
export declare const normalizePushMode: (raw: string | undefined) => GatePushMode;
|
|
40
|
+
export declare const decidePush: (input: GateInput) => GateVerdict;
|
|
41
|
+
export declare const stateDir: () => string;
|
|
42
|
+
/**
|
|
43
|
+
* The first field of POSIX `cksum` over `data`: the CRC of the bytes, then of
|
|
44
|
+
* the length (low byte first, no trailing zero bytes), complemented.
|
|
45
|
+
*/
|
|
46
|
+
export declare const posixCksum: (data: Uint8Array) => number;
|
|
47
|
+
/** `cksum` of the project dir as the plugin computes it. */
|
|
48
|
+
export declare const projectHash: (dir: string) => string;
|
|
49
|
+
/** Marker path for a project hash: `<stateDir>/remote-<cksum(projectDir)>`. */
|
|
50
|
+
export declare const remoteMarkerPath: (hash: string) => string;
|
|
51
|
+
/**
|
|
52
|
+
* sha256 over the ASCII-only-trimmed text — recorded at inject time,
|
|
53
|
+
* recomputed at prompt submit. Trim strips ONLY ' \t\r\n\f\v', NOT
|
|
54
|
+
* String.trim(): trim() also eats Unicode whitespace (U+00A0 etc.) that
|
|
55
|
+
* the bash reader keeps, and both sides must hash identical bytes.
|
|
56
|
+
*/
|
|
57
|
+
export declare const remoteDigest: (text: string) => string;
|
|
58
|
+
/**
|
|
59
|
+
* How long REMOTE stays live without a refresh. Generous on purpose: the state
|
|
60
|
+
* is refreshed on every phone prompt and every answered `hwip_ask`, so it only
|
|
61
|
+
* has to outlive a working session, never an idle user. It is the backstop and
|
|
62
|
+
* not the usual exit — a terminal-typed prompt, a Done-like answer and the
|
|
63
|
+
* model's `<!-- hwip: exit -->` all delete the file outright. What none of them
|
|
64
|
+
* covers is a session that simply died: there is no SessionEnd hook, so without
|
|
65
|
+
* a TTL a crash or Ctrl-C would latch REMOTE forever.
|
|
66
|
+
*/
|
|
67
|
+
export declare const REMOTE_TTL_SEC = 14400;
|
|
68
|
+
/** State path for a project hash: `<stateDir>/remote-active-<cksum(dir)>`. */
|
|
69
|
+
export declare const remoteStatePath: (hash: string) => string;
|
|
70
|
+
/**
|
|
71
|
+
* True while REMOTE is live for this project. A time ahead of the clock counts
|
|
72
|
+
* as now (and is rewritten as now). State that is expired or
|
|
73
|
+
* unparseable is deleted on sight — the same housekeeping a stale entry
|
|
74
|
+
* marker gets, since state that can never flag again is dead weight.
|
|
75
|
+
*/
|
|
76
|
+
export declare const isRemoteActive: (dir: string, now?: () => number) => boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Enter REMOTE, or push its expiry back. Best-effort by contract: the prompt
|
|
79
|
+
* hooks that call this must never fail a prompt over state IO.
|
|
80
|
+
*/
|
|
81
|
+
export declare const touchRemoteActive: (dir: string, now?: () => number) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Leave REMOTE. Best-effort like its twin: an absent file is success, and the
|
|
84
|
+
* caller is a hook with its own job to finish. Bash twin: gate.sh
|
|
85
|
+
* hwip_remote_clear.
|
|
86
|
+
*/
|
|
87
|
+
export declare const clearRemoteActive: (dir: string) => void;
|
|
88
|
+
/** True when the user ran /hwip-mode mute for this project. */
|
|
89
|
+
export declare const isMuted: (dir: string) => boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Push mode for an install that has never set a dial. The twin is
|
|
92
|
+
* plugin/hooks/gate.sh's missing-5th-argument default; the shared vectors
|
|
93
|
+
* never reach either one (every vector passes pushMode explicitly), so both
|
|
94
|
+
* sides pin it in their own tests.
|
|
95
|
+
*/
|
|
96
|
+
export declare const PUSHMODE_DEFAULT: GatePushMode;
|
|
97
|
+
/**
|
|
98
|
+
* `notify` flag naming the push mode to assume when the project has no dial.
|
|
99
|
+
* Written by templates.ts into every hook-driven agent's completion hook and
|
|
100
|
+
* read back in cli.ts — shared so the two can never drift apart.
|
|
101
|
+
*/
|
|
102
|
+
export declare const PUSHMODE_DEFAULT_FLAG = "pushmode-default";
|
|
103
|
+
/**
|
|
104
|
+
* `notify` flags carrying the turn's tool counts. Only the drop-in artifacts
|
|
105
|
+
* (pi extension, opencode plugin) can supply them — they are the only installed
|
|
106
|
+
* hooks that see per-tool events. Shared for the same reason as the flag above:
|
|
107
|
+
* templates.ts writes them, cli.ts reads them, and a rename on one side alone
|
|
108
|
+
* would send `gateCount` back to GATE_DEFAULTS with nothing to show for it.
|
|
109
|
+
*/
|
|
110
|
+
export declare const TOOL_COUNT_FLAG = "tools";
|
|
111
|
+
export declare const NONREADONLY_COUNT_FLAG = "nonreadonly";
|
|
112
|
+
/**
|
|
113
|
+
* The user's session push-mode dial (/hwip-mode quiet | /hwip-mode loud | /hwip-mode normal).
|
|
114
|
+
*
|
|
115
|
+
* Three failure shapes, three answers — "no dial" is the only one that gets
|
|
116
|
+
* the quiet default:
|
|
117
|
+
* - no dial file → `fallback` (PUSHMODE_DEFAULT unless the caller
|
|
118
|
+
* overrides it with --pushmode-default)
|
|
119
|
+
* - unusable dial file → normal. An unreadable
|
|
120
|
+
* file, or a garbled/empty value is a broken
|
|
121
|
+
* setting, and resolving breakage to silence
|
|
122
|
+
* leaves the user with no symptom to debug. The
|
|
123
|
+
* point of the new default is quiet, not hidden
|
|
124
|
+
* errors.
|
|
125
|
+
* - readable dial file → whatever it says.
|
|
126
|
+
*/
|
|
127
|
+
export declare const readPushMode: (dir: string, fallback?: GatePushMode) => GatePushMode;
|
|
128
|
+
/**
|
|
129
|
+
* Push mode for a `--auto` notify: the user's dial if they set one, otherwise
|
|
130
|
+
* the mode named by `--pushmode-default`, otherwise the built-in quiet.
|
|
131
|
+
*
|
|
132
|
+
* The dial outranks the flag deliberately. The flag exists so an agent whose
|
|
133
|
+
* hook cannot participate in the heuristic still pushes out of the box; if it
|
|
134
|
+
* outranked the dial, `/hwip-mode quiet` would silently do nothing for that agent.
|
|
135
|
+
*
|
|
136
|
+
* A flag value that isn't one of the three modes resolves to `normal`, not to
|
|
137
|
+
* the quiet default — same rule as a garbled dial file. A caller that passes
|
|
138
|
+
* nonsense has a bug, and answering a bug with silence hides it.
|
|
139
|
+
*/
|
|
140
|
+
export declare const autoPushMode: (dir: string, flag: string | boolean | undefined) => GatePushMode;
|
|
141
|
+
//# sourceMappingURL=gate.d.ts.map
|