harnery 0.46.0 → 0.47.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/README.md +24 -0
- package/bin/README.md +10 -0
- package/bin/agent-coord.mjs +3 -0
- package/bin/agent-hook.mjs +3 -0
- package/bin/harn.mjs +3 -0
- package/bin/run.mjs +43 -0
- package/dist/commander.d.ts +18 -6
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +74 -2
- package/dist/commands/agents.d.ts +25 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -27
- package/dist/commands/autoupdate.d.ts +12 -0
- package/dist/commands/autoupdate.d.ts.map +1 -0
- package/dist/commands/autoupdate.js +190 -0
- package/dist/commands/computer.d.ts +45 -0
- package/dist/commands/computer.d.ts.map +1 -0
- package/dist/commands/computer.js +509 -0
- package/dist/commands/docs.js +3 -1
- package/dist/commands/fetch.js +11 -1
- package/dist/commands/grep.d.ts +72 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +200 -47
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -9
- package/dist/commands/rm.js +2 -2
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +2 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -3
- package/dist/core/agents/coord-bin.d.ts +3 -0
- package/dist/core/agents/coord-bin.d.ts.map +1 -1
- package/dist/core/agents/coord-bin.js +6 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +6 -2
- package/dist/core/agents/render/prompt-context.d.ts +55 -7
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +122 -48
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +36 -16
- package/dist/core/agents/rules/stop-hook.d.ts +6 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +20 -1
- package/dist/core/agents/state/proc-start.d.ts +3 -1
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +26 -4
- package/dist/core/autoupdate/engine.d.ts +97 -0
- package/dist/core/autoupdate/engine.d.ts.map +1 -0
- package/dist/core/autoupdate/engine.js +548 -0
- package/dist/core/autoupdate/fingerprint.d.ts +16 -0
- package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
- package/dist/core/autoupdate/fingerprint.js +73 -0
- package/dist/core/autoupdate/git.d.ts +30 -0
- package/dist/core/autoupdate/git.d.ts.map +1 -0
- package/dist/core/autoupdate/git.js +82 -0
- package/dist/core/autoupdate/index.d.ts +6 -0
- package/dist/core/autoupdate/index.d.ts.map +1 -0
- package/dist/core/autoupdate/index.js +4 -0
- package/dist/core/autoupdate/state.d.ts +59 -0
- package/dist/core/autoupdate/state.d.ts.map +1 -0
- package/dist/core/autoupdate/state.js +214 -0
- package/dist/core/autoupdate/types.d.ts +113 -0
- package/dist/core/autoupdate/types.d.ts.map +1 -0
- package/dist/core/autoupdate/types.js +35 -0
- package/dist/core/config.d.ts +53 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +114 -1
- package/dist/core/events/v3/bootstrap.js +1 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +1 -0
- package/dist/core/hooks/adapter/wiring.d.ts +12 -2
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +38 -8
- package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
- package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
- package/dist/core/hooks/autoupdate-cue.js +53 -0
- package/dist/core/hooks/cli.js +52 -4
- package/dist/core/hooks/effects/index.js +4 -4
- package/dist/core/hooks/resolve/coord-root.js +1 -0
- package/dist/core/hooks/resolve/transcript.d.ts +12 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +54 -0
- package/dist/core/semantic/contract.d.ts +3 -3
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +4 -2
- package/dist/core/storage/modes.d.ts +1 -1
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +5 -1
- package/dist/core/workflow/durable-record.d.ts +2 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +13 -1
- package/dist/core/workflow/engine.js +4 -0
- package/dist/core/workflow/spawn-claude.d.ts +7 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +16 -4
- package/dist/core/workflow/specialists.d.ts.map +1 -1
- package/dist/core/workflow/specialists.js +1 -0
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -9
- package/dist/lib/computer/cua.d.ts +50 -0
- package/dist/lib/computer/cua.d.ts.map +1 -0
- package/dist/lib/computer/cua.js +474 -0
- package/dist/lib/computer/host.d.ts +19 -0
- package/dist/lib/computer/host.d.ts.map +1 -0
- package/dist/lib/computer/host.js +72 -0
- package/dist/lib/computer/lease.d.ts +13 -0
- package/dist/lib/computer/lease.d.ts.map +1 -0
- package/dist/lib/computer/lease.js +269 -0
- package/dist/lib/computer/types.d.ts +163 -0
- package/dist/lib/computer/types.d.ts.map +1 -0
- package/dist/lib/computer/types.js +10 -0
- package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
- package/dist/lib/docs-metadata-v2.js +6 -5
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/exec.js +1 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +5 -4
- package/dist/lib/headless/index.d.ts +33 -0
- package/dist/lib/headless/index.d.ts.map +1 -1
- package/dist/lib/headless/index.js +38 -2
- package/dist/lib/http/client.d.ts +2 -9
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +30 -9
- package/dist/lib/instructions/apply.d.ts +9 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +92 -14
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
- package/dist/lib/instructions/git-hooks.js +8 -4
- package/dist/lib/instructions/templates.d.ts +11 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +81 -1
- package/package.json +5 -5
- package/schemas/config.schema.json +89 -1
- package/src/commander.ts +98 -8
- package/src/commands/agents.ts +100 -32
- package/src/commands/autoupdate.ts +250 -0
- package/src/commands/computer.ts +682 -0
- package/src/commands/docs.ts +3 -1
- package/src/commands/fetch.ts +11 -1
- package/src/commands/grep.ts +274 -59
- package/src/commands/init.ts +17 -7
- package/src/commands/rm.ts +2 -2
- package/src/commands/web.ts +2 -0
- package/src/core/agents/canonical-emit.ts +2 -3
- package/src/core/agents/coord-bin.ts +30 -0
- package/src/core/agents/coord-client.ts +6 -2
- package/src/core/agents/render/prompt-context.ts +152 -53
- package/src/core/agents/rules/claim-conflict.ts +50 -18
- package/src/core/agents/rules/stop-hook.ts +28 -1
- package/src/core/agents/state/proc-start.ts +29 -5
- package/src/core/autoupdate/engine.ts +691 -0
- package/src/core/autoupdate/fingerprint.ts +89 -0
- package/src/core/autoupdate/git.ts +120 -0
- package/src/core/autoupdate/index.ts +29 -0
- package/src/core/autoupdate/state.ts +269 -0
- package/src/core/autoupdate/types.ts +135 -0
- package/src/core/config.ts +142 -1
- package/src/core/events/v3/bootstrap.ts +1 -0
- package/src/core/events/v3/runtime-build.ts +1 -0
- package/src/core/hooks/adapter/wiring.ts +51 -6
- package/src/core/hooks/autoupdate-cue.ts +72 -0
- package/src/core/hooks/cli.ts +54 -1
- package/src/core/hooks/effects/index.ts +4 -4
- package/src/core/hooks/resolve/coord-root.ts +1 -0
- package/src/core/hooks/resolve/transcript.ts +61 -0
- package/src/core/semantic/service.ts +6 -2
- package/src/core/storage/modes.ts +7 -1
- package/src/core/workflow/durable-record.ts +15 -1
- package/src/core/workflow/engine.ts +3 -0
- package/src/core/workflow/spawn-claude.ts +16 -4
- package/src/core/workflow/specialists.ts +1 -0
- package/src/core/workflow/types.ts +10 -0
- package/src/core/workflow/workspaces/leases.ts +1 -9
- package/src/lib/computer/cua.ts +572 -0
- package/src/lib/computer/host.ts +87 -0
- package/src/lib/computer/lease.ts +299 -0
- package/src/lib/computer/types.ts +177 -0
- package/src/lib/docs-metadata-v2.ts +24 -5
- package/src/lib/exec.ts +1 -0
- package/src/lib/guarded-remove.ts +5 -3
- package/src/lib/headless/index.ts +66 -2
- package/src/lib/http/client.ts +33 -19
- package/src/lib/instructions/apply.ts +100 -12
- package/src/lib/instructions/git-hooks.ts +8 -4
- package/src/lib/instructions/templates.ts +95 -1
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
// `computer`: opt-in desktop control through one pinned desktop driver
|
|
2
|
+
// (ADR 0228). The driver adapter (src/lib/computer/cua.ts), host detection and
|
|
3
|
+
// the cross-call desktop lease live in src/lib/computer/; this command owns
|
|
4
|
+
// the config gate, flag parsing, lease policy for `call`, screenshot redaction,
|
|
5
|
+
// rendering, and exit codes.
|
|
6
|
+
//
|
|
7
|
+
// Exit codes: 0 ok · 1 error (disabled, unsupported host, not installed,
|
|
8
|
+
// driver failure; `call` propagates the driver's own exit code) · 3 the
|
|
9
|
+
// desktop lease is held by another agent.
|
|
10
|
+
import { hostname, userInfo } from "node:os";
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
import { resolveCoordRoot, resolveOwner } from "../core/agents/coord-client.js";
|
|
13
|
+
import { agentDisplayName, resolveShortName } from "../core/agents/state/activity-log.js";
|
|
14
|
+
import { readComputerConfig } from "../core/config.js";
|
|
15
|
+
/** Exit code for a desktop held by another agent. */
|
|
16
|
+
export const EXIT_LEASE_HELD = 3;
|
|
17
|
+
/** `call` waits this long for another agent's lease when nothing overrides it. */
|
|
18
|
+
export const DEFAULT_CALL_WAIT_SECONDS = 30;
|
|
19
|
+
/**
|
|
20
|
+
* Who is asking for the desktop: the coordinated agent when one is resolvable,
|
|
21
|
+
* else the calling OS user (`shell:<user>@<hostname>`, label `shell-<user>`), so
|
|
22
|
+
* separate shell invocations by the same person share one lease.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveLeaseOwner() {
|
|
25
|
+
const root = resolveCoordRoot();
|
|
26
|
+
const id = resolveOwner();
|
|
27
|
+
if (id) {
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
label: root ? resolveShortName(root, id) : agentDisplayName(id),
|
|
31
|
+
...(root ? { project: root } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const user = userInfo().username;
|
|
35
|
+
return {
|
|
36
|
+
id: `shell:${user}@${hostname()}`,
|
|
37
|
+
label: `shell-${user}`,
|
|
38
|
+
...(root ? { project: root } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function loadDefaultDeps() {
|
|
42
|
+
const [{ detectHost }, { cuaDriver }, { desktopLease, leaseDir, DEFAULT_LEASE_IDLE_MS }] = await Promise.all([
|
|
43
|
+
import("../lib/computer/host.js"),
|
|
44
|
+
import("../lib/computer/cua.js"),
|
|
45
|
+
import("../lib/computer/lease.js"),
|
|
46
|
+
]);
|
|
47
|
+
return {
|
|
48
|
+
host: () => detectHost(),
|
|
49
|
+
driver: cuaDriver,
|
|
50
|
+
lease: desktopLease,
|
|
51
|
+
leaseDir,
|
|
52
|
+
defaultIdleMs: DEFAULT_LEASE_IDLE_MS,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Fill in every dependency the caller did not inject; driver modules load lazily. */
|
|
56
|
+
async function resolveDeps(input) {
|
|
57
|
+
const needsDefaults = !input.host ||
|
|
58
|
+
!input.driver ||
|
|
59
|
+
!input.lease ||
|
|
60
|
+
!input.leaseDir ||
|
|
61
|
+
input.defaultIdleMs === undefined;
|
|
62
|
+
const defaults = needsDefaults ? await loadDefaultDeps() : undefined;
|
|
63
|
+
return {
|
|
64
|
+
emit: input.emit,
|
|
65
|
+
binName: input.binName,
|
|
66
|
+
host: input.host ?? defaults?.host,
|
|
67
|
+
driver: input.driver ?? defaults?.driver,
|
|
68
|
+
lease: input.lease ?? defaults?.lease,
|
|
69
|
+
leaseDir: input.leaseDir ?? defaults?.leaseDir,
|
|
70
|
+
defaultIdleMs: input.defaultIdleMs ?? defaults?.defaultIdleMs,
|
|
71
|
+
owner: input.owner ?? resolveLeaseOwner,
|
|
72
|
+
config: input.config ?? (() => readComputerConfig()),
|
|
73
|
+
writeStderr: input.writeStderr ?? ((text) => process.stderr.write(text)),
|
|
74
|
+
now: input.now ?? Date.now,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
class ComputerError extends Error {
|
|
78
|
+
code;
|
|
79
|
+
exitCode;
|
|
80
|
+
hint;
|
|
81
|
+
constructor(code, message, exitCode = 1, hint) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.code = code;
|
|
84
|
+
this.exitCode = exitCode;
|
|
85
|
+
this.hint = hint;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function enableHint() {
|
|
89
|
+
return 'Add `"computer": { "enabled": true }` to .harnery/config.jsonc to enable it for this project.';
|
|
90
|
+
}
|
|
91
|
+
function requireEnabled(deps) {
|
|
92
|
+
if (deps.config().enabled)
|
|
93
|
+
return;
|
|
94
|
+
throw new ComputerError("computer_disabled", `Computer use is off in this project. ${enableHint()}`);
|
|
95
|
+
}
|
|
96
|
+
function requireSupported(host) {
|
|
97
|
+
if (host.kind === "unsupported" || !host.desktopStateDir) {
|
|
98
|
+
throw new ComputerError("computer_unsupported_host", `Computer use is not available on this host: ${host.reason ?? "no supported desktop driver build"}`);
|
|
99
|
+
}
|
|
100
|
+
return host;
|
|
101
|
+
}
|
|
102
|
+
function requireInstalled(deps, host) {
|
|
103
|
+
const exe = deps.driver.installedExecutable(host);
|
|
104
|
+
if (exe)
|
|
105
|
+
return exe;
|
|
106
|
+
throw new ComputerError("computer_not_installed", `The pinned desktop driver (${deps.driver.id} ${deps.driver.version}) is not installed. Run \`${deps.binName} computer install --yes\`.`);
|
|
107
|
+
}
|
|
108
|
+
function leaseOptions(deps, host, waitMs = 0) {
|
|
109
|
+
const idleSeconds = deps.config().idle_seconds;
|
|
110
|
+
return {
|
|
111
|
+
dir: deps.leaseDir(host.desktopStateDir),
|
|
112
|
+
idleMs: idleSeconds !== undefined ? Math.round(idleSeconds * 1000) : deps.defaultIdleMs,
|
|
113
|
+
waitMs,
|
|
114
|
+
now: deps.now,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function expiresInSeconds(lease, nowMs) {
|
|
118
|
+
return Math.max(0, Math.ceil((Date.parse(lease.expires_at) - nowMs) / 1000));
|
|
119
|
+
}
|
|
120
|
+
function describeLease(lease, nowMs) {
|
|
121
|
+
return `${lease.owner_label} (generation ${lease.generation}, idle-expires in ${expiresInSeconds(lease, nowMs)}s)`;
|
|
122
|
+
}
|
|
123
|
+
function heldError(deps, holder) {
|
|
124
|
+
return new ComputerError("computer_lease_held", `Desktop is held by ${describeLease(holder, deps.now())}. Wait, or ask them to run \`${deps.binName} computer lease release\`.`, EXIT_LEASE_HELD);
|
|
125
|
+
}
|
|
126
|
+
function parseSeconds(raw, flag) {
|
|
127
|
+
if (raw === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
const value = Number(raw);
|
|
130
|
+
if (!Number.isFinite(value) || value < 0 || value > 3600) {
|
|
131
|
+
throw new ComputerError("computer_usage", `${flag} must be a number of seconds from 0 to 3600`);
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
function parseCallArgs(raw) {
|
|
136
|
+
if (raw === undefined || raw.trim() === "")
|
|
137
|
+
return {};
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse(raw);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
throw new ComputerError("computer_usage", `Tool arguments must be JSON: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
146
|
+
throw new ComputerError("computer_usage", "Tool arguments must be a JSON object.");
|
|
147
|
+
}
|
|
148
|
+
return parsed;
|
|
149
|
+
}
|
|
150
|
+
const PNG_B64_RE = /"([A-Za-z0-9_]*png_b64)"(\s*:\s*)"((?:[^"\\]|\\.)*)"/g;
|
|
151
|
+
/**
|
|
152
|
+
* Replace inline base64 screenshots in driver output with a short note, so a
|
|
153
|
+
* single state read cannot flood the calling agent's context. Works on the raw
|
|
154
|
+
* text, so the driver's formatting survives.
|
|
155
|
+
*/
|
|
156
|
+
export function redactInlineScreenshots(stdout) {
|
|
157
|
+
return stdout.replace(PNG_B64_RE, (_match, key, sep, value) => `"${key}"${sep}"<omitted ${value.length} chars; pass screenshot_out_file or --keep-screenshot>"`);
|
|
158
|
+
}
|
|
159
|
+
function printDriverResult(deps, result, stdout = result.stdout) {
|
|
160
|
+
if (stdout)
|
|
161
|
+
emitLine(deps.emit, stdout);
|
|
162
|
+
if (result.stderr)
|
|
163
|
+
deps.writeStderr(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
|
|
164
|
+
if (result.exitCode !== 0)
|
|
165
|
+
deps.emit.setExitCode(result.exitCode);
|
|
166
|
+
}
|
|
167
|
+
async function ensureDaemon(deps, host) {
|
|
168
|
+
const current = deps.driver.daemonStatus(host);
|
|
169
|
+
if (current.running)
|
|
170
|
+
return current;
|
|
171
|
+
const started = await deps.driver.startDaemon(host);
|
|
172
|
+
if (!started.running) {
|
|
173
|
+
throw new ComputerError("computer_daemon_failed", `The desktop driver daemon did not start${started.detail ? `: ${started.detail}` : "."}`);
|
|
174
|
+
}
|
|
175
|
+
return started;
|
|
176
|
+
}
|
|
177
|
+
/** Write human-readable text, ending it with exactly one newline. */
|
|
178
|
+
function emitLine(emit, s) {
|
|
179
|
+
emit.text(s.endsWith("\n") ? s : `${s}\n`);
|
|
180
|
+
}
|
|
181
|
+
/** Run one action, turning a ComputerError (or any throw) into emit.error + exit code. */
|
|
182
|
+
function guarded(input, body) {
|
|
183
|
+
return async (...args) => {
|
|
184
|
+
const deps = await resolveDeps(input);
|
|
185
|
+
try {
|
|
186
|
+
await body(deps, ...args);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
if (err instanceof ComputerError) {
|
|
190
|
+
deps.emit.error({
|
|
191
|
+
code: err.code,
|
|
192
|
+
message: err.message,
|
|
193
|
+
...(err.hint ? { hint: err.hint } : {}),
|
|
194
|
+
});
|
|
195
|
+
deps.emit.setExitCode(err.exitCode);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
deps.emit.error({ code: "computer_error", message: err.message ?? String(err) });
|
|
199
|
+
deps.emit.setExitCode(1);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Pull `{name, status, message}` checks out of the driver's health report, leniently. */
|
|
204
|
+
function healthChecks(json) {
|
|
205
|
+
const source = Array.isArray(json)
|
|
206
|
+
? json
|
|
207
|
+
: json && typeof json === "object"
|
|
208
|
+
? json.checks
|
|
209
|
+
: undefined;
|
|
210
|
+
const toCheck = (name, value) => {
|
|
211
|
+
if (!value || typeof value !== "object")
|
|
212
|
+
return undefined;
|
|
213
|
+
const record = value;
|
|
214
|
+
const status = record.status ??
|
|
215
|
+
record.state ??
|
|
216
|
+
(record.ok === true ? "pass" : record.ok === false ? "fail" : undefined);
|
|
217
|
+
if (typeof status !== "string")
|
|
218
|
+
return undefined;
|
|
219
|
+
const message = record.message ?? record.detail;
|
|
220
|
+
return {
|
|
221
|
+
name: typeof record.name === "string" ? record.name : name,
|
|
222
|
+
status,
|
|
223
|
+
...(typeof message === "string" ? { message } : {}),
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
if (Array.isArray(source)) {
|
|
227
|
+
const checks = source
|
|
228
|
+
.map((value, i) => toCheck(`check-${i + 1}`, value))
|
|
229
|
+
.filter((c) => c !== undefined);
|
|
230
|
+
return checks.length > 0 ? checks : undefined;
|
|
231
|
+
}
|
|
232
|
+
if (source && typeof source === "object") {
|
|
233
|
+
const checks = Object.entries(source)
|
|
234
|
+
.map(([name, value]) => toCheck(name, value))
|
|
235
|
+
.filter((c) => c !== undefined);
|
|
236
|
+
return checks.length > 0 ? checks : undefined;
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
// "skip" is a check that does not apply on this platform, not a failure.
|
|
241
|
+
const PASSING = new Set(["pass", "passed", "ok", "healthy", "success", "skip", "skipped", "info"]);
|
|
242
|
+
/**
|
|
243
|
+
* Build the `computer` command tree under `parent` (a fresh root when omitted)
|
|
244
|
+
* and return it. Tests pass fakes for host/driver/lease/owner/config.
|
|
245
|
+
*/
|
|
246
|
+
export function createComputerCommand(input, parent = new Command()) {
|
|
247
|
+
const bin = input.binName;
|
|
248
|
+
const computer = parent
|
|
249
|
+
.command("computer")
|
|
250
|
+
.description("Opt-in desktop control through a pinned desktop driver: install it, run its daemon, " +
|
|
251
|
+
"and call its tools while holding a desktop lease shared by every agent on this machine.")
|
|
252
|
+
.addHelpText("after", `\nEnable per project with "computer": { "enabled": true } in .harnery/config.jsonc.\n` +
|
|
253
|
+
`Exit codes: 0 ok · 1 error · ${EXIT_LEASE_HELD} desktop held by another agent.\n` +
|
|
254
|
+
`Start with \`${bin} computer status\`.`);
|
|
255
|
+
// ------------------------------------------------------------------ status
|
|
256
|
+
computer
|
|
257
|
+
.command("status")
|
|
258
|
+
.description("Show the host, pinned driver, install, daemon, desktop lease holder, and enabled flag.")
|
|
259
|
+
.option("--json", "Emit the status as JSON.")
|
|
260
|
+
.action(guarded(input, (deps, opts) => {
|
|
261
|
+
const config = deps.config();
|
|
262
|
+
const host = deps.host();
|
|
263
|
+
const supported = host.kind !== "unsupported" && Boolean(host.desktopStateDir);
|
|
264
|
+
const installed = supported ? (deps.driver.installedExecutable(host) ?? null) : null;
|
|
265
|
+
let daemon = null;
|
|
266
|
+
if (installed) {
|
|
267
|
+
try {
|
|
268
|
+
daemon = deps.driver.daemonStatus(host);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
daemon = { running: false, detail: err.message };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const lease = supported ? deps.lease.read(leaseOptions(deps, host)) : null;
|
|
275
|
+
const nowMs = deps.now();
|
|
276
|
+
if (opts.json) {
|
|
277
|
+
deps.emit.data({
|
|
278
|
+
enabled: config.enabled,
|
|
279
|
+
host: { kind: host.kind, ...(host.reason ? { reason: host.reason } : {}) },
|
|
280
|
+
driver: { id: deps.driver.id, version: deps.driver.version },
|
|
281
|
+
installed,
|
|
282
|
+
daemon,
|
|
283
|
+
lease: lease ? { ...lease, expires_in_seconds: expiresInSeconds(lease, nowMs) } : null,
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const lines = [
|
|
288
|
+
`enabled: ${config.enabled ? "yes" : `no (${enableHint()})`}`,
|
|
289
|
+
`host: ${host.kind}${host.reason ? ` (${host.reason})` : ""}`,
|
|
290
|
+
`driver: ${deps.driver.id} ${deps.driver.version} (pinned)`,
|
|
291
|
+
];
|
|
292
|
+
if (supported) {
|
|
293
|
+
lines.push(`installed: ${installed ?? "not installed"}`);
|
|
294
|
+
lines.push(`daemon: ${daemon
|
|
295
|
+
? daemon.running
|
|
296
|
+
? `running${daemon.pid !== undefined ? ` (pid ${daemon.pid})` : ""}`
|
|
297
|
+
: `stopped${daemon.detail ? ` (${daemon.detail})` : ""}`
|
|
298
|
+
: "n/a"}`);
|
|
299
|
+
lines.push(`lease: ${lease ? `held by ${describeLease(lease, nowMs)}` : "free"}`);
|
|
300
|
+
}
|
|
301
|
+
emitLine(deps.emit, lines.join("\n"));
|
|
302
|
+
}));
|
|
303
|
+
// ----------------------------------------------------------------- install
|
|
304
|
+
computer
|
|
305
|
+
.command("install")
|
|
306
|
+
.description("Preview the pinned driver install (URL, SHA-256, install dir, preference file); --yes downloads, verifies, and installs it.")
|
|
307
|
+
.option("--yes", "Apply the install. Without it, only the plan is shown.")
|
|
308
|
+
.option("--json", "Emit the plan or result as JSON.")
|
|
309
|
+
.action(guarded(input, async (deps, opts) => {
|
|
310
|
+
requireEnabled(deps);
|
|
311
|
+
const host = requireSupported(deps.host());
|
|
312
|
+
const plan = deps.driver.planInstall(host);
|
|
313
|
+
if (!opts.yes) {
|
|
314
|
+
if (opts.json) {
|
|
315
|
+
deps.emit.data({ applied: false, plan });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
emitLine(deps.emit, [
|
|
319
|
+
`${plan.alreadyInstalled ? "Already installed" : "Would install"}: ${plan.driver} ${plan.version} for ${plan.host}`,
|
|
320
|
+
` url: ${plan.artifact.url}`,
|
|
321
|
+
` sha256: ${plan.artifact.sha256}`,
|
|
322
|
+
` install dir: ${plan.installDir}`,
|
|
323
|
+
` executable: ${plan.executable}`,
|
|
324
|
+
` preferences: ${plan.preferenceFile} ${JSON.stringify(plan.preferences)}`,
|
|
325
|
+
plan.alreadyInstalled
|
|
326
|
+
? "Nothing to do."
|
|
327
|
+
: `Re-run with --yes to install: \`${deps.binName} computer install --yes\`.`,
|
|
328
|
+
].join("\n"));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const result = await deps.driver.install(plan);
|
|
332
|
+
if (opts.json) {
|
|
333
|
+
deps.emit.data({ applied: true, result });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
emitLine(deps.emit, result.installed
|
|
337
|
+
? `Installed ${result.driver} ${result.version} at ${result.executable}.\nStart it with \`${deps.binName} computer up\`.`
|
|
338
|
+
: `${result.driver} ${result.version} was already installed at ${result.executable}.`);
|
|
339
|
+
}));
|
|
340
|
+
// ---------------------------------------------------------------- up/down
|
|
341
|
+
computer
|
|
342
|
+
.command("up")
|
|
343
|
+
.description("Start the desktop driver daemon (unelevated) if it is not running.")
|
|
344
|
+
.action(guarded(input, async (deps) => {
|
|
345
|
+
requireEnabled(deps);
|
|
346
|
+
const host = requireSupported(deps.host());
|
|
347
|
+
requireInstalled(deps, host);
|
|
348
|
+
const status = await ensureDaemon(deps, host);
|
|
349
|
+
emitLine(deps.emit, `Daemon running${status.pid !== undefined ? ` (pid ${status.pid})` : ""}.`);
|
|
350
|
+
}));
|
|
351
|
+
computer
|
|
352
|
+
.command("down")
|
|
353
|
+
.description("Stop the desktop driver daemon.")
|
|
354
|
+
.action(guarded(input, (deps) => {
|
|
355
|
+
const host = requireSupported(deps.host());
|
|
356
|
+
requireInstalled(deps, host);
|
|
357
|
+
const status = deps.driver.stopDaemon(host);
|
|
358
|
+
emitLine(deps.emit, status.running ? `Daemon still running: ${status.detail}` : "Daemon stopped.");
|
|
359
|
+
if (status.running)
|
|
360
|
+
deps.emit.setExitCode(1);
|
|
361
|
+
}));
|
|
362
|
+
// ------------------------------------------------------------------ doctor
|
|
363
|
+
computer
|
|
364
|
+
.command("doctor")
|
|
365
|
+
.description("Run the driver's own health report; any non-passing check exits 1.")
|
|
366
|
+
.option("--json", "Emit the checks as JSON.")
|
|
367
|
+
.action(guarded(input, (deps, opts) => {
|
|
368
|
+
const host = requireSupported(deps.host());
|
|
369
|
+
requireInstalled(deps, host);
|
|
370
|
+
const result = deps.driver.health(host);
|
|
371
|
+
let json = result.json;
|
|
372
|
+
if (json === undefined) {
|
|
373
|
+
try {
|
|
374
|
+
json = JSON.parse(result.stdout);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
json = undefined;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const checks = healthChecks(json);
|
|
381
|
+
const failing = checks?.filter((c) => !PASSING.has(c.status.toLowerCase())) ?? [];
|
|
382
|
+
const ok = result.exitCode === 0 && checks !== undefined && failing.length === 0;
|
|
383
|
+
if (opts.json) {
|
|
384
|
+
deps.emit.data({
|
|
385
|
+
ok,
|
|
386
|
+
exit_code: result.exitCode,
|
|
387
|
+
checks: checks ?? null,
|
|
388
|
+
report: json ?? result.stdout,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
else if (checks) {
|
|
392
|
+
emitLine(deps.emit, checks
|
|
393
|
+
.map((c) => `${c.status.padEnd(6)} ${c.name}${c.message ? `: ${c.message}` : ""}`)
|
|
394
|
+
.join("\n"));
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
if (result.stdout)
|
|
398
|
+
emitLine(deps.emit, result.stdout);
|
|
399
|
+
emitLine(deps.emit, "No structured checks in the driver's health report.");
|
|
400
|
+
}
|
|
401
|
+
if (result.stderr)
|
|
402
|
+
deps.writeStderr(result.stderr);
|
|
403
|
+
if (!ok)
|
|
404
|
+
deps.emit.setExitCode(1);
|
|
405
|
+
}));
|
|
406
|
+
// ---------------------------------------------------------- tools/describe
|
|
407
|
+
computer
|
|
408
|
+
.command("tools")
|
|
409
|
+
.description("List the driver's tools (name: description).")
|
|
410
|
+
.action(guarded(input, (deps) => {
|
|
411
|
+
const host = requireSupported(deps.host());
|
|
412
|
+
requireInstalled(deps, host);
|
|
413
|
+
printDriverResult(deps, deps.driver.listTools(host));
|
|
414
|
+
}));
|
|
415
|
+
computer
|
|
416
|
+
.command("describe <tool>")
|
|
417
|
+
.description("Show one tool's description and input schema.")
|
|
418
|
+
.action(guarded(input, (deps, tool) => {
|
|
419
|
+
const host = requireSupported(deps.host());
|
|
420
|
+
requireInstalled(deps, host);
|
|
421
|
+
printDriverResult(deps, deps.driver.describe(host, tool));
|
|
422
|
+
}));
|
|
423
|
+
// -------------------------------------------------------------------- call
|
|
424
|
+
computer
|
|
425
|
+
.command("call <tool> [json]")
|
|
426
|
+
.description("Call one driver tool with a JSON object of arguments, holding the desktop lease. " +
|
|
427
|
+
"`session` defaults to your agent name; inline base64 screenshots are redacted unless --keep-screenshot.")
|
|
428
|
+
.option("--wait <seconds>", "How long to wait for another agent's lease (default: config wait_seconds or 30).")
|
|
429
|
+
.option("--keep-screenshot", "Print inline base64 screenshots instead of redacting them.")
|
|
430
|
+
.action(guarded(input, async (deps, tool, rawJson, opts) => {
|
|
431
|
+
requireEnabled(deps);
|
|
432
|
+
const host = requireSupported(deps.host());
|
|
433
|
+
requireInstalled(deps, host);
|
|
434
|
+
const args = parseCallArgs(rawJson);
|
|
435
|
+
const waitSeconds = parseSeconds(opts.wait, "--wait") ??
|
|
436
|
+
deps.config().wait_seconds ??
|
|
437
|
+
DEFAULT_CALL_WAIT_SECONDS;
|
|
438
|
+
const owner = deps.owner();
|
|
439
|
+
if (!("session" in args))
|
|
440
|
+
args.session = owner.label;
|
|
441
|
+
const acquired = await deps.lease.acquire(owner, leaseOptions(deps, host, Math.round(waitSeconds * 1000)));
|
|
442
|
+
if (!acquired.ok)
|
|
443
|
+
throw heldError(deps, acquired.holder);
|
|
444
|
+
await ensureDaemon(deps, host);
|
|
445
|
+
const translated = deps.driver.translateArgsPaths(host, tool, args);
|
|
446
|
+
const result = deps.driver.call(host, tool, translated);
|
|
447
|
+
let stdout = deps.driver.translateResultPaths(host, result.stdout);
|
|
448
|
+
if (!opts.keepScreenshot)
|
|
449
|
+
stdout = redactInlineScreenshots(stdout);
|
|
450
|
+
printDriverResult(deps, result, stdout);
|
|
451
|
+
}));
|
|
452
|
+
// ------------------------------------------------------------------- lease
|
|
453
|
+
const lease = computer
|
|
454
|
+
.command("lease")
|
|
455
|
+
.description("Inspect, take, or release the desktop lease shared by every agent on this machine.");
|
|
456
|
+
lease
|
|
457
|
+
.command("status")
|
|
458
|
+
.description("Show the current desktop lease holder, or that the desktop is free.")
|
|
459
|
+
.option("--json", "Emit the lease as JSON (null when free).")
|
|
460
|
+
.action(guarded(input, (deps, opts) => {
|
|
461
|
+
const host = requireSupported(deps.host());
|
|
462
|
+
const current = deps.lease.read(leaseOptions(deps, host));
|
|
463
|
+
const nowMs = deps.now();
|
|
464
|
+
if (opts.json) {
|
|
465
|
+
deps.emit.data({
|
|
466
|
+
lease: current
|
|
467
|
+
? { ...current, expires_in_seconds: expiresInSeconds(current, nowMs) }
|
|
468
|
+
: null,
|
|
469
|
+
});
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
emitLine(deps.emit, current ? `held by ${describeLease(current, nowMs)}` : "free");
|
|
473
|
+
}));
|
|
474
|
+
lease
|
|
475
|
+
.command("acquire")
|
|
476
|
+
.description("Take (or refresh) the desktop lease for yourself without calling a tool.")
|
|
477
|
+
.option("--wait <seconds>", "How long to wait for another agent's lease (default 0).")
|
|
478
|
+
.action(guarded(input, async (deps, opts) => {
|
|
479
|
+
requireEnabled(deps);
|
|
480
|
+
const host = requireSupported(deps.host());
|
|
481
|
+
const waitSeconds = parseSeconds(opts.wait, "--wait") ?? 0;
|
|
482
|
+
const result = await deps.lease.acquire(deps.owner(), leaseOptions(deps, host, Math.round(waitSeconds * 1000)));
|
|
483
|
+
if (!result.ok)
|
|
484
|
+
throw heldError(deps, result.holder);
|
|
485
|
+
emitLine(deps.emit, `${result.fresh ? "Acquired" : "Refreshed"} the desktop lease: ${describeLease(result.lease, deps.now())}.`);
|
|
486
|
+
}));
|
|
487
|
+
lease
|
|
488
|
+
.command("release")
|
|
489
|
+
.description("Release the desktop lease if you hold it.")
|
|
490
|
+
.action(guarded(input, (deps) => {
|
|
491
|
+
const host = requireSupported(deps.host());
|
|
492
|
+
const owner = deps.owner();
|
|
493
|
+
const released = deps.lease.release(owner, leaseOptions(deps, host));
|
|
494
|
+
if (released) {
|
|
495
|
+
emitLine(deps.emit, `Released the desktop lease held by ${owner.label}.`);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const current = deps.lease.read(leaseOptions(deps, host));
|
|
499
|
+
if (!current) {
|
|
500
|
+
emitLine(deps.emit, "The desktop was not leased.");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
throw new ComputerError("computer_lease_held", `Not released: the desktop is held by ${describeLease(current, deps.now())}, not ${owner.label}.`, EXIT_LEASE_HELD);
|
|
504
|
+
}));
|
|
505
|
+
return computer;
|
|
506
|
+
}
|
|
507
|
+
export function registerComputerCommand(program, emit, binName) {
|
|
508
|
+
createComputerCommand({ emit, binName }, program);
|
|
509
|
+
}
|
package/dist/commands/docs.js
CHANGED
|
@@ -277,7 +277,9 @@ async function handleLint(opts) {
|
|
|
277
277
|
error_count: errors.length,
|
|
278
278
|
warning_count: warnings.length,
|
|
279
279
|
cold_handoffs: cold,
|
|
280
|
-
|
|
280
|
+
// Errors first: a reader (or a truncated pipe) sees what blocks before
|
|
281
|
+
// thousands of advisory warnings.
|
|
282
|
+
violations: [...errors, ...warnings],
|
|
281
283
|
});
|
|
282
284
|
if (errors.length > 0)
|
|
283
285
|
emit.setExitCode(1);
|
package/dist/commands/fetch.js
CHANGED
|
@@ -56,6 +56,17 @@ async function runFetch(url, opts, emit, context) {
|
|
|
56
56
|
}
|
|
57
57
|
headers[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
|
|
58
58
|
}
|
|
59
|
+
// Caller headers only: the User-Agent above is ours, not the caller's.
|
|
60
|
+
if (context?.headerPolicy) {
|
|
61
|
+
const host = new URL(url).hostname;
|
|
62
|
+
for (const h of opts.header ?? []) {
|
|
63
|
+
const idx = h.indexOf(":");
|
|
64
|
+
const name = h.slice(0, idx).trim();
|
|
65
|
+
const verdict = context.headerPolicy(host, name, h.slice(idx + 1).trim());
|
|
66
|
+
if (!verdict.allow)
|
|
67
|
+
throw new Error(verdict.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
59
70
|
const jar = opts.cookies !== false
|
|
60
71
|
? new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-fetch" })
|
|
61
72
|
: null;
|
|
@@ -74,7 +85,6 @@ async function runFetch(url, opts, emit, context) {
|
|
|
74
85
|
signal: ac.signal,
|
|
75
86
|
extraHeaders: context?.extraHeaders,
|
|
76
87
|
extraCookies: context?.extraCookies,
|
|
77
|
-
clientRedirects: context?.clientRedirects?.() || undefined,
|
|
78
88
|
responseType: "bytes",
|
|
79
89
|
});
|
|
80
90
|
}
|
package/dist/commands/grep.d.ts
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { EmitContext, HarneryProgramContext } from "../commander.js";
|
|
3
3
|
import { type SearchEngine } from "../lib/tools/ripgrep.js";
|
|
4
|
+
/**
|
|
5
|
+
* `grep`: monorepo-aware code search. Prefers ripgrep (`rg`) when it is on
|
|
6
|
+
* PATH and falls back to GNU/BSD grep transparently — both engines are driven
|
|
7
|
+
* with equivalent flags and their output is parsed into the same envelope, so
|
|
8
|
+
* results are identical (pinned by tests/unit/grep-engine.test.ts).
|
|
9
|
+
* Smart default excludes (skip dist/.next/node_modules/.git/...), repo
|
|
10
|
+
* scoping (`--repo <name>` or `--all-repos`), language presets, file-level
|
|
11
|
+
* boolean composition (`--and` / `--without`), and context (`-C`/`-A`/`-B`).
|
|
12
|
+
*
|
|
13
|
+
* Engine selection: `HARNERY_GREP_ENGINE=rg|grep` forces one; otherwise `rg`
|
|
14
|
+
* is resolved (managed install, PATH, or opt-in auto-provision; see
|
|
15
|
+
* resolveEngine) and used when available. Repos are searched in parallel, and
|
|
16
|
+
* in `--all-repos` mode the parent scan prunes submodule directories so each
|
|
17
|
+
* match is attributed to exactly one repo.
|
|
18
|
+
*
|
|
19
|
+
* Output framing: content searches request the NUL filename delimiter
|
|
20
|
+
* (`--null` on both engines — the long spelling, because BSD grep repurposes
|
|
21
|
+
* `-Z` for decompression) so filename boundaries are never inferred from
|
|
22
|
+
* punctuation. Context lines are NOT requested from the engines: matches are
|
|
23
|
+
* selected (and budgeted) first, then context windows are materialized from
|
|
24
|
+
* one file read per selected file, which keeps `--limit` semantics exact and
|
|
25
|
+
* both engines byte-identical.
|
|
26
|
+
*
|
|
27
|
+
* Default behavior matches extended-regex semantics (`grep -E` / ripgrep's
|
|
28
|
+
* default). Use `-F` / `--literal` to pin to literal-string mode. Output is
|
|
29
|
+
* line-oriented `file:line:content` in TTY mode (context rows render
|
|
30
|
+
* grep-style as `file-line-content`); `--json` emits the full GrepResult
|
|
31
|
+
* envelope `{ pattern, mode, engine, and_patterns, without_patterns, repos,
|
|
32
|
+
* total_matches, total_files, truncated, elapsed_ms }`. Matches are sorted
|
|
33
|
+
* (file, then line) for stable output across runs and engines.
|
|
34
|
+
*
|
|
35
|
+
* Output and paging: a search shows every result row unless `--limit <n>`
|
|
36
|
+
* sets a page size, and clips each printed line to DEFAULT_MAX_COLUMNS
|
|
37
|
+
* characters (`--max-columns 0` turns clipping off). 500 columns keeps any
|
|
38
|
+
* hand-written source line and long prose lines while cutting minified or
|
|
39
|
+
* generated ones; on the v2 tune split a 300-column clip lost a claim that
|
|
40
|
+
* 500 kept, at about 8% more tokens than 300. There is
|
|
41
|
+
* no default row bound: on the context bench's v2 tune split, every default
|
|
42
|
+
* from 50 to 300 rows lost answer evidence that the unbounded search found
|
|
43
|
+
* (plan 2026-10-31_context-round-trips, Track D). When rows are left out, a count pass
|
|
44
|
+
* (the same search with `-c`, which prints one short row per file) supplies
|
|
45
|
+
* the totals, and the last line states how many matches and files were left
|
|
46
|
+
* out and the `--page <n>` that continues. Pages are cut from one global order
|
|
47
|
+
* (repo order, then file path, then line), so `--page 2` starts exactly where
|
|
48
|
+
* page 1 stopped, whichever engine ran. A content page is fetched by searching
|
|
49
|
+
* only the files that hold its rows.
|
|
50
|
+
*/
|
|
51
|
+
/** Characters kept per printed line unless --max-columns says otherwise. */
|
|
52
|
+
export declare const DEFAULT_MAX_COLUMNS = 500;
|
|
4
53
|
export type GrepEngine = SearchEngine;
|
|
5
54
|
export interface GrepOpts {
|
|
6
55
|
repo?: string;
|
|
@@ -18,6 +67,11 @@ export interface GrepOpts {
|
|
|
18
67
|
beforeContext?: string;
|
|
19
68
|
maxCount?: string;
|
|
20
69
|
limit?: string;
|
|
70
|
+
/** 1-based page of `limit`-sized pages. */
|
|
71
|
+
page?: string;
|
|
72
|
+
/** Lift the default row bound (no paging). */
|
|
73
|
+
/** Characters kept per line; "0" disables clipping. */
|
|
74
|
+
maxColumns?: string;
|
|
21
75
|
include?: string[];
|
|
22
76
|
exclude?: string[];
|
|
23
77
|
and?: string[];
|
|
@@ -33,6 +87,8 @@ export interface Match {
|
|
|
33
87
|
text: string;
|
|
34
88
|
/** "match" = primary result row (counts toward totals + --limit); "context" = free -C/-A/-B row. */
|
|
35
89
|
kind: "match" | "context";
|
|
90
|
+
/** Characters removed from `text` by --max-columns clipping (absent when whole). */
|
|
91
|
+
clipped?: number;
|
|
36
92
|
}
|
|
37
93
|
export declare function registerGrepCommand(program: Command, emit: EmitContext, context?: HarneryProgramContext): void;
|
|
38
94
|
export interface GrepResult {
|
|
@@ -47,9 +103,23 @@ export interface GrepResult {
|
|
|
47
103
|
matches: Match[];
|
|
48
104
|
truncated: boolean;
|
|
49
105
|
}[];
|
|
106
|
+
/** Result rows shown on this page (context rows excluded). */
|
|
50
107
|
total_matches: number;
|
|
108
|
+
/** Files with at least one shown row. */
|
|
51
109
|
total_files: number;
|
|
110
|
+
/** More rows exist after this page. */
|
|
52
111
|
truncated: boolean;
|
|
112
|
+
/** Page size; null without --limit. */
|
|
113
|
+
limit: number | null;
|
|
114
|
+
page: number;
|
|
115
|
+
/** Result rows / files the whole search found, across every page. */
|
|
116
|
+
available_matches: number;
|
|
117
|
+
available_files: number;
|
|
118
|
+
/** Rows / files the search found that this page does not show. */
|
|
119
|
+
omitted_matches: number;
|
|
120
|
+
omitted_files: number;
|
|
121
|
+
/** The --page value that continues, or null on the last page. */
|
|
122
|
+
next_page: number | null;
|
|
53
123
|
elapsed_ms: number;
|
|
54
124
|
}
|
|
55
125
|
/** Exported for tests (not part of the package exports map). */
|
|
@@ -77,5 +147,7 @@ export declare class NulDecoder {
|
|
|
77
147
|
flush(): Omit<Match, "repo" | "kind">[];
|
|
78
148
|
private decodeRecord;
|
|
79
149
|
}
|
|
150
|
+
/** Exported for tests (not part of the package exports map). */
|
|
151
|
+
export declare function renderResult(r: GrepResult, opts: GrepOpts): string;
|
|
80
152
|
export {};
|
|
81
153
|
//# sourceMappingURL=grep.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../src/commands/grep.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../src/commands/grep.ts"],"names":[],"mappings":"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"}
|