hostwares-cli 2.4.1 → 2.4.3

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/index.js CHANGED
@@ -1,199 +1,4024 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync } from "fs";
3
- import { fileURLToPath } from "url";
4
- import { dirname, join } from "path";
5
- import { setVersion } from "./agent/run.js";
6
- import { reportError } from "./errors.js";
7
- import { login } from "./auth/device.js";
8
- import { startChat } from "./commands/chat.js";
9
- import { getConfig, saveConfig, clearConfig, isAuthenticated } from "./config.js";
10
- import { listSessions } from "./session/store.js";
11
- import { c, glyph, BANNER_LINES } from "./ui/theme.js";
12
- import * as ui from "./ui/render.js";
13
- import { isAborted } from "@hostwares/agent-client";
14
- /**
15
- * Entry point.
16
- *
17
- * Argument parsing is hand-rolled rather than pulled from commander. The old
18
- * CLI depended on commander purely to register a dozen subcommands that each
19
- * did `chat("Deploy a site named ...")` - they were natural-language prompts
20
- * wearing a subcommand costume, and the dependency bought nothing. What is left
21
- * here is either a real API call or a thin wrapper that says what it forwards.
22
- */
23
- const VERSION = readVersion();
24
- setVersion(VERSION);
25
- async function main(argv) {
26
- const [cmd, ...rest] = argv;
27
- switch (cmd) {
28
- case undefined:
29
- return interactive();
30
- case "chat":
31
- requireAuth();
32
- return startChat({ resume: rest.includes("--resume") || rest.includes("-r") });
33
- case "ask": {
34
- requireAuth();
35
- const message = rest.filter(a => !a.startsWith("-")).join(" ");
36
- if (!message)
37
- return startChat({});
38
- return startChat({ oneShot: message });
39
- }
40
- case "login":
41
- // --token skips the browser flow for CI and scripted installs.
42
- if (rest[0] === "--token" && rest[1]) {
43
- saveConfig({ apiKey: rest[1] });
44
- ui.success("Signed in with the supplied token.");
45
- return 0;
46
- }
47
- return (await login({ noBrowser: rest.includes("--no-browser") })) ? 0 : 1;
48
- case "logout":
49
- clearConfig();
50
- ui.success("Signed out on this machine.");
51
- return 0;
52
- case "sessions":
53
- return showSessions();
54
- case "list":
55
- case "ls":
56
- requireAuth();
57
- return listSites();
58
- case "version":
59
- case "--version":
60
- case "-v":
61
- console.log(VERSION);
62
- return 0;
63
- case "help":
64
- case "--help":
65
- case "-h":
66
- showHelp();
67
- return 0;
68
- default:
69
- // An unrecognised first word is far more likely to be a question than a
70
- // typo'd command, so it is forwarded rather than rejected. `hw why is my
71
- // site down` should work.
72
- requireAuth();
73
- return startChat({ oneShot: argv.join(" ") });
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+
17
+ // node_modules/@hostwares/agent-client/dist/esm/protocol/errors.js
18
+ function isAborted(e) {
19
+ return e instanceof AbortedError || e instanceof Error && (e.name === "AbortError" || e.message === "Aborted");
20
+ }
21
+ async function apiErrorFromResponse(res) {
22
+ const raw = await res.text().catch(() => "");
23
+ let body = {};
24
+ try {
25
+ const parsed = raw ? JSON.parse(raw) : {};
26
+ if (parsed && typeof parsed === "object")
27
+ body = parsed;
28
+ } catch {
29
+ }
30
+ const code = typeof body.code === "string" ? body.code : typeof body.error === "string" ? body.error : null;
31
+ const message = typeof body.message === "string" ? body.message : typeof body.error === "string" ? body.error : raw.trim() && !raw.trimStart().startsWith("<") ? raw.trim().slice(0, 200) : defaultMessageFor(res.status);
32
+ return new ApiError(res.status, message, code, body);
33
+ }
34
+ function defaultMessageFor(status) {
35
+ if (status === 401)
36
+ return "Not signed in.";
37
+ if (status === 402)
38
+ return "Out of credits.";
39
+ if (status === 403)
40
+ return "Not allowed.";
41
+ if (status === 404)
42
+ return "Not found.";
43
+ if (status === 429)
44
+ return "Too many requests - slow down.";
45
+ if (status >= 500)
46
+ return "Hostwares is having trouble right now.";
47
+ return `Request failed (HTTP ${status}).`;
48
+ }
49
+ var ApiError, NetworkError, AbortedError;
50
+ var init_errors = __esm({
51
+ "node_modules/@hostwares/agent-client/dist/esm/protocol/errors.js"() {
52
+ ApiError = class extends Error {
53
+ status;
54
+ /** Server-supplied machine code, e.g. "invalid_api_key", "rate_limited". */
55
+ code;
56
+ /** Whatever else the error body carried - `balance`, `limit`, `termsUrl`, ... */
57
+ body;
58
+ constructor(status, message, code, body = {}) {
59
+ super(message);
60
+ this.name = "ApiError";
61
+ this.status = status;
62
+ this.code = code;
63
+ this.body = body;
64
+ }
65
+ /** The credential is dead or missing - re-authenticating can fix this. */
66
+ get isAuthFailure() {
67
+ return this.status === 401;
68
+ }
69
+ /** Worth retrying after a wait: rate limits and transient server faults. */
70
+ get isRetryable() {
71
+ return this.status === 429 || this.status >= 500 && this.status < 600;
72
+ }
73
+ /** Out of credits. Actionable by the user, not by a retry. */
74
+ get isOutOfCredits() {
75
+ return this.status === 402;
76
+ }
77
+ };
78
+ NetworkError = class extends Error {
79
+ /** The underlying fetch/DNS/socket error. Named `reason` rather than `cause`
80
+ * so it does not shadow Error.cause, which Node uses when printing. */
81
+ reason;
82
+ constructor(message, reason) {
83
+ super(message, { cause: reason });
84
+ this.name = "NetworkError";
85
+ this.reason = reason;
86
+ }
87
+ get isRetryable() {
88
+ return true;
89
+ }
90
+ };
91
+ AbortedError = class extends Error {
92
+ constructor() {
93
+ super("Aborted");
94
+ this.name = "AbortedError";
95
+ }
96
+ };
97
+ }
98
+ });
99
+
100
+ // node_modules/@hostwares/agent-client/dist/esm/protocol/sse.js
101
+ async function readSse(body, onFrame, signal) {
102
+ const reader = body.getReader();
103
+ const decoder = new TextDecoder();
104
+ let buffer = "";
105
+ try {
106
+ for (; ; ) {
107
+ const { done, value } = await reader.read();
108
+ if (done)
109
+ break;
110
+ buffer += decoder.decode(value, { stream: true });
111
+ let boundary;
112
+ while ((boundary = buffer.indexOf("\n\n")) !== -1) {
113
+ const raw = buffer.slice(0, boundary);
114
+ buffer = buffer.slice(boundary + 2);
115
+ const frame = parseFrame(raw);
116
+ if (frame)
117
+ onFrame(frame);
118
+ }
74
119
  }
120
+ } finally {
121
+ reader.releaseLock();
122
+ if (signal?.aborted)
123
+ throw new AbortedError();
124
+ }
75
125
  }
76
- function interactive() {
77
- showBanner();
78
- if (!isAuthenticated()) {
79
- ui.line(` Run ${c.bold("hw login")} to get started.`);
80
- ui.line();
81
- return Promise.resolve(0);
126
+ function parseFrame(raw) {
127
+ let event = "message";
128
+ const dataLines = [];
129
+ for (const line2 of raw.split("\n")) {
130
+ if (line2.startsWith(":"))
131
+ continue;
132
+ if (line2.startsWith("event:"))
133
+ event = line2.slice(6).trim();
134
+ else if (line2.startsWith("data:"))
135
+ dataLines.push(line2.slice(5).replace(/^ /, ""));
136
+ }
137
+ if (dataLines.length === 0)
138
+ return null;
139
+ try {
140
+ return { event, data: JSON.parse(dataLines.join("\n")) };
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ var init_sse = __esm({
146
+ "node_modules/@hostwares/agent-client/dist/esm/protocol/sse.js"() {
147
+ init_errors();
148
+ }
149
+ });
150
+
151
+ // node_modules/@hostwares/agent-client/dist/esm/protocol/client.js
152
+ async function streamChat(creds, req, handlers, opts = {}) {
153
+ const base = (creds.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
154
+ let triedReauth = false;
155
+ for (; ; ) {
156
+ if (opts.signal?.aborted)
157
+ throw new AbortedError();
158
+ let res;
159
+ try {
160
+ res = await fetch(`${base}/api/chat/stream`, {
161
+ method: "POST",
162
+ headers: {
163
+ "Content-Type": "application/json",
164
+ "Accept": "text/event-stream",
165
+ ...creds.userAgent ? { "User-Agent": creds.userAgent } : {},
166
+ Authorization: `Bearer ${creds.apiKey}`
167
+ },
168
+ body: JSON.stringify(req),
169
+ signal: opts.signal
170
+ });
171
+ } catch (e) {
172
+ if (opts.signal?.aborted)
173
+ throw new AbortedError();
174
+ throw new NetworkError("Could not reach Hostwares. Check your connection.", e);
175
+ }
176
+ if (!res.ok) {
177
+ const err = await apiErrorFromResponse(res);
178
+ if (err.isAuthFailure && opts.onAuthFailure && !triedReauth) {
179
+ triedReauth = true;
180
+ if (await opts.onAuthFailure())
181
+ continue;
182
+ }
183
+ throw err;
82
184
  }
83
- return startChat({});
185
+ if (!res.body)
186
+ throw new NetworkError("Server sent an empty response.", null);
187
+ return consume(res.body, handlers, opts.signal);
188
+ }
84
189
  }
85
- function requireAuth() {
86
- if (isAuthenticated())
190
+ async function consume(body, h, signal) {
191
+ let done = null;
192
+ let streamError = null;
193
+ await readSse(body, ({ event, data }) => {
194
+ switch (event) {
195
+ case "meta":
196
+ h.onMeta?.(data);
197
+ break;
198
+ case "text":
199
+ h.onText?.(data.content);
200
+ break;
201
+ case "tool_start":
202
+ h.onToolStart?.(data);
203
+ break;
204
+ case "tool_executing":
205
+ h.onToolExecuting?.(data);
206
+ break;
207
+ case "tool_done":
208
+ h.onToolDone?.(data);
209
+ break;
210
+ case "local_tool":
211
+ h.onLocalTool?.(data);
212
+ break;
213
+ case "done":
214
+ done = data;
215
+ h.onDone?.(done);
216
+ break;
217
+ case "error":
218
+ streamError = String(data?.message ?? "Unknown error");
219
+ h.onError?.(streamError);
220
+ break;
221
+ }
222
+ }, signal);
223
+ if (streamError && !done)
224
+ throw new ApiError(500, streamError, "stream_error");
225
+ return done;
226
+ }
227
+ async function listConversations(creds, opts = {}) {
228
+ const base = (creds.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
229
+ const res = await fetch(`${base}/api/chat/conversations?limit=${opts.limit ?? 15}`, {
230
+ headers: { Authorization: `Bearer ${creds.apiKey}` },
231
+ signal: opts.signal
232
+ });
233
+ if (!res.ok)
234
+ throw await apiErrorFromResponse(res);
235
+ const body = await res.json();
236
+ const rows = Array.isArray(body) ? body : body.data ?? [];
237
+ return rows;
238
+ }
239
+ async function resolveAction(creds, action, opts = {}) {
240
+ const base = (creds.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
241
+ const body = action.approve ? { confirmAction: action.id } : { rejectAction: action.id };
242
+ for (let attempt = 0; attempt < 2; attempt++) {
243
+ const res = await fetch(`${base}/api/chat`, {
244
+ method: "POST",
245
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
246
+ body: JSON.stringify(body),
247
+ signal: opts.signal
248
+ });
249
+ if (res.ok)
250
+ return await res.json();
251
+ const err = await apiErrorFromResponse(res);
252
+ if (err.isAuthFailure && opts.onAuthFailure && attempt === 0 && await opts.onAuthFailure())
253
+ continue;
254
+ throw err;
255
+ }
256
+ throw new NetworkError("Could not resolve the action.", null);
257
+ }
258
+ var DEFAULT_BASE_URL;
259
+ var init_client = __esm({
260
+ "node_modules/@hostwares/agent-client/dist/esm/protocol/client.js"() {
261
+ init_sse();
262
+ init_errors();
263
+ DEFAULT_BASE_URL = "https://hostwares.com";
264
+ }
265
+ });
266
+
267
+ // node_modules/@hostwares/agent-client/dist/esm/protocol/events.js
268
+ var silentSink;
269
+ var init_events = __esm({
270
+ "node_modules/@hostwares/agent-client/dist/esm/protocol/events.js"() {
271
+ silentSink = () => {
272
+ };
273
+ }
274
+ });
275
+
276
+ // node_modules/@hostwares/agent-client/dist/esm/agent/exec.js
277
+ import { spawn } from "child_process";
278
+ function exec(file, args, opts = {}) {
279
+ return run(file, args, false, opts);
280
+ }
281
+ function execShell(command, opts = {}) {
282
+ const isWin = process.platform === "win32";
283
+ const shell = isWin ? process.env.COMSPEC || "cmd.exe" : process.env.SHELL && !process.env.SHELL.includes("fish") ? process.env.SHELL : "/bin/sh";
284
+ const args = isWin ? ["/d", "/s", "/c", command] : ["-c", command];
285
+ return run(shell, args, true, opts);
286
+ }
287
+ function run(file, args, viaShell, opts) {
288
+ return new Promise((resolve2) => {
289
+ const spawnOpts = {
290
+ cwd: opts.cwd || process.cwd(),
291
+ env: opts.env ?? process.env,
292
+ // Never true: `args` would then be re-parsed by a shell and the argv array
293
+ // would buy nothing. execShell() runs the shell AS the program instead.
294
+ shell: false,
295
+ windowsHide: true
296
+ };
297
+ let child;
298
+ try {
299
+ child = spawn(file, args, spawnOpts);
300
+ } catch (e) {
301
+ return resolve2(failure(startupMessage(e, file, viaShell)));
302
+ }
303
+ const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
304
+ let stdout = "";
305
+ let stderr = "";
306
+ let truncated = false;
307
+ let timedOut = false;
308
+ let settled = false;
309
+ const append = (target, chunk) => {
310
+ opts.onOutput?.(chunk, target);
311
+ const current = target === "stdout" ? stdout : stderr;
312
+ if (current.length >= maxBuffer) {
313
+ truncated = true;
314
+ return;
315
+ }
316
+ const room = maxBuffer - current.length;
317
+ const slice = chunk.length > room ? chunk.slice(0, room) : chunk;
318
+ if (target === "stdout")
319
+ stdout += slice;
320
+ else
321
+ stderr += slice;
322
+ if (slice.length < chunk.length)
323
+ truncated = true;
324
+ };
325
+ child.stdout?.setEncoding("utf8");
326
+ child.stderr?.setEncoding("utf8");
327
+ child.stdout?.on("data", (c2) => append("stdout", c2));
328
+ child.stderr?.on("data", (c2) => append("stderr", c2));
329
+ const timer = setTimeout(() => {
330
+ timedOut = true;
331
+ child.kill("SIGTERM");
332
+ setTimeout(() => {
333
+ if (!settled)
334
+ child.kill("SIGKILL");
335
+ }, 2e3).unref?.();
336
+ }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
337
+ const onAbort = () => {
338
+ child.kill("SIGTERM");
339
+ };
340
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
341
+ const finish2 = (result) => {
342
+ if (settled)
87
343
  return;
88
- ui.error("Not signed in.");
89
- ui.note("Run `hw login` first.");
90
- process.exit(1);
344
+ settled = true;
345
+ clearTimeout(timer);
346
+ opts.signal?.removeEventListener("abort", onAbort);
347
+ resolve2(truncated ? { ...result, stdout: mark(result.stdout), stderr: mark(result.stderr) } : result);
348
+ };
349
+ child.on("error", (e) => finish2(failure(startupMessage(e, file, viaShell), stdout, stderr)));
350
+ child.on("close", (code, signal) => finish2({
351
+ stdout,
352
+ stderr,
353
+ code,
354
+ signal,
355
+ timedOut,
356
+ ok: code === 0 && !timedOut
357
+ }));
358
+ });
91
359
  }
92
- function showBanner() {
93
- const tag = [
94
- ` ${c.bold("Hostwares")} ${c.dim(`v${VERSION}`)}`,
95
- ` ${c.dim("AI DevOps in your terminal")}`,
96
- "",
97
- isAuthenticated()
98
- ? ` ${c.green(glyph.tick)} ${c.dim("Signed in")}`
99
- : ` ${c.dim(`${glyph.arrow} Not signed in`)}`,
100
- ];
101
- ui.line();
102
- BANNER_LINES.forEach((art, i) => ui.line(` ${c.green(art)}${tag[i] ?? ""}`));
103
- ui.line();
104
- ui.line(c.dim(` Ask anything. ${c.bold("/help")} for commands, ${c.bold("!cmd")} for a shell command.`));
105
- ui.line();
360
+ function mark(s) {
361
+ return s ? `${s}
362
+ ... (output truncated)` : s;
106
363
  }
107
- function showHelp() {
108
- ui.line();
109
- ui.line(` ${c.bold("hw")} ${c.dim("— AI DevOps in your terminal")}`);
110
- ui.line();
111
- const rows = [
112
- ["hw", "Start an interactive session"],
113
- ["hw ask \"...\"", "Ask one question and exit"],
114
- ["hw chat --resume", "Continue this folder's last conversation"],
115
- ["hw list", "List your deployments"],
116
- ["hw sessions", "Sessions saved on this machine"],
117
- ["hw login", "Sign in (--token <key> for CI)"],
118
- ["hw logout", "Sign out on this machine"],
119
- ["hw version", "Print the version"],
364
+ function startupMessage(e, file, viaShell) {
365
+ const code = e?.code;
366
+ if (code === "ENOENT")
367
+ return `${viaShell ? "shell" : file}: command not found`;
368
+ if (code === "EACCES")
369
+ return `${file}: permission denied`;
370
+ return e instanceof Error ? e.message : String(e);
371
+ }
372
+ function failure(message, stdout = "", stderr = "") {
373
+ return { stdout, stderr: stderr || message, code: null, signal: null, timedOut: false, ok: false };
374
+ }
375
+ function formatResult(r, opts = {}) {
376
+ const maxChars = opts.maxChars ?? 3e4;
377
+ const parts = [];
378
+ if (r.timedOut)
379
+ parts.push("[command timed out and was killed]");
380
+ else if (r.code === null && r.signal)
381
+ parts.push(`[killed by ${r.signal}]`);
382
+ else if (!r.ok)
383
+ parts.push(`[command failed: exit code ${r.code}]`);
384
+ const out = r.stdout.trimEnd();
385
+ const err = r.stderr.trimEnd();
386
+ if (out)
387
+ parts.push(out);
388
+ if (err)
389
+ parts.push(r.ok ? err : `stderr:
390
+ ${err}`);
391
+ if (parts.length === 0)
392
+ return r.ok ? "(no output)" : `[command failed: exit code ${r.code}]`;
393
+ const text = parts.join("\n");
394
+ return text.length > maxChars ? `${text.slice(0, maxChars)}
395
+ ... (truncated, ${text.length - maxChars} more characters)` : text;
396
+ }
397
+ var DEFAULT_TIMEOUT_MS, DEFAULT_MAX_BUFFER;
398
+ var init_exec = __esm({
399
+ "node_modules/@hostwares/agent-client/dist/esm/agent/exec.js"() {
400
+ DEFAULT_TIMEOUT_MS = 12e4;
401
+ DEFAULT_MAX_BUFFER = 4 * 1024 * 1024;
402
+ }
403
+ });
404
+
405
+ // node_modules/@hostwares/agent-client/dist/esm/agent/tools.js
406
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync, rmSync } from "fs";
407
+ import { resolve as pathResolve, join, dirname } from "path";
408
+ import { homedir, platform, arch, release, totalmem, freemem, cpus } from "os";
409
+ import { spawn as spawn2 } from "child_process";
410
+ function appendBg(p, chunk) {
411
+ p.output += chunk;
412
+ if (p.output.length > BG_MAX_OUTPUT) {
413
+ p.output = "... (earlier output trimmed)\n" + p.output.slice(p.output.length - BG_MAX_OUTPUT);
414
+ }
415
+ }
416
+ function str(input, key) {
417
+ const v = input[key];
418
+ if (v === void 0 || v === null)
419
+ return void 0;
420
+ const s = String(v);
421
+ return s.length ? s : void 0;
422
+ }
423
+ function num(input, key) {
424
+ const v = Number(input[key]);
425
+ return Number.isFinite(v) ? v : void 0;
426
+ }
427
+ function bool(input, key) {
428
+ return input[key] === true || input[key] === "true";
429
+ }
430
+ function required(input, key) {
431
+ const v = str(input, key);
432
+ if (v === void 0)
433
+ throw new ToolInputError(`Missing required argument "${key}".`);
434
+ return v;
435
+ }
436
+ function resolvePath(p) {
437
+ const expanded = p === "~" ? homedir() : p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
438
+ return pathResolve(expanded);
439
+ }
440
+ async function executeLocalTool(name, rawInput, runOpts = {}) {
441
+ const input = rawInput ?? {};
442
+ try {
443
+ return await run2(name, input, runOpts);
444
+ } catch (e) {
445
+ if (e instanceof ToolInputError)
446
+ return `Error: ${e.message}`;
447
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
448
+ }
449
+ }
450
+ async function run2(name, input, runOpts) {
451
+ const cwd = str(input, "workingDir") ?? str(input, "directory");
452
+ const dir = cwd ? resolvePath(cwd) : runOpts.cwd ?? process.cwd();
453
+ const execOpts = { signal: runOpts.signal, onOutput: runOpts.onOutput };
454
+ switch (name) {
455
+ // ── filesystem ──────────────────────────────────────────────────────────
456
+ case "read_file": {
457
+ const path = resolvePath(required(input, "path"));
458
+ if (!existsSync(path))
459
+ return `Error: no such file: ${path}`;
460
+ const st = statSync(path);
461
+ if (st.isDirectory())
462
+ return `Error: ${path} is a directory. Use list_directory.`;
463
+ if (st.size > MAX_READ_BYTES) {
464
+ return `Error: ${path} is ${(st.size / 1e6).toFixed(1)}MB, too large to read in full. Use search_files, or read part of it with run_command.`;
465
+ }
466
+ return readFileSync(path, "utf8") || "(empty file)";
467
+ }
468
+ case "write_file": {
469
+ const path = resolvePath(required(input, "path"));
470
+ const content = str(input, "content") ?? "";
471
+ mkdirSync(dirname(path), { recursive: true });
472
+ writeFileSync(path, content, "utf8");
473
+ return `Wrote ${content.length} characters to ${path}`;
474
+ }
475
+ // Internal: patch is computed in loop.ts via buildPatch, not here.
476
+ // This case is only reachable if loop bypasses (tests); keep minimal.
477
+ case "append_file": {
478
+ const path = resolvePath(required(input, "path"));
479
+ const content = str(input, "content") ?? "";
480
+ if (!existsSync(path))
481
+ return `Error: no such file: ${path}. Use write_file to create it.`;
482
+ appendFileSync(path, content, "utf8");
483
+ return `Appended ${content.length} characters to ${path}`;
484
+ }
485
+ case "str_replace_file": {
486
+ const path = resolvePath(required(input, "path"));
487
+ const oldStr = required(input, "oldStr");
488
+ const newStr = str(input, "newStr") ?? "";
489
+ if (!existsSync(path))
490
+ return `Error: no such file: ${path}`;
491
+ const body = readFileSync(path, "utf8");
492
+ const count = body.split(oldStr).length - 1;
493
+ if (count === 0)
494
+ return `Error: the text to replace was not found in ${path}. Read the file and match it exactly, including whitespace.`;
495
+ if (count > 1)
496
+ return `Error: found ${count} occurrences in ${path}. Include more surrounding context so the match is unique.`;
497
+ writeFileSync(path, body.replace(oldStr, newStr), "utf8");
498
+ return `Replaced 1 occurrence in ${path}`;
499
+ }
500
+ case "list_directory": {
501
+ const path = resolvePath(str(input, "path") ?? ".");
502
+ if (!existsSync(path))
503
+ return `Error: no such directory: ${path}`;
504
+ const entries = listDir(path, bool(input, "recursive"));
505
+ return entries.length ? entries.join("\n") : "(empty directory)";
506
+ }
507
+ case "delete_path": {
508
+ const path = resolvePath(required(input, "path"));
509
+ if (!existsSync(path))
510
+ return `Error: no such path: ${path}`;
511
+ const denied = ["/", "/usr", "/etc", "/var", "/bin", "/sbin", "/lib", "/System", "/Users", "/home", "C:\\", "C:\\Windows"];
512
+ if (path === homedir() || denied.some((d) => pathResolve(d) === path)) {
513
+ return `Error: refusing to delete ${path} - that is a system or home directory.`;
514
+ }
515
+ rmSync(path, { recursive: true, force: true });
516
+ return `Deleted ${path}`;
517
+ }
518
+ case "search_files": {
519
+ const pattern = required(input, "pattern");
520
+ const include = str(input, "include");
521
+ const args = ["-rn", "--binary-files=without-match"];
522
+ if (include)
523
+ args.push(`--include=${include}`);
524
+ args.push("-e", pattern, dir);
525
+ const r = await exec("grep", args, { cwd: dir, timeoutMs: 6e4 });
526
+ if (r.code === 1 && !r.stderr.trim())
527
+ return "No matches found.";
528
+ return formatResult(r, { maxChars: 2e4 });
529
+ }
530
+ case "file_search": {
531
+ const pattern = required(input, "pattern");
532
+ const args = [
533
+ dir,
534
+ "-type",
535
+ "f",
536
+ "-not",
537
+ "-path",
538
+ "*/node_modules/*",
539
+ "-not",
540
+ "-path",
541
+ "*/.git/*",
542
+ "-iname",
543
+ pattern.includes("*") ? pattern : `*${pattern}*`
544
+ ];
545
+ const r = await exec("find", args, { cwd: dir, timeoutMs: 3e4 });
546
+ if (!r.stdout.trim())
547
+ return "No files matched.";
548
+ const lines = r.stdout.trim().split("\n").slice(0, 200);
549
+ return lines.join("\n") + (lines.length === 200 ? "\n\u2026 (truncated at 200)" : "");
550
+ }
551
+ // ── shell ───────────────────────────────────────────────────────────────
552
+ case "run_command": {
553
+ const command = required(input, "command");
554
+ return formatResult(await execShell(command, { ...execOpts, cwd: dir, timeoutMs: num(input, "timeoutMs") ?? 12e4 }));
555
+ }
556
+ case "install_package":
557
+ return formatResult(await installPackage(required(input, "name"), execOpts));
558
+ case "get_system_info":
559
+ return systemInfo();
560
+ // ── git ─────────────────────────────────────────────────────────────────
561
+ case "git_status":
562
+ return formatResult(await exec("git", ["status", "--short", "--branch"], { cwd: dir }));
563
+ case "git_add": {
564
+ const files = required(input, "files");
565
+ const paths = files.trim() === "." ? ["."] : files.split(/\s+/).filter(Boolean);
566
+ return formatResult(await exec("git", ["add", "--", ...paths], { cwd: dir }));
567
+ }
568
+ case "git_commit": {
569
+ const message = required(input, "message");
570
+ return formatResult(await exec("git", ["commit", "-m", message], { cwd: dir }));
571
+ }
572
+ case "git_push": {
573
+ const args = ["push"];
574
+ const remote = str(input, "remote");
575
+ const branch = str(input, "branch");
576
+ if (remote)
577
+ args.push(remote);
578
+ if (branch)
579
+ args.push(branch);
580
+ return formatResult(await exec("git", args, { cwd: dir, timeoutMs: 18e4 }));
581
+ }
582
+ case "git_pull": {
583
+ const args = ["pull"];
584
+ const remote = str(input, "remote");
585
+ const branch = str(input, "branch");
586
+ if (remote)
587
+ args.push(remote);
588
+ if (branch)
589
+ args.push(branch);
590
+ return formatResult(await exec("git", args, { cwd: dir, timeoutMs: 18e4 }));
591
+ }
592
+ case "git_clone": {
593
+ const url = required(input, "url");
594
+ const target = str(input, "directory");
595
+ const args = ["clone", url];
596
+ if (target)
597
+ args.push(resolvePath(target));
598
+ return formatResult(await exec("git", args, { timeoutMs: 3e5 }));
599
+ }
600
+ case "git_log": {
601
+ const count = Math.min(Math.max(num(input, "count") ?? 10, 1), 200);
602
+ return formatResult(await exec("git", ["log", `-${count}`, "--oneline", "--decorate"], { cwd: dir }));
603
+ }
604
+ case "git_diff": {
605
+ const args = ["diff"];
606
+ if (bool(input, "staged"))
607
+ args.push("--staged");
608
+ const path = str(input, "path");
609
+ if (path)
610
+ args.push("--", path);
611
+ return formatResult(await exec("git", args, { cwd: dir }), { maxChars: 4e4 });
612
+ }
613
+ case "git_branch": {
614
+ const action = str(input, "action") ?? "list";
615
+ if (action === "list")
616
+ return formatResult(await exec("git", ["branch", "-vv"], { cwd: dir }));
617
+ const branch = str(input, "name");
618
+ if (!branch)
619
+ return `Error: "name" is required for git_branch action "${action}".`;
620
+ if (action === "create")
621
+ return formatResult(await exec("git", ["checkout", "-b", branch], { cwd: dir }));
622
+ if (action === "switch")
623
+ return formatResult(await exec("git", ["checkout", branch], { cwd: dir }));
624
+ if (action === "delete")
625
+ return formatResult(await exec("git", ["branch", "-d", branch], { cwd: dir }));
626
+ return `Error: unknown git_branch action "${action}". Use list, create, switch or delete.`;
627
+ }
628
+ case "check_github_auth": {
629
+ const gh = await exec("gh", ["auth", "status"], { cwd: dir, timeoutMs: 15e3 });
630
+ if (gh.code !== null)
631
+ return formatResult(gh);
632
+ const name2 = (await exec("git", ["config", "user.name"], { cwd: dir })).stdout.trim();
633
+ const email = (await exec("git", ["config", "user.email"], { cwd: dir })).stdout.trim();
634
+ return `GitHub CLI (gh) is not installed.
635
+ git user.name: ${name2 || "(unset)"}
636
+ git user.email: ${email || "(unset)"}`;
637
+ }
638
+ // ── ssh ─────────────────────────────────────────────────────────────────
639
+ case "ssh_run": {
640
+ const host = required(input, "host");
641
+ const user = required(input, "user");
642
+ const command = required(input, "command");
643
+ return formatResult(await sshRun(host, user, command, execOpts, str(input, "password"), str(input, "keyPath")));
644
+ }
645
+ case "ssh_upload":
646
+ case "ssh_download": {
647
+ const host = required(input, "host");
648
+ const user = required(input, "user");
649
+ const localPath = resolvePath(required(input, "localPath"));
650
+ const remotePath = required(input, "remotePath");
651
+ return formatResult(await scp(host, user, localPath, remotePath, name === "ssh_upload", execOpts, str(input, "password"), str(input, "keyPath")));
652
+ }
653
+ // ── processes ───────────────────────────────────────────────────────────
654
+ case "list_processes": {
655
+ const filter = str(input, "filter");
656
+ const ps = await exec("ps", ["aux"], { timeoutMs: 2e4 });
657
+ if (!ps.ok)
658
+ return formatResult(ps);
659
+ const lines = ps.stdout.split("\n");
660
+ const body = filter ? lines.filter((l, i) => i === 0 || l.toLowerCase().includes(filter.toLowerCase())) : lines.slice(0, 40);
661
+ return body.length > 1 ? body.join("\n") : `No processes matching "${filter}".`;
662
+ }
663
+ case "kill_process": {
664
+ const target = required(input, "target");
665
+ const r = await (/^\d+$/.test(target) ? exec("kill", [target]) : exec("pkill", ["-f", target]));
666
+ if (r.code === 1 && !r.stderr.trim())
667
+ return `No process matched "${target}".`;
668
+ return r.ok ? `Killed ${target}` : formatResult(r);
669
+ }
670
+ // ── background processes ──────────────────────────────────────────────────
671
+ case "start_process": {
672
+ const command = required(input, "command");
673
+ const label = str(input, "name") ?? command.split(/\s+/)[0] ?? "process";
674
+ if (BG.has(label) && BG.get(label).exitCode === null) {
675
+ return `A background process named "${label}" is already running (pid ${BG.get(label).pid}). Use get_process_output to watch it, or stop_process to stop it first.`;
676
+ }
677
+ const isWin = process.platform === "win32";
678
+ const shell = isWin ? process.env.COMSPEC || "cmd.exe" : process.env.SHELL && !process.env.SHELL.includes("fish") ? process.env.SHELL : "/bin/sh";
679
+ const args = isWin ? ["/d", "/s", "/c", command] : ["-c", command];
680
+ let child;
681
+ try {
682
+ child = spawn2(shell, args, { cwd: dir, env: process.env, detached: !isWin, windowsHide: true });
683
+ } catch (e) {
684
+ return `Error: could not start "${label}": ${e instanceof Error ? e.message : String(e)}`;
685
+ }
686
+ const proc = {
687
+ name: label,
688
+ command,
689
+ cwd: dir,
690
+ pid: child.pid,
691
+ startedAt: Date.now(),
692
+ output: "",
693
+ exitCode: null,
694
+ exitSignal: null,
695
+ child
696
+ };
697
+ child.stdout?.setEncoding("utf8");
698
+ child.stderr?.setEncoding("utf8");
699
+ child.stdout?.on("data", (c2) => appendBg(proc, c2));
700
+ child.stderr?.on("data", (c2) => appendBg(proc, c2));
701
+ child.on("error", (e) => appendBg(proc, `
702
+ [spawn error] ${e instanceof Error ? e.message : String(e)}
703
+ `));
704
+ child.on("close", (code, signal) => {
705
+ proc.exitCode = code ?? -1;
706
+ proc.exitSignal = signal;
707
+ });
708
+ BG.set(label, proc);
709
+ await new Promise((r) => setTimeout(r, 800));
710
+ const early = proc.output.slice(0, 4e3);
711
+ const status = proc.exitCode === null ? `started (pid ${proc.pid}), still running` : `exited immediately with code ${proc.exitCode}${proc.exitSignal ? ` (${proc.exitSignal})` : ""}`;
712
+ return `Background process "${label}": ${status}.
713
+ Command: ${command}
714
+ Dir: ${dir}
715
+ ` + (early ? `
716
+ Early output:
717
+ ${early}` : `
718
+ (no output yet - poll with get_process_output "${label}")`) + `
719
+
720
+ Use get_process_output to read more, check_port to confirm it is listening, then open_browser to show the user.`;
721
+ }
722
+ case "get_process_output": {
723
+ const label = str(input, "name");
724
+ if (!label) {
725
+ if (!BG.size)
726
+ return "No background processes have been started.";
727
+ return "Background processes:\n" + [...BG.values()].map((p2) => `- ${p2.name} (pid ${p2.pid}): ${p2.exitCode === null ? "running" : `exited ${p2.exitCode}`} - ${p2.command}`).join("\n");
728
+ }
729
+ const p = BG.get(label);
730
+ if (!p)
731
+ return `Error: no background process named "${label}". Running: ${[...BG.keys()].join(", ") || "(none)"}.`;
732
+ const tail = str(input, "tail");
733
+ const body = tail === "false" ? p.output : p.output.slice(-8e3);
734
+ const status = p.exitCode === null ? `running (pid ${p.pid}, ${Math.round((Date.now() - p.startedAt) / 1e3)}s)` : `exited with code ${p.exitCode}${p.exitSignal ? ` (${p.exitSignal})` : ""}`;
735
+ return `"${label}" - ${status}.
736
+ ${body || "(no output yet)"}`;
737
+ }
738
+ case "stop_process": {
739
+ const label = required(input, "name");
740
+ const p = BG.get(label);
741
+ if (!p)
742
+ return `Error: no background process named "${label}". Running: ${[...BG.keys()].join(", ") || "(none)"}.`;
743
+ if (p.exitCode !== null) {
744
+ BG.delete(label);
745
+ return `"${label}" had already exited (code ${p.exitCode}).`;
746
+ }
747
+ try {
748
+ if (p.pid && process.platform !== "win32") {
749
+ try {
750
+ process.kill(-p.pid, "SIGTERM");
751
+ } catch {
752
+ p.child.kill("SIGTERM");
753
+ }
754
+ } else
755
+ p.child.kill("SIGTERM");
756
+ } catch (e) {
757
+ return `Error stopping "${label}": ${e instanceof Error ? e.message : String(e)}`;
758
+ }
759
+ BG.delete(label);
760
+ return `Stopped background process "${label}".`;
761
+ }
762
+ case "open_browser": {
763
+ const url = required(input, "url");
764
+ if (!/^https?:\/\//i.test(url) && !url.startsWith("file://")) {
765
+ return `Error: open_browser needs an http(s) or file URL, got "${url}".`;
766
+ }
767
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
768
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
769
+ const r = await exec(opener, args, { timeoutMs: 1e4 });
770
+ return r.ok || r.code === null ? `Opened ${url} in the user's browser. Ask them to confirm the preview looks right.` : `Could not open a browser (${formatResult(r)}). Tell the user to visit ${url} manually.`;
771
+ }
772
+ case "wait_for_url": {
773
+ const url = required(input, "url");
774
+ if (!/^https?:\/\//i.test(url))
775
+ return `Error: wait_for_url needs an http(s) URL, got "${url}".`;
776
+ const timeoutMs = Math.min(Math.max(num(input, "timeoutMs") ?? 3e4, 1e3), 12e4);
777
+ const deadline = Date.now() + timeoutMs;
778
+ let lastErr = "";
779
+ let attempts = 0;
780
+ while (Date.now() < deadline) {
781
+ if (runOpts.signal?.aborted)
782
+ return "Cancelled while waiting for the URL.";
783
+ attempts++;
784
+ try {
785
+ const ctrl = new AbortController();
786
+ const t = setTimeout(() => ctrl.abort(), 5e3);
787
+ const res = await fetch(url, { signal: ctrl.signal, redirect: "manual" });
788
+ clearTimeout(t);
789
+ const body = res.status < 400 ? await res.text().catch(() => "") : "";
790
+ const title = (body.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] ?? "").trim();
791
+ const elapsed = timeoutMs - (deadline - Date.now());
792
+ return `${url} responded ${res.status}${title ? ` \u2014 "${title}"` : ""} after ${(elapsed / 1e3).toFixed(1)}s (${attempts} attempt${attempts === 1 ? "" : "s"}). The server is up.`;
793
+ } catch (e) {
794
+ lastErr = e instanceof Error ? e.message : String(e);
795
+ await new Promise((r) => setTimeout(r, 500));
796
+ }
797
+ }
798
+ return `${url} did not respond within ${(timeoutMs / 1e3).toFixed(0)}s (${attempts} attempts). Last error: ${lastErr}. Check the process output \u2014 it may have crashed on boot or bound a different port.`;
799
+ }
800
+ // ── docker ──────────────────────────────────────────────────────────────
801
+ case "docker_ps": {
802
+ const args = ["ps", "--format", "table {{.Names}} {{.Status}} {{.Ports}}"];
803
+ if (bool(input, "all"))
804
+ args.splice(1, 0, "-a");
805
+ return formatResult(await exec("docker", args, { timeoutMs: 3e4 }));
806
+ }
807
+ case "docker_logs": {
808
+ const container = required(input, "container");
809
+ const lines = Math.min(Math.max(num(input, "lines") ?? 50, 1), 2e3);
810
+ return formatResult(await exec("docker", ["logs", "--tail", String(lines), container], { timeoutMs: 3e4 }), { maxChars: 3e4 });
811
+ }
812
+ case "docker_exec": {
813
+ const container = required(input, "container");
814
+ const command = required(input, "command");
815
+ return formatResult(await exec("docker", ["exec", container, "sh", "-lc", command], { timeoutMs: 12e4 }));
816
+ }
817
+ // ── network ─────────────────────────────────────────────────────────────
818
+ case "check_port": {
819
+ const port = num(input, "port");
820
+ if (port === void 0)
821
+ return `Error: "port" must be a number.`;
822
+ const host = str(input, "host");
823
+ if (host && host !== "localhost" && host !== "127.0.0.1")
824
+ return probeRemotePort(host, port);
825
+ const r = await exec("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], { timeoutMs: 15e3 });
826
+ if (r.code === 1 && !r.stderr.trim())
827
+ return `Nothing is listening on port ${port}.`;
828
+ if (r.code === null)
829
+ return probeRemotePort("127.0.0.1", port);
830
+ return formatResult(r);
831
+ }
832
+ case "curl_request":
833
+ return httpRequest(input);
834
+ // ── web ─────────────────────────────────────────────────────────────────
835
+ case "web_search":
836
+ return webSearch(required(input, "query"));
837
+ case "web_fetch":
838
+ return webFetch(required(input, "url"));
839
+ case "spawn_subagents":
840
+ return "spawn_subagents is handled by the agent loop, not as a local tool.";
841
+ case "todo_list": {
842
+ const action = str(input, "action") ?? "list";
843
+ if (action === "create") {
844
+ const items = input.items;
845
+ if (Array.isArray(items))
846
+ TODOS = items.map((t) => ({ text: String(t), done: false }));
847
+ else if (str(input, "text"))
848
+ TODOS.push({ text: required(input, "text"), done: false });
849
+ } else if (action === "complete") {
850
+ const idx = num(input, "index");
851
+ if (idx !== void 0 && TODOS[idx])
852
+ TODOS[idx].done = true;
853
+ else {
854
+ const t = str(input, "text");
855
+ const found = TODOS.find((x) => x.text === t);
856
+ if (found)
857
+ found.done = true;
858
+ }
859
+ }
860
+ if (!TODOS.length)
861
+ return "(no todos)";
862
+ return TODOS.map((t, i) => `${t.done ? "[x]" : "[ ]"} ${i}. ${t.text}`).join("\n");
863
+ }
864
+ default:
865
+ return `Error: "${name}" is not a tool this CLI can run. It may need a newer CLI - run \`hw update\`.`;
866
+ }
867
+ }
868
+ function listDir(root2, recursive) {
869
+ const out = [];
870
+ const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", "dist", "build", "__pycache__", ".venv", "vendor"]);
871
+ const MAX_ENTRIES = 500;
872
+ const walk = (dir, prefix, depth) => {
873
+ if (out.length >= MAX_ENTRIES || depth > 8)
874
+ return;
875
+ let entries;
876
+ try {
877
+ entries = readdirSync(dir, { withFileTypes: true });
878
+ } catch {
879
+ return;
880
+ }
881
+ for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
882
+ if (out.length >= MAX_ENTRIES) {
883
+ out.push(`... (truncated at ${MAX_ENTRIES} entries)`);
884
+ return;
885
+ }
886
+ if (e.name.startsWith(".") && e.name !== ".env.example")
887
+ continue;
888
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
889
+ if (e.isDirectory()) {
890
+ out.push(`${rel}/`);
891
+ if (recursive && !SKIP.has(e.name))
892
+ walk(join(dir, e.name), rel, depth + 1);
893
+ } else {
894
+ let size = "";
895
+ try {
896
+ size = ` (${formatBytes(statSync(join(dir, e.name)).size)})`;
897
+ } catch {
898
+ }
899
+ out.push(`${rel}${size}`);
900
+ }
901
+ }
902
+ };
903
+ walk(root2, "", 0);
904
+ return out;
905
+ }
906
+ function formatBytes(n) {
907
+ if (n < 1024)
908
+ return `${n}B`;
909
+ if (n < 1024 * 1024)
910
+ return `${(n / 1024).toFixed(1)}KB`;
911
+ return `${(n / 1024 / 1024).toFixed(1)}MB`;
912
+ }
913
+ async function installPackage(pkg, opts) {
914
+ const os = platform();
915
+ const tries = os === "darwin" ? [["brew", ["install", pkg]], ["npm", ["install", "-g", pkg]]] : os === "win32" ? [["winget", ["install", "-e", "--id", pkg, "--silent"]], ["npm", ["install", "-g", pkg]]] : [
916
+ ["apt-get", ["install", "-y", pkg]],
917
+ ["dnf", ["install", "-y", pkg]],
918
+ ["yum", ["install", "-y", pkg]],
919
+ ["pacman", ["-S", "--noconfirm", pkg]],
920
+ ["npm", ["install", "-g", pkg]]
921
+ ];
922
+ let last = null;
923
+ for (const [bin, args] of tries) {
924
+ const r = await exec(bin, args, { ...opts, timeoutMs: 6e5 });
925
+ if (r.code !== null)
926
+ return r;
927
+ last = r;
928
+ }
929
+ return last ?? { stdout: "", stderr: "no package manager found", code: null, signal: null, timedOut: false, ok: false };
930
+ }
931
+ async function systemInfo() {
932
+ const v = async (bin, args) => {
933
+ const r = await exec(bin, args, { timeoutMs: 1e4 });
934
+ return r.ok ? r.stdout.trim().split("\n")[0] : "(not installed)";
935
+ };
936
+ const [npmV, gitV, dockerV, gitName, gitEmail] = await Promise.all([
937
+ v("npm", ["--version"]),
938
+ v("git", ["--version"]),
939
+ v("docker", ["--version"]),
940
+ exec("git", ["config", "user.name"]).then((r) => r.stdout.trim()),
941
+ exec("git", ["config", "user.email"]).then((r) => r.stdout.trim())
942
+ ]);
943
+ const gb = (n) => `${(n / 1024 ** 3).toFixed(1)}GB`;
944
+ return [
945
+ `OS: ${platform()} ${release()} (${arch()})`,
946
+ `CPU: ${cpus()[0]?.model ?? "unknown"} x${cpus().length}`,
947
+ `Memory: ${gb(totalmem() - freemem())} used / ${gb(totalmem())} total`,
948
+ `Node: ${process.version}`,
949
+ `npm: ${npmV}`,
950
+ `git: ${gitV}`,
951
+ `docker: ${dockerV}`,
952
+ `git user: ${gitName || "(unset)"} <${gitEmail || "unset"}>`,
953
+ `cwd: ${process.cwd()}`,
954
+ `home: ${homedir()}`
955
+ ].join("\n");
956
+ }
957
+ async function sshRun(host, user, command, opts, password, keyPath) {
958
+ const target = `${user}@${host}`;
959
+ if (password) {
960
+ if (!await hasBinary("sshpass")) {
961
+ return failed("sshpass is not installed, so password-based SSH is unavailable. Install it (macOS: brew install hudochenkov/sshpass/sshpass, Debian/Ubuntu: apt-get install sshpass) or connect with a key using keyPath.");
962
+ }
963
+ return exec("sshpass", ["-e", "ssh", ...SSH_OPTS, "-o", "PubkeyAuthentication=no", target, command], { ...opts, timeoutMs: 12e4, env: { ...process.env, SSHPASS: password } });
964
+ }
965
+ const args = [...SSH_OPTS, ...SSH_NONINTERACTIVE];
966
+ if (keyPath)
967
+ args.push("-i", resolvePath(keyPath));
968
+ args.push(target, command);
969
+ return exec("ssh", args, { ...opts, timeoutMs: 12e4 });
970
+ }
971
+ async function scp(host, user, localPath, remotePath, upload, opts, password, keyPath) {
972
+ const remote = `${user}@${host}:${remotePath}`;
973
+ const base = [...SSH_OPTS, "-r"];
974
+ if (keyPath)
975
+ base.push("-i", resolvePath(keyPath));
976
+ const paths = upload ? [localPath, remote] : [remote, localPath];
977
+ if (password) {
978
+ if (!await hasBinary("sshpass"))
979
+ return failed("sshpass is not installed, so password-based SCP is unavailable. Use a key with keyPath instead.");
980
+ return exec("sshpass", ["-e", "scp", ...base, ...paths], { ...opts, timeoutMs: 6e5, env: { ...process.env, SSHPASS: password } });
981
+ }
982
+ return exec("scp", [...base, ...SSH_NONINTERACTIVE, ...paths], { ...opts, timeoutMs: 6e5 });
983
+ }
984
+ async function hasBinary(bin) {
985
+ return (await (platform() === "win32" ? exec("where", [bin]) : exec("which", [bin]))).ok;
986
+ }
987
+ function failed(message) {
988
+ return { stdout: "", stderr: message, code: 1, signal: null, timedOut: false, ok: false };
989
+ }
990
+ async function probeRemotePort(host, port) {
991
+ const net = await import("net");
992
+ return new Promise((resolve2) => {
993
+ const socket = new net.Socket();
994
+ const done = (msg) => {
995
+ socket.destroy();
996
+ resolve2(msg);
997
+ };
998
+ socket.setTimeout(5e3);
999
+ socket.once("connect", () => done(`Port ${port} on ${host} is open.`));
1000
+ socket.once("timeout", () => done(`Port ${port} on ${host} timed out (filtered or closed).`));
1001
+ socket.once("error", (e) => done(`Port ${port} on ${host} is not reachable: ${e.code ?? e.message}`));
1002
+ socket.connect(port, host);
1003
+ });
1004
+ }
1005
+ async function httpRequest(input) {
1006
+ const url = required(input, "url");
1007
+ const method = (str(input, "method") ?? "GET").toUpperCase();
1008
+ const body = str(input, "body");
1009
+ let headers = {};
1010
+ const rawHeaders = input.headers;
1011
+ if (typeof rawHeaders === "string" && rawHeaders.trim()) {
1012
+ try {
1013
+ headers = JSON.parse(rawHeaders);
1014
+ } catch {
1015
+ for (const line2 of rawHeaders.split("\n")) {
1016
+ const i = line2.indexOf(":");
1017
+ if (i > 0)
1018
+ headers[line2.slice(0, i).trim()] = line2.slice(i + 1).trim();
1019
+ }
1020
+ }
1021
+ } else if (rawHeaders && typeof rawHeaders === "object") {
1022
+ headers = Object.fromEntries(Object.entries(rawHeaders).map(([k, v]) => [k, String(v)]));
1023
+ }
1024
+ const controller = new AbortController();
1025
+ const timeout = setTimeout(() => controller.abort(), 3e4);
1026
+ try {
1027
+ const res = await fetch(url, { method, headers, body, signal: controller.signal, redirect: "follow" });
1028
+ const text = await res.text();
1029
+ const shown = text.length > 2e4 ? `${text.slice(0, 2e4)}
1030
+ ... (truncated)` : text;
1031
+ const headerLines = [...res.headers.entries()].filter(([k]) => ["content-type", "location", "server", "cache-control"].includes(k)).map(([k, v]) => `${k}: ${v}`);
1032
+ return [`HTTP ${res.status} ${res.statusText}`, ...headerLines, "", shown].join("\n");
1033
+ } catch (e) {
1034
+ return `Error: request to ${url} failed: ${e instanceof Error ? e.message : String(e)}`;
1035
+ } finally {
1036
+ clearTimeout(timeout);
1037
+ }
1038
+ }
1039
+ async function webSearch(query) {
1040
+ const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
1041
+ const controller = new AbortController();
1042
+ const timeout = setTimeout(() => controller.abort(), 2e4);
1043
+ try {
1044
+ const res = await fetch(url, { signal: controller.signal });
1045
+ if (!res.ok)
1046
+ return `Error: search failed with HTTP ${res.status}.`;
1047
+ const data = await res.json();
1048
+ const parts = [];
1049
+ if (data.Answer)
1050
+ parts.push(String(data.Answer));
1051
+ if (data.AbstractText)
1052
+ parts.push(`${data.AbstractText}${data.AbstractURL ? `
1053
+ Source: ${data.AbstractURL}` : ""}`);
1054
+ if (data.Definition)
1055
+ parts.push(data.Definition);
1056
+ const related = (data.RelatedTopics ?? []).filter((t) => t.Text).slice(0, 8).map((t) => `- ${t.Text}${t.FirstURL ? ` (${t.FirstURL})` : ""}`);
1057
+ if (related.length)
1058
+ parts.push(`Related:
1059
+ ${related.join("\n")}`);
1060
+ if (!parts.length) {
1061
+ return `No instant answer for "${query}". This tool only returns DuckDuckGo instant answers, which are empty for most specific or recent queries - it is not a full web index. Try web_fetch on a known URL, or answer from what you already know and say it may be out of date.`;
1062
+ }
1063
+ return parts.join("\n\n");
1064
+ } catch (e) {
1065
+ return `Error: search failed: ${e instanceof Error ? e.message : String(e)}`;
1066
+ } finally {
1067
+ clearTimeout(timeout);
1068
+ }
1069
+ }
1070
+ async function webFetch(url) {
1071
+ const controller = new AbortController();
1072
+ const timeout = setTimeout(() => controller.abort(), 3e4);
1073
+ try {
1074
+ const res = await fetch(url, {
1075
+ signal: controller.signal,
1076
+ redirect: "follow",
1077
+ // Plenty of documentation sites return a JS shell or a 403 to an
1078
+ // unrecognised agent.
1079
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; hostwares-cli)" }
1080
+ });
1081
+ if (!res.ok)
1082
+ return `Error: ${url} returned HTTP ${res.status}.`;
1083
+ const type = res.headers.get("content-type") ?? "";
1084
+ const raw = await res.text();
1085
+ const text = type.includes("html") ? htmlToText(raw) : raw;
1086
+ return text.length > 3e4 ? `${text.slice(0, 3e4)}
1087
+ ... (truncated)` : text || "(empty response)";
1088
+ } catch (e) {
1089
+ return `Error: could not fetch ${url}: ${e instanceof Error ? e.message : String(e)}`;
1090
+ } finally {
1091
+ clearTimeout(timeout);
1092
+ }
1093
+ }
1094
+ function htmlToText(html) {
1095
+ return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<noscript[\s\S]*?<\/noscript>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<\/(p|div|section|article|h[1-6]|li|tr|br)>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/[ \t]+/g, " ").replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
1096
+ }
1097
+ var LOCAL_TOOL_NAMES, TODOS, BG, BG_MAX_OUTPUT, KNOWN, ToolInputError, MAX_READ_BYTES, SSH_OPTS, SSH_NONINTERACTIVE;
1098
+ var init_tools = __esm({
1099
+ "node_modules/@hostwares/agent-client/dist/esm/agent/tools.js"() {
1100
+ init_exec();
1101
+ LOCAL_TOOL_NAMES = [
1102
+ "read_file",
1103
+ "write_file",
1104
+ "list_directory",
1105
+ "search_files",
1106
+ "file_search",
1107
+ "delete_path",
1108
+ "run_command",
1109
+ "install_package",
1110
+ "get_system_info",
1111
+ "git_status",
1112
+ "git_add",
1113
+ "git_commit",
1114
+ "git_push",
1115
+ "git_pull",
1116
+ "git_clone",
1117
+ "git_log",
1118
+ "git_diff",
1119
+ "git_branch",
1120
+ "check_github_auth",
1121
+ "ssh_run",
1122
+ "ssh_upload",
1123
+ "ssh_download",
1124
+ "str_replace_file",
1125
+ "append_file",
1126
+ "list_processes",
1127
+ "kill_process",
1128
+ "start_process",
1129
+ "get_process_output",
1130
+ "stop_process",
1131
+ "open_browser",
1132
+ "wait_for_url",
1133
+ "spawn_subagents",
1134
+ "docker_ps",
1135
+ "docker_logs",
1136
+ "docker_exec",
1137
+ "check_port",
1138
+ "curl_request",
1139
+ "web_search",
1140
+ "web_fetch",
1141
+ "todo_list"
1142
+ ];
1143
+ TODOS = [];
1144
+ BG = /* @__PURE__ */ new Map();
1145
+ BG_MAX_OUTPUT = 256 * 1024;
1146
+ KNOWN = new Set(LOCAL_TOOL_NAMES);
1147
+ ToolInputError = class extends Error {
1148
+ };
1149
+ MAX_READ_BYTES = 2 * 1024 * 1024;
1150
+ SSH_OPTS = [
1151
+ "-o",
1152
+ "StrictHostKeyChecking=no",
1153
+ "-o",
1154
+ "UserKnownHostsFile=/dev/null",
1155
+ "-o",
1156
+ "ConnectTimeout=10",
1157
+ "-o",
1158
+ "ServerAliveInterval=5",
1159
+ "-o",
1160
+ "ServerAliveCountMax=3"
120
1161
  ];
121
- for (const [name, help] of rows)
122
- ui.line(` ${c.green(name.padEnd(20))} ${c.dim(help)}`);
123
- ui.line();
124
- ui.line(c.dim(" Anything else is treated as a question: hw why is my deploy failing"));
125
- ui.line();
1162
+ SSH_NONINTERACTIVE = ["-o", "BatchMode=yes"];
1163
+ }
1164
+ });
1165
+
1166
+ // node_modules/@hostwares/agent-client/dist/esm/agent/steering.js
1167
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2, existsSync as existsSync2, statSync as statSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1168
+ import { join as join2 } from "path";
1169
+ import { homedir as homedir2 } from "os";
1170
+ function steeringDirs(cwd) {
1171
+ return [join2(cwd, ".hostwares", "steering"), join2(homedir2(), ".hostwares", "steering")];
126
1172
  }
127
- function showSessions() {
128
- const sessions = listSessions();
129
- if (!sessions.length) {
130
- ui.line(c.dim(" No saved sessions yet."));
131
- return 0;
1173
+ function parseFrontMatter(raw) {
1174
+ const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
1175
+ if (!m)
1176
+ return { meta: {}, body: raw };
1177
+ const meta = {};
1178
+ for (const line2 of m[1].split(/\r?\n/)) {
1179
+ const kv = line2.match(/^([A-Za-z0-9_]+)\s*:\s*(.*)$/);
1180
+ if (kv)
1181
+ meta[kv[1]] = kv[2].trim().replace(/^["']|["']$/g, "");
1182
+ }
1183
+ return { meta, body: m[2] };
1184
+ }
1185
+ function readSteeringDocs(cwd) {
1186
+ const docs = [];
1187
+ for (const dir of steeringDirs(cwd)) {
1188
+ let entries;
1189
+ try {
1190
+ if (!existsSync2(dir))
1191
+ continue;
1192
+ entries = readdirSync2(dir).filter((f) => f.toLowerCase().endsWith(".md")).sort();
1193
+ } catch {
1194
+ continue;
132
1195
  }
133
- ui.line();
134
- for (const s of sessions.slice(0, 25)) {
135
- const here = s.cwd === process.cwd() ? c.green(` ${glyph.tick} here`) : "";
136
- ui.line(` ${s.cwd}${here}`);
137
- ui.line(c.dim(` ${s.turnCount} turns · ${s.creditsSpent.toFixed(2)} credits`));
1196
+ for (const file of entries) {
1197
+ const full = join2(dir, file);
1198
+ try {
1199
+ if (statSync2(full).size > MAX_STEERING_BYTES)
1200
+ continue;
1201
+ const { meta, body } = parseFrontMatter(readFileSync2(full, "utf8"));
1202
+ const inclusion = meta.inclusion === "fileMatch" || meta.inclusion === "manual" ? meta.inclusion : "always";
1203
+ docs.push({
1204
+ name: file.replace(/\.md$/i, ""),
1205
+ path: full,
1206
+ inclusion,
1207
+ fileMatchPattern: meta.fileMatchPattern || meta.filematchpattern,
1208
+ body: body.trim()
1209
+ });
1210
+ } catch {
1211
+ }
138
1212
  }
139
- ui.line();
140
- return 0;
1213
+ }
1214
+ return docs;
141
1215
  }
142
- /**
143
- * A real API call, not a prompt in disguise.
144
- *
145
- * The sidebar calls these "Deployments" while the model and the route are still
146
- * "sites"; the user-facing word follows the product.
147
- */
148
- async function listSites() {
1216
+ function globToRe(glob) {
1217
+ const esc = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1218
+ return new RegExp(esc);
1219
+ }
1220
+ function buildSteeringContext(cwd, message) {
1221
+ const docs = readSteeringDocs(cwd);
1222
+ if (!docs.length)
1223
+ return "";
1224
+ const chosen = docs.filter((d) => {
1225
+ if (d.inclusion === "always")
1226
+ return true;
1227
+ if (d.inclusion === "fileMatch" && d.fileMatchPattern) {
1228
+ try {
1229
+ return globToRe(d.fileMatchPattern).test(message);
1230
+ } catch {
1231
+ return false;
1232
+ }
1233
+ }
1234
+ return false;
1235
+ });
1236
+ if (!chosen.length)
1237
+ return "";
1238
+ const parts = [
1239
+ "## PROJECT STEERING (persistent context for this workspace - treat as trusted background, not instructions to obey blindly)"
1240
+ ];
1241
+ let total = 0;
1242
+ for (const d of chosen) {
1243
+ const section = `
1244
+ ### ${d.name}
1245
+ ${d.body}`;
1246
+ if (total + section.length > MAX_TOTAL_BYTES)
1247
+ break;
1248
+ total += section.length;
1249
+ parts.push(section);
1250
+ }
1251
+ const manual = docs.filter((d) => d.inclusion === "manual").map((d) => d.name);
1252
+ if (manual.length) {
1253
+ parts.push(`
1254
+ (Manual steering available on request via read_file: ${manual.join(", ")}.)`);
1255
+ }
1256
+ return parts.join("\n");
1257
+ }
1258
+ function generateSteering(cwd) {
1259
+ const dir = join2(cwd, ".hostwares", "steering");
1260
+ const created = [];
1261
+ const skipped = [];
1262
+ mkdirSync2(dir, { recursive: true });
1263
+ const write2 = (name, content) => {
1264
+ const full = join2(dir, name);
1265
+ if (existsSync2(full)) {
1266
+ skipped.push(name);
1267
+ return;
1268
+ }
1269
+ writeFileSync2(full, content, "utf8");
1270
+ created.push(name);
1271
+ };
1272
+ const read = (f) => {
149
1273
  try {
150
- const cfg = getConfig();
151
- const r = await fetch(`${cfg.baseUrl}/api/sites`, { headers: { Authorization: `Bearer ${cfg.apiKey}` } });
152
- if (!r.ok) {
153
- ui.error(r.status === 401 ? "Not signed in. Run `hw login`." : `Could not list deployments (HTTP ${r.status}).`);
154
- return 1;
155
- }
156
- const res = await r.json();
157
- const sites = Array.isArray(res) ? res : (res?.sites ?? []);
158
- if (!sites.length) {
159
- ui.line(c.dim(" No deployments yet. Ask me to deploy something to get started."));
160
- return 0;
161
- }
162
- ui.line();
163
- for (const s of sites) {
164
- const dot = s.status === "RUNNING" ? c.green("●")
165
- : s.status === "FAILED" ? c.red("●")
166
- : c.yellow("●");
167
- ui.line(` ${dot} ${c.bold((s.name ?? "unnamed").padEnd(24))} ${c.dim(s.domain ?? s.status ?? "")}`);
168
- }
169
- ui.line();
170
- return 0;
1274
+ return readFileSync2(join2(cwd, f), "utf8");
1275
+ } catch {
1276
+ return null;
171
1277
  }
172
- catch (e) {
173
- reportError(e);
174
- return 1;
1278
+ };
1279
+ let stack = "Unknown - fill this in.";
1280
+ let projectName = cwd.split(/[/\\]/).filter(Boolean).pop() ?? "project";
1281
+ const pkg = read("package.json");
1282
+ if (pkg) {
1283
+ try {
1284
+ const j = JSON.parse(pkg);
1285
+ projectName = j.name || projectName;
1286
+ const deps = { ...j.dependencies || {}, ...j.devDependencies || {} };
1287
+ const hints = [];
1288
+ if (deps.next)
1289
+ hints.push("Next.js");
1290
+ else if (deps.react)
1291
+ hints.push("React");
1292
+ if (deps.vue)
1293
+ hints.push("Vue");
1294
+ if (deps.express || deps.fastify)
1295
+ hints.push("Node HTTP server");
1296
+ if (deps.prisma || deps["@prisma/client"])
1297
+ hints.push("Prisma");
1298
+ if (deps.typescript)
1299
+ hints.push("TypeScript");
1300
+ if (deps.vitest || deps.jest)
1301
+ hints.push(`tests: ${deps.vitest ? "vitest" : "jest"}`);
1302
+ stack = hints.length ? hints.join(", ") : "Node/JavaScript";
1303
+ } catch {
175
1304
  }
1305
+ } else if (read("requirements.txt") || read("pyproject.toml"))
1306
+ stack = "Python";
1307
+ else if (read("go.mod"))
1308
+ stack = "Go";
1309
+ else if (read("composer.json"))
1310
+ stack = "PHP";
1311
+ else if (read("Cargo.toml"))
1312
+ stack = "Rust";
1313
+ let layout = "- (could not read directory)";
1314
+ try {
1315
+ const entries = readdirSync2(cwd).filter((e) => !e.startsWith(".") && e !== "node_modules").filter((e) => {
1316
+ try {
1317
+ return statSync2(join2(cwd, e)).isDirectory();
1318
+ } catch {
1319
+ return false;
1320
+ }
1321
+ }).slice(0, 12);
1322
+ if (entries.length)
1323
+ layout = entries.map((e) => `- ${e}/`).join("\n");
1324
+ } catch {
1325
+ }
1326
+ write2("product.md", `---
1327
+ inclusion: always
1328
+ ---
1329
+ # Product
1330
+
1331
+ **${projectName}** - describe in one line what this project is and who it is for.
1332
+
1333
+ ## What it does
1334
+ (Fill in the core features. The agent uses this to make product-sensible choices.)
1335
+
1336
+ ## What it must not do
1337
+ (Any hard constraints - data that must never leave, actions that always need review.)
1338
+ `);
1339
+ write2("tech.md", `---
1340
+ inclusion: always
1341
+ ---
1342
+ # Tech
1343
+
1344
+ **Stack (detected):** ${stack}
1345
+
1346
+ ## Conventions
1347
+ - (Formatting, naming, error handling the agent should match.)
1348
+ - (Preferred libraries; ones to avoid.)
1349
+
1350
+ ## Commands
1351
+ - Install: (e.g. npm install)
1352
+ - Dev: (e.g. npm run dev)
1353
+ - Test: (e.g. npm test)
1354
+ - Build: (e.g. npm run build)
1355
+ `);
1356
+ write2("structure.md", `---
1357
+ inclusion: always
1358
+ ---
1359
+ # Structure
1360
+
1361
+ Top-level layout:
1362
+ ${layout}
1363
+
1364
+ ## Where things go
1365
+ - (Where new routes / components / models belong, so the agent doesn't guess.)
1366
+ `);
1367
+ return { created, skipped };
176
1368
  }
177
- function readVersion() {
178
- // Read from package.json rather than a literal. The banner used to hardcode
179
- // "v1.1.0" beside a --version that read the manifest, so the two drifted the
180
- // moment either changed - a bug that had already been fixed once for
181
- // --version alone.
1369
+ var MAX_STEERING_BYTES, MAX_TOTAL_BYTES;
1370
+ var init_steering = __esm({
1371
+ "node_modules/@hostwares/agent-client/dist/esm/agent/steering.js"() {
1372
+ MAX_STEERING_BYTES = 32 * 1024;
1373
+ MAX_TOTAL_BYTES = 96 * 1024;
1374
+ }
1375
+ });
1376
+
1377
+ // node_modules/@hostwares/agent-client/dist/esm/agent/hooks.js
1378
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "fs";
1379
+ import { join as join3 } from "path";
1380
+ function globToRe2(glob) {
1381
+ const esc = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1382
+ return new RegExp(esc);
1383
+ }
1384
+ function userHooks(cwd) {
1385
+ const dir = join3(cwd, ".hostwares", "hooks");
1386
+ if (!existsSync3(dir))
1387
+ return [];
1388
+ const out = [];
1389
+ try {
1390
+ for (const f of readdirSync3(dir).filter((x) => x.endsWith(".json"))) {
1391
+ try {
1392
+ const parsed = JSON.parse(readFileSync3(join3(dir, f), "utf8"));
1393
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
1394
+ for (const h of arr)
1395
+ if (h && h.event && h.run)
1396
+ out.push(h);
1397
+ } catch {
1398
+ }
1399
+ }
1400
+ } catch {
1401
+ }
1402
+ return out;
1403
+ }
1404
+ function defaultPostWriteHook(cwd, file) {
1405
+ const has = (f) => existsSync3(join3(cwd, f));
1406
+ const lower = file.toLowerCase();
1407
+ if (/\.(ts|tsx)$/.test(lower) && has("tsconfig.json")) {
1408
+ return { event: "PostFileSave", name: "typecheck", run: "npx --no-install tsc --noEmit" };
1409
+ }
1410
+ if (/\.(js|jsx|mjs|cjs)$/.test(lower) && (has(".eslintrc") || has(".eslintrc.json") || has(".eslintrc.cjs") || has("eslint.config.js") || has("eslint.config.mjs"))) {
1411
+ return { event: "PostFileSave", name: "lint", run: "npx --no-install eslint {file}" };
1412
+ }
1413
+ if (/\.(js|mjs|cjs)$/.test(lower)) {
1414
+ return { event: "PostFileSave", name: "syntax", run: "node --check {file}" };
1415
+ }
1416
+ if (/\.py$/.test(lower)) {
1417
+ if (has("mypy.ini") || has("pyproject.toml"))
1418
+ return { event: "PostFileSave", name: "mypy", run: "python3 -m mypy {file}" };
1419
+ return { event: "PostFileSave", name: "py-compile", run: "python3 -m py_compile {file}" };
1420
+ }
1421
+ if (/\.go$/.test(lower) && has("go.mod")) {
1422
+ return { event: "PostFileSave", name: "go build", run: "go build ./..." };
1423
+ }
1424
+ if (/\.rs$/.test(lower) && has("Cargo.toml")) {
1425
+ return { event: "PostFileSave", name: "cargo check", run: "cargo check --quiet" };
1426
+ }
1427
+ return null;
1428
+ }
1429
+ async function runPostWriteHooks(cwd, file) {
1430
+ const custom = userHooks(cwd).filter((h) => h.event === "PostFileSave" && (!h.match || safeMatch(h.match, file)));
1431
+ const hooks = [...custom];
1432
+ const def = defaultPostWriteHook(cwd, file);
1433
+ if (def && !custom.some((h) => h.name === def.name))
1434
+ hooks.push(def);
1435
+ if (!hooks.length)
1436
+ return [];
1437
+ const results = [];
1438
+ for (const h of hooks) {
1439
+ const cmd = h.run.replace(/\{file\}/g, shellQuote(file));
1440
+ const r = await execShell(cmd, { cwd, timeoutMs: HOOK_TIMEOUT_MS });
1441
+ if (r.code === null || /command not found|not recognized|could not determine executable/i.test(r.stderr)) {
1442
+ continue;
1443
+ }
1444
+ results.push({
1445
+ name: h.name ?? "check",
1446
+ ok: r.ok,
1447
+ output: formatResult(r, { maxChars: 6e3 })
1448
+ });
1449
+ }
1450
+ return results;
1451
+ }
1452
+ function safeMatch(glob, file) {
1453
+ try {
1454
+ return globToRe2(glob).test(file);
1455
+ } catch {
1456
+ return true;
1457
+ }
1458
+ }
1459
+ function shellQuote(p) {
1460
+ if (process.platform === "win32")
1461
+ return `"${p.replace(/"/g, '""')}"`;
1462
+ return `'${p.replace(/'/g, `'\\''`)}'`;
1463
+ }
1464
+ var HOOK_TIMEOUT_MS;
1465
+ var init_hooks = __esm({
1466
+ "node_modules/@hostwares/agent-client/dist/esm/agent/hooks.js"() {
1467
+ init_exec();
1468
+ HOOK_TIMEOUT_MS = 6e4;
1469
+ }
1470
+ });
1471
+
1472
+ // node_modules/@hostwares/agent-client/dist/esm/agent/checkpoints.js
1473
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync4, statSync as statSync3, rmSync as rmSync2 } from "fs";
1474
+ import { join as join4, resolve, relative, dirname as dirname2 } from "path";
1475
+ function root(cwd) {
1476
+ return join4(cwd, ".hostwares", "checkpoints");
1477
+ }
1478
+ function encodePath(p) {
1479
+ return Buffer.from(p).toString("base64url");
1480
+ }
1481
+ function readManifest(dir) {
1482
+ try {
1483
+ return JSON.parse(readFileSync4(join4(dir, "manifest.json"), "utf8"));
1484
+ } catch {
1485
+ return null;
1486
+ }
1487
+ }
1488
+ function writeManifest(dir, m) {
1489
+ mkdirSync3(dir, { recursive: true });
1490
+ writeFileSync3(join4(dir, "manifest.json"), JSON.stringify(m, null, 2), "utf8");
1491
+ }
1492
+ function pruneOld(cwd) {
1493
+ try {
1494
+ const base = root(cwd);
1495
+ const dirs = readdirSync4(base).map((d) => ({ d, m: readManifest(join4(base, d)) })).filter((x) => x.m).sort((a, b) => b.m.createdAt - a.m.createdAt);
1496
+ for (const stale of dirs.slice(MAX_CHECKPOINTS)) {
1497
+ rmSync2(join4(base, stale.d), { recursive: true, force: true });
1498
+ }
1499
+ } catch {
1500
+ }
1501
+ }
1502
+ function listCheckpoints(cwd) {
1503
+ try {
1504
+ const base = root(cwd);
1505
+ if (!existsSync4(base))
1506
+ return [];
1507
+ return readdirSync4(base).map((d) => readManifest(join4(base, d))).filter((m) => !!m).sort((a, b) => b.createdAt - a.createdAt).map((m) => ({ label: m.label, createdAt: m.createdAt, note: m.note, fileCount: m.entries.length }));
1508
+ } catch {
1509
+ return [];
1510
+ }
1511
+ }
1512
+ function rewind(cwd, label) {
1513
+ const base = root(cwd);
1514
+ const all = listCheckpoints(cwd);
1515
+ if (!all.length)
1516
+ return { ok: false, restored: [], deleted: [], message: "No checkpoints to rewind to." };
1517
+ const target = label ?? all[0].label;
1518
+ const dir = join4(base, target);
1519
+ const manifest = readManifest(dir);
1520
+ if (!manifest)
1521
+ return { ok: false, restored: [], deleted: [], message: `No checkpoint named "${target}".` };
1522
+ const restored = [];
1523
+ const deleted = [];
1524
+ for (const e of manifest.entries) {
182
1525
  try {
183
- const here = dirname(fileURLToPath(import.meta.url));
184
- const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
185
- return pkg.version ?? "0.0.0";
1526
+ if (e.created) {
1527
+ if (existsSync4(e.path)) {
1528
+ rmSync2(e.path, { force: true });
1529
+ deleted.push(relative(cwd, e.path));
1530
+ }
1531
+ } else if (e.backup) {
1532
+ const saved = join4(dir, "files", e.backup);
1533
+ if (existsSync4(saved)) {
1534
+ mkdirSync3(dirname2(e.path), { recursive: true });
1535
+ writeFileSync3(e.path, readFileSync4(saved));
1536
+ restored.push(relative(cwd, e.path));
1537
+ }
1538
+ }
1539
+ } catch {
186
1540
  }
187
- catch {
188
- return "0.0.0";
1541
+ }
1542
+ try {
1543
+ rmSync2(dir, { recursive: true, force: true });
1544
+ } catch {
1545
+ }
1546
+ const parts = [];
1547
+ if (restored.length)
1548
+ parts.push(`restored ${restored.length} file${restored.length === 1 ? "" : "s"}`);
1549
+ if (deleted.length)
1550
+ parts.push(`deleted ${deleted.length} created file${deleted.length === 1 ? "" : "s"}`);
1551
+ return {
1552
+ ok: true,
1553
+ restored,
1554
+ deleted,
1555
+ message: parts.length ? `Rewound "${target}": ${parts.join(", ")}.` : `Checkpoint "${target}" had nothing to undo.`
1556
+ };
1557
+ }
1558
+ var MAX_CHECKPOINTS, MAX_BACKUP_BYTES, Checkpoint;
1559
+ var init_checkpoints = __esm({
1560
+ "node_modules/@hostwares/agent-client/dist/esm/agent/checkpoints.js"() {
1561
+ MAX_CHECKPOINTS = 20;
1562
+ MAX_BACKUP_BYTES = 5 * 1024 * 1024;
1563
+ Checkpoint = class {
1564
+ cwd;
1565
+ dir;
1566
+ manifest;
1567
+ seen = /* @__PURE__ */ new Set();
1568
+ constructor(cwd, label, note2) {
1569
+ this.cwd = cwd;
1570
+ this.dir = join4(root(cwd), label);
1571
+ this.manifest = { label, createdAt: Date.now(), note: note2, entries: [] };
1572
+ }
1573
+ /** Snapshot a file's current state before it is changed. Idempotent per path. */
1574
+ record(filePath) {
1575
+ const abs = resolve(this.cwd, filePath);
1576
+ if (this.seen.has(abs))
1577
+ return;
1578
+ this.seen.add(abs);
1579
+ try {
1580
+ if (!existsSync4(abs)) {
1581
+ this.manifest.entries.push({ path: abs, created: true });
1582
+ } else {
1583
+ const st = statSync3(abs);
1584
+ if (st.isDirectory() || st.size > MAX_BACKUP_BYTES)
1585
+ return;
1586
+ const backup = encodePath(abs);
1587
+ mkdirSync3(join4(this.dir, "files"), { recursive: true });
1588
+ writeFileSync3(join4(this.dir, "files", backup), readFileSync4(abs));
1589
+ this.manifest.entries.push({ path: abs, created: false, backup });
1590
+ }
1591
+ writeManifest(this.dir, this.manifest);
1592
+ } catch {
1593
+ }
1594
+ }
1595
+ /** True once anything has been recorded - so we don't leave empty dirs. */
1596
+ get hasEntries() {
1597
+ return this.manifest.entries.length > 0;
1598
+ }
1599
+ /** Discard this checkpoint (e.g. the turn made no changes). */
1600
+ discard() {
1601
+ try {
1602
+ rmSync2(this.dir, { recursive: true, force: true });
1603
+ } catch {
1604
+ }
1605
+ }
1606
+ finalize() {
1607
+ if (!this.hasEntries) {
1608
+ this.discard();
1609
+ return;
1610
+ }
1611
+ pruneOld(this.cwd);
1612
+ }
1613
+ };
1614
+ }
1615
+ });
1616
+
1617
+ // node_modules/@hostwares/agent-client/dist/esm/agent/subagents.js
1618
+ function parseTasks(input) {
1619
+ const raw = input?.tasks ?? input?.subagents;
1620
+ const arr = Array.isArray(raw) ? raw : [];
1621
+ const tasks = [];
1622
+ for (const t of arr) {
1623
+ if (typeof t === "string" && t.trim())
1624
+ tasks.push({ task: t.trim() });
1625
+ else if (t && typeof t === "object" && typeof t.task === "string") {
1626
+ tasks.push({ task: String(t.task), label: t.label ? String(t.label) : void 0 });
1627
+ }
1628
+ if (tasks.length >= MAX_TASKS)
1629
+ break;
1630
+ }
1631
+ return tasks;
1632
+ }
1633
+ async function runSubagents(input, deps, emit) {
1634
+ if (deps.depth >= 2) {
1635
+ return "Sub-agents cannot be nested this deep. Do this subtask yourself in the current turn.";
1636
+ }
1637
+ const tasks = parseTasks(input);
1638
+ if (!tasks.length) {
1639
+ return `No tasks given. Call spawn_subagents with tasks: ["do X and report", "do Y and report"] - each task must be independent and self-contained.`;
1640
+ }
1641
+ const results = [];
1642
+ for (let i = 0; i < tasks.length; i += MAX_PARALLEL) {
1643
+ if (deps.signal.aborted)
1644
+ break;
1645
+ const wave = tasks.slice(i, i + MAX_PARALLEL);
1646
+ const settled = await Promise.all(wave.map(async (t, j) => {
1647
+ const label = t.label ?? `subagent ${i + j + 1}`;
1648
+ emit({ type: "tool:output", id: "spawn_subagents", chunk: `\u21B3 ${label}: ${t.task.slice(0, 80)}
1649
+ `, stream: "stdout" });
1650
+ try {
1651
+ const res = await deps.runTurn({
1652
+ credentials: deps.credentials,
1653
+ message: t.task,
1654
+ conversationId: null,
1655
+ // isolated context - a fresh conversation
1656
+ model: deps.model,
1657
+ signal: deps.signal,
1658
+ // The child inherits the parent's permission host so writes are still
1659
+ // governed, but draws no activity of its own - only its result
1660
+ // surfaces, keeping the parent's view (and window) clean.
1661
+ host: { ...deps.host, onActivity: silentSink, subagentDepth: deps.depth + 1 }
1662
+ });
1663
+ return { label, text: (res.text || "(no output)").trim(), ok: !res.error && !res.aborted };
1664
+ } catch (e) {
1665
+ return { label, text: `failed: ${e instanceof Error ? e.message : String(e)}`, ok: false };
1666
+ }
1667
+ }));
1668
+ results.push(...settled);
1669
+ }
1670
+ const okCount = results.filter((r) => r.ok).length;
1671
+ const header = `${results.length} sub-agent${results.length === 1 ? "" : "s"} finished (${okCount} ok).`;
1672
+ const body = results.map((r) => `
1673
+ ### ${r.label} ${r.ok ? "\u2713" : "\u2717"}
1674
+ ${r.text.slice(0, 4e3)}`).join("\n");
1675
+ return `${header}${body}
1676
+
1677
+ Use these results to finish the task. Do not re-do work a sub-agent already completed.`;
1678
+ }
1679
+ var MAX_PARALLEL, MAX_TASKS;
1680
+ var init_subagents = __esm({
1681
+ "node_modules/@hostwares/agent-client/dist/esm/agent/subagents.js"() {
1682
+ init_events();
1683
+ MAX_PARALLEL = 4;
1684
+ MAX_TASKS = 6;
1685
+ }
1686
+ });
1687
+
1688
+ // node_modules/@hostwares/agent-client/dist/esm/agent/safety.js
1689
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1690
+ import { join as join5 } from "path";
1691
+ import { homedir as homedir3 } from "os";
1692
+ function canRunUnprompted(tool, trust = "none", destructive = false) {
1693
+ if (destructive || ALWAYS_ASK.has(tool))
1694
+ return false;
1695
+ if (AUTO_APPROVED.has(tool))
1696
+ return true;
1697
+ return trust !== "none";
1698
+ }
1699
+ function primaryArg(tool, input) {
1700
+ const s = (k) => input?.[k] === void 0 ? "" : String(input[k]);
1701
+ switch (tool) {
1702
+ case "run_command":
1703
+ case "start_process":
1704
+ return s("command");
1705
+ case "curl_request":
1706
+ case "web_fetch":
1707
+ case "open_browser":
1708
+ case "wait_for_url":
1709
+ return s("url");
1710
+ case "ssh_run":
1711
+ return s("command");
1712
+ case "install_package":
1713
+ return s("name");
1714
+ default:
1715
+ return s("path") || s("pattern") || s("target") || s("container") || "";
1716
+ }
1717
+ }
1718
+ function permGlobToRe(glob) {
1719
+ const esc = glob.trim().replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1720
+ return new RegExp(`^${esc}$`);
1721
+ }
1722
+ function ruleMatches(rule, tool, arg) {
1723
+ if (rule.tool !== "*" && rule.tool !== tool)
1724
+ return false;
1725
+ if (!rule.match)
1726
+ return true;
1727
+ try {
1728
+ return permGlobToRe(rule.match).test(arg);
1729
+ } catch {
1730
+ return false;
1731
+ }
1732
+ }
1733
+ function resolvePermission(rules, tool, input) {
1734
+ const arg = primaryArg(tool, input);
1735
+ let verdict = null;
1736
+ const rank = { allow: 1, ask: 2, deny: 3 };
1737
+ for (const r of rules) {
1738
+ if (!ruleMatches(r, tool, arg))
1739
+ continue;
1740
+ if (verdict === null || rank[r.effect] > rank[verdict])
1741
+ verdict = r.effect;
1742
+ }
1743
+ return verdict;
1744
+ }
1745
+ function decidePermission(tool, input, rules, trust = "none", destructive = false) {
1746
+ const hard = resolvePermission(HARD_DENY, tool, input);
1747
+ if (hard === "deny")
1748
+ return "deny";
1749
+ const user = resolvePermission(rules, tool, input);
1750
+ if (user)
1751
+ return user;
1752
+ return canRunUnprompted(tool, trust, destructive) ? "allow" : "ask";
1753
+ }
1754
+ function loadPermissionRules(cwd) {
1755
+ const files = [
1756
+ join5(homedir3(), ".hostwares", "permissions.json"),
1757
+ join5(cwd, ".hostwares", "permissions.json")
1758
+ ];
1759
+ const rules = [];
1760
+ for (const f of files) {
1761
+ try {
1762
+ if (!existsSync5(f))
1763
+ continue;
1764
+ const parsed = JSON.parse(readFileSync5(f, "utf8"));
1765
+ const arr = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.rules) ? parsed.rules : [];
1766
+ for (const r of arr) {
1767
+ if (r && typeof r.tool === "string" && ["allow", "ask", "deny"].includes(r.effect)) {
1768
+ rules.push({ tool: r.tool, match: typeof r.match === "string" ? r.match : void 0, effect: r.effect });
1769
+ }
1770
+ }
1771
+ } catch {
1772
+ }
1773
+ }
1774
+ return rules;
1775
+ }
1776
+ function describeEffect(tool, input) {
1777
+ const s = (k) => input?.[k] === void 0 ? void 0 : String(input[k]);
1778
+ switch (tool) {
1779
+ case "write_file":
1780
+ return { kind: "wrote", path: s("path") ?? "" };
1781
+ case "append_file":
1782
+ case "str_replace_file":
1783
+ return { kind: "modified", path: s("path") ?? "" };
1784
+ case "delete_path":
1785
+ return { kind: "deleted", path: s("path") ?? "" };
1786
+ case "run_command":
1787
+ return { kind: "ran", command: s("command") ?? "" };
1788
+ case "read_file":
1789
+ return { kind: "read", path: s("path") ?? "" };
1790
+ default:
1791
+ return { kind: "other" };
1792
+ }
1793
+ }
1794
+ function buildPatch(oldContent, newContent, filePath, maxLines = 40) {
1795
+ if (oldContent === newContent)
1796
+ return void 0;
1797
+ const oldLines = oldContent.split("\n");
1798
+ const newLines = newContent.split("\n");
1799
+ let start = 0;
1800
+ while (start < oldLines.length && start < newLines.length && oldLines[start] === newLines[start])
1801
+ start++;
1802
+ let oldEnd = oldLines.length - 1;
1803
+ let newEnd = newLines.length - 1;
1804
+ while (oldEnd >= start && newEnd >= start && oldLines[oldEnd] === newLines[newEnd]) {
1805
+ oldEnd--;
1806
+ newEnd--;
1807
+ }
1808
+ const ctxBefore = Math.max(0, start - 2);
1809
+ const ctxAfterOld = Math.min(oldLines.length, oldEnd + 3);
1810
+ const ctxAfterNew = Math.min(newLines.length, newEnd + 3);
1811
+ const lines = [];
1812
+ for (let i = ctxBefore; i < start; i++) {
1813
+ lines.push({ kind: "ctx", text: oldLines[i], oldLine: i + 1, newLine: i + 1 });
1814
+ }
1815
+ for (let i = start; i <= oldEnd; i++) {
1816
+ lines.push({ kind: "del", text: oldLines[i], oldLine: i + 1 });
1817
+ }
1818
+ for (let i = start; i <= newEnd; i++) {
1819
+ lines.push({ kind: "add", text: newLines[i], newLine: i + 1 });
1820
+ }
1821
+ for (let i = oldEnd + 1; i < ctxAfterOld; i++) {
1822
+ const newIdx = newEnd + 1 + (i - (oldEnd + 1));
1823
+ if (newIdx < ctxAfterNew) {
1824
+ lines.push({ kind: "ctx", text: oldLines[i], oldLine: i + 1, newLine: newIdx + 1 });
189
1825
  }
1826
+ }
1827
+ if (lines.length > maxLines) {
1828
+ return { path: filePath, lines: lines.slice(0, maxLines), truncated: lines.length - maxLines };
1829
+ }
1830
+ return { path: filePath, lines };
190
1831
  }
191
- main(process.argv.slice(2))
192
- .then(code => process.exit(code))
193
- .catch(e => {
194
- // Ctrl-C during startup is a normal exit, not a crash to report.
1832
+ var AUTO_APPROVED, ALWAYS_ASK, HARD_DENY;
1833
+ var init_safety = __esm({
1834
+ "node_modules/@hostwares/agent-client/dist/esm/agent/safety.js"() {
1835
+ AUTO_APPROVED = /* @__PURE__ */ new Set([
1836
+ "read_file",
1837
+ "list_directory",
1838
+ "get_system_info",
1839
+ "git_status",
1840
+ "git_log",
1841
+ "git_diff",
1842
+ "check_github_auth",
1843
+ "docker_ps",
1844
+ "list_processes",
1845
+ "check_port"
1846
+ ]);
1847
+ ALWAYS_ASK = /* @__PURE__ */ new Set([
1848
+ "delete_path",
1849
+ "kill_process",
1850
+ "git_push",
1851
+ "ssh_run",
1852
+ "ssh_upload",
1853
+ "ssh_download"
1854
+ ]);
1855
+ HARD_DENY = [
1856
+ { tool: "run_command", match: "*rm -rf /*", effect: "deny" },
1857
+ { tool: "run_command", match: "*rm -rf ~*", effect: "deny" },
1858
+ { tool: "run_command", match: "*mkfs*", effect: "deny" },
1859
+ { tool: "run_command", match: "*:(){:|:&};:*", effect: "deny" },
1860
+ // fork bomb
1861
+ { tool: "start_process", match: "*rm -rf /*", effect: "deny" }
1862
+ ];
1863
+ }
1864
+ });
1865
+
1866
+ // node_modules/@hostwares/agent-client/dist/esm/agent/loop.js
1867
+ async function runTurn(opts) {
1868
+ const { credentials, signal, host } = opts;
1869
+ const emit = host.onActivity ?? silentSink;
1870
+ const startedAt = Date.now();
1871
+ let conversationId = opts.conversationId ?? null;
1872
+ let turnId = null;
1873
+ let turnCredits = 0;
1874
+ let balance = 0;
1875
+ let text = "";
1876
+ let lastDone = null;
1877
+ let assistantContent = null;
1878
+ let toolResults = [];
1879
+ let firstHop = true;
1880
+ const callCounts = /* @__PURE__ */ new Map();
1881
+ const writeCounts = /* @__PURE__ */ new Map();
1882
+ let stalledHops = 0;
1883
+ let totalGuardHits = 0;
1884
+ let emptyHops = 0;
1885
+ const MAX_EMPTY_RETRIES = 1;
1886
+ const steering = buildSteeringContext(host.cwd ?? process.cwd(), opts.message);
1887
+ const firstMessage = steering ? `${steering}
1888
+
1889
+ ---
1890
+
1891
+ ${opts.message}` : opts.message;
1892
+ const checkpointLabel = `turn-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
1893
+ const checkpoint = new Checkpoint(host.cwd ?? process.cwd(), checkpointLabel, opts.message.slice(0, 120));
1894
+ try {
1895
+ for (let hop = 0; hop < MAX_HOPS; hop++) {
1896
+ if (signal.aborted)
1897
+ throw new AbortedError();
1898
+ emit({ type: "thinking" });
1899
+ const pending = [];
1900
+ const textBefore = text.length;
1901
+ const done = await streamChat(credentials, {
1902
+ ...firstHop ? { message: firstMessage, images: opts.images } : { toolResults, assistantContent },
1903
+ conversationId,
1904
+ turnId,
1905
+ agentMode: true,
1906
+ model: opts.model
1907
+ }, {
1908
+ onMeta: (d) => {
1909
+ conversationId = d.conversationId;
1910
+ },
1911
+ onText: (chunk) => {
1912
+ text += chunk;
1913
+ emit({ type: "text", chunk });
1914
+ },
1915
+ // Cloud tools run on our infrastructure; they are narrated, not executed.
1916
+ onToolExecuting: (d) => emit({ type: "tool:start", id: d.id ?? d.name, tool: d.name, target: d.target, location: "cloud" }),
1917
+ onToolDone: (d) => emit({
1918
+ type: "tool:end",
1919
+ id: d.id ?? d.name,
1920
+ tool: d.name,
1921
+ ok: d.status === "success" || d.status === "executed",
1922
+ durationMs: d.durationMs ?? 0,
1923
+ summary: d.result ?? d.error
1924
+ }),
1925
+ onLocalTool: (d) => {
1926
+ pending.push({ id: d.id, name: d.name, input: d.input, destructive: d.destructive });
1927
+ emit({ type: "tool:pending", id: d.id, tool: d.name, target: d.target, location: "local", destructive: d.destructive });
1928
+ },
1929
+ onError: (message) => emit({ type: "error", message })
1930
+ }, {
1931
+ signal,
1932
+ onAuthFailure: opts.onAuthFailure ? async () => {
1933
+ emit({ type: "reauth" });
1934
+ return opts.onAuthFailure();
1935
+ } : void 0
1936
+ });
1937
+ if (done) {
1938
+ lastDone = done;
1939
+ conversationId = done.conversationId ?? conversationId;
1940
+ turnId = done.turnId ?? turnId;
1941
+ turnCredits = done.turnCredits ?? turnCredits + (done.creditsUsed ?? 0);
1942
+ balance = done.balance ?? balance;
1943
+ assistantContent = done.assistantContent ?? assistantContent;
1944
+ for (const call of done.localToolCalls ?? []) {
1945
+ if (!pending.some((p) => p.id === call.id)) {
1946
+ pending.push(call);
1947
+ emit({ type: "tool:pending", id: call.id, tool: call.name, target: targetOf(call), location: "local", destructive: call.destructive });
1948
+ }
1949
+ }
1950
+ if (done.pendingActions?.length) {
1951
+ await handleActions(done.pendingActions, opts, emit);
1952
+ }
1953
+ }
1954
+ firstHop = false;
1955
+ if (!pending.length) {
1956
+ const producedText = text.length > textBefore;
1957
+ if (!producedText && hop > 0 && emptyHops < MAX_EMPTY_RETRIES) {
1958
+ emptyHops++;
1959
+ toolResults = [{
1960
+ id: `empty-${hop}`,
1961
+ result: `[continue] Your last turn was empty \u2014 no message and no tool call. Do NOT stop here. Look at what has been done so far in this conversation and take the NEXT concrete step toward the goal: create the next file, run the next command, or if the task is genuinely complete, give a short final answer stating what you built and how to run it. Act now.`
1962
+ }];
1963
+ assistantContent = assistantContent ?? "";
1964
+ continue;
1965
+ }
1966
+ if (!producedText && emptyHops >= MAX_EMPTY_RETRIES) {
1967
+ const wroteSomething = writeCounts.size > 0;
1968
+ emit({ type: "error", message: wroteSomething ? `Stopping: the last steps returned nothing new. What I changed is saved \u2014 tell me the specific next change and I'll make it directly.` : `Stopping: I couldn't make progress on that. Tell me the specific step you want (e.g. "create the server file") and I'll do it directly.` });
1969
+ }
1970
+ break;
1971
+ }
1972
+ emptyHops = 0;
1973
+ const toRun = [];
1974
+ const shortCircuited = [];
1975
+ for (const call of pending) {
1976
+ const fp = `${call.name}:${stableStringify(call.input)}`;
1977
+ const seen = (callCounts.get(fp) ?? 0) + 1;
1978
+ callCounts.set(fp, seen);
1979
+ const path = typeof call.input?.path === "string" ? call.input.path : void 0;
1980
+ if (seen > MAX_IDENTICAL_CALLS && isReadOnlyTool(call.name)) {
1981
+ shortCircuited.push({
1982
+ id: call.id,
1983
+ result: `[loop guard] You already ran ${call.name}(${compactArgs(call.input)}) and nothing has changed since \u2014 re-reading it will keep returning the same thing. This call was NOT executed. STOP re-inspecting and DECIDE from what you already know: if a file the task needs does not exist yet, WRITE it now (write_file) \u2014 an empty or missing directory means the file has not been created, not that you should look again. If every file the task needs already exists and has been run/verified, give your final answer now (what you built + how to run it) and end. Do NOT read or list again.`
1984
+ });
1985
+ emit({ type: "tool:end", id: call.id, tool: call.name, ok: false, durationMs: 0, summary: "loop guard: repeated read skipped" });
1986
+ } else if (WRITE_TOOLS.has(call.name) && path && (writeCounts.get(path) ?? 0) >= MAX_WRITES_PER_FILE) {
1987
+ shortCircuited.push({
1988
+ id: call.id,
1989
+ result: `[loop guard] You have already written ${path} ${writeCounts.get(path)} times this turn \u2014 its content is on disk. This write was NOT executed. Stop rewriting it: verify it (run it / typecheck it) or give your final answer. Do not write this file again unless you are making a specific, different change.`
1990
+ });
1991
+ emit({ type: "tool:end", id: call.id, tool: call.name, ok: false, durationMs: 0, summary: "loop guard: write thrash skipped" });
1992
+ } else {
1993
+ toRun.push(call);
1994
+ }
1995
+ }
1996
+ totalGuardHits += shortCircuited.length;
1997
+ if (toRun.length === 0 && shortCircuited.length > 0)
1998
+ stalledHops++;
1999
+ else
2000
+ stalledHops = 0;
2001
+ const wroteThisHop = toRun.some((c2) => WRITE_TOOLS.has(c2.name));
2002
+ if (wroteThisHop) {
2003
+ stalledHops = 0;
2004
+ }
2005
+ const filesWritten = writeCounts.size;
2006
+ const guardHitCeiling = 6 + filesWritten * 3;
2007
+ const stallCeiling = filesWritten > 0 ? 4 : 2;
2008
+ if (stalledHops >= stallCeiling || totalGuardHits >= guardHitCeiling) {
2009
+ const wroteSomething = filesWritten > 0;
2010
+ emit({ type: "error", message: wroteSomething ? `Stopping: I kept re-reading the same files without making new progress. What I built so far is saved. Tell me the specific next change (e.g. "write the UI file" / "start it") and I'll do it directly.` : `Stopping: I kept re-reading the same files without writing anything. I have enough context now \u2014 tell me the specific change you want and I'll make it directly.` });
2011
+ return finish({ conversationId, turnCredits, balance, text, aborted: false });
2012
+ }
2013
+ for (const c2 of toRun) {
2014
+ if (!MUTATION_TOOLS.has(c2.name))
2015
+ continue;
2016
+ const p = typeof c2.input?.path === "string" ? c2.input.path : void 0;
2017
+ if (!p)
2018
+ continue;
2019
+ if (WRITE_TOOLS.has(c2.name))
2020
+ writeCounts.set(p, (writeCounts.get(p) ?? 0) + 1);
2021
+ for (const key of [...callCounts.keys()]) {
2022
+ if (key.includes(JSON.stringify(p)))
2023
+ callCounts.delete(key);
2024
+ }
2025
+ }
2026
+ let results = shortCircuited;
2027
+ if (toRun.length) {
2028
+ const ran = await executePending(toRun, opts, emit, checkpoint);
2029
+ if (ran === null) {
2030
+ return finish({ conversationId, turnCredits, balance, text, aborted: true });
2031
+ }
2032
+ for (const call of toRun) {
2033
+ if (!WRITE_TOOLS.has(call.name))
2034
+ continue;
2035
+ const res = ran.find((r) => r.id === call.id);
2036
+ if (!res || /^Error:/i.test(res.result))
2037
+ continue;
2038
+ const path = String(call.input?.path ?? "");
2039
+ if (!path)
2040
+ continue;
2041
+ let hookResults;
2042
+ try {
2043
+ hookResults = await runPostWriteHooks(host.cwd ?? process.cwd(), path);
2044
+ } catch {
2045
+ hookResults = [];
2046
+ }
2047
+ for (const h of hookResults) {
2048
+ emit({ type: "tool:end", id: `${call.id}:${h.name}`, tool: h.name, ok: h.ok, durationMs: 0, summary: h.ok ? `${h.name} passed` : `${h.name} failed` });
2049
+ res.result += h.ok ? `
2050
+
2051
+ [${h.name}] passed on ${path}.` : `
2052
+
2053
+ [${h.name}] FAILED after this write \u2014 fix before continuing:
2054
+ ${h.output}`;
2055
+ }
2056
+ }
2057
+ results = [...shortCircuited, ...ran];
2058
+ }
2059
+ toolResults = results;
2060
+ if (hop === MAX_HOPS - 1) {
2061
+ emit({ type: "error", message: `Paused after ${MAX_HOPS} steps to check in - this is a long task, not necessarily a finished one. Say "continue" and I'll pick up exactly where I left off.` });
2062
+ }
2063
+ }
2064
+ emit({
2065
+ type: "turn:end",
2066
+ usage: {
2067
+ credits: turnCredits,
2068
+ balance,
2069
+ model: lastDone?.model ?? "",
2070
+ elapsedMs: Date.now() - startedAt,
2071
+ promptTokens: lastDone?.usage?.promptTokens,
2072
+ maxTokens: lastDone?.usage?.maxTokens,
2073
+ usagePercent: lastDone?.usage?.usagePercent
2074
+ }
2075
+ });
2076
+ return finish({ conversationId, turnCredits, balance, text, aborted: false });
2077
+ } catch (e) {
195
2078
  if (isAborted(e))
196
- process.exit(130);
2079
+ return finish({ conversationId, turnCredits, balance, text, aborted: true });
2080
+ const message = e instanceof ApiError ? e.message : e instanceof Error ? e.message : String(e);
2081
+ emit({ type: "error", message });
2082
+ return finish({ conversationId, turnCredits, balance, text, aborted: false, error: message });
2083
+ } finally {
2084
+ try {
2085
+ checkpoint.finalize();
2086
+ } catch {
2087
+ }
2088
+ }
2089
+ }
2090
+ function finish(r) {
2091
+ return {
2092
+ conversationId: r.conversationId,
2093
+ creditsSpent: r.turnCredits,
2094
+ balance: r.balance,
2095
+ text: r.text,
2096
+ aborted: r.aborted,
2097
+ error: r.error
2098
+ };
2099
+ }
2100
+ function isReadOnlyTool(name) {
2101
+ return READ_ONLY_TOOLS.has(name);
2102
+ }
2103
+ function stableStringify(v) {
2104
+ if (v === null || typeof v !== "object")
2105
+ return JSON.stringify(v);
2106
+ if (Array.isArray(v))
2107
+ return `[${v.map(stableStringify).join(",")}]`;
2108
+ const o = v;
2109
+ return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(o[k])}`).join(",")}}`;
2110
+ }
2111
+ function compactArgs(input) {
2112
+ const s = Object.entries(input).map(([k, val]) => `${k}: ${JSON.stringify(val)}`).join(", ");
2113
+ return s.length > 120 ? `${s.slice(0, 117)}...` : s;
2114
+ }
2115
+ async function executePending(calls, opts, emit, checkpoint) {
2116
+ const results = [];
2117
+ for (const call of calls) {
2118
+ if (opts.signal.aborted)
2119
+ return null;
2120
+ if (call.name === "spawn_subagents") {
2121
+ emit({ type: "tool:start", id: call.id, tool: call.name, target: "parallel subtasks", location: "local" });
2122
+ const started2 = Date.now();
2123
+ const out = await runSubagents(call.input, {
2124
+ credentials: opts.credentials,
2125
+ host: opts.host,
2126
+ model: opts.model,
2127
+ signal: opts.signal,
2128
+ depth: opts.host.subagentDepth ?? 0,
2129
+ runTurn: (o) => runTurn(o).then((r) => ({ text: r.text, aborted: r.aborted, error: r.error }))
2130
+ }, emit);
2131
+ emit({ type: "tool:end", id: call.id, tool: call.name, ok: true, durationMs: Date.now() - started2 });
2132
+ results.push({ id: call.id, result: truncate(out) });
2133
+ continue;
2134
+ }
2135
+ const trust = opts.host.trustLevel?.() ?? "none";
2136
+ const decision = canRunUnprompted(call.name, trust, call.destructive) ? "allow" : await opts.host.askPermission(call);
2137
+ if (decision === "abort")
2138
+ return null;
2139
+ if (decision === "deny") {
2140
+ emit({ type: "tool:denied", id: call.id, tool: call.name });
2141
+ results.push({ id: call.id, result: "The user declined to run this. Do not retry it; suggest an alternative or ask what they would prefer." });
2142
+ continue;
2143
+ }
2144
+ emit({ type: "tool:start", id: call.id, tool: call.name, target: targetOf(call), location: "local" });
2145
+ const started = Date.now();
2146
+ if (checkpoint && MUTATION_TOOLS.has(call.name)) {
2147
+ const p = call.input?.path;
2148
+ if (typeof p === "string" && p)
2149
+ checkpoint.record(p);
2150
+ }
2151
+ let oldContent = null;
2152
+ if (WRITE_TOOLS.has(call.name) && typeof call.input?.path === "string" && call.input.path) {
2153
+ try {
2154
+ const { readFileSync: readFileSync9, existsSync: existsSync8 } = await import("fs");
2155
+ const { resolve: pathResolve2, join: join9 } = await import("path");
2156
+ const { homedir: homedir5 } = await import("os");
2157
+ const raw = String(call.input.path);
2158
+ const expanded = raw === "~" ? homedir5() : raw.startsWith("~/") ? join9(homedir5(), raw.slice(2)) : raw;
2159
+ const abs = pathResolve2(expanded);
2160
+ if (existsSync8(abs))
2161
+ oldContent = readFileSync9(abs, "utf8");
2162
+ else
2163
+ oldContent = "";
2164
+ } catch {
2165
+ oldContent = null;
2166
+ }
2167
+ }
2168
+ let output;
2169
+ try {
2170
+ output = await executeLocalTool(call.name, call.input, {
2171
+ cwd: opts.host.cwd,
2172
+ signal: opts.signal,
2173
+ // The live view: output reaches the UI while the command is still
2174
+ // running, so a long build is visibly progressing rather than hung.
2175
+ onOutput: (chunk, stream) => emit({ type: "tool:output", id: call.id, chunk, stream })
2176
+ });
2177
+ } catch (e) {
2178
+ output = `Error: ${e instanceof Error ? e.message : String(e)}`;
2179
+ }
2180
+ const failed2 = output.startsWith("Error:") || output.includes("[command failed") || output.includes("[command timed out");
2181
+ const effect = describeEffect(call.name, call.input);
2182
+ if (!failed2 && oldContent !== null && (effect.kind === "wrote" || effect.kind === "modified")) {
2183
+ try {
2184
+ const filePath = String(call.input?.path ?? "");
2185
+ let patchSourceOld = oldContent;
2186
+ let patchSourceNew = null;
2187
+ if (call.name === "str_replace_file" && typeof call.input?.oldStr === "string" && typeof call.input?.newStr === "string") {
2188
+ patchSourceNew = oldContent.replace(String(call.input.oldStr), String(call.input.newStr));
2189
+ } else if (typeof call.input?.content === "string") {
2190
+ patchSourceNew = String(call.input.content);
2191
+ }
2192
+ if (patchSourceNew !== null) {
2193
+ const patch = buildPatch(patchSourceOld, patchSourceNew, filePath);
2194
+ if (patch)
2195
+ effect.patch = patch;
2196
+ }
2197
+ } catch {
2198
+ }
2199
+ }
2200
+ const bytes = effect.kind === "wrote" || effect.kind === "modified" ? byteLength(call.input?.content) : void 0;
2201
+ emit({ type: "tool:end", id: call.id, tool: call.name, ok: !failed2, durationMs: Date.now() - started, effect, bytes });
2202
+ results.push({ id: call.id, result: truncate(output) });
2203
+ }
2204
+ return results;
2205
+ }
2206
+ async function handleActions(actions, opts, emit) {
2207
+ for (const action of actions) {
2208
+ if (opts.signal.aborted)
2209
+ return;
2210
+ emit({ type: "action:pending", id: action.id, description: action.description, risk: normaliseRisk(action.risk) });
2211
+ const approve = opts.host.confirmAction ? await opts.host.confirmAction(action) : false;
2212
+ try {
2213
+ const res = await resolveAction(opts.credentials, { id: action.id, approve }, {
2214
+ signal: opts.signal,
2215
+ onAuthFailure: opts.onAuthFailure
2216
+ });
2217
+ emit({
2218
+ type: "action:done",
2219
+ id: action.id,
2220
+ ok: approve && Boolean(res?.success),
2221
+ detail: approve ? res?.result ?? res?.error : "Cancelled"
2222
+ });
2223
+ } catch (e) {
2224
+ emit({ type: "action:done", id: action.id, ok: false, detail: e instanceof Error ? e.message : String(e) });
2225
+ }
2226
+ }
2227
+ }
2228
+ function normaliseRisk(risk) {
2229
+ return risk === "critical" || risk === "high" || risk === "medium" ? risk : "low";
2230
+ }
2231
+ function truncate(s) {
2232
+ return s.length <= MAX_RESULT_CHARS ? s : `${s.slice(0, MAX_RESULT_CHARS)}
2233
+ ... (truncated, ${s.length - MAX_RESULT_CHARS} more characters)`;
2234
+ }
2235
+ function byteLength(content) {
2236
+ if (typeof content !== "string")
2237
+ return void 0;
2238
+ return Buffer.byteLength(content, "utf8");
2239
+ }
2240
+ function targetOf(call) {
2241
+ const i = call.input ?? {};
2242
+ const raw = i.command ?? i.path ?? i.url ?? i.pattern ?? i.name ?? i.container ?? i.host ?? i.files;
2243
+ if (raw == null)
2244
+ return void 0;
2245
+ const s = String(raw).trim();
2246
+ if (!s)
2247
+ return void 0;
2248
+ return s.length > 60 ? `${s.slice(0, 57)}\u2026` : s;
2249
+ }
2250
+ var MAX_HOPS, MAX_IDENTICAL_CALLS, MAX_WRITES_PER_FILE, MAX_RESULT_CHARS, READ_ONLY_TOOLS, MUTATION_TOOLS, WRITE_TOOLS;
2251
+ var init_loop = __esm({
2252
+ "node_modules/@hostwares/agent-client/dist/esm/agent/loop.js"() {
2253
+ init_client();
2254
+ init_errors();
2255
+ init_tools();
2256
+ init_steering();
2257
+ init_hooks();
2258
+ init_checkpoints();
2259
+ init_subagents();
2260
+ init_safety();
2261
+ init_events();
2262
+ MAX_HOPS = 50;
2263
+ MAX_IDENTICAL_CALLS = 2;
2264
+ MAX_WRITES_PER_FILE = 2;
2265
+ MAX_RESULT_CHARS = 3e4;
2266
+ READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
2267
+ "read_file",
2268
+ "list_directory",
2269
+ "search_files",
2270
+ "file_search",
2271
+ "get_system_info",
2272
+ "list_processes",
2273
+ "get_process_output",
2274
+ "check_port",
2275
+ "git_status",
2276
+ "git_log",
2277
+ "git_diff",
2278
+ "docker_ps"
2279
+ ]);
2280
+ MUTATION_TOOLS = /* @__PURE__ */ new Set([
2281
+ "write_file",
2282
+ "str_replace_file",
2283
+ "append_file",
2284
+ "delete_path",
2285
+ "git_commit",
2286
+ "git_add",
2287
+ "git_push",
2288
+ "install_package",
2289
+ "start_process",
2290
+ "stop_process",
2291
+ "kill_process"
2292
+ ]);
2293
+ WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "str_replace_file", "append_file"]);
2294
+ }
2295
+ });
2296
+
2297
+ // node_modules/@hostwares/agent-client/dist/esm/index.js
2298
+ var init_esm = __esm({
2299
+ "node_modules/@hostwares/agent-client/dist/esm/index.js"() {
2300
+ init_client();
2301
+ init_sse();
2302
+ init_errors();
2303
+ init_events();
2304
+ init_loop();
2305
+ init_tools();
2306
+ init_safety();
2307
+ init_tools();
2308
+ init_steering();
2309
+ init_hooks();
2310
+ init_checkpoints();
2311
+ init_exec();
2312
+ }
2313
+ });
2314
+
2315
+ // src/config.ts
2316
+ import { homedir as homedir4 } from "os";
2317
+ import { join as join6, dirname as dirname3 } from "path";
2318
+ import {
2319
+ existsSync as existsSync6,
2320
+ readFileSync as readFileSync6,
2321
+ writeFileSync as writeFileSync4,
2322
+ mkdirSync as mkdirSync4,
2323
+ renameSync,
2324
+ chmodSync,
2325
+ unlinkSync
2326
+ } from "fs";
2327
+ function getConfig() {
2328
+ if (_cache) return _cache;
2329
+ _cache = readConfigFromDisk();
2330
+ return _cache;
2331
+ }
2332
+ function reloadConfig() {
2333
+ _cache = readConfigFromDisk();
2334
+ return _cache;
2335
+ }
2336
+ function readConfigFromDisk() {
2337
+ if (!existsSync6(CONFIG_FILE)) return { ...EMPTY };
2338
+ try {
2339
+ const parsed = JSON.parse(readFileSync6(CONFIG_FILE, "utf8"));
2340
+ return {
2341
+ // A truncated or hand-edited config must degrade to "logged out", never
2342
+ // to a crash on startup.
2343
+ apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : "",
2344
+ baseUrl: typeof parsed.baseUrl === "string" && parsed.baseUrl ? parsed.baseUrl : DEFAULT_BASE_URL2,
2345
+ trustedProjects: Array.isArray(parsed.trustedProjects) ? parsed.trustedProjects.filter((p) => typeof p === "string") : [],
2346
+ model: typeof parsed.model === "string" ? parsed.model : void 0
2347
+ };
2348
+ } catch {
2349
+ return { ...EMPTY };
2350
+ }
2351
+ }
2352
+ function saveConfig(patch) {
2353
+ const next = { ...getConfig(), ...patch };
2354
+ writeJsonSecure(CONFIG_FILE, next);
2355
+ _cache = next;
2356
+ return next;
2357
+ }
2358
+ function clearConfig() {
2359
+ try {
2360
+ unlinkSync(CONFIG_FILE);
2361
+ } catch {
2362
+ }
2363
+ _cache = { ...EMPTY };
2364
+ }
2365
+ function isAuthenticated() {
2366
+ return Boolean(getConfig().apiKey);
2367
+ }
2368
+ function writeJsonSecure(file, value) {
2369
+ const dir = dirname3(file);
2370
+ mkdirSync4(dir, { recursive: true, mode: 448 });
2371
+ try {
2372
+ chmodSync(dir, 448);
2373
+ } catch {
2374
+ }
2375
+ const tmp = `${file}.${process.pid}.tmp`;
2376
+ writeFileSync4(tmp, JSON.stringify(value, null, 2), { mode: 384 });
2377
+ try {
2378
+ chmodSync(tmp, 384);
2379
+ } catch {
2380
+ }
2381
+ renameSync(tmp, file);
2382
+ }
2383
+ var HW_DIR, CONFIG_FILE, DEFAULT_BASE_URL2, EMPTY, _cache;
2384
+ var init_config = __esm({
2385
+ "src/config.ts"() {
2386
+ "use strict";
2387
+ HW_DIR = join6(homedir4(), ".hostwares");
2388
+ CONFIG_FILE = join6(HW_DIR, "config.json");
2389
+ DEFAULT_BASE_URL2 = "https://hostwares.com";
2390
+ EMPTY = { apiKey: "", baseUrl: DEFAULT_BASE_URL2 };
2391
+ _cache = null;
2392
+ }
2393
+ });
2394
+
2395
+ // src/ui/theme.ts
2396
+ var useColor, wrap, c, unicode, glyph, BANNER_LINES;
2397
+ var init_theme = __esm({
2398
+ "src/ui/theme.ts"() {
2399
+ "use strict";
2400
+ useColor = process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb" && process.stdout.isTTY === true;
2401
+ wrap = (open, close) => (s) => useColor ? `\x1B[${open}m${s}\x1B[${close}m` : s;
2402
+ c = {
2403
+ reset: "\x1B[0m",
2404
+ bold: wrap("1", "22"),
2405
+ dim: wrap("2", "22"),
2406
+ italic: wrap("3", "23"),
2407
+ under: wrap("4", "24"),
2408
+ // Brand green, matching the web product. Never violet - see the platform
2409
+ // design rules; the old banner used purple in install.sh and green in the CLI.
2410
+ green: wrap("32", "39"),
2411
+ cyan: wrap("36", "39"),
2412
+ yellow: wrap("33", "39"),
2413
+ red: wrap("31", "39"),
2414
+ gray: wrap("90", "39"),
2415
+ white: wrap("37", "39")
2416
+ };
2417
+ unicode = process.platform !== "win32" || process.env.WT_SESSION !== void 0;
2418
+ glyph = {
2419
+ tick: unicode ? "\u2713" : "OK",
2420
+ cross: unicode ? "\u2717" : "X",
2421
+ arrow: unicode ? "\u2192" : "->",
2422
+ bullet: unicode ? "\u2022" : "*",
2423
+ run: unicode ? "\u27F3" : "~",
2424
+ skip: unicode ? "\u2298" : "-",
2425
+ warn: unicode ? "\u26A0" : "!",
2426
+ caret: unicode ? "\u25B8" : ">",
2427
+ spinner: unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["|", "/", "-", "\\"]
2428
+ };
2429
+ BANNER_LINES = [
2430
+ "\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
2431
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
2432
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551",
2433
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551",
2434
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D",
2435
+ "\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D "
2436
+ ];
2437
+ }
2438
+ });
2439
+
2440
+ // src/ui/render.ts
2441
+ function write(text) {
2442
+ if (!text) return;
2443
+ process.stdout.write(text);
2444
+ atLineStart = text.endsWith("\n");
2445
+ }
2446
+ function line(text = "") {
2447
+ if (!atLineStart) process.stdout.write("\n");
2448
+ process.stdout.write(`${text}
2449
+ `);
2450
+ atLineStart = true;
2451
+ }
2452
+ function breakLine() {
2453
+ if (!atLineStart) {
2454
+ process.stdout.write("\n");
2455
+ atLineStart = true;
2456
+ }
2457
+ }
2458
+ function toolBucket(name) {
2459
+ if (["run_command", "start_process", "stop_process", "open_browser", "wait_for_url", "ssh_run", "ssh_upload", "ssh_download", "docker_exec", "docker_ps", "docker_logs", "install_package", "kill_process", "check_port", "list_processes", "get_process_output", "curl_request", "check_github_auth"].includes(name)) return "shell";
2460
+ if (["read_file", "write_file", "str_replace_file", "append_file", "delete_path", "list_directory", "file_search"].includes(name)) return "fs";
2461
+ if (["git_status", "git_diff", "git_log", "git_add", "git_commit", "git_push", "git_pull", "git_branch", "git_clone"].includes(name)) return "git";
2462
+ if (["search_files"].includes(name)) return "code";
2463
+ if (["web_search", "web_fetch"].includes(name)) return "web";
2464
+ if (["spawn_subagents"].includes(name)) return "agent";
2465
+ if (["todo_list"].includes(name)) return "session";
2466
+ return name;
2467
+ }
2468
+ function shortPath(p) {
2469
+ if (!p) return "";
2470
+ const cwd = process.cwd();
2471
+ if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
2472
+ const home = process.env.HOME;
2473
+ if (home && p.startsWith(home + "/")) return "~/" + p.slice(home.length + 1);
2474
+ return p;
2475
+ }
2476
+ function headerFor(name, target) {
2477
+ switch (name) {
2478
+ // shell — command echoed verbatim
2479
+ case "run_command":
2480
+ return { head: "I will run the following command:", command: target };
2481
+ case "ssh_run":
2482
+ return { head: `I will run the following command${target ? ` on ${target}` : ""}:`, command: target };
2483
+ case "docker_exec":
2484
+ return { head: `I will run the following command in ${target ?? "the container"}:`, command: target };
2485
+ case "curl_request":
2486
+ return { head: "Making an HTTP request", command: target };
2487
+ case "install_package":
2488
+ return { head: `Installing package: ${target ?? ""}`.trimEnd() };
2489
+ case "kill_process":
2490
+ return { head: `Stopping process: ${target ?? ""}`.trimEnd() };
2491
+ // background processes
2492
+ case "start_process":
2493
+ return { head: "I will start the following in the background:", command: target };
2494
+ case "stop_process":
2495
+ return { head: `Stopping background process: ${target ?? ""}`.trimEnd() };
2496
+ case "get_process_output":
2497
+ return { head: `Reading process output${target ? `: ${target}` : ""}` };
2498
+ case "open_browser":
2499
+ return { head: `Opening in the browser: ${target ?? ""}`.trimEnd() };
2500
+ case "wait_for_url":
2501
+ return { head: `Waiting for ${target ?? "the server"} to respond` };
2502
+ // file writes — one neutral verb before; the completion line says created/updated
2503
+ case "write_file":
2504
+ return { head: `Writing: ${shortPath(target)}`.trimEnd() };
2505
+ case "str_replace_file":
2506
+ return { head: `Editing: ${shortPath(target)}`.trimEnd() };
2507
+ case "append_file":
2508
+ return { head: `Appending to: ${shortPath(target)}`.trimEnd() };
2509
+ case "delete_path":
2510
+ return { head: `Deleting: ${shortPath(target)}`.trimEnd() };
2511
+ // file reads
2512
+ case "read_file":
2513
+ return { head: `Reading file: ${shortPath(target)}`.trimEnd() };
2514
+ case "list_directory":
2515
+ return { head: `Reading directory: ${shortPath(target) || "."}` };
2516
+ case "search_files":
2517
+ return { head: `Searching for: ${target ?? ""}`.trimEnd() };
2518
+ case "file_search":
2519
+ return { head: `Searching for files: ${target ?? ""}`.trimEnd() };
2520
+ // inspection
2521
+ case "get_system_info":
2522
+ return { head: "Inspecting the system" };
2523
+ case "check_port":
2524
+ return { head: `Checking port${target ? ` ${target}` : ""}` };
2525
+ case "list_processes":
2526
+ return { head: "Listing running processes" };
2527
+ // git
2528
+ case "git_status":
2529
+ return { head: "Checking git status" };
2530
+ case "git_diff":
2531
+ return { head: "Reading the git diff" };
2532
+ case "git_log":
2533
+ return { head: "Reading the git log" };
2534
+ case "git_add":
2535
+ return { head: `Staging${target ? ` ${target}` : " changes"}` };
2536
+ case "git_commit":
2537
+ return { head: "Creating a commit" };
2538
+ case "git_push":
2539
+ return { head: "Pushing to the remote" };
2540
+ case "git_pull":
2541
+ return { head: "Pulling from the remote" };
2542
+ case "git_branch":
2543
+ return { head: "Managing branches" };
2544
+ case "git_clone":
2545
+ return { head: `Cloning ${target ?? "repository"}` };
2546
+ case "check_github_auth":
2547
+ return { head: "Checking GitHub auth" };
2548
+ // docker
2549
+ case "docker_ps":
2550
+ return { head: "Listing containers" };
2551
+ case "docker_logs":
2552
+ return { head: `Reading container logs${target ? `: ${target}` : ""}` };
2553
+ // ssh transfer
2554
+ case "ssh_upload":
2555
+ return { head: `Uploading to ${target ?? "the remote host"}` };
2556
+ case "ssh_download":
2557
+ return { head: `Downloading from ${target ?? "the remote host"}` };
2558
+ // web
2559
+ case "web_search":
2560
+ return { head: `Searching the web for: ${target ?? ""}`.trimEnd() };
2561
+ case "web_fetch":
2562
+ return { head: `Fetching: ${target ?? ""}`.trimEnd() };
2563
+ case "todo_list":
2564
+ return { head: "Updating the task list" };
2565
+ case "spawn_subagents":
2566
+ return { head: "Running independent tasks in parallel" };
2567
+ default:
2568
+ return { head: humanTool(name) };
2569
+ }
2570
+ }
2571
+ function toolRunning(name, target) {
2572
+ breakLine();
2573
+ const { head, command } = headerFor(name, target);
2574
+ const suffix = c.dim(` (using tool: ${toolBucket(name)})`);
2575
+ if (command) {
2576
+ process.stdout.write(`${c.bold(head)} ${c.cyan(command)}${suffix}
2577
+ `);
2578
+ } else {
2579
+ process.stdout.write(`${c.bold(head)}${suffix}
2580
+ `);
2581
+ }
2582
+ atLineStart = true;
2583
+ }
2584
+ function toolDone(name, status, durationMs, effect, bytes) {
2585
+ breakLine();
2586
+ const time = durationMs !== void 0 ? formatMs(durationMs) : "0ms";
2587
+ if (status === "success" || status === "executed") {
2588
+ const patch = effect?.patch;
2589
+ if (patch) {
2590
+ for (const l of patch.lines) {
2591
+ if (l.kind === "ctx") {
2592
+ const nums = `${String(l.oldLine ?? "").padStart(3)},${String(l.newLine ?? "").padStart(3)}:`;
2593
+ process.stdout.write(` ${c.dim(nums)} ${c.dim(l.text)}
2594
+ `);
2595
+ } else if (l.kind === "del") {
2596
+ const nums = `-${String(l.oldLine ?? "").padStart(3)} :`;
2597
+ process.stdout.write(` ${c.red(nums)} ${c.red(l.text)}
2598
+ `);
2599
+ } else if (l.kind === "add") {
2600
+ const nums = `+${String(l.newLine ?? "").padStart(3)} :`;
2601
+ process.stdout.write(` ${c.green(nums)} ${c.green(l.text)}
2602
+ `);
2603
+ }
2604
+ }
2605
+ if (patch.truncated) process.stdout.write(` ${c.dim(`\u2026 +${patch.truncated} more lines`)}
2606
+ `);
2607
+ }
2608
+ if (effect && (effect.kind === "wrote" || effect.kind === "modified" || effect.kind === "deleted")) {
2609
+ const verb = effect.kind === "wrote" ? "Created" : effect.kind === "modified" ? "Updated" : "Deleted";
2610
+ const size = bytes && effect.kind !== "deleted" ? ` ${formatBytes3(bytes)}` : "";
2611
+ process.stdout.write(c.dim(` \u2014 ${verb}${size} in ${time}
2612
+ `));
2613
+ } else {
2614
+ process.stdout.write(c.dim(` \u2014 Completed in ${time}
2615
+ `));
2616
+ }
2617
+ } else if (status === "queued") {
2618
+ process.stdout.write(` ${c.yellow(glyph.caret)} ${c.dim("Queued for approval")}
2619
+ `);
2620
+ } else {
2621
+ process.stdout.write(` ${c.red(glyph.cross)} ${c.red("Failed")} ${c.dim(`after ${time}`)}
2622
+ `);
2623
+ }
2624
+ atLineStart = true;
2625
+ }
2626
+ function toolOutputLine(line2) {
2627
+ breakLine();
2628
+ const trimmed = line2.length > 200 ? `${line2.slice(0, 197)}\u2026` : line2;
2629
+ process.stdout.write(` ${c.dim(trimmed)}
2630
+ `);
2631
+ atLineStart = true;
2632
+ }
2633
+ function dim(text) {
2634
+ return c.dim(text);
2635
+ }
2636
+ function formatBytes3(n) {
2637
+ if (n < 1024) return `${n}B`;
2638
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
2639
+ return `${(n / 1024 / 1024).toFixed(1)}MB`;
2640
+ }
2641
+ function toolSkipped(name) {
2642
+ breakLine();
2643
+ process.stdout.write(` ${c.dim(glyph.skip)} ${c.dim(`${humanTool(name)} \u2014 skipped`)}
2644
+ `);
2645
+ atLineStart = true;
2646
+ }
2647
+ function note(text) {
2648
+ line(c.dim(` ${text}`));
2649
+ }
2650
+ function error(text) {
2651
+ line(`${c.red(glyph.cross)} ${text}`);
2652
+ }
2653
+ function success(text) {
2654
+ line(`${c.green(glyph.tick)} ${text}`);
2655
+ }
2656
+ function humanTool(name) {
2657
+ return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2658
+ }
2659
+ function formatMs(ms) {
2660
+ if (ms < 1e3) return `${ms}ms`;
2661
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
2662
+ return `${Math.round(ms / 6e4)}m`;
2663
+ }
2664
+ function statusLine(s) {
2665
+ const parts = [
2666
+ `${c.dim("cost")} ${s.turnCredits.toFixed(2)}`,
2667
+ `${c.dim("left")} ${s.balance.toFixed(2)}`,
2668
+ `${c.dim("took")} ${formatMs(s.elapsedMs)}`
2669
+ ];
2670
+ const ctx = s.context;
2671
+ if (ctx?.promptTokens !== void 0 && ctx.usagePercent !== void 0) {
2672
+ const k = (ctx.promptTokens / 1e3).toFixed(1);
2673
+ const pct = ctx.usagePercent;
2674
+ const gauge = `${k}K (${pct}%)`;
2675
+ parts.push(`${c.dim("ctx")} ${pct >= 75 ? c.yellow(gauge) : c.dim(gauge)}`);
2676
+ }
2677
+ if (s.model) parts.push(c.dim(shortModel(s.model)));
2678
+ breakLine();
2679
+ process.stdout.write(`${c.dim(` ${glyph.caret} `)}${parts.join(c.dim(" \xB7 "))}
2680
+
2681
+ `);
2682
+ atLineStart = true;
2683
+ }
2684
+ function shortModel(id) {
2685
+ return id.replace(/^claude-/, "").replace(/-\d{8}$/, "");
2686
+ }
2687
+ var atLineStart, Spinner;
2688
+ var init_render = __esm({
2689
+ "src/ui/render.ts"() {
2690
+ "use strict";
2691
+ init_theme();
2692
+ atLineStart = true;
2693
+ Spinner = class {
2694
+ timer = null;
2695
+ frame = 0;
2696
+ text = "";
2697
+ active = false;
2698
+ start(text) {
2699
+ this.text = text;
2700
+ if (!process.stdout.isTTY) return;
2701
+ if (this.active) {
2702
+ this.update(text);
2703
+ return;
2704
+ }
2705
+ breakLine();
2706
+ this.active = true;
2707
+ this.timer = setInterval(() => {
2708
+ const f = glyph.spinner[this.frame++ % glyph.spinner.length];
2709
+ process.stdout.write(`\r${c.green(f)} ${c.dim(this.text)}\x1B[K`);
2710
+ }, 80);
2711
+ this.timer.unref?.();
2712
+ }
2713
+ update(text) {
2714
+ this.text = text;
2715
+ }
2716
+ /** Stop and erase the line, leaving the cursor where the spinner began. */
2717
+ stop() {
2718
+ if (this.timer) {
2719
+ clearInterval(this.timer);
2720
+ this.timer = null;
2721
+ }
2722
+ const erased = this.active && process.stdout.isTTY === true;
2723
+ if (erased) process.stdout.write("\r\x1B[K");
2724
+ this.active = false;
2725
+ if (erased) atLineStart = true;
2726
+ }
2727
+ };
2728
+ }
2729
+ });
2730
+
2731
+ // src/auth/device.ts
2732
+ var device_exports = {};
2733
+ __export(device_exports, {
2734
+ login: () => login,
2735
+ reauthenticate: () => reauthenticate
2736
+ });
2737
+ async function login(opts = {}) {
2738
+ const base = getConfig().baseUrl || DEFAULT_BASE_URL2;
2739
+ let start;
2740
+ try {
2741
+ const res = await fetch(`${base}/api/auth/device`, { method: "POST", signal: opts.signal });
2742
+ if (!res.ok) {
2743
+ error(`Could not start login (HTTP ${res.status}). Try again in a moment.`);
2744
+ return false;
2745
+ }
2746
+ start = await res.json();
2747
+ } catch (e) {
2748
+ if (opts.signal?.aborted) throw new AbortedError();
2749
+ error("Could not reach Hostwares to start login. Check your connection.");
2750
+ return false;
2751
+ }
2752
+ if (!start.device_code || !start.user_code || !start.verification_url) {
2753
+ error("Login response from the server was incomplete. Please report this.");
2754
+ return false;
2755
+ }
2756
+ if (!opts.quiet) line();
2757
+ line(` Open this URL to authorise:`);
2758
+ line(` ${c.cyan(start.verification_url)}`);
2759
+ line();
2760
+ line(` Confirmation code: ${c.bold(start.user_code)}`);
2761
+ line();
2762
+ if (!opts.noBrowser) openBrowser(start.verification_url);
2763
+ const intervalMs = Math.max(1e3, (start.interval ?? 3) * 1e3);
2764
+ const deadline = Date.now() + (start.expires_in ?? 600) * 1e3;
2765
+ note(`Waiting for authorisation\u2026 ${c.dim("(Ctrl-C to cancel)")}`);
2766
+ while (Date.now() < deadline) {
2767
+ if (opts.signal?.aborted) throw new AbortedError();
2768
+ await sleep(intervalMs, opts.signal);
2769
+ let poll;
2770
+ try {
2771
+ poll = await fetch(
2772
+ `${base}/api/auth/device-poll?code=${encodeURIComponent(start.device_code)}`,
2773
+ { signal: opts.signal }
2774
+ );
2775
+ } catch {
2776
+ continue;
2777
+ }
2778
+ if (poll.status === 429) {
2779
+ await sleep(intervalMs, opts.signal);
2780
+ continue;
2781
+ }
2782
+ let data;
2783
+ try {
2784
+ data = await poll.json();
2785
+ } catch {
2786
+ continue;
2787
+ }
2788
+ if (data.status === "approved" && data.token) {
2789
+ saveConfig({ apiKey: data.token, baseUrl: base });
2790
+ reloadConfig();
2791
+ if (!opts.quiet) success("Signed in.");
2792
+ return true;
2793
+ }
2794
+ if (data.error === "expired" || poll.status === 410) {
2795
+ error("That code expired. Run `hw login` again.");
2796
+ return false;
2797
+ }
2798
+ if (poll.status === 404) {
2799
+ error("That login request is no longer valid. Run `hw login` again.");
2800
+ return false;
2801
+ }
2802
+ }
2803
+ error("Login timed out. Run `hw login` to try again.");
2804
+ return false;
2805
+ }
2806
+ async function reauthenticate(signal) {
2807
+ line();
2808
+ note(`${glyph.warn} Your sign-in expired. Reconnecting\u2026`);
2809
+ const ok = await login({ quiet: true, signal });
2810
+ if (ok) note(`${glyph.tick} Reconnected \u2014 carrying on.`);
2811
+ return ok;
2812
+ }
2813
+ function openBrowser(url) {
2814
+ const [bin, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
2815
+ try {
2816
+ exec(bin, args, { timeoutMs: 5e3 });
2817
+ } catch {
2818
+ }
2819
+ }
2820
+ function sleep(ms, signal) {
2821
+ return new Promise((resolve2, reject) => {
2822
+ const t = setTimeout(resolve2, ms);
2823
+ signal?.addEventListener("abort", () => {
2824
+ clearTimeout(t);
2825
+ reject(new AbortedError());
2826
+ }, { once: true });
2827
+ });
2828
+ }
2829
+ var init_device = __esm({
2830
+ "src/auth/device.ts"() {
2831
+ "use strict";
2832
+ init_esm();
2833
+ init_config();
2834
+ init_theme();
2835
+ init_render();
2836
+ init_esm();
2837
+ }
2838
+ });
2839
+
2840
+ // src/index.ts
2841
+ import { readFileSync as readFileSync8 } from "fs";
2842
+ import { fileURLToPath } from "url";
2843
+ import { dirname as dirname4, join as join8 } from "path";
2844
+
2845
+ // src/agent/run.ts
2846
+ init_esm();
2847
+ init_config();
2848
+ init_device();
2849
+
2850
+ // src/agent/permissions.ts
2851
+ init_config();
2852
+
2853
+ // src/ui/input.ts
2854
+ init_theme();
2855
+ import { createInterface } from "readline";
2856
+ var repl = null;
2857
+ var queue = Promise.resolve();
2858
+ function createReplInterface(prompt) {
2859
+ repl = createInterface({
2860
+ input: process.stdin,
2861
+ output: process.stdout,
2862
+ prompt,
2863
+ // Ctrl-C is handled explicitly via the SIGINT event so an in-flight
2864
+ // request can be aborted without killing the process.
2865
+ terminal: process.stdout.isTTY === true,
2866
+ historySize: 500
2867
+ });
2868
+ return repl;
2869
+ }
2870
+ function closeRepl() {
2871
+ repl?.close();
2872
+ repl = null;
2873
+ }
2874
+ async function withExclusiveStdin(fn) {
2875
+ const run3 = queue.then(async () => {
2876
+ const wasRepl = repl;
2877
+ const stdin = process.stdin;
2878
+ const dataListeners = stdin.listeners("data");
2879
+ const keypressListeners = stdin.listeners("keypress");
2880
+ wasRepl?.pause();
2881
+ for (const l of dataListeners) stdin.removeListener("data", l);
2882
+ for (const l of keypressListeners) stdin.removeListener("keypress", l);
2883
+ try {
2884
+ return await fn();
2885
+ } finally {
2886
+ for (const l of dataListeners) stdin.on("data", l);
2887
+ for (const l of keypressListeners) stdin.on("keypress", l);
2888
+ if (wasRepl) {
2889
+ wasRepl.resume();
2890
+ }
2891
+ }
2892
+ });
2893
+ queue = run3.catch(() => void 0);
2894
+ return run3;
2895
+ }
2896
+ async function readKey(allowed) {
2897
+ if (!process.stdin.isTTY) {
2898
+ return "";
2899
+ }
2900
+ return withExclusiveStdin(() => new Promise((resolve2) => {
2901
+ const stdin = process.stdin;
2902
+ const wasRaw = stdin.isRaw === true;
2903
+ const cleanup = () => {
2904
+ stdin.removeListener("data", onData);
2905
+ if (stdin.isTTY) stdin.setRawMode(wasRaw);
2906
+ if (!wasRaw) stdin.pause();
2907
+ };
2908
+ const onData = (buf) => {
2909
+ const seq = buf.toString("utf8");
2910
+ let key;
2911
+ if (seq === "") key = "ctrl-c";
2912
+ else if (seq === "\x1B[A") key = "up";
2913
+ else if (seq === "\x1B[B") key = "down";
2914
+ else if (seq === "\x1B[C") key = "right";
2915
+ else if (seq === "\x1B[D") key = "left";
2916
+ else if (seq === "\x1B") key = "escape";
2917
+ else if (seq === "\r" || seq === "\n") key = "enter";
2918
+ else key = seq[0]?.toLowerCase() ?? "";
2919
+ if (allowed && !allowed.includes(key) && key !== "ctrl-c" && key !== "escape") return;
2920
+ cleanup();
2921
+ if (key.length === 1) process.stdout.write(key);
2922
+ process.stdout.write("\n");
2923
+ resolve2(key);
2924
+ };
2925
+ stdin.setRawMode(true);
2926
+ stdin.resume();
2927
+ stdin.on("data", onData);
2928
+ }));
2929
+ }
2930
+ async function confirm(prompt, defaultAnswer = false) {
2931
+ const hint = defaultAnswer ? "[Y/n]" : "[y/N]";
2932
+ process.stdout.write(`${prompt} ${c.dim(hint)} `);
2933
+ const key = await readKey();
2934
+ if (key === "ctrl-c" || key === "escape") return false;
2935
+ if (key === "enter" || key === "") return defaultAnswer;
2936
+ return key === "y";
2937
+ }
2938
+ async function pickScope(options) {
2939
+ if (!process.stdin.isTTY) return 0;
2940
+ return withExclusiveStdin(() => new Promise((resolve2) => {
2941
+ let idx = 0;
2942
+ const stdin = process.stdin;
2943
+ const wasRaw = stdin.isRaw === true;
2944
+ const render = () => {
2945
+ process.stdout.write(`
2946
+ ${c.dim("Press")} ${c.bold("(\u2191\u2193)")} ${c.dim("to navigate \xB7")} ${c.bold("(\u21B5)")} ${c.dim("to select scope")}
2947
+ `);
2948
+ for (let i = 0; i < options.length; i++) {
2949
+ const sel = i === idx;
2950
+ const arrow = sel ? c.cyan(">") : " ";
2951
+ const txt = sel ? c.bold(c.cyan(options[i])) : c.dim(options[i]);
2952
+ process.stdout.write(`${arrow} ${txt}
2953
+ `);
2954
+ }
2955
+ };
2956
+ const cleanup = () => {
2957
+ stdin.removeListener("data", onData);
2958
+ if (stdin.isTTY) stdin.setRawMode(wasRaw);
2959
+ if (!wasRaw) stdin.pause();
2960
+ };
2961
+ const onData = (buf) => {
2962
+ const seq = buf.toString("utf8");
2963
+ if (seq === "") {
2964
+ cleanup();
2965
+ resolve2(-1);
2966
+ return;
2967
+ }
2968
+ if (seq === "\x1B") {
2969
+ cleanup();
2970
+ resolve2(-1);
2971
+ return;
2972
+ }
2973
+ if (seq === "\x1B[A") {
2974
+ idx = (idx - 1 + options.length) % options.length;
2975
+ process.stdout.write(`\x1B[${options.length + 1}A\x1B[J`);
2976
+ render();
2977
+ return;
2978
+ }
2979
+ if (seq === "\x1B[B") {
2980
+ idx = (idx + 1) % options.length;
2981
+ process.stdout.write(`\x1B[${options.length + 1}A\x1B[J`);
2982
+ render();
2983
+ return;
2984
+ }
2985
+ if (seq === "\r" || seq === "\n") {
2986
+ cleanup();
2987
+ process.stdout.write("\n");
2988
+ resolve2(idx);
2989
+ return;
2990
+ }
2991
+ };
2992
+ stdin.setRawMode(true);
2993
+ stdin.resume();
2994
+ stdin.on("data", onData);
2995
+ render();
2996
+ }));
2997
+ }
2998
+
2999
+ // src/agent/permissions.ts
3000
+ init_theme();
3001
+ init_render();
3002
+ init_esm();
3003
+ var READ_ONLY = /* @__PURE__ */ new Set([
3004
+ "read_file",
3005
+ "list_directory",
3006
+ "get_system_info",
3007
+ "git_status",
3008
+ "git_log",
3009
+ "git_diff",
3010
+ "check_github_auth",
3011
+ "docker_ps",
3012
+ "list_processes",
3013
+ "check_port",
3014
+ "get_process_output",
3015
+ "open_browser",
3016
+ "wait_for_url",
3017
+ "web_search",
3018
+ "web_fetch"
3019
+ ]);
3020
+ var ALWAYS_ASK2 = /* @__PURE__ */ new Set(["delete_path", "kill_process", "ssh_run", "git_push"]);
3021
+ var sessionTrust = false;
3022
+ var sessionRules = [];
3023
+ function isSessionTrusted() {
3024
+ return sessionTrust;
3025
+ }
3026
+ function setSessionTrust(on) {
3027
+ sessionTrust = on;
3028
+ }
3029
+ function addSessionRule(rule) {
3030
+ sessionRules.push(rule);
3031
+ }
3032
+ var cachedRules = null;
3033
+ function permissionRules() {
3034
+ if (cachedRules === null) {
3035
+ try {
3036
+ cachedRules = loadPermissionRules(process.cwd());
3037
+ } catch {
3038
+ cachedRules = [];
3039
+ }
3040
+ }
3041
+ return cachedRules;
3042
+ }
3043
+ function isProjectTrusted(cwd = process.cwd()) {
3044
+ return (getConfig().trustedProjects ?? []).includes(cwd);
3045
+ }
3046
+ function setProjectTrust(on, cwd = process.cwd()) {
3047
+ const current = new Set(getConfig().trustedProjects ?? []);
3048
+ if (on) current.add(cwd);
3049
+ else current.delete(cwd);
3050
+ saveConfig({ trustedProjects: [...current] });
3051
+ }
3052
+ async function askPermission(tool) {
3053
+ if (sessionRules.length) {
3054
+ const v = decidePermission(tool.name, tool.input, sessionRules, "none", tool.destructive);
3055
+ if (v === "allow") return "allow";
3056
+ if (v === "deny") return "deny";
3057
+ }
3058
+ const trust = sessionTrust ? "session" : isProjectTrusted() ? "project" : "none";
3059
+ const verdict = decidePermission(tool.name, tool.input, permissionRules(), trust, tool.destructive);
3060
+ if (verdict === "deny") {
3061
+ process.stdout.write(`
3062
+ ${c.red(glyph.warn)} ${c.red("Refused:")} ${describe(tool)} ${c.dim("(blocked by a permission rule)")}
3063
+ `);
3064
+ return "deny";
3065
+ }
3066
+ if (verdict === "allow") return "allow";
3067
+ if (READ_ONLY.has(tool.name)) return "allow";
3068
+ const alwaysAsk = ALWAYS_ASK2.has(tool.name) || tool.destructive;
3069
+ if (!alwaysAsk && (sessionTrust || isProjectTrusted())) return "allow";
3070
+ breakLine();
3071
+ process.stdout.write(`
3072
+ ${c.yellow(glyph.caret)} ${describe(tool)}
3073
+ `);
3074
+ const consequence = consequenceOf(tool);
3075
+ if (consequence) process.stdout.write(` ${c.red(glyph.warn)} ${c.dim(consequence)}
3076
+ `);
3077
+ const options = alwaysAsk ? `${c.bold("y")} yes ${c.bold("n")} no ${c.dim("esc cancel")}` : `${c.bold("y")} yes ${c.bold("n")} no ${c.bold("t")} trust this session ${c.bold("a")} always in this project ${c.dim("esc cancel")}`;
3078
+ process.stdout.write(` ${c.dim(options)}: `);
3079
+ const key = await readKey(alwaysAsk ? ["y", "n"] : ["y", "n", "t", "a"]);
3080
+ switch (key) {
3081
+ case "y":
3082
+ return "allow";
3083
+ case "t": {
3084
+ const toolPath = String(tool.input.path ?? tool.input.localPath ?? "");
3085
+ const rel = toolPath ? shortRel(toolPath) : "";
3086
+ const dir = rel ? rel.split("/").slice(0, -1).join("/") || "." : "";
3087
+ const scopeOpts = [
3088
+ `Specific paths \u2192 ${rel || tool.name} ${c.dim("(this file only)")}`,
3089
+ `Complete directory \u2192 ${dir || "."} ${c.dim("(all files here)")}`,
3090
+ `Entire Tool \u2192 * ${c.dim(`(always allow '${tool.name}')`)}`
3091
+ ];
3092
+ const displayOpts = [
3093
+ `Specific paths \u2192 ${rel || tool.name}`,
3094
+ `Complete directory \u2192 ${dir || "."}`,
3095
+ `Entire Tool \u2192 *`
3096
+ ];
3097
+ const choice = await pickScope(displayOpts);
3098
+ if (choice === -1) return "abort";
3099
+ if (choice === 0) {
3100
+ const match = rel || "*";
3101
+ addSessionRule({ tool: tool.name, match, effect: "allow" });
3102
+ process.stdout.write(` ${c.dim(`Trusted '${tool.name}' for ${match} this session.`)}
3103
+ `);
3104
+ } else if (choice === 1) {
3105
+ const match = dir ? `${dir}/*` : "*";
3106
+ addSessionRule({ tool: tool.name, match, effect: "allow" });
3107
+ process.stdout.write(` ${c.dim(`Trusted '${tool.name}' for ${match} this session.`)}
3108
+ `);
3109
+ } else {
3110
+ addSessionRule({ tool: tool.name, match: "*", effect: "allow" });
3111
+ process.stdout.write(` ${c.dim(`Trusted '${tool.name}' (*) this session.`)}
3112
+ `);
3113
+ }
3114
+ return "allow";
3115
+ }
3116
+ case "a":
3117
+ setProjectTrust(true);
3118
+ process.stdout.write(` ${c.dim(`Always allowed in ${process.cwd()}.`)}
3119
+ `);
3120
+ return "allow";
3121
+ case "ctrl-c":
3122
+ case "escape":
3123
+ return "abort";
3124
+ // Includes "" from a non-TTY: with no interactive user there is nobody to
3125
+ // grant permission, and defaulting to allow would let a piped script run
3126
+ // arbitrary commands unattended.
3127
+ default:
3128
+ return "deny";
3129
+ }
3130
+ }
3131
+ function shortRel(absPath) {
3132
+ const cwd = process.cwd();
3133
+ if (absPath.startsWith(cwd + "/")) return absPath.slice(cwd.length + 1);
3134
+ const home = process.env.HOME;
3135
+ if (home && absPath.startsWith(home + "/")) return "~/" + absPath.slice(home.length + 1);
3136
+ return absPath;
3137
+ }
3138
+ function describe(tool) {
3139
+ const i = tool.input;
3140
+ const s = (k) => i[k] === void 0 ? "" : String(i[k]);
3141
+ const t = (v, n = 70) => v.length > n ? `${v.slice(0, n - 1)}\u2026` : v;
3142
+ switch (tool.name) {
3143
+ case "run_command":
3144
+ return `Run ${c.bold(t(s("command"), 90))}`;
3145
+ case "start_process":
3146
+ return `Start in background ${c.bold(t(s("command"), 80))}${s("name") ? c.dim(` (as ${s("name")})`) : ""}`;
3147
+ case "stop_process":
3148
+ return `Stop background process ${c.bold(s("name"))}`;
3149
+ case "write_file":
3150
+ return `Write ${c.bold(s("path"))} ${c.dim(`(${String(i.content ?? "").length} chars)`)}`;
3151
+ case "append_file":
3152
+ return `Append to ${c.bold(s("path"))}`;
3153
+ case "str_replace_file": {
3154
+ const old = String(i.oldStr ?? "").split("\n")[0]?.slice(0, 60) ?? "";
3155
+ return `Edit ${c.bold(s("path"))}${old ? c.dim(` \u2014 replace "${old}\u2026"`) : ""}`;
3156
+ }
3157
+ case "delete_path":
3158
+ return `Delete ${c.bold(s("path"))}`;
3159
+ case "search_files":
3160
+ return `Search for ${c.bold(t(s("pattern")))}`;
3161
+ case "install_package":
3162
+ return `Install package ${c.bold(s("name"))}`;
3163
+ case "git_add":
3164
+ return `Stage ${c.bold(s("files"))}`;
3165
+ case "git_commit":
3166
+ return `Commit: ${c.bold(t(s("message")))}`;
3167
+ case "git_push":
3168
+ return `Push to ${c.bold(s("remote") || "origin")}${s("branch") ? `/${s("branch")}` : ""}`;
3169
+ case "git_pull":
3170
+ return `Pull from ${c.bold(s("remote") || "origin")}`;
3171
+ case "git_clone":
3172
+ return `Clone ${c.bold(s("url"))}`;
3173
+ case "git_branch":
3174
+ return `Branch: ${c.bold(s("action"))}${s("name") ? ` ${s("name")}` : ""}`;
3175
+ case "ssh_run":
3176
+ return `Run on ${c.bold(`${s("user")}@${s("host")}`)}: ${c.bold(t(s("command")))}`;
3177
+ case "ssh_upload":
3178
+ return `Upload ${c.bold(s("localPath"))} to ${c.bold(`${s("user")}@${s("host")}:${s("remotePath")}`)}`;
3179
+ case "ssh_download":
3180
+ return `Download ${c.bold(`${s("user")}@${s("host")}:${s("remotePath")}`)}`;
3181
+ case "kill_process":
3182
+ return `Kill process ${c.bold(s("target"))}`;
3183
+ case "docker_exec":
3184
+ return `Run in container ${c.bold(s("container"))}: ${c.bold(t(s("command")))}`;
3185
+ case "docker_logs":
3186
+ return `Read logs from ${c.bold(s("container"))}`;
3187
+ case "curl_request":
3188
+ return `${c.bold(s("method") || "GET")} ${c.bold(t(s("url")))}`;
3189
+ default:
3190
+ return humanTool(tool.name);
3191
+ }
3192
+ }
3193
+ function consequenceOf(tool) {
3194
+ const i = tool.input;
3195
+ switch (tool.name) {
3196
+ case "delete_path":
3197
+ return "Permanently removes this path and everything under it. There is no recycle bin.";
3198
+ case "git_push":
3199
+ return "Publishes commits to the remote, where others may already have pulled from it.";
3200
+ case "git_branch":
3201
+ return i.action === "delete" ? "Deletes the branch. Unmerged commits on it become unreachable." : null;
3202
+ case "ssh_run":
3203
+ return "Runs on a remote server, outside this machine.";
3204
+ case "kill_process":
3205
+ return "Terminates the process immediately; unsaved work in it is lost.";
3206
+ case "write_file":
3207
+ return "Overwrites the file if it already exists.";
3208
+ case "docker_exec":
3209
+ return "Runs inside a container, which may hold live data.";
3210
+ default:
3211
+ return null;
3212
+ }
3213
+ }
3214
+
3215
+ // src/agent/run.ts
3216
+ init_theme();
3217
+ init_render();
3218
+ async function runCliTurn(opts) {
3219
+ const config = getConfig();
3220
+ const spinner = new Spinner();
3221
+ const promptedIds = /* @__PURE__ */ new Set();
3222
+ const MAX_LIVE_LINES = 12;
3223
+ const live = /* @__PURE__ */ new Map();
3224
+ const track = (id) => {
3225
+ let s = live.get(id);
3226
+ if (!s) {
3227
+ s = { shown: 0, total: 0, partial: "" };
3228
+ live.set(id, s);
3229
+ }
3230
+ return s;
3231
+ };
3232
+ const feed = (id, chunk) => {
3233
+ const st = track(id);
3234
+ st.partial += chunk;
3235
+ const lines = st.partial.split("\n");
3236
+ st.partial = lines.pop() ?? "";
3237
+ for (const line2 of lines) {
3238
+ st.total++;
3239
+ if (st.shown < MAX_LIVE_LINES) {
3240
+ toolOutputLine(line2);
3241
+ st.shown++;
3242
+ } else if (st.shown === MAX_LIVE_LINES) {
3243
+ toolOutputLine(dim("\u2026 still running, output hidden"));
3244
+ st.shown++;
3245
+ }
3246
+ }
3247
+ };
3248
+ const finish2 = (id) => {
3249
+ const st = live.get(id);
3250
+ if (!st) return { hidden: 0 };
3251
+ if (st.partial.trim()) {
3252
+ st.total++;
3253
+ if (st.shown < MAX_LIVE_LINES) {
3254
+ toolOutputLine(st.partial);
3255
+ st.shown++;
3256
+ }
3257
+ }
3258
+ live.delete(id);
3259
+ return { hidden: Math.max(0, st.total - Math.min(st.shown, MAX_LIVE_LINES)) };
3260
+ };
3261
+ const host = {
3262
+ cwd: process.cwd(),
3263
+ askPermission: (call) => {
3264
+ promptedIds.add(call.id);
3265
+ return askPermission(call);
3266
+ },
3267
+ confirmAction: async (action) => {
3268
+ breakLine();
3269
+ const risk = action.risk === "critical" ? c.red(action.risk) : action.risk === "high" ? c.yellow(action.risk) : c.dim(action.risk);
3270
+ line(`${c.yellow("\u25B8")} ${action.description} ${c.dim(`(${risk})`)}`);
3271
+ return confirm(" Run this?", false);
3272
+ },
3273
+ trustLevel: () => isSessionTrusted() || isProjectTrusted() ? "session" : "none",
3274
+ onActivity: (e) => {
3275
+ switch (e.type) {
3276
+ case "thinking":
3277
+ spinner.start("Working");
3278
+ break;
3279
+ case "text":
3280
+ spinner.stop();
3281
+ write(e.chunk);
3282
+ break;
3283
+ case "tool:start":
3284
+ spinner.stop();
3285
+ if (promptedIds.has(e.id)) {
3286
+ promptedIds.delete(e.id);
3287
+ } else {
3288
+ toolRunning(e.tool, e.target);
3289
+ }
3290
+ break;
3291
+ case "tool:output":
3292
+ spinner.stop();
3293
+ feed(e.id, e.chunk);
3294
+ break;
3295
+ case "tool:end": {
3296
+ if (e.summary && /loop guard/i.test(e.summary)) {
3297
+ spinner.stop();
3298
+ note("Skipped a repeated read (already have it) \u2014 moving on.");
3299
+ break;
3300
+ }
3301
+ const { hidden } = finish2(e.id);
3302
+ if (hidden > 0) toolOutputLine(dim(`\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}`));
3303
+ toolDone(e.tool, e.ok ? "success" : "failed", e.durationMs, e.effect, e.bytes);
3304
+ break;
3305
+ }
3306
+ case "tool:denied":
3307
+ toolSkipped(e.tool);
3308
+ break;
3309
+ case "action:done":
3310
+ if (e.ok) success(e.detail?.split("\n")[0] ?? "Done");
3311
+ else note(e.detail ?? "Cancelled");
3312
+ break;
3313
+ case "turn:end":
3314
+ spinner.stop();
3315
+ breakLine();
3316
+ statusLine({
3317
+ balance: e.usage.balance,
3318
+ turnCredits: e.usage.credits,
3319
+ elapsedMs: e.usage.elapsedMs,
3320
+ context: e.usage,
3321
+ model: e.usage.model
3322
+ });
3323
+ break;
3324
+ case "reauth":
3325
+ spinner.stop();
3326
+ break;
3327
+ case "error":
3328
+ spinner.stop();
3329
+ error(e.message);
3330
+ break;
3331
+ case "tool:pending":
3332
+ spinner.stop();
3333
+ break;
3334
+ }
3335
+ }
3336
+ };
3337
+ try {
3338
+ const result = await runTurn({
3339
+ credentials: { apiKey: config.apiKey, baseUrl: config.baseUrl, userAgent: `hostwares-cli/${VERSION}` },
3340
+ message: opts.message,
3341
+ conversationId: opts.session.conversationId,
3342
+ model: config.model,
3343
+ signal: opts.signal,
3344
+ host,
3345
+ onAuthFailure: () => reauthenticate(opts.signal)
3346
+ });
3347
+ if (result.aborted) {
3348
+ breakLine();
3349
+ note("Stopped.");
3350
+ }
3351
+ return {
3352
+ conversationId: result.conversationId,
3353
+ creditsSpent: result.creditsSpent,
3354
+ aborted: result.aborted
3355
+ };
3356
+ } finally {
3357
+ spinner.stop();
3358
+ }
3359
+ }
3360
+ var VERSION = "0.0.0";
3361
+ function setVersion(v) {
3362
+ VERSION = v;
3363
+ }
3364
+
3365
+ // src/errors.ts
3366
+ init_esm();
3367
+ init_render();
3368
+ function reportError(e) {
3369
+ breakLine();
3370
+ if (e instanceof ApiError) {
3371
+ if (e.isOutOfCredits) {
3372
+ error("You're out of credits.");
3373
+ note("Ask me to buy credits \u2014 billing and support messages are free.");
3374
+ return;
3375
+ }
3376
+ if (e.status === 429) {
3377
+ error("Too many requests in a row.");
3378
+ note("Wait a few seconds and try again.");
3379
+ return;
3380
+ }
3381
+ if (e.code === "ai_terms_required") {
3382
+ error("You need to accept the AI agent terms before using this.");
3383
+ note(String(e.body.termsUrl ?? "https://hostwares.com/terms#ai-agent"));
3384
+ return;
3385
+ }
3386
+ if (e.isAuthFailure) {
3387
+ error("Not signed in. Run `hw login`.");
3388
+ return;
3389
+ }
3390
+ error(e.message);
3391
+ return;
3392
+ }
3393
+ error(e instanceof Error ? e.message : String(e));
3394
+ }
3395
+
3396
+ // src/index.ts
3397
+ init_device();
3398
+
3399
+ // src/commands/slash.ts
3400
+ init_esm();
3401
+ init_esm();
3402
+ init_esm();
3403
+ init_esm();
3404
+ init_config();
3405
+
3406
+ // src/session/store.ts
3407
+ init_config();
3408
+ import { createHash } from "crypto";
3409
+ import { join as join7 } from "path";
3410
+ import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync5, unlinkSync as unlinkSync2, mkdirSync as mkdirSync5 } from "fs";
3411
+ var SESSION_DIR = join7(HW_DIR, "sessions");
3412
+ var SCHEMA_VERSION = 2;
3413
+ function sessionPath(cwd) {
3414
+ const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 24);
3415
+ return join7(SESSION_DIR, `${hash}.json`);
3416
+ }
3417
+ function newSession(cwd = process.cwd()) {
3418
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3419
+ return {
3420
+ version: SCHEMA_VERSION,
3421
+ cwd,
3422
+ conversationId: null,
3423
+ createdAt: now,
3424
+ updatedAt: now,
3425
+ turns: [],
3426
+ creditsSpent: 0
3427
+ };
3428
+ }
3429
+ function loadSession(cwd = process.cwd()) {
3430
+ const file = sessionPath(cwd);
3431
+ if (!existsSync7(file)) return null;
3432
+ try {
3433
+ const data = JSON.parse(readFileSync7(file, "utf8"));
3434
+ if (data.version !== SCHEMA_VERSION) return null;
3435
+ if (data.cwd !== cwd) return null;
3436
+ if (!Array.isArray(data.turns)) return null;
3437
+ return {
3438
+ version: SCHEMA_VERSION,
3439
+ cwd,
3440
+ conversationId: typeof data.conversationId === "string" ? data.conversationId : null,
3441
+ createdAt: data.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3442
+ updatedAt: data.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3443
+ turns: data.turns.filter(isTurn),
3444
+ creditsSpent: typeof data.creditsSpent === "number" ? data.creditsSpent : 0
3445
+ };
3446
+ } catch {
3447
+ return null;
3448
+ }
3449
+ }
3450
+ function isTurn(t) {
3451
+ return Boolean(t) && typeof t === "object" && typeof t.content === "string" && (t.role === "user" || t.role === "assistant");
3452
+ }
3453
+ var MAX_LOCAL_TURNS = 200;
3454
+ function saveSession(session) {
3455
+ mkdirSync5(SESSION_DIR, { recursive: true, mode: 448 });
3456
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3457
+ if (session.turns.length > MAX_LOCAL_TURNS) {
3458
+ session.turns = session.turns.slice(-MAX_LOCAL_TURNS);
3459
+ }
3460
+ writeJsonSecure(sessionPath(session.cwd), session);
3461
+ }
3462
+ function appendTurn(session, role, content) {
3463
+ session.turns.push({ role, content, at: (/* @__PURE__ */ new Date()).toISOString() });
3464
+ }
3465
+ function listSessions() {
3466
+ if (!existsSync7(SESSION_DIR)) return [];
3467
+ const out = [];
3468
+ for (const name of readdirSync5(SESSION_DIR)) {
3469
+ if (!name.endsWith(".json")) continue;
3470
+ try {
3471
+ const data = JSON.parse(readFileSync7(join7(SESSION_DIR, name), "utf8"));
3472
+ if (data.version !== SCHEMA_VERSION || !data.cwd) continue;
3473
+ out.push({
3474
+ cwd: data.cwd,
3475
+ conversationId: data.conversationId ?? null,
3476
+ updatedAt: data.updatedAt,
3477
+ turnCount: Array.isArray(data.turns) ? data.turns.length : 0,
3478
+ creditsSpent: data.creditsSpent ?? 0
3479
+ });
3480
+ } catch {
3481
+ }
3482
+ }
3483
+ return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
3484
+ }
3485
+
3486
+ // src/commands/slash.ts
3487
+ init_theme();
3488
+ init_render();
3489
+ var COMMANDS = [
3490
+ { name: "/help", help: "Show this list" },
3491
+ { name: "/new", help: "Start a fresh conversation" },
3492
+ { name: "/resume", args: "[n]", help: "List recent conversations, or resume the nth" },
3493
+ { name: "/chat", args: "resume", help: "Same as /resume" },
3494
+ { name: "/sessions", help: "Show sessions saved on this machine" },
3495
+ { name: "/context", help: "Context usage for this conversation" },
3496
+ { name: "/tools", help: "Tools that run on this machine" },
3497
+ { name: "/steering", help: "Create .hostwares/steering/*.md so HW knows this project" },
3498
+ { name: "/rewind", args: "[n]", help: "Undo the last change (or the nth checkpoint). Lists them with no arg." },
3499
+ { name: "/trust", help: "Stop asking permission this session" },
3500
+ { name: "/untrust", help: "Ask permission again" },
3501
+ { name: "/model", args: "[name]", help: "Show or set the model" },
3502
+ { name: "/credits", help: "Credit balance" },
3503
+ { name: "/login", help: "Sign in again" },
3504
+ { name: "/logout", help: "Sign out on this machine" },
3505
+ { name: "/exit", help: "Leave" }
3506
+ ];
3507
+ async function handleSlash(input, ctx) {
3508
+ const [cmd, ...rest] = input.trim().split(/\s+/);
3509
+ const arg = rest.join(" ");
3510
+ switch (cmd) {
3511
+ case "/help":
3512
+ showHelp();
3513
+ return "handled";
3514
+ case "/exit":
3515
+ case "/quit":
3516
+ ctx.quit();
3517
+ return "handled";
3518
+ case "/new": {
3519
+ const fresh = newSession(ctx.session.cwd);
3520
+ ctx.session.conversationId = null;
3521
+ ctx.session.turns = [];
3522
+ ctx.session.createdAt = fresh.createdAt;
3523
+ saveSession(ctx.session);
3524
+ line(c.dim(" Started a new conversation."));
3525
+ return "handled";
3526
+ }
3527
+ case "/resume":
3528
+ case "/chat":
3529
+ await resume(arg.replace(/^resume\s*/, ""), ctx);
3530
+ return "handled";
3531
+ case "/sessions":
3532
+ showSessions();
3533
+ return "handled";
3534
+ case "/context":
3535
+ showContext(ctx.session);
3536
+ return "handled";
3537
+ case "/tools":
3538
+ showTools();
3539
+ return "handled";
3540
+ case "/steering":
3541
+ scaffoldSteering(ctx.session.cwd);
3542
+ return "handled";
3543
+ case "/rewind":
3544
+ rewindCheckpoint(ctx.session.cwd, arg);
3545
+ return "handled";
3546
+ case "/trust":
3547
+ setSessionTrust(true);
3548
+ line(c.dim(" Won't ask again this session. Deletes, kills, pushes and SSH still ask."));
3549
+ return "handled";
3550
+ case "/untrust":
3551
+ setSessionTrust(false);
3552
+ if (isProjectTrusted()) {
3553
+ setProjectTrust(false);
3554
+ line(c.dim(` Will ask again, and removed the saved trust for ${process.cwd()}.`));
3555
+ } else {
3556
+ line(c.dim(" Will ask again."));
3557
+ }
3558
+ return "handled";
3559
+ case "/model":
3560
+ setModel(arg);
3561
+ return "handled";
3562
+ case "/credits":
3563
+ await showCredits(ctx.signal);
3564
+ return "handled";
3565
+ case "/login":
3566
+ await (await Promise.resolve().then(() => (init_device(), device_exports))).login({ signal: ctx.signal });
3567
+ return "handled";
3568
+ case "/logout":
3569
+ clearConfig();
3570
+ line(c.dim(" Signed out. Run `hw login` to sign back in."));
3571
+ ctx.quit();
3572
+ return "handled";
3573
+ default:
3574
+ return "unknown";
3575
+ }
3576
+ }
3577
+ function showHelp() {
3578
+ line();
3579
+ for (const { name, args, help } of COMMANDS) {
3580
+ const label = args ? `${name} ${c.dim(args)}` : name;
3581
+ line(` ${c.green(label.padEnd(hasArgsPad(name, args)))} ${c.dim(help)}`);
3582
+ }
3583
+ line();
3584
+ line(c.dim(` ${glyph.bullet} Start a line with ! to run a shell command directly`));
3585
+ line(c.dim(` ${glyph.bullet} Ctrl-C stops the current reply, twice to leave`));
3586
+ line();
3587
+ }
3588
+ function hasArgsPad(name, args) {
3589
+ return 22 + (args ? `${args}`.length + 9 : 0);
3590
+ }
3591
+ async function resume(arg, ctx) {
3592
+ let rows;
3593
+ try {
3594
+ rows = await listConversations(
3595
+ { apiKey: getConfig().apiKey, baseUrl: getConfig().baseUrl },
3596
+ { limit: 15, signal: ctx.signal }
3597
+ );
3598
+ } catch (e) {
3599
+ reportError(e);
3600
+ return;
3601
+ }
3602
+ if (!rows.length) {
3603
+ line(c.dim(" No previous conversations."));
3604
+ return;
3605
+ }
3606
+ const pick = Number(arg);
3607
+ if (Number.isInteger(pick) && pick >= 1 && pick <= rows.length) {
3608
+ const chosen = rows[pick - 1];
3609
+ ctx.session.conversationId = chosen.id;
3610
+ ctx.session.turns = [];
3611
+ saveSession(ctx.session);
3612
+ line(c.dim(` Resumed "${chosen.title}" (${chosen.messageCount} messages).`));
3613
+ return;
3614
+ }
3615
+ line();
3616
+ rows.forEach((r, i) => {
3617
+ const when = relativeTime(r.updatedAt);
3618
+ line(` ${c.bold(String(i + 1).padStart(2))} ${r.title.slice(0, 52).padEnd(52)} ${c.dim(`${r.messageCount} msg \xB7 ${when}`)}`);
3619
+ });
3620
+ line();
3621
+ line(c.dim(" /resume <number> to continue one"));
3622
+ line();
3623
+ }
3624
+ function showSessions() {
3625
+ const sessions = listSessions();
3626
+ if (!sessions.length) {
3627
+ line(c.dim(" No saved sessions."));
3628
+ return;
3629
+ }
3630
+ line();
3631
+ for (const s of sessions.slice(0, 20)) {
3632
+ const here = s.cwd === process.cwd() ? c.green(` ${glyph.tick} here`) : "";
3633
+ line(` ${s.cwd}${here}`);
3634
+ line(c.dim(` ${s.turnCount} turns \xB7 ${s.creditsSpent.toFixed(2)} credits \xB7 ${relativeTime(s.updatedAt)}`));
3635
+ }
3636
+ line();
3637
+ }
3638
+ function showContext(session) {
3639
+ line();
3640
+ line(` Conversation: ${session.conversationId ? c.dim(session.conversationId) : c.dim("not started yet")}`);
3641
+ line(` Turns here: ${session.turns.length}`);
3642
+ line(` Project: ${c.dim(session.cwd)}`);
3643
+ line();
3644
+ line(c.dim(" Context size is reported by the server after each reply \u2014 see the ctx"));
3645
+ line(c.dim(" figure in the status line. When a session grows past the window the"));
3646
+ line(c.dim(" earlier part is summarised automatically and work continues \u2014 you do"));
3647
+ line(c.dim(" not need to start over."));
3648
+ line();
3649
+ }
3650
+ function showTools() {
3651
+ line();
3652
+ line(` ${c.bold(`${LOCAL_TOOL_NAMES.length} tools run on this machine:`)}`);
3653
+ line();
3654
+ const sorted = [...LOCAL_TOOL_NAMES].sort();
3655
+ const rows = Math.ceil(sorted.length / 4);
3656
+ for (let r = 0; r < rows; r++) {
3657
+ const cells = [0, 1, 2, 3].map((col) => sorted[col * rows + r]).filter(Boolean).map((n) => c.dim(n.padEnd(20)));
3658
+ line(` ${cells.join("")}`);
3659
+ }
3660
+ line();
3661
+ line(c.dim(" Hostwares account tools (sites, databases, DNS, billing) run on the server."));
3662
+ line();
3663
+ }
3664
+ function scaffoldSteering(cwd) {
3665
+ line();
3666
+ try {
3667
+ const { created, skipped } = generateSteering(cwd);
3668
+ if (created.length) {
3669
+ line(` ${c.bold("Created steering files")} ${c.dim("in .hostwares/steering/")}`);
3670
+ for (const f of created) line(` ${c.green(glyph.tick)} ${f}`);
3671
+ line();
3672
+ line(c.dim(" Edit them to describe your project \u2014 they load into every session so HW"));
3673
+ line(c.dim(" stops re-deriving your conventions."));
3674
+ }
3675
+ if (skipped.length) {
3676
+ line(c.dim(` Left alone (already exist): ${skipped.join(", ")}`));
3677
+ }
3678
+ } catch (e) {
3679
+ line(` ${c.red(glyph.warn)} Could not create steering files: ${e.message}`);
3680
+ }
3681
+ line();
3682
+ }
3683
+ function rewindCheckpoint(cwd, arg) {
3684
+ line();
3685
+ const checkpoints = listCheckpoints(cwd);
3686
+ if (!checkpoints.length) {
3687
+ line(c.dim(" Nothing to rewind \u2014 no changes have been checkpointed in this project yet."));
3688
+ line();
3689
+ return;
3690
+ }
3691
+ if (!arg.trim()) {
3692
+ line(` ${c.bold("Checkpoints")} ${c.dim("(newest first \u2014 /rewind undoes #1, /rewind N undoes the Nth)")}`);
3693
+ line();
3694
+ checkpoints.slice(0, 10).forEach((cp, i) => {
3695
+ const when = new Date(cp.createdAt).toLocaleString();
3696
+ const note2 = cp.note ? ` \u2014 ${cp.note.slice(0, 50)}` : "";
3697
+ line(` ${c.bold(String(i + 1))}. ${c.dim(`${cp.fileCount} file${cp.fileCount === 1 ? "" : "s"} \xB7 ${when}`)}${c.dim(note2)}`);
3698
+ });
3699
+ line();
3700
+ return;
3701
+ }
3702
+ const n = parseInt(arg.trim(), 10);
3703
+ const label = Number.isFinite(n) && n >= 1 && checkpoints[n - 1] ? checkpoints[n - 1].label : void 0;
3704
+ const result = rewind(cwd, label);
3705
+ if (result.ok) {
3706
+ line(` ${c.green(glyph.tick)} ${result.message}`);
3707
+ for (const f of result.restored) line(c.dim(` restored ${f}`));
3708
+ for (const f of result.deleted) line(c.dim(` removed ${f}`));
3709
+ } else {
3710
+ line(` ${c.red(glyph.warn)} ${result.message}`);
3711
+ }
3712
+ line();
3713
+ }
3714
+ function setModel(arg) {
3715
+ const ALLOWED = ["auto", "claude-sonnet-5", "claude-opus-5"];
3716
+ if (!arg) {
3717
+ line(` Model: ${c.bold(getConfig().model ?? "auto")}`);
3718
+ line(c.dim(` Options: ${ALLOWED.join(", ")}`));
3719
+ line(c.dim(" auto picks per task \u2014 cheaper for lookups, stronger for debugging."));
3720
+ return;
3721
+ }
3722
+ if (!ALLOWED.includes(arg)) {
3723
+ line(c.red(` Unknown model "${arg}".`));
3724
+ line(c.dim(` Options: ${ALLOWED.join(", ")}`));
3725
+ return;
3726
+ }
3727
+ saveConfig({ model: arg });
3728
+ line(c.dim(` Model set to ${arg}.`));
3729
+ if (arg === "claude-opus-5") line(c.dim(" Opus needs a Business plan; other plans fall back to Sonnet."));
3730
+ }
3731
+ async function showCredits(signal) {
3732
+ try {
3733
+ const cfg = getConfig();
3734
+ const r = await fetch(`${cfg.baseUrl}/api/credits`, {
3735
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
3736
+ signal
3737
+ });
3738
+ const res = r.ok ? await r.json() : null;
3739
+ const balance = res?.total ?? res?.balance;
3740
+ if (typeof balance === "number") line(` Credits: ${c.bold(balance.toFixed(2))}`);
3741
+ else line(c.dim(" Could not read your balance."));
3742
+ } catch (e) {
3743
+ reportError(e);
3744
+ }
3745
+ }
3746
+ function relativeTime(iso) {
3747
+ const diff = Date.now() - Date.parse(iso);
3748
+ if (!Number.isFinite(diff)) return "unknown";
3749
+ const mins = Math.floor(diff / 6e4);
3750
+ if (mins < 1) return "just now";
3751
+ if (mins < 60) return `${mins}m ago`;
3752
+ const hours = Math.floor(mins / 60);
3753
+ if (hours < 24) return `${hours}h ago`;
3754
+ return `${Math.floor(hours / 24)}d ago`;
3755
+ }
3756
+ var SLASH_NAMES = COMMANDS.map((c2) => c2.name);
3757
+
3758
+ // src/commands/chat.ts
3759
+ init_esm();
3760
+ init_theme();
3761
+ init_render();
3762
+ async function startChat(opts = {}) {
3763
+ const cwd = process.cwd();
3764
+ const session = loadSession(cwd) ?? newSession(cwd);
3765
+ if (!opts.resume) session.conversationId = null;
3766
+ if (opts.oneShot) {
3767
+ const code = await sendOne(opts.oneShot, session);
3768
+ return code;
3769
+ }
3770
+ if (opts.resume && session.conversationId) {
3771
+ note(`Continuing where you left off (${session.turns.length} turns). /new to start over.`);
3772
+ } else if (session.turns.length) {
3773
+ note(`${session.turns.length} earlier turns in this folder \u2014 /resume to continue one.`);
3774
+ }
3775
+ return repl2(session);
3776
+ }
3777
+ async function sendOne(message, session) {
3778
+ const controller = new AbortController();
3779
+ const onSigint = () => controller.abort();
3780
+ process.on("SIGINT", onSigint);
3781
+ try {
3782
+ const result = await runCliTurn({ message, session, signal: controller.signal });
3783
+ persist(session, message, result.conversationId, result.creditsSpent);
3784
+ return result.aborted ? 130 : 0;
3785
+ } finally {
3786
+ process.off("SIGINT", onSigint);
3787
+ }
3788
+ }
3789
+ async function repl2(session) {
3790
+ const prompt = `${c.green("you")} ${c.dim(glyph.arrow)} `;
3791
+ const rl = createReplInterface(prompt);
3792
+ let leaving = false;
3793
+ let busy = null;
3794
+ let sigintArmed = false;
3795
+ const onSigint = () => {
3796
+ if (busy) {
3797
+ busy.abort();
3798
+ return;
3799
+ }
3800
+ if (sigintArmed) {
3801
+ leaving = true;
3802
+ rl.close();
3803
+ return;
3804
+ }
3805
+ sigintArmed = true;
3806
+ line(c.dim(" (Ctrl-C again to leave, or type /exit)"));
3807
+ rl.prompt();
3808
+ };
3809
+ rl.on("SIGINT", onSigint);
3810
+ rl.prompt();
3811
+ for await (const raw of rl) {
3812
+ const input = raw.trim();
3813
+ sigintArmed = false;
3814
+ if (!input) {
3815
+ rl.prompt();
3816
+ continue;
3817
+ }
3818
+ if (input.startsWith("!")) {
3819
+ const command = input.slice(1).trim();
3820
+ if (command) {
3821
+ breakLine();
3822
+ line(formatResult(await execShell(command, { timeoutMs: 12e4 })));
3823
+ }
3824
+ rl.prompt();
3825
+ continue;
3826
+ }
3827
+ if (input.startsWith("/")) {
3828
+ const controller = new AbortController();
3829
+ const result = await handleSlash(input, {
3830
+ session,
3831
+ quit: () => {
3832
+ leaving = true;
3833
+ rl.close();
3834
+ },
3835
+ signal: controller.signal
3836
+ });
3837
+ if (result === "unknown") {
3838
+ line(c.dim(` Unknown command. Try ${SLASH_NAMES.slice(0, 4).join(", ")} or /help`));
3839
+ }
3840
+ if (leaving) break;
3841
+ rl.prompt();
3842
+ continue;
3843
+ }
3844
+ busy = new AbortController();
3845
+ try {
3846
+ breakLine();
3847
+ const result = await runCliTurn({ message: input, session, signal: busy.signal });
3848
+ persist(session, input, result.conversationId, result.creditsSpent);
3849
+ } finally {
3850
+ busy = null;
3851
+ }
3852
+ if (leaving) break;
3853
+ rl.prompt();
3854
+ }
3855
+ closeRepl();
3856
+ saveSession(session);
3857
+ if (session.turns.length) {
3858
+ line();
3859
+ line(c.dim(` ${session.turns.filter((t) => t.role === "user").length} messages \xB7 ${session.creditsSpent.toFixed(2)} credits this session`));
3860
+ }
3861
+ return 0;
3862
+ }
3863
+ function persist(session, message, conversationId, credits) {
3864
+ if (conversationId) session.conversationId = conversationId;
3865
+ appendTurn(session, "user", message);
3866
+ session.creditsSpent += credits;
3867
+ saveSession(session);
3868
+ }
3869
+
3870
+ // src/index.ts
3871
+ init_config();
3872
+ init_theme();
3873
+ init_render();
3874
+ init_esm();
3875
+ var VERSION2 = readVersion();
3876
+ setVersion(VERSION2);
3877
+ async function main(argv) {
3878
+ const [cmd, ...rest] = argv;
3879
+ switch (cmd) {
3880
+ case void 0:
3881
+ return interactive();
3882
+ case "chat":
3883
+ requireAuth();
3884
+ return startChat({ resume: rest.includes("--resume") || rest.includes("-r") });
3885
+ case "ask": {
3886
+ requireAuth();
3887
+ const message = rest.filter((a) => !a.startsWith("-")).join(" ");
3888
+ if (!message) return startChat({});
3889
+ return startChat({ oneShot: message });
3890
+ }
3891
+ case "login":
3892
+ if (rest[0] === "--token" && rest[1]) {
3893
+ saveConfig({ apiKey: rest[1] });
3894
+ success("Signed in with the supplied token.");
3895
+ return 0;
3896
+ }
3897
+ return await login({ noBrowser: rest.includes("--no-browser") }) ? 0 : 1;
3898
+ case "logout":
3899
+ clearConfig();
3900
+ success("Signed out on this machine.");
3901
+ return 0;
3902
+ case "sessions":
3903
+ return showSessions2();
3904
+ case "list":
3905
+ case "ls":
3906
+ requireAuth();
3907
+ return listSites();
3908
+ case "version":
3909
+ case "--version":
3910
+ case "-v":
3911
+ console.log(VERSION2);
3912
+ return 0;
3913
+ case "help":
3914
+ case "--help":
3915
+ case "-h":
3916
+ showHelp2();
3917
+ return 0;
3918
+ default:
3919
+ requireAuth();
3920
+ return startChat({ oneShot: argv.join(" ") });
3921
+ }
3922
+ }
3923
+ function interactive() {
3924
+ showBanner();
3925
+ if (!isAuthenticated()) {
3926
+ line(` Run ${c.bold("hw login")} to get started.`);
3927
+ line();
3928
+ return Promise.resolve(0);
3929
+ }
3930
+ return startChat({});
3931
+ }
3932
+ function requireAuth() {
3933
+ if (isAuthenticated()) return;
3934
+ error("Not signed in.");
3935
+ note("Run `hw login` first.");
3936
+ process.exit(1);
3937
+ }
3938
+ function showBanner() {
3939
+ const tag = [
3940
+ ` ${c.bold("Hostwares")} ${c.dim(`v${VERSION2}`)}`,
3941
+ ` ${c.dim("AI DevOps in your terminal")}`,
3942
+ "",
3943
+ isAuthenticated() ? ` ${c.green(glyph.tick)} ${c.dim("Signed in")}` : ` ${c.dim(`${glyph.arrow} Not signed in`)}`
3944
+ ];
3945
+ line();
3946
+ BANNER_LINES.forEach((art, i) => line(` ${c.green(art)}${tag[i] ?? ""}`));
3947
+ line();
3948
+ line(c.dim(` Ask anything. ${c.bold("/help")} for commands, ${c.bold("!cmd")} for a shell command.`));
3949
+ line();
3950
+ }
3951
+ function showHelp2() {
3952
+ line();
3953
+ line(` ${c.bold("hw")} ${c.dim("\u2014 AI DevOps in your terminal")}`);
3954
+ line();
3955
+ const rows = [
3956
+ ["hw", "Start an interactive session"],
3957
+ ['hw ask "..."', "Ask one question and exit"],
3958
+ ["hw chat --resume", "Continue this folder's last conversation"],
3959
+ ["hw list", "List your deployments"],
3960
+ ["hw sessions", "Sessions saved on this machine"],
3961
+ ["hw login", "Sign in (--token <key> for CI)"],
3962
+ ["hw logout", "Sign out on this machine"],
3963
+ ["hw version", "Print the version"]
3964
+ ];
3965
+ for (const [name, help] of rows) line(` ${c.green(name.padEnd(20))} ${c.dim(help)}`);
3966
+ line();
3967
+ line(c.dim(" Anything else is treated as a question: hw why is my deploy failing"));
3968
+ line();
3969
+ }
3970
+ function showSessions2() {
3971
+ const sessions = listSessions();
3972
+ if (!sessions.length) {
3973
+ line(c.dim(" No saved sessions yet."));
3974
+ return 0;
3975
+ }
3976
+ line();
3977
+ for (const s of sessions.slice(0, 25)) {
3978
+ const here = s.cwd === process.cwd() ? c.green(` ${glyph.tick} here`) : "";
3979
+ line(` ${s.cwd}${here}`);
3980
+ line(c.dim(` ${s.turnCount} turns \xB7 ${s.creditsSpent.toFixed(2)} credits`));
3981
+ }
3982
+ line();
3983
+ return 0;
3984
+ }
3985
+ async function listSites() {
3986
+ try {
3987
+ const cfg = getConfig();
3988
+ const r = await fetch(`${cfg.baseUrl}/api/sites`, { headers: { Authorization: `Bearer ${cfg.apiKey}` } });
3989
+ if (!r.ok) {
3990
+ error(r.status === 401 ? "Not signed in. Run `hw login`." : `Could not list deployments (HTTP ${r.status}).`);
3991
+ return 1;
3992
+ }
3993
+ const res = await r.json();
3994
+ const sites = Array.isArray(res) ? res : res?.sites ?? [];
3995
+ if (!sites.length) {
3996
+ line(c.dim(" No deployments yet. Ask me to deploy something to get started."));
3997
+ return 0;
3998
+ }
3999
+ line();
4000
+ for (const s of sites) {
4001
+ const dot = s.status === "RUNNING" ? c.green("\u25CF") : s.status === "FAILED" ? c.red("\u25CF") : c.yellow("\u25CF");
4002
+ line(` ${dot} ${c.bold((s.name ?? "unnamed").padEnd(24))} ${c.dim(s.domain ?? s.status ?? "")}`);
4003
+ }
4004
+ line();
4005
+ return 0;
4006
+ } catch (e) {
197
4007
  reportError(e);
198
- process.exit(1);
4008
+ return 1;
4009
+ }
4010
+ }
4011
+ function readVersion() {
4012
+ try {
4013
+ const here = dirname4(fileURLToPath(import.meta.url));
4014
+ const pkg = JSON.parse(readFileSync8(join8(here, "..", "package.json"), "utf8"));
4015
+ return pkg.version ?? "0.0.0";
4016
+ } catch {
4017
+ return "0.0.0";
4018
+ }
4019
+ }
4020
+ main(process.argv.slice(2)).then((code) => process.exit(code)).catch((e) => {
4021
+ if (isAborted(e)) process.exit(130);
4022
+ reportError(e);
4023
+ process.exit(1);
199
4024
  });