hostwares-cli 2.4.4 → 2.4.6

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.
Files changed (2) hide show
  1. package/dist/index.js +83 -27
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1864,6 +1864,7 @@ var init_safety = __esm({
1864
1864
  });
1865
1865
 
1866
1866
  // node_modules/@hostwares/agent-client/dist/esm/agent/loop.js
1867
+ import { readdirSync as readdirSync5 } from "fs";
1867
1868
  async function runTurn(opts) {
1868
1869
  const { credentials, signal, host } = opts;
1869
1870
  const emit = host.onActivity ?? silentSink;
@@ -1878,7 +1879,9 @@ async function runTurn(opts) {
1878
1879
  let toolResults = [];
1879
1880
  let firstHop = true;
1880
1881
  const steering = buildSteeringContext(host.cwd ?? process.cwd(), opts.message);
1881
- const firstMessage = steering ? `${steering}
1882
+ const cwdGrounding = buildCwdGrounding(host.cwd ?? process.cwd());
1883
+ const preamble = [steering, cwdGrounding].filter(Boolean).join("\n\n");
1884
+ const firstMessage = preamble ? `${preamble}
1882
1885
 
1883
1886
  ---
1884
1887
 
@@ -2031,14 +2034,14 @@ async function executePending(calls, opts, emit, checkpoint) {
2031
2034
  let oldContent = null;
2032
2035
  if (WRITE_TOOLS.has(call.name) && typeof call.input?.path === "string" && call.input.path) {
2033
2036
  try {
2034
- const { readFileSync: readFileSync9, existsSync: existsSync8 } = await import("fs");
2035
- const { resolve: pathResolve2, join: join9 } = await import("path");
2036
- const { homedir: homedir5 } = await import("os");
2037
+ const { readFileSync: readFileSync10, existsSync: existsSync9 } = await import("fs");
2038
+ const { resolve: pathResolve2, join: join10 } = await import("path");
2039
+ const { homedir: homedir6 } = await import("os");
2037
2040
  const raw = String(call.input.path);
2038
- const expanded = raw === "~" ? homedir5() : raw.startsWith("~/") ? join9(homedir5(), raw.slice(2)) : raw;
2041
+ const expanded = raw === "~" ? homedir6() : raw.startsWith("~/") ? join10(homedir6(), raw.slice(2)) : raw;
2039
2042
  const abs = pathResolve2(expanded);
2040
- if (existsSync8(abs))
2041
- oldContent = readFileSync9(abs, "utf8");
2043
+ if (existsSync9(abs))
2044
+ oldContent = readFileSync10(abs, "utf8");
2042
2045
  else
2043
2046
  oldContent = "";
2044
2047
  } catch {
@@ -2135,6 +2138,21 @@ function byteLength(content) {
2135
2138
  return void 0;
2136
2139
  return Buffer.byteLength(content, "utf8");
2137
2140
  }
2141
+ function buildCwdGrounding(cwd) {
2142
+ try {
2143
+ const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", "dist", "build", ".cache", ".DS_Store"]);
2144
+ const entries = readdirSync5(cwd, { withFileTypes: true }).filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 40).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
2145
+ const listing = entries.length ? entries.map((e) => `- ${e}`).join("\n") : "(empty)";
2146
+ return `## Working directory
2147
+ You are in: ${cwd}
2148
+ It contains:
2149
+ ${listing}
2150
+
2151
+ When I say "it", "this", "the app/file", or ask to change/fix/edit something without naming a path, I mean the files in THIS directory \u2014 act on them directly, don't search elsewhere or use a remembered path. If the request is genuinely ambiguous and nothing here makes the target obvious, ask ONE short question instead of calling tools to guess.`;
2152
+ } catch {
2153
+ return "";
2154
+ }
2155
+ }
2138
2156
  function targetOf(call) {
2139
2157
  const i = call.input ?? {};
2140
2158
  const raw = i.command ?? i.path ?? i.url ?? i.pattern ?? i.name ?? i.container ?? i.host ?? i.files;
@@ -2721,9 +2739,9 @@ var init_device = __esm({
2721
2739
  });
2722
2740
 
2723
2741
  // src/index.ts
2724
- import { readFileSync as readFileSync8 } from "fs";
2742
+ import { readFileSync as readFileSync9 } from "fs";
2725
2743
  import { fileURLToPath } from "url";
2726
- import { dirname as dirname4, join as join8 } from "path";
2744
+ import { dirname as dirname4, join as join9 } from "path";
2727
2745
 
2728
2746
  // src/agent/run.ts
2729
2747
  init_esm();
@@ -2883,6 +2901,9 @@ async function pickScope(options) {
2883
2901
  init_theme();
2884
2902
  init_render();
2885
2903
  init_esm();
2904
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, renameSync as renameSync2, chmodSync as chmodSync2 } from "fs";
2905
+ import { join as join7 } from "path";
2906
+ import { homedir as homedir5 } from "os";
2886
2907
  var READ_ONLY = /* @__PURE__ */ new Set([
2887
2908
  "read_file",
2888
2909
  "list_directory",
@@ -2923,6 +2944,40 @@ function permissionRules() {
2923
2944
  }
2924
2945
  return cachedRules;
2925
2946
  }
2947
+ function reloadPermissionRules() {
2948
+ cachedRules = null;
2949
+ }
2950
+ function saveGlobalPermissionRule(rule) {
2951
+ const { HW_DIR: HW_DIR2 } = getHwDir();
2952
+ const file = join7(HW_DIR2, "permissions.json");
2953
+ let rules = [];
2954
+ try {
2955
+ if (existsSync7(file)) {
2956
+ const parsed = JSON.parse(readFileSync7(file, "utf8"));
2957
+ const arr = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.rules) ? parsed.rules : [];
2958
+ rules = arr.filter((r) => !!r && typeof r.tool === "string" && ["allow", "ask", "deny"].includes(r.effect));
2959
+ }
2960
+ } catch {
2961
+ rules = [];
2962
+ }
2963
+ const exists = rules.some((r) => r.tool === rule.tool && (r.match ?? "") === (rule.match ?? "") && r.effect === rule.effect);
2964
+ if (!exists) rules.push(rule);
2965
+ try {
2966
+ if (!existsSync7(HW_DIR2)) mkdirSync5(HW_DIR2, { recursive: true, mode: 448 });
2967
+ const tmp = join7(HW_DIR2, `permissions.json.tmp-${process.pid}`);
2968
+ writeFileSync5(tmp, JSON.stringify(rules, null, 2) + "\n", { mode: 384 });
2969
+ renameSync2(tmp, file);
2970
+ try {
2971
+ chmodSync2(file, 384);
2972
+ } catch {
2973
+ }
2974
+ } catch {
2975
+ }
2976
+ reloadPermissionRules();
2977
+ }
2978
+ function getHwDir() {
2979
+ return { HW_DIR: join7(homedir5(), ".hostwares") };
2980
+ }
2926
2981
  function isProjectTrusted(cwd = process.cwd()) {
2927
2982
  return (getConfig().trustedProjects ?? []).includes(cwd);
2928
2983
  }
@@ -2967,15 +3022,11 @@ ${c.yellow(glyph.caret)} ${describe(tool)}
2967
3022
  const toolPath = String(tool.input.path ?? tool.input.localPath ?? "");
2968
3023
  const rel = toolPath ? shortRel(toolPath) : "";
2969
3024
  const dir = rel ? rel.split("/").slice(0, -1).join("/") || "." : "";
2970
- const scopeOpts = [
2971
- `Specific paths \u2192 ${rel || tool.name} ${c.dim("(this file only)")}`,
2972
- `Complete directory \u2192 ${dir || "."} ${c.dim("(all files here)")}`,
2973
- `Entire Tool \u2192 * ${c.dim(`(always allow '${tool.name}')`)}`
2974
- ];
2975
3025
  const displayOpts = [
2976
3026
  `Specific paths \u2192 ${rel || tool.name}`,
2977
3027
  `Complete directory \u2192 ${dir || "."}`,
2978
- `Entire Tool \u2192 *`
3028
+ `Entire Tool \u2192 * (this session)`,
3029
+ `Always, everywhere \u2192 ${tool.name} (never ask again \u2014 saved)`
2979
3030
  ];
2980
3031
  const choice = await pickScope(displayOpts);
2981
3032
  if (choice === -1) return "abort";
@@ -2989,9 +3040,14 @@ ${c.yellow(glyph.caret)} ${describe(tool)}
2989
3040
  addSessionRule({ tool: tool.name, match, effect: "allow" });
2990
3041
  process.stdout.write(` ${c.dim(`Trusted '${tool.name}' for ${match} this session.`)}
2991
3042
  `);
2992
- } else {
3043
+ } else if (choice === 2) {
2993
3044
  addSessionRule({ tool: tool.name, match: "*", effect: "allow" });
2994
3045
  process.stdout.write(` ${c.dim(`Trusted '${tool.name}' (*) this session.`)}
3046
+ `);
3047
+ } else {
3048
+ addSessionRule({ tool: tool.name, match: "*", effect: "allow" });
3049
+ saveGlobalPermissionRule({ tool: tool.name, match: "*", effect: "allow" });
3050
+ process.stdout.write(` ${c.green(glyph.tick)} ${c.dim(`Always allowing '${tool.name}' everywhere \u2014 saved to ~/.hostwares/permissions.json. You won't be asked again.`)}
2995
3051
  `);
2996
3052
  }
2997
3053
  return "allow";
@@ -3289,13 +3345,13 @@ init_config();
3289
3345
  // src/session/store.ts
3290
3346
  init_config();
3291
3347
  import { createHash } from "crypto";
3292
- import { join as join7 } from "path";
3293
- import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync5, unlinkSync as unlinkSync2, mkdirSync as mkdirSync5 } from "fs";
3294
- var SESSION_DIR = join7(HW_DIR, "sessions");
3348
+ import { join as join8 } from "path";
3349
+ import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync6, unlinkSync as unlinkSync2, mkdirSync as mkdirSync6 } from "fs";
3350
+ var SESSION_DIR = join8(HW_DIR, "sessions");
3295
3351
  var SCHEMA_VERSION = 2;
3296
3352
  function sessionPath(cwd) {
3297
3353
  const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 24);
3298
- return join7(SESSION_DIR, `${hash}.json`);
3354
+ return join8(SESSION_DIR, `${hash}.json`);
3299
3355
  }
3300
3356
  function newSession(cwd = process.cwd()) {
3301
3357
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -3311,9 +3367,9 @@ function newSession(cwd = process.cwd()) {
3311
3367
  }
3312
3368
  function loadSession(cwd = process.cwd()) {
3313
3369
  const file = sessionPath(cwd);
3314
- if (!existsSync7(file)) return null;
3370
+ if (!existsSync8(file)) return null;
3315
3371
  try {
3316
- const data = JSON.parse(readFileSync7(file, "utf8"));
3372
+ const data = JSON.parse(readFileSync8(file, "utf8"));
3317
3373
  if (data.version !== SCHEMA_VERSION) return null;
3318
3374
  if (data.cwd !== cwd) return null;
3319
3375
  if (!Array.isArray(data.turns)) return null;
@@ -3335,7 +3391,7 @@ function isTurn(t) {
3335
3391
  }
3336
3392
  var MAX_LOCAL_TURNS = 200;
3337
3393
  function saveSession(session) {
3338
- mkdirSync5(SESSION_DIR, { recursive: true, mode: 448 });
3394
+ mkdirSync6(SESSION_DIR, { recursive: true, mode: 448 });
3339
3395
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3340
3396
  if (session.turns.length > MAX_LOCAL_TURNS) {
3341
3397
  session.turns = session.turns.slice(-MAX_LOCAL_TURNS);
@@ -3346,12 +3402,12 @@ function appendTurn(session, role, content) {
3346
3402
  session.turns.push({ role, content, at: (/* @__PURE__ */ new Date()).toISOString() });
3347
3403
  }
3348
3404
  function listSessions() {
3349
- if (!existsSync7(SESSION_DIR)) return [];
3405
+ if (!existsSync8(SESSION_DIR)) return [];
3350
3406
  const out = [];
3351
- for (const name of readdirSync5(SESSION_DIR)) {
3407
+ for (const name of readdirSync6(SESSION_DIR)) {
3352
3408
  if (!name.endsWith(".json")) continue;
3353
3409
  try {
3354
- const data = JSON.parse(readFileSync7(join7(SESSION_DIR, name), "utf8"));
3410
+ const data = JSON.parse(readFileSync8(join8(SESSION_DIR, name), "utf8"));
3355
3411
  if (data.version !== SCHEMA_VERSION || !data.cwd) continue;
3356
3412
  out.push({
3357
3413
  cwd: data.cwd,
@@ -3894,7 +3950,7 @@ async function listSites() {
3894
3950
  function readVersion() {
3895
3951
  try {
3896
3952
  const here = dirname4(fileURLToPath(import.meta.url));
3897
- const pkg = JSON.parse(readFileSync8(join8(here, "..", "package.json"), "utf8"));
3953
+ const pkg = JSON.parse(readFileSync9(join9(here, "..", "package.json"), "utf8"));
3898
3954
  return pkg.version ?? "0.0.0";
3899
3955
  } catch {
3900
3956
  return "0.0.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostwares-cli",
3
- "version": "2.4.4",
3
+ "version": "2.4.6",
4
4
  "description": "Hostwares CLI - AI DevOps in your terminal",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",