@hmharness/agent 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/dist/runner.js ADDED
@@ -0,0 +1,311 @@
1
+ /**
2
+ * @hmharness/agent - runner
3
+ * The shared agent-task execution layer. CLI maps its events to terminal
4
+ * output; the web frontend maps them to SSE - one behavior, two frontends.
5
+ * Also owns the native registry factory (spawn_agent recursion) and the
6
+ * approval gate construction.
7
+ */
8
+ import { homeDir, loadConfig, resolveProvider, mcpServerTools, Registry, runLoop, Session, } from '@hmharness/kernel';
9
+ import { appendMemory, listSkills, readInsights, readNotes, recentInsights, recordInsight, retrieveMemory, skillsToPrompt, sessionGetsCanary, canaryWatermark, listCanary } from '@hmharness/evolution';
10
+ import { harmonyTools } from '@hmharness/domain-harmony';
11
+ import { opsTools } from '@hmharness/domain-ops';
12
+ import * as readline from 'node:readline/promises';
13
+ import { stdin } from 'node:process';
14
+ import { baseTools } from "./tools.js";
15
+ import { buildSystemPrompt } from "./prompt.js";
16
+ import { strings } from "./i18n.js";
17
+ import { makeSpawnTool, MAX_SPAWN_DEPTH } from "./spawn.js";
18
+ /** Flatten the config.json shape into the runtime discriminated union. */
19
+ export function toServerConfig(c) {
20
+ if (c.type === 'http')
21
+ return { type: 'http', url: c.url ?? '', headers: c.headers, trusted: c.trusted };
22
+ return { type: 'stdio', command: c.command ?? '', args: c.args, env: c.env, trusted: c.trusted };
23
+ }
24
+ /**
25
+ * Current spawn base, set per task so a long-lived registry (REPL, web
26
+ * server) always routes sub-agents to the CURRENT session and gate.
27
+ */
28
+ export const spawnBase = {};
29
+ export function nativeRegistry(depth) {
30
+ const reg = new Registry();
31
+ reg.registerAll(baseTools).registerAll(harmonyTools).registerAll(opsTools);
32
+ if (depth < MAX_SPAWN_DEPTH) {
33
+ reg.register(makeSpawnTool({
34
+ depth,
35
+ getBase: () => spawnBase.current ?? {
36
+ provider: { baseUrl: '', apiKey: '', model: '' },
37
+ ctx: { cwd: process.cwd(), home: homeDir() },
38
+ },
39
+ buildChildRegistry: nativeRegistry,
40
+ }));
41
+ }
42
+ return reg;
43
+ }
44
+ export async function buildRegistry(opts = {}) {
45
+ const reg = nativeRegistry(0);
46
+ const clients = [];
47
+ if (opts.mcp !== false) {
48
+ const cfg = await loadConfig();
49
+ const servers = Object.entries(cfg.mcpServers ?? {});
50
+ if (servers.length > 0) {
51
+ await Promise.all(servers.map(async ([name, raw]) => {
52
+ try {
53
+ const { client, tools } = await mcpServerTools(name, toServerConfig(raw));
54
+ for (const t of tools) {
55
+ try {
56
+ reg.register(t);
57
+ }
58
+ catch {
59
+ /* name collision after sanitization - first server wins */
60
+ }
61
+ }
62
+ clients.push(client);
63
+ if (opts.announce !== false)
64
+ console.log(` [mcp] ${name}: ${tools.length} tools attached`);
65
+ }
66
+ catch (err) {
67
+ if (opts.announce !== false)
68
+ console.log(` [mcp] ${name}: unavailable (${String(err).slice(0, 140)})`);
69
+ }
70
+ }));
71
+ }
72
+ }
73
+ return { reg, clients };
74
+ }
75
+ /** Retrieval-based context pack: task-relevant memories, not the whole file.
76
+ * P0 canary: ~20% of sessions (deterministic by session id) also receive
77
+ * the canary skill block, watermarked as experimental references - the
78
+ * impact loop compares these sessions against the rest. */
79
+ export async function contextPack(task, sessionId) {
80
+ const home = homeDir();
81
+ const [memory, skills, insights] = await Promise.all([
82
+ retrieveMemory(home, task),
83
+ listSkills(home),
84
+ recentInsights(home),
85
+ ]);
86
+ let canaryBlock = '';
87
+ let canaryNames = [];
88
+ if (sessionId && sessionGetsCanary(sessionId)) {
89
+ const canary = await listCanary(home);
90
+ if (canary.length > 0) {
91
+ canaryNames = canary.map((s) => s.name);
92
+ canaryBlock = canaryWatermark(canaryNames) + '\n' + skillsToPrompt(canary);
93
+ }
94
+ }
95
+ return { memory, skills: skillsToPrompt(skills) + (canaryBlock ? '\n' + canaryBlock : ''), insights, skillsInjected: [...skills.map((s) => s.name), ...canaryNames] };
96
+ }
97
+ /**
98
+ * Terminal approval gate: auto mode passes everything; a TTY gets a y/N
99
+ * prompt (reusing a caller-provided readline); a pipe gets a safe deny.
100
+ * The kernel loop denies by default when no gate is wired at all.
101
+ */
102
+ export function makeApproval(cfg, yes, sharedRl) {
103
+ const t = strings(cfg.locale ?? 'zh');
104
+ return {
105
+ async ask(toolName, args) {
106
+ if (yes || cfg.approval === 'auto')
107
+ return true;
108
+ const brief = JSON.stringify(args).slice(0, 120);
109
+ if (!stdin.isTTY) {
110
+ process.stdout.write(`\x1b[33m${t.approvalDeniedNoTty(toolName, brief)}\x1b[0m\n`);
111
+ return false;
112
+ }
113
+ const rl = sharedRl ?? readline.createInterface({ input: stdin, output: process.stdout });
114
+ let answer;
115
+ try {
116
+ answer = (await rl.question(`\x1b[33m${t.approvalPrompt(toolName, brief)}\x1b[0m`)).trim().toLowerCase();
117
+ }
118
+ finally {
119
+ if (!sharedRl)
120
+ rl.close();
121
+ }
122
+ return answer === 'y' || answer === 'yes';
123
+ },
124
+ };
125
+ }
126
+ /** Run one full agent task end-to-end; audit + insight recording included. */
127
+ export async function runAgentTask(opts) {
128
+ const cfg = opts.cfg ?? (await loadConfig());
129
+ const ctx = opts.ctx ?? { cwd: process.cwd(), home: homeDir() };
130
+ const events = opts.events ?? {};
131
+ const session = new Session(ctx.home, ctx.cwd, cfg.provider.model);
132
+ // contextPack needs the session id: canary injection is deterministic
133
+ // per-session (stable attribution), decided before the prompt is built
134
+ const pack = await contextPack(opts.task, session.id);
135
+ const system = buildSystemPrompt({
136
+ cwd: ctx.cwd,
137
+ home: ctx.home,
138
+ memory: pack.memory,
139
+ skills: pack.skills,
140
+ insights: pack.insights,
141
+ model: cfg.provider.model,
142
+ locale: cfg.locale,
143
+ });
144
+ await session.user(opts.task);
145
+ const approval = opts.approvalAsk ? { ask: opts.approvalAsk } : makeApproval(cfg, opts.yes === true);
146
+ spawnBase.current = {
147
+ provider: resolveProvider(cfg, 'chat'),
148
+ ctx,
149
+ approval,
150
+ session,
151
+ onLine: (l) => events.onLine?.(l),
152
+ };
153
+ const messages = [
154
+ { role: 'system', content: system },
155
+ ...(opts.resumeMessages ?? []),
156
+ { role: 'user', content: opts.task },
157
+ ];
158
+ const toolsUsed = [];
159
+ // self-noted failure patterns: 2+ errors from one tool become a memory
160
+ // note, so the NEXT session starts knowing what broke this one (the
161
+ // self-evolution loop's missing per-session feedback channel)
162
+ const toolErrors = new Map();
163
+ const result = await runLoop({
164
+ provider: resolveProvider(cfg, 'chat'),
165
+ registry: opts.registry,
166
+ messages,
167
+ ctx,
168
+ maxTurns: cfg.maxTurns,
169
+ maxContextChars: cfg.maxContextChars,
170
+ approval: spawnBase.current.approval,
171
+ events: {
172
+ onDelta: (kind, chunk) => events.onDelta?.(kind, chunk),
173
+ onToolCall: (name, args) => {
174
+ toolsUsed.push(name);
175
+ events.onToolCall?.(name, args);
176
+ },
177
+ onToolResult: (name, output, isError) => {
178
+ if (isError) {
179
+ const list = toolErrors.get(name) ?? [];
180
+ list.push(output.split('\n')[0].slice(0, 120));
181
+ toolErrors.set(name, list);
182
+ }
183
+ void session.tool(name, output, isError);
184
+ events.onToolResult?.(name, output, isError);
185
+ },
186
+ onApproval: (name, args, granted) => {
187
+ void session.approval(name, granted);
188
+ events.onApproval?.(name, args, granted);
189
+ },
190
+ onAssistant: async (m) => {
191
+ await session.assistant(m.content ?? null, m.tool_calls);
192
+ },
193
+ },
194
+ });
195
+ // ---- instant feedback: learn from THIS task's mistakes, not 8 tasks later ----
196
+ // Tier 1 (always, zero cost): raw error pattern → memory self-note. Lowered
197
+ // to 1 failure for system-level patterns (shell incompat, auth, missing
198
+ // binary) - those never self-correct on retry; 2 for generic errors.
199
+ try {
200
+ const SYSTEM_ERR = /not (recognized|found|exist)|ENOENT|EACCES|ECONN|HTTP 4\d\d|authentication|unauthorized|command not found|is not an? (internal|external)/i;
201
+ for (const [name, errs] of toolErrors) {
202
+ const systemLevel = errs.some((e) => SYSTEM_ERR.test(e));
203
+ const threshold = systemLevel ? 1 : 2;
204
+ if (errs.length < threshold)
205
+ continue;
206
+ const notes = await readNotes(ctx.home);
207
+ const last = notes.slice(-40).map((n) => n.text).join('\n');
208
+ if (last.includes(`[self-note] tool ${name}`))
209
+ continue;
210
+ await appendMemory(ctx.home, `[self-note] tool ${name} failed ${errs.length}x in one session; samples: ${[...new Set(errs)].slice(0, 2).join(' | ')}`);
211
+ }
212
+ }
213
+ catch {
214
+ /* memory is best-effort; never fail the task on it */
215
+ }
216
+ // Tier 2 (if errors occurred): one quick model call - "what went wrong,
217
+ // what to do differently" - written to memory immediately. This is the
218
+ // per-session reflection the user asked for: mistakes corrected in real
219
+ // time, not batched 8 sessions later.
220
+ if (toolErrors.size > 0) {
221
+ void (async () => {
222
+ try {
223
+ const { chat: chatFn } = await import('@hmharness/kernel');
224
+ const provider = resolveProvider(cfg, 'evolve');
225
+ if (!provider.apiKey)
226
+ return;
227
+ const errSummary = [...toolErrors.entries()].map(([n, e]) => `${n}: ${[...new Set(e)].slice(0, 2).join('; ')}`).join('\n').slice(0, 600);
228
+ const task = opts.task.slice(0, 150);
229
+ const r = await chatFn(provider, [
230
+ { role: 'system', content: 'You distill agent failure lessons. Given a task and its tool errors, output ONE actionable note (max 180 chars) starting with a verb: what to do differently next time on THIS machine/environment. If the errors are trivial/transient, output exactly NONE.' },
231
+ { role: 'user', content: `Task: ${task}\nTool errors:\n${errSummary}` },
232
+ ]);
233
+ const lesson = (r.message.content ?? '').trim();
234
+ if (lesson && lesson.toUpperCase() !== 'NONE' && lesson.length < 300) {
235
+ await appendMemory(ctx.home, `[lesson] ${lesson}`);
236
+ }
237
+ }
238
+ catch {
239
+ /* reflection is best-effort */
240
+ }
241
+ })();
242
+ }
243
+ await session.final(result.text, result.turns, result.toolUses);
244
+ await recordInsight(ctx.home, {
245
+ time: new Date().toISOString(),
246
+ session: session.id,
247
+ task: opts.task.slice(0, 120),
248
+ outcome: result.turns >= cfg.maxTurns ? 'turn-budget' : 'ok',
249
+ turns: result.turns,
250
+ toolUses: result.toolUses,
251
+ toolsUsed: [...new Set(toolsUsed)],
252
+ skillsInjected: pack.skillsInjected,
253
+ });
254
+ // daily self-evolution: every N insights, one background cycle fires
255
+ // (default on; autoEvolveEvery: 0 disables). Fire-and-forget - it never
256
+ // blocks the reply, and its own guards (bench gate, holdout, poison
257
+ // screen, skills/+memory/ only) apply unchanged.
258
+ const every = cfg.autoEvolveEvery ?? 3;
259
+ if (every > 0) {
260
+ try {
261
+ const count = (await readInsights(ctx.home, 10_000)).length;
262
+ if (count > 0 && count % every === 0)
263
+ void triggerBackgroundEvolve(ctx.home);
264
+ }
265
+ catch {
266
+ /* insight count is best-effort */
267
+ }
268
+ }
269
+ events.onFinal?.({ text: result.text, turns: result.turns, toolUses: result.toolUses, sessionId: session.id, usage: result.usage });
270
+ return { ...result, sessionId: session.id, toolsUsed: [...new Set(toolsUsed)] };
271
+ }
272
+ /** One background evolution cycle (auto-triggered). Logs to the evolution
273
+ * journal only; failures never surface into the user's chat. */
274
+ async function triggerBackgroundEvolve(home) {
275
+ try {
276
+ const { runEvolution } = await import('@hmharness/evolution');
277
+ const { defaultConfig, loadConfig, resolveProvider, chat } = await import('@hmharness/kernel');
278
+ const cfg = await loadConfig();
279
+ const provider = resolveProvider(cfg, 'evolve');
280
+ if (!provider.apiKey)
281
+ return; // no provider configured - stay quiet
282
+ const reg = nativeRegistry(0);
283
+ await runEvolution({
284
+ home,
285
+ provider,
286
+ runCase: async (c) => {
287
+ if (c.tools) {
288
+ const { buildSystemPrompt } = await import("./prompt.js");
289
+ const res2 = await runLoop({
290
+ provider,
291
+ registry: reg,
292
+ messages: [
293
+ { role: 'system', content: buildSystemPrompt({ cwd: process.cwd(), home, memory: '', skills: '', insights: '', model: provider.model }) },
294
+ { role: 'user', content: c.prompt },
295
+ ],
296
+ ctx: { cwd: process.cwd(), home },
297
+ maxTurns: 6,
298
+ });
299
+ return res2.text;
300
+ }
301
+ const r = await chat(provider, [{ role: 'user', content: c.prompt }]);
302
+ return r.message.content ?? '';
303
+ },
304
+ log: () => undefined,
305
+ });
306
+ void defaultConfig; // referenced for type stability of the dynamic import
307
+ }
308
+ catch {
309
+ /* background cycle failures are recorded by runEvolution itself or stay silent */
310
+ }
311
+ }
@@ -0,0 +1,32 @@
1
+ import { type Session, type LoopApproval, type Registry, type Tool } from '@hmharness/kernel';
2
+ export declare const MAX_SPAWN_DEPTH = 2;
3
+ export interface SpawnBase {
4
+ provider: import('@hmharness/kernel').ProviderConfig;
5
+ ctx: import('@hmharness/kernel').ToolContext;
6
+ approval?: LoopApproval;
7
+ session?: Session;
8
+ /** Bubbled tool traffic for display: `[sub1] list_dir {...}`. */
9
+ onLine?(line: string): void;
10
+ }
11
+ /** Aggregated per-role outcomes from the spawn journal (HMH_HOME). */
12
+ export interface RoleStat {
13
+ role: string;
14
+ spawns: number;
15
+ /** ok = finished within its turn budget without tool errors */
16
+ ok: number;
17
+ }
18
+ export declare function recordRole(home: string, role: string, ok: boolean): Promise<void>;
19
+ export declare function roleStats(home: string, limit?: number): Promise<RoleStat[]>;
20
+ /** One-line leaderboard the parent sees before it delegates (>=3 samples). */
21
+ export declare function roleStatsLine(stats: RoleStat[]): string;
22
+ /**
23
+ * Resolved lazily at each spawn so a long-lived REPL registry always sees the
24
+ * CURRENT task's session/approval, not the one from when it was built.
25
+ */
26
+ export interface SpawnDeps {
27
+ depth: number;
28
+ getBase(): SpawnBase;
29
+ /** Build the registry for a child at the given depth (no spawn at the cap). */
30
+ buildChildRegistry(depth: number): Registry;
31
+ }
32
+ export declare function makeSpawnTool(deps: SpawnDeps): Tool;
package/dist/spawn.js ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @hmharness/cli - spawn
3
+ * The sub-agent tool: run a nested agent loop with a FRESH context on a
4
+ * self-contained subtask and return its final answer. Children share the
5
+ * tool registry (minus MCP - children stay fast and deterministic) and the
6
+ * approval gate, but never the parent's conversation - context isolation
7
+ * is the point. Depth-capped so a confused model can't fork-bomb itself.
8
+ *
9
+ * P3 role records: an optional `role` labels the sub-agent (e.g.
10
+ * "explorer", "reviewer"); outcome stats accumulate in HMH_HOME and are
11
+ * surfaced in the parent's next spawn - the honest minimal version of
12
+ * agent-topology evolution: no MARL, just per-role success rates the
13
+ * model can read before delegating. Weak roles get called out; the
14
+ * model (or user) stops asking them for that kind of work.
15
+ */
16
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
17
+ import { join } from 'node:path';
18
+ import { runLoop } from '@hmharness/kernel';
19
+ export const MAX_SPAWN_DEPTH = 2;
20
+ export async function recordRole(home, role, ok) {
21
+ try {
22
+ const dir = join(home, 'evolution');
23
+ await mkdir(dir, { recursive: true });
24
+ await appendFile(join(dir, 'spawn-roles.jsonl'), JSON.stringify({ time: new Date().toISOString(), role, ok }) + '\n', 'utf8');
25
+ }
26
+ catch { /* stats are best-effort */ }
27
+ }
28
+ export async function roleStats(home, limit = 200) {
29
+ try {
30
+ const text = await readFile(join(home, 'evolution', 'spawn-roles.jsonl'), 'utf8');
31
+ const lines = text.trim().split('\n').filter(Boolean).slice(-limit);
32
+ const by = new Map();
33
+ for (const l of lines) {
34
+ try {
35
+ const r = JSON.parse(l);
36
+ const s = by.get(r.role) ?? { role: r.role, spawns: 0, ok: 0 };
37
+ s.spawns++;
38
+ if (r.ok)
39
+ s.ok++;
40
+ by.set(r.role, s);
41
+ }
42
+ catch { /* skip corrupt */ }
43
+ }
44
+ return [...by.values()].sort((a, b) => (b.ok / b.spawns) - (a.ok / a.spawns));
45
+ }
46
+ catch {
47
+ return [];
48
+ }
49
+ }
50
+ /** One-line leaderboard the parent sees before it delegates (>=3 samples). */
51
+ export function roleStatsLine(stats) {
52
+ const shown = stats.filter((s) => s.spawns >= 3).slice(0, 5);
53
+ if (shown.length === 0)
54
+ return '';
55
+ return `Role track record (prefer roles with high ok-rates for similar work): ${shown.map((s) => `${s.role} ${(100 * s.ok / s.spawns).toFixed(0)}% (${s.spawns}x)`).join(', ')}.`;
56
+ }
57
+ export function makeSpawnTool(deps) {
58
+ return {
59
+ name: 'spawn_agent',
60
+ description: 'Run a sub-agent with a fresh context on a self-contained subtask (e.g. "explore the project layout and report module names", "find which file defines X"). Returns the sub-agent\'s final answer. The sub-agent has the same tools but NO conversation history and no MCP tools - include every detail it needs in the task. Use it to keep this conversation small: delegate exploration and focused lookups.',
61
+ parameters: {
62
+ type: 'object',
63
+ properties: {
64
+ task: { type: 'string', description: 'complete, self-contained instructions for the sub-agent' },
65
+ role: { type: 'string', description: 'optional label for this delegation, e.g. "explorer", "reviewer", "build-fixer" - roles accumulate success rates you will see next time' },
66
+ max_turns: { type: 'number', description: 'turn budget for the sub-agent (default 8, max 12)' },
67
+ },
68
+ required: ['task'],
69
+ },
70
+ async execute(args) {
71
+ const base = deps.getBase();
72
+ if (deps.depth >= MAX_SPAWN_DEPTH) {
73
+ return { output: `spawn depth cap (${MAX_SPAWN_DEPTH}) reached. Do the work directly instead.`, isError: true };
74
+ }
75
+ const task = String(args.task ?? '').trim();
76
+ if (!task)
77
+ return { output: 'spawn_agent requires a non-empty task.', isError: true };
78
+ const role = String(args.role ?? '').trim().toLowerCase().slice(0, 24);
79
+ const childDepth = deps.depth + 1;
80
+ const tag = role || `sub${childDepth}`;
81
+ const maxTurns = Math.min(Math.max(Number(args.max_turns ?? 8), 1), 12);
82
+ const registry = deps.buildChildRegistry(childDepth);
83
+ // P3 tournament signal: show the role leaderboard (if any) to the
84
+ // parent model in the delegation prompt when the child runs with a role
85
+ let roleLine = '';
86
+ if (role) {
87
+ try {
88
+ const stats = await roleStats(base.ctx.home, 200);
89
+ roleLine = roleStatsLine(stats) + '\n';
90
+ }
91
+ catch { /* best-effort */ }
92
+ }
93
+ const system = [
94
+ `You are a hmh sub-agent (depth ${childDepth}${role ? `, role: ${role}` : ''}). You have no conversation history beyond this task.`,
95
+ role ? `Perform the ${role} duty with that specialty's discipline.` : '',
96
+ 'Do exactly what the task asks, use tools as needed, verify before answering, and reply with a concise result (the caller only sees your final answer).',
97
+ ].filter(Boolean).join('\n');
98
+ base.onLine?.(`[${tag}] start: ${task.slice(0, 80)}`);
99
+ let sawError = false;
100
+ const result = await runLoop({
101
+ provider: base.provider,
102
+ registry,
103
+ messages: [
104
+ { role: 'system', content: roleLine + system },
105
+ { role: 'user', content: task },
106
+ ],
107
+ ctx: base.ctx,
108
+ maxTurns,
109
+ approval: base.approval,
110
+ events: {
111
+ onToolCall: (name, a) => base.onLine?.(`[${tag}] ${name} ${JSON.stringify(a).slice(0, 80)}`),
112
+ onToolResult: (name, output, isError) => {
113
+ if (isError) {
114
+ sawError = true;
115
+ base.onLine?.(`[${tag}] ${name} ERROR: ${output.slice(0, 100)}`);
116
+ }
117
+ void base.session?.tool(`${tag}>${name}`, output, isError);
118
+ },
119
+ },
120
+ });
121
+ const text = result.text || '(sub-agent returned empty output)';
122
+ // P3 record: ok = answered within budget and no tool errored
123
+ if (role)
124
+ await recordRole(base.ctx.home, role, result.turns < maxTurns && !sawError);
125
+ base.onLine?.(`[${tag}] done (${result.turns} turns, ${result.toolUses} tool uses)`);
126
+ return { output: text.length > 20_000 ? text.slice(0, 20_000) + '\n...[truncated]' : text };
127
+ },
128
+ };
129
+ }
@@ -0,0 +1,52 @@
1
+ import { type Tool } from '@hmharness/kernel';
2
+ export declare const readFileTool: Tool;
3
+ export declare const writeFileTool: Tool;
4
+ export declare const listDirTool: Tool;
5
+ /** Host-shell pipe preflight: on Windows cmd, Unix-isms fail with cryptic
6
+ * mojibake and the model retries for many turns. Only the FIRST word of
7
+ * each host segment (split on | || && ;) is checked - Unix words inside
8
+ * arguments (docker exec c ls /app) belong to the container and stay legal.
9
+ * (Pure, testable.) */
10
+ export declare function unixPipeOnWindows(command: string, platform?: string): string | null;
11
+ export declare function commandPreflight(command: string, cwd: string): Promise<string | null>;
12
+ export declare const runCommandTool: Tool;
13
+ /** Zero-dependency web search: DuckDuckGo HTML endpoint, no API key. */
14
+ export declare const webSearchTool: Tool;
15
+ /** Fetch a URL and return readable text (tags stripped, entities decoded,
16
+ * size-bounded). Pairs with web_search: search finds, fetch reads. */
17
+ export declare const webFetchTool: Tool;
18
+ /** Desktop automation, step 1 (first-class): screenshot the primary display
19
+ * to a PNG and hand the path back - the agent then reads it with see_image
20
+ * (vision chain). Windows via PowerShell System.Drawing; other platforms
21
+ * report the gap honestly instead of failing silently. */
22
+ export declare const desktopScreenshotTool: Tool;
23
+ /** Desktop automation steps 2-3: click and type. PowerShell + Win32 for
24
+ * mouse (SetCursorPos + mouse_event), SendKeys for keyboard. Pair with
25
+ * desktop_screenshot + see_image: look → act → verify. */
26
+ export declare const desktopClickTool: Tool;
27
+ export declare const desktopTypeTool: Tool;
28
+ /** Browser automation, first-class entry point: open a URL in the user's
29
+ * default browser (visible window), then drive it with the desktop triad:
30
+ * desktop_screenshot + see_image to LOOK, desktop_click/desktop_type to
31
+ * ACT, desktop_screenshot again to VERIFY. This sees-plan-act loop works
32
+ * with any browser and any page (full JS rendering, login states, CAPTCHAs
33
+ * - everything a real user sees). Headless alternatives (--dump-dom etc)
34
+ * are unreliable on Windows; the visible browser is the honest primitive.
35
+ */
36
+ export declare const browserOpenTool: Tool;
37
+ /** Remote command execution over SSH (zero-dep: OpenSSH binary).
38
+ * Hosts come from config sshHosts (name/host/user/port/keyPath); secrets
39
+ * never leave HMH_HOME. Read-only commands skip the approval card,
40
+ * everything else is gated. */
41
+ export interface SshHost {
42
+ name: string;
43
+ host: string;
44
+ user: string;
45
+ port?: number;
46
+ /** private-key path; defaults to the agent's own key resolution (~/.ssh) */
47
+ keyPath?: string;
48
+ }
49
+ export declare const sshRunTool: Tool;
50
+ export declare const rememberTool: Tool;
51
+ export declare const seeImageTool: Tool;
52
+ export declare const baseTools: Tool[];