@ganglion/xacpx 0.17.0-beta.1 → 0.17.0-beta.2

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/cli.js CHANGED
@@ -25585,6 +25585,9 @@ function terminalIdleTimeoutSeconds(config4) {
25585
25585
  const v = config4.terminal?.idleTimeoutSeconds;
25586
25586
  return typeof v === "number" && v > 0 ? v : 900;
25587
25587
  }
25588
+ function filesWriteEnabled(config4) {
25589
+ return config4.files?.writeEnabled === true;
25590
+ }
25588
25591
 
25589
25592
  // src/orchestration/orchestration-server.ts
25590
25593
  import { chmod as chmod4, rm as rm8 } from "node:fs/promises";
@@ -33681,11 +33684,11 @@ async function readNativeSessionHistory(opts) {
33681
33684
  var init_native_session_history = () => {};
33682
33685
 
33683
33686
  // src/control/workspace-fs.ts
33684
- import { execFile } from "node:child_process";
33687
+ import { execFile, spawn as spawn11 } from "node:child_process";
33685
33688
  import { promisify } from "node:util";
33686
33689
  import { homedir as homedir12 } from "node:os";
33687
- import { readdir as readdir4, realpath, stat as stat3, open as open5 } from "node:fs/promises";
33688
- import { isAbsolute as isAbsolute3, relative, resolve as resolve3, sep } from "node:path";
33690
+ import { readdir as readdir4, realpath, stat as stat3, open as open5, readFile as readFile15, writeFile as writeFile8, mkdir as mkdir9, rename as fsRename, cp, rm as rm10 } from "node:fs/promises";
33691
+ import { basename as basename3, dirname as dirname12, isAbsolute as isAbsolute3, relative, resolve as resolve3, sep } from "node:path";
33689
33692
  function expandHome2(p) {
33690
33693
  if (p === "~")
33691
33694
  return homedir12();
@@ -33723,8 +33726,93 @@ class WorkspaceFs {
33723
33726
  const rel = abs === root ? "" : relative(root, abs).split(sep).join("/");
33724
33727
  return { root, abs, rel };
33725
33728
  }
33726
- async listDirectory(workspace3, relPath) {
33729
+ async resolveParent(workspace3, relPath) {
33730
+ if (relPath && isAbsolute3(relPath))
33731
+ throw new Error("path-must-be-relative");
33732
+ const trimmed = (relPath ?? "").replace(/\/+$/, "");
33733
+ if (!trimmed)
33734
+ throw new Error("bad-target");
33735
+ const name = basename3(trimmed);
33736
+ if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
33737
+ throw new Error("bad-target");
33738
+ }
33739
+ const parentRel = dirname12(trimmed);
33740
+ const { root, abs: parentAbs, rel: parentRelNorm } = await this.resolve(workspace3, parentRel === "." ? undefined : parentRel);
33741
+ const targetAbs = resolve3(parentAbs, name);
33742
+ const rel = parentRelNorm ? `${parentRelNorm}/${name}` : name;
33743
+ return { root, parentAbs, name, targetAbs, rel };
33744
+ }
33745
+ async createFile(workspace3, relPath) {
33746
+ const { targetAbs, rel } = await this.resolveParent(workspace3, relPath);
33747
+ try {
33748
+ await writeFile8(targetAbs, "", { flag: "wx" });
33749
+ } catch (e) {
33750
+ if (e.code === "EEXIST")
33751
+ throw new Error("already-exists");
33752
+ throw e;
33753
+ }
33754
+ return { path: rel };
33755
+ }
33756
+ async createDir(workspace3, relPath) {
33757
+ const { targetAbs, rel } = await this.resolveParent(workspace3, relPath);
33758
+ try {
33759
+ await mkdir9(targetAbs);
33760
+ } catch (e) {
33761
+ if (e.code === "EEXIST")
33762
+ throw new Error("already-exists");
33763
+ throw e;
33764
+ }
33765
+ return { path: rel };
33766
+ }
33767
+ async rename(workspace3, relPath, newName) {
33768
+ const src = await this.resolve(workspace3, relPath);
33769
+ if (!src.rel)
33770
+ throw new Error("bad-target");
33771
+ if (newName.includes("/") || newName.includes("\\"))
33772
+ throw new Error("bad-target");
33773
+ const destRelInput = src.rel.includes("/") ? `${src.rel.slice(0, src.rel.lastIndexOf("/"))}/${newName}` : newName;
33774
+ const dest = await this.resolveParent(workspace3, destRelInput);
33775
+ try {
33776
+ await stat3(dest.targetAbs);
33777
+ throw new Error("already-exists");
33778
+ } catch (e) {
33779
+ if (e.message === "already-exists")
33780
+ throw e;
33781
+ }
33782
+ await fsRename(src.abs, dest.targetAbs);
33783
+ return { path: dest.rel };
33784
+ }
33785
+ async duplicate(workspace3, relPath) {
33786
+ const src = await this.resolve(workspace3, relPath);
33787
+ if (!src.rel)
33788
+ throw new Error("bad-target");
33789
+ const parent = await this.resolveParent(workspace3, src.rel);
33790
+ const siblings = new Set(await readdir4(parent.parentAbs));
33791
+ const newBase = copyName(siblings, parent.name);
33792
+ const destAbs = resolve3(parent.parentAbs, newBase);
33793
+ await cp(src.abs, destAbs, { recursive: true });
33794
+ const destRel = parent.rel.includes("/") ? `${parent.rel.slice(0, parent.rel.lastIndexOf("/"))}/${newBase}` : newBase;
33795
+ return { path: destRel };
33796
+ }
33797
+ async remove(workspace3, relPath) {
33798
+ const { abs, rel } = await this.resolve(workspace3, relPath);
33799
+ if (!rel)
33800
+ throw new Error("refuse-delete-root");
33801
+ await rm10(abs, { recursive: true, force: false });
33802
+ return { path: rel };
33803
+ }
33804
+ async readFileBytes(workspace3, relPath) {
33727
33805
  const { abs, rel } = await this.resolve(workspace3, relPath);
33806
+ const info = await stat3(abs);
33807
+ if (!info.isFile())
33808
+ throw new Error("not-a-file");
33809
+ if (info.size > DOWNLOAD_MAX)
33810
+ throw new Error("file-too-large");
33811
+ const buf = await readFile15(abs);
33812
+ return { path: rel, base64: buf.toString("base64"), size: info.size, mimeType: mimeForName(basename3(abs)) };
33813
+ }
33814
+ async listDirectory(workspace3, relPath) {
33815
+ const { root, abs, rel } = await this.resolve(workspace3, relPath);
33728
33816
  const dirents = await readdir4(abs, { withFileTypes: true });
33729
33817
  const entries = [];
33730
33818
  for (const d of dirents.slice(0, MAX_ENTRIES)) {
@@ -33741,7 +33829,49 @@ class WorkspaceFs {
33741
33829
  }
33742
33830
  }
33743
33831
  entries.sort((a, b) => a.type !== b.type ? a.type === "dir" ? -1 : 1 : a.name.localeCompare(b.name));
33744
- return { workspace: workspace3, path: rel, entries };
33832
+ const relOf = (name, isDir) => (rel ? `${rel}/${name}` : name) + (isDir ? "/" : "");
33833
+ const ignoredSet = await this.gitCheckIgnore(root, entries.map((e) => relOf(e.name, e.type === "dir")));
33834
+ for (const e of entries) {
33835
+ if (ignoredSet.has(relOf(e.name, e.type === "dir")))
33836
+ e.ignored = true;
33837
+ }
33838
+ return { workspace: workspace3, path: rel, entries, root, sep };
33839
+ }
33840
+ gitCheckIgnore(root, relPaths) {
33841
+ return new Promise((resolvePromise) => {
33842
+ if (!relPaths.length)
33843
+ return resolvePromise(new Set);
33844
+ const ignored = new Set;
33845
+ let out = "";
33846
+ let child;
33847
+ try {
33848
+ child = spawn11("git", ["-C", root, "check-ignore", "-z", "--stdin"], { stdio: ["pipe", "pipe", "ignore"] });
33849
+ } catch {
33850
+ return resolvePromise(ignored);
33851
+ }
33852
+ const killer = setTimeout(() => {
33853
+ try {
33854
+ child.kill("SIGKILL");
33855
+ } catch {}
33856
+ }, GIT_TIMEOUT_MS);
33857
+ child.on("error", () => {
33858
+ clearTimeout(killer);
33859
+ resolvePromise(ignored);
33860
+ });
33861
+ child.stdout.on("data", (b) => {
33862
+ out += b.toString();
33863
+ });
33864
+ child.on("close", () => {
33865
+ clearTimeout(killer);
33866
+ for (const p of out.split("\x00")) {
33867
+ if (p)
33868
+ ignored.add(p);
33869
+ }
33870
+ resolvePromise(ignored);
33871
+ });
33872
+ child.stdin.on("error", () => {});
33873
+ child.stdin.end(relPaths.join("\x00") + "\x00");
33874
+ });
33745
33875
  }
33746
33876
  async readFile(workspace3, relPath) {
33747
33877
  const { abs, rel } = await this.resolve(workspace3, relPath);
@@ -33766,16 +33896,28 @@ class WorkspaceFs {
33766
33896
  await fh.close();
33767
33897
  }
33768
33898
  }
33769
- async search(workspace3, query) {
33770
- const { root } = await this.resolve(workspace3, undefined);
33771
- const needle = query.trim().toLowerCase();
33899
+ async search(workspace3, opts) {
33900
+ const { root, abs: base, rel: scopeRel } = await this.resolve(workspace3, opts.path);
33901
+ const query = opts.query.trim();
33902
+ const empty = { workspace: workspace3, query, matches: [], hits: [], truncated: false };
33903
+ if (!query)
33904
+ return empty;
33905
+ if ((opts.mode ?? "name") === "content") {
33906
+ return this.contentGrep(workspace3, root, base, scopeRel, opts, query);
33907
+ }
33908
+ const matcher = buildMatcher(query, opts);
33909
+ const inc = opts.include ? globToRegExp(opts.include) : null;
33910
+ const exc = opts.exclude ? globToRegExp(opts.exclude) : null;
33772
33911
  const matches = [];
33773
- if (!needle)
33774
- return { workspace: workspace3, query, matches, truncated: false };
33775
33912
  let scanned = 0;
33776
33913
  let truncated = false;
33777
- const queue = [root];
33914
+ const deadline = Date.now() + SEARCH_DEADLINE_MS;
33915
+ const queue = [base];
33778
33916
  while (queue.length) {
33917
+ if (Date.now() > deadline) {
33918
+ truncated = true;
33919
+ break;
33920
+ }
33779
33921
  const dir = queue.shift();
33780
33922
  let dirents;
33781
33923
  try {
@@ -33784,6 +33926,10 @@ class WorkspaceFs {
33784
33926
  continue;
33785
33927
  }
33786
33928
  for (const d of dirents) {
33929
+ if (Date.now() > deadline) {
33930
+ truncated = true;
33931
+ break;
33932
+ }
33787
33933
  if (++scanned > SEARCH_MAX_SCAN) {
33788
33934
  truncated = true;
33789
33935
  break;
@@ -33795,7 +33941,11 @@ class WorkspaceFs {
33795
33941
  queue.push(resolve3(dir, d.name));
33796
33942
  } else if (d.isFile()) {
33797
33943
  const rel = relative(root, resolve3(dir, d.name)).split(sep).join("/");
33798
- if (rel.toLowerCase().includes(needle)) {
33944
+ if (inc && !inc.test(rel))
33945
+ continue;
33946
+ if (exc && exc.test(rel))
33947
+ continue;
33948
+ if (matcher(rel)) {
33799
33949
  matches.push(rel);
33800
33950
  if (matches.length >= SEARCH_MAX_RESULTS) {
33801
33951
  truncated = true;
@@ -33808,18 +33958,199 @@ class WorkspaceFs {
33808
33958
  break;
33809
33959
  }
33810
33960
  matches.sort();
33811
- return { workspace: workspace3, query, matches, truncated };
33961
+ return { workspace: workspace3, query, matches, hits: [], truncated };
33962
+ }
33963
+ async contentGrep(workspace3, root, base, scopeRel, opts, query) {
33964
+ const inc = opts.include ? globToRegExp(opts.include) : null;
33965
+ const exc = opts.exclude ? globToRegExp(opts.exclude) : null;
33966
+ const inGit = await execFileAsync("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" }).then(() => true).catch(() => false);
33967
+ if (inGit) {
33968
+ const args = ["-C", root, "grep", "-n", "--column", "-I", "--no-color", "--untracked"];
33969
+ if (!opts.matchCase)
33970
+ args.push("-i");
33971
+ if (opts.wholeWord)
33972
+ args.push("-w");
33973
+ args.push(opts.regex ? "-E" : "-F");
33974
+ args.push("-e", query, "--", scopeRel || ".");
33975
+ let stdout2 = "";
33976
+ let killed = false;
33977
+ try {
33978
+ stdout2 = (await execFileAsync("git", args, { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout;
33979
+ } catch (e) {
33980
+ killed = e.killed === true || e.signal === "SIGKILL";
33981
+ const code = e.code;
33982
+ if (code === 1)
33983
+ stdout2 = "";
33984
+ else {
33985
+ const out = e.stdout;
33986
+ stdout2 = typeof out === "string" ? out : "";
33987
+ }
33988
+ }
33989
+ const hits2 = [];
33990
+ let truncated2 = killed;
33991
+ for (const raw of stdout2.split(`
33992
+ `)) {
33993
+ if (!raw)
33994
+ continue;
33995
+ const m = raw.match(/^(.*?):(\d+):(\d+):(.*)$/);
33996
+ if (!m)
33997
+ continue;
33998
+ const p = m[1].split(sep).join("/");
33999
+ if (inc && !inc.test(p))
34000
+ continue;
34001
+ if (exc && exc.test(p))
34002
+ continue;
34003
+ hits2.push({ path: p, line: Number(m[2]), text: m[4].slice(0, 400) });
34004
+ if (hits2.length >= SEARCH_CONTENT_MAX_HITS) {
34005
+ truncated2 = true;
34006
+ break;
34007
+ }
34008
+ }
34009
+ return { workspace: workspace3, query, matches: [], hits: hits2, truncated: truncated2 };
34010
+ }
34011
+ try {
34012
+ const args = ["grep", "--no-index", "-n", "--column", "-I", "--no-color"];
34013
+ if (!opts.matchCase)
34014
+ args.push("-i");
34015
+ if (opts.wholeWord)
34016
+ args.push("-w");
34017
+ args.push(opts.regex ? "-E" : "-F");
34018
+ args.push("-e", query, "--", ".", ":(exclude)node_modules/", ":(exclude).git/");
34019
+ let stdout2 = "";
34020
+ let killed = false;
34021
+ try {
34022
+ stdout2 = (await execFileAsync("git", args, { cwd: base, maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout;
34023
+ } catch (e) {
34024
+ if (e.code === "ENOENT")
34025
+ throw e;
34026
+ killed = e.killed === true || e.signal === "SIGKILL";
34027
+ const code = e.code;
34028
+ if (code === 1)
34029
+ stdout2 = "";
34030
+ else {
34031
+ const out = e.stdout;
34032
+ stdout2 = typeof out === "string" ? out : "";
34033
+ }
34034
+ }
34035
+ const hits2 = [];
34036
+ let truncated2 = killed;
34037
+ for (const raw of stdout2.split(`
34038
+ `)) {
34039
+ if (!raw)
34040
+ continue;
34041
+ const m = raw.match(/^(.*?):(\d+):(\d+):(.*)$/);
34042
+ if (!m)
34043
+ continue;
34044
+ const relToBase = m[1].split(sep).join("/");
34045
+ const p = scopeRel ? `${scopeRel}/${relToBase}` : relToBase;
34046
+ if (inc && !inc.test(p))
34047
+ continue;
34048
+ if (exc && exc.test(p))
34049
+ continue;
34050
+ hits2.push({ path: p, line: Number(m[2]), text: m[4].slice(0, 400) });
34051
+ if (hits2.length >= SEARCH_CONTENT_MAX_HITS) {
34052
+ truncated2 = true;
34053
+ break;
34054
+ }
34055
+ }
34056
+ return { workspace: workspace3, query, matches: [], hits: hits2, truncated: truncated2 };
34057
+ } catch (e) {
34058
+ if (e.code !== "ENOENT") {
34059
+ return { workspace: workspace3, query, matches: [], hits: [], truncated: false };
34060
+ }
34061
+ }
34062
+ const matcher = buildLineMatcher(query, opts);
34063
+ const hits = [];
34064
+ let scanned = 0;
34065
+ let truncated = false;
34066
+ const deadline = Date.now() + SEARCH_DEADLINE_MS;
34067
+ const queue = [base];
34068
+ while (queue.length) {
34069
+ if (Date.now() > deadline) {
34070
+ truncated = true;
34071
+ break;
34072
+ }
34073
+ const dir = queue.shift();
34074
+ let dirents;
34075
+ try {
34076
+ dirents = await readdir4(dir, { withFileTypes: true });
34077
+ } catch {
34078
+ continue;
34079
+ }
34080
+ for (const d of dirents) {
34081
+ if (Date.now() > deadline) {
34082
+ truncated = true;
34083
+ break;
34084
+ }
34085
+ if (++scanned > SEARCH_MAX_SCAN) {
34086
+ truncated = true;
34087
+ break;
34088
+ }
34089
+ if (d.isSymbolicLink())
34090
+ continue;
34091
+ const full = resolve3(dir, d.name);
34092
+ if (d.isDirectory()) {
34093
+ if (!SEARCH_SKIP_DIRS.has(d.name))
34094
+ queue.push(full);
34095
+ continue;
34096
+ }
34097
+ if (!d.isFile())
34098
+ continue;
34099
+ const rel = relative(root, full).split(sep).join("/");
34100
+ if (inc && !inc.test(rel))
34101
+ continue;
34102
+ if (exc && exc.test(rel))
34103
+ continue;
34104
+ let info;
34105
+ try {
34106
+ info = await stat3(full);
34107
+ } catch {
34108
+ continue;
34109
+ }
34110
+ if (info.size > SEARCH_CONTENT_MAX_FILE)
34111
+ continue;
34112
+ let text;
34113
+ try {
34114
+ text = await readFile15(full, "utf8");
34115
+ } catch {
34116
+ continue;
34117
+ }
34118
+ if (text.includes("\x00"))
34119
+ continue;
34120
+ const lines = text.split(`
34121
+ `);
34122
+ for (let i = 0;i < lines.length; i++) {
34123
+ if (Date.now() > deadline) {
34124
+ truncated = true;
34125
+ break;
34126
+ }
34127
+ const line = lines[i];
34128
+ if (matcher(line.length > LINE_MATCH_CAP ? line.slice(0, LINE_MATCH_CAP) : line)) {
34129
+ hits.push({ path: rel, line: i + 1, text: line.slice(0, 400) });
34130
+ if (hits.length >= SEARCH_CONTENT_MAX_HITS) {
34131
+ truncated = true;
34132
+ break;
34133
+ }
34134
+ }
34135
+ }
34136
+ if (truncated)
34137
+ break;
34138
+ }
34139
+ if (truncated)
34140
+ break;
34141
+ }
34142
+ return { workspace: workspace3, query, matches: [], hits, truncated };
33812
34143
  }
33813
34144
  async gitDiff(workspace3, relPath) {
33814
34145
  const { root, rel } = await this.resolve(workspace3, relPath);
33815
34146
  try {
33816
- await execFileAsync("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { maxBuffer: GIT_MAX_BUFFER });
34147
+ await execFileAsync("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" });
33817
34148
  } catch {
33818
34149
  throw new Error("not-a-git-repo");
33819
34150
  }
33820
34151
  const files = [];
33821
34152
  try {
33822
- const { stdout: stdout2 } = await execFileAsync("git", ["-C", root, "-c", "core.quotePath=false", "status", "--porcelain", "-z"], { maxBuffer: GIT_MAX_BUFFER });
34153
+ const { stdout: stdout2 } = await execFileAsync("git", ["-C", root, "-c", "core.quotePath=false", "status", "--porcelain", "-z"], { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" });
33823
34154
  const fields = stdout2.split("\x00");
33824
34155
  for (let i = 0;i < fields.length; i++) {
33825
34156
  const field = fields[i];
@@ -33835,17 +34166,17 @@ class WorkspaceFs {
33835
34166
  const diffArgs = (base) => ["-C", root, ...base, ...rel ? ["--", rel] : []];
33836
34167
  let diff = "";
33837
34168
  try {
33838
- diff = (await execFileAsync("git", diffArgs(["diff", "HEAD"]), { maxBuffer: GIT_MAX_BUFFER })).stdout;
34169
+ diff = (await execFileAsync("git", diffArgs(["diff", "HEAD"]), { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout;
33839
34170
  } catch {
33840
34171
  try {
33841
- diff = (await execFileAsync("git", diffArgs(["diff"]), { maxBuffer: GIT_MAX_BUFFER })).stdout;
34172
+ diff = (await execFileAsync("git", diffArgs(["diff"]), { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout;
33842
34173
  } catch {
33843
34174
  diff = "";
33844
34175
  }
33845
34176
  }
33846
34177
  if (rel && !diff) {
33847
34178
  try {
33848
- diff = (await execFileAsync("git", ["-C", root, "-c", "core.quotePath=false", "diff", "--no-index", "--", "/dev/null", rel], { maxBuffer: GIT_MAX_BUFFER })).stdout;
34179
+ diff = (await execFileAsync("git", ["-C", root, "-c", "core.quotePath=false", "diff", "--no-index", "--", "/dev/null", rel], { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout;
33849
34180
  } catch (e) {
33850
34181
  const out = e.stdout;
33851
34182
  if (typeof out === "string")
@@ -33858,7 +34189,7 @@ class WorkspaceFs {
33858
34189
  async gitContext(root) {
33859
34190
  const run = async (...args) => {
33860
34191
  try {
33861
- return (await execFileAsync("git", ["-C", root, ...args], { maxBuffer: GIT_MAX_BUFFER })).stdout.trim();
34192
+ return (await execFileAsync("git", ["-C", root, ...args], { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout.trim();
33862
34193
  } catch {
33863
34194
  return null;
33864
34195
  }
@@ -33878,13 +34209,101 @@ class WorkspaceFs {
33878
34209
  return ctx;
33879
34210
  }
33880
34211
  }
33881
- var execFileAsync, MAX_ENTRIES = 2000, FILE_READ_CAP, DIFF_CAP, GIT_MAX_BUFFER, SEARCH_MAX_RESULTS = 200, SEARCH_MAX_SCAN = 20000, SEARCH_SKIP_DIRS;
34212
+ function escapeRe(s) {
34213
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34214
+ }
34215
+ function copyName(existing, name) {
34216
+ const dot = name.lastIndexOf(".");
34217
+ const hasExt = dot > 0;
34218
+ const base = hasExt ? name.slice(0, dot) : name;
34219
+ const ext = hasExt ? name.slice(dot) : "";
34220
+ let candidate = `${base} copy${ext}`;
34221
+ let n = 2;
34222
+ while (existing.has(candidate))
34223
+ candidate = `${base} copy ${n++}${ext}`;
34224
+ return candidate;
34225
+ }
34226
+ function mimeForName(name) {
34227
+ const dot = name.lastIndexOf(".");
34228
+ const ext = dot > 0 ? name.slice(dot).toLowerCase() : "";
34229
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
34230
+ }
34231
+ function buildMatcher(query, opts) {
34232
+ return buildLineMatcher(query, opts);
34233
+ }
34234
+ function buildLineMatcher(query, opts) {
34235
+ const flags = opts.matchCase ? "" : "i";
34236
+ let pattern;
34237
+ if (opts.regex)
34238
+ pattern = query;
34239
+ else
34240
+ pattern = escapeRe(query);
34241
+ if (opts.wholeWord)
34242
+ pattern = `\\b${pattern}\\b`;
34243
+ let re = null;
34244
+ try {
34245
+ re = new RegExp(pattern, flags);
34246
+ } catch {
34247
+ re = null;
34248
+ }
34249
+ if (!re) {
34250
+ const needle = opts.matchCase ? query : query.toLowerCase();
34251
+ return (s) => (opts.matchCase ? s : s.toLowerCase()).includes(needle);
34252
+ }
34253
+ return (s) => re.test(s);
34254
+ }
34255
+ function globToRegExp(glob) {
34256
+ let re = "^";
34257
+ for (let i = 0;i < glob.length; i++) {
34258
+ const c = glob[i];
34259
+ if (c === "*") {
34260
+ if (glob[i + 1] === "*") {
34261
+ re += ".*";
34262
+ i++;
34263
+ if (glob[i + 1] === "/")
34264
+ i++;
34265
+ } else
34266
+ re += "[^/]*";
34267
+ } else if (c === "?")
34268
+ re += "[^/]";
34269
+ else
34270
+ re += escapeRe(c);
34271
+ }
34272
+ re += "$";
34273
+ try {
34274
+ return new RegExp(re);
34275
+ } catch {
34276
+ return /$^/;
34277
+ }
34278
+ }
34279
+ var execFileAsync, MAX_ENTRIES = 2000, FILE_READ_CAP, DIFF_CAP, GIT_MAX_BUFFER, SEARCH_MAX_RESULTS = 200, SEARCH_MAX_SCAN = 20000, SEARCH_SKIP_DIRS, SEARCH_CONTENT_MAX_HITS = 500, SEARCH_CONTENT_MAX_FILE, LINE_MATCH_CAP = 2000, DOWNLOAD_MAX, GIT_TIMEOUT_MS = 1e4, SEARCH_DEADLINE_MS = 4000, MIME_BY_EXT;
33882
34280
  var init_workspace_fs = __esm(() => {
33883
34281
  execFileAsync = promisify(execFile);
33884
34282
  FILE_READ_CAP = 256 * 1024;
33885
34283
  DIFF_CAP = 512 * 1024;
33886
34284
  GIT_MAX_BUFFER = 32 * 1024 * 1024;
33887
34285
  SEARCH_SKIP_DIRS = new Set([".git", "node_modules"]);
34286
+ SEARCH_CONTENT_MAX_FILE = 1024 * 1024;
34287
+ DOWNLOAD_MAX = 5 * 1024 * 1024;
34288
+ MIME_BY_EXT = {
34289
+ ".txt": "text/plain",
34290
+ ".md": "text/markdown",
34291
+ ".json": "application/json",
34292
+ ".js": "text/javascript",
34293
+ ".ts": "text/plain",
34294
+ ".html": "text/html",
34295
+ ".css": "text/css",
34296
+ ".csv": "text/csv",
34297
+ ".xml": "application/xml",
34298
+ ".png": "image/png",
34299
+ ".jpg": "image/jpeg",
34300
+ ".jpeg": "image/jpeg",
34301
+ ".gif": "image/gif",
34302
+ ".svg": "image/svg+xml",
34303
+ ".webp": "image/webp",
34304
+ ".pdf": "application/pdf",
34305
+ ".zip": "application/zip"
34306
+ };
33888
34307
  });
33889
34308
 
33890
34309
  // src/control/control-service.ts
@@ -33906,8 +34325,31 @@ class ControlService {
33906
34325
  workspaceGitDiff(workspace3, path16) {
33907
34326
  return this.workspaceFs.gitDiff(workspace3, path16);
33908
34327
  }
33909
- searchWorkspace(workspace3, query) {
33910
- return this.workspaceFs.search(workspace3, query);
34328
+ searchWorkspace(workspace3, opts) {
34329
+ return this.workspaceFs.search(workspace3, opts);
34330
+ }
34331
+ async fsCreate(workspace3, path16, kind) {
34332
+ if (!this.deps.filesWriteEnabled())
34333
+ throw new Error("files-write-disabled");
34334
+ return kind === "dir" ? this.workspaceFs.createDir(workspace3, path16) : this.workspaceFs.createFile(workspace3, path16);
34335
+ }
34336
+ async fsRename(workspace3, path16, newName) {
34337
+ if (!this.deps.filesWriteEnabled())
34338
+ throw new Error("files-write-disabled");
34339
+ return this.workspaceFs.rename(workspace3, path16, newName);
34340
+ }
34341
+ async fsDelete(workspace3, path16) {
34342
+ if (!this.deps.filesWriteEnabled())
34343
+ throw new Error("files-write-disabled");
34344
+ return this.workspaceFs.remove(workspace3, path16);
34345
+ }
34346
+ async fsCopy(workspace3, path16) {
34347
+ if (!this.deps.filesWriteEnabled())
34348
+ throw new Error("files-write-disabled");
34349
+ return this.workspaceFs.duplicate(workspace3, path16);
34350
+ }
34351
+ async fsDownload(workspace3, path16) {
34352
+ return this.workspaceFs.readFileBytes(workspace3, path16);
33911
34353
  }
33912
34354
  async uploadFile(input) {
33913
34355
  return this.deps.uploadStore.save(input.filename, input.content, input.mimeType);
@@ -34445,7 +34887,7 @@ function realPtySpawn(file, args, opts) {
34445
34887
  return spawnPty2(file, args, opts);
34446
34888
  }
34447
34889
  function createTerminalService(deps) {
34448
- const spawn11 = deps.spawn ?? realPtySpawn;
34890
+ const spawn12 = deps.spawn ?? realPtySpawn;
34449
34891
  const platform = deps.platform ?? process.platform;
34450
34892
  const sessions = new Map;
34451
34893
  const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
@@ -34471,7 +34913,7 @@ function createTerminalService(deps) {
34471
34913
  if (platform === "win32")
34472
34914
  throw new Error("terminal-unsupported-platform");
34473
34915
  const terminalId = randomUUID4();
34474
- const handle = spawn11(defaultShell(platform), [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34916
+ const handle = spawn12(defaultShell(platform), [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34475
34917
  const session3 = { handle, seq: 0, idleTimer: null };
34476
34918
  sessions.set(terminalId, session3);
34477
34919
  handle.onData((data) => {
@@ -34545,7 +34987,7 @@ var init_terminal_service = __esm(() => {
34545
34987
  });
34546
34988
 
34547
34989
  // src/control/upload-store.ts
34548
- import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm10, stat as stat4, writeFile as writeFile8 } from "node:fs/promises";
34990
+ import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm11, stat as stat4, writeFile as writeFile9 } from "node:fs/promises";
34549
34991
  import { homedir as homedir13 } from "node:os";
34550
34992
  import path16 from "node:path";
34551
34993
  function defaultRootDir() {
@@ -34581,11 +35023,11 @@ class UploadStore {
34581
35023
  if (bytes.byteLength > this.maxBytes)
34582
35024
  throw new Error("file-too-large");
34583
35025
  const safeName = sanitizeUploadFilename(filename);
34584
- const { mkdir: mkdir9 } = await import("node:fs/promises");
34585
- await mkdir9(this.rootDir, { recursive: true });
35026
+ const { mkdir: mkdir10 } = await import("node:fs/promises");
35027
+ await mkdir10(this.rootDir, { recursive: true });
34586
35028
  const dir = await mkdtemp2(path16.join(this.rootDir, "u-"));
34587
35029
  const filePath = path16.join(dir, safeName);
34588
- await writeFile8(filePath, bytes);
35030
+ await writeFile9(filePath, bytes);
34589
35031
  return {
34590
35032
  id: path16.basename(dir),
34591
35033
  path: filePath,
@@ -34608,7 +35050,7 @@ class UploadStore {
34608
35050
  try {
34609
35051
  const info = await stat4(dir);
34610
35052
  if (info.mtimeMs < cutoff) {
34611
- await rm10(dir, { recursive: true, force: true });
35053
+ await rm11(dir, { recursive: true, force: true });
34612
35054
  removed += 1;
34613
35055
  }
34614
35056
  } catch {}
@@ -34666,12 +35108,12 @@ var init_agent_catalog = __esm(() => {
34666
35108
 
34667
35109
  // src/config/config-watcher.ts
34668
35110
  import { watch } from "node:fs";
34669
- import { basename as basename3, dirname as dirname12 } from "node:path";
35111
+ import { basename as basename4, dirname as dirname13 } from "node:path";
34670
35112
  function startConfigWatcher(options) {
34671
35113
  const { configPath, onChange, debounceMs = 250, logger: logger2 } = options;
34672
35114
  const watchFactory = options.watchFactory ?? watch;
34673
- const dir = dirname12(configPath);
34674
- const target = basename3(configPath);
35115
+ const dir = dirname13(configPath);
35116
+ const target = basename4(configPath);
34675
35117
  let timer;
34676
35118
  let watcher;
34677
35119
  const fire = () => {
@@ -34686,7 +35128,7 @@ function startConfigWatcher(options) {
34686
35128
  };
34687
35129
  try {
34688
35130
  watcher = watchFactory(dir, { persistent: false }, (_event, filename) => {
34689
- if (filename !== null && basename3(filename.toString()) !== target)
35131
+ if (filename !== null && basename4(filename.toString()) !== target)
34690
35132
  return;
34691
35133
  if (timer)
34692
35134
  clearTimeout(timer);
@@ -34726,7 +35168,7 @@ __export(exports_main, {
34726
35168
  });
34727
35169
  import { randomUUID as randomUUID5 } from "node:crypto";
34728
35170
  import { homedir as homedir14 } from "node:os";
34729
- import { dirname as dirname13, join as join23 } from "node:path";
35171
+ import { dirname as dirname14, join as join23 } from "node:path";
34730
35172
  import { fileURLToPath as fileURLToPath5 } from "node:url";
34731
35173
  function startProgressHeartbeat(orchestration3, config4, logger2, channel) {
34732
35174
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
@@ -35271,7 +35713,8 @@ async function buildApp(paths, deps = {}) {
35271
35713
  },
35272
35714
  uploadStore,
35273
35715
  terminal: terminalService,
35274
- terminalEnabled: () => terminalEnabled(config4)
35716
+ terminalEnabled: () => terminalEnabled(config4),
35717
+ filesWriteEnabled: () => filesWriteEnabled(config4)
35275
35718
  });
35276
35719
  const workspaceSignature = (cfg) => JSON.stringify(Object.keys(cfg.workspaces).sort().map((name) => {
35277
35720
  const ws = cfg.workspaces[name];
@@ -35424,7 +35867,7 @@ async function main() {
35424
35867
  }
35425
35868
  }
35426
35869
  async function prepareChannelMedia(configPath, config4) {
35427
- const runtimeDir = join23(dirname13(configPath), "runtime");
35870
+ const runtimeDir = join23(dirname14(configPath), "runtime");
35428
35871
  const mediaRootDir = join23(runtimeDir, "media");
35429
35872
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
35430
35873
  await mediaStore.cleanupExpired().catch((error2) => {
@@ -35439,7 +35882,7 @@ function resolveRuntimePaths() {
35439
35882
  throw new Error("Unable to resolve the current user home directory");
35440
35883
  }
35441
35884
  const configPath = coreEnv("CONFIG") ?? join23(coreHomeDir(home), "config.json");
35442
- const runtimeDir = join23(dirname13(configPath), "runtime");
35885
+ const runtimeDir = join23(dirname14(configPath), "runtime");
35443
35886
  return {
35444
35887
  configPath,
35445
35888
  statePath: coreEnv("STATE") ?? join23(coreHomeDir(home), "state.json"),
@@ -35454,12 +35897,12 @@ function resolveBridgeEntryPath() {
35454
35897
  return fileURLToPath5(new URL("./bridge/bridge-main.ts", import.meta.url));
35455
35898
  }
35456
35899
  function resolveAppLogPath(configPath) {
35457
- const rootDir = dirname13(configPath);
35900
+ const rootDir = dirname14(configPath);
35458
35901
  const runtimeDir = join23(rootDir, "runtime");
35459
35902
  return join23(runtimeDir, "app.log");
35460
35903
  }
35461
35904
  function resolvePerfLogPath(configPath) {
35462
- const rootDir = dirname13(configPath);
35905
+ const rootDir = dirname14(configPath);
35463
35906
  const runtimeDir = join23(rootDir, "runtime");
35464
35907
  return join23(runtimeDir, "perf.log");
35465
35908
  }
@@ -35517,7 +35960,7 @@ var init_main = __esm(async () => {
35517
35960
  });
35518
35961
 
35519
35962
  // src/doctor/checks/acpx-check.ts
35520
- import { spawn as spawn11 } from "node:child_process";
35963
+ import { spawn as spawn12 } from "node:child_process";
35521
35964
  async function checkAcpx(options = {}) {
35522
35965
  const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
35523
35966
  try {
@@ -35564,7 +36007,7 @@ function buildDetails(metadata, version2, verbose) {
35564
36007
  async function defaultRunVersion(command) {
35565
36008
  const spawnSpec = resolveSpawnCommand(command, ["--version"]);
35566
36009
  return await new Promise((resolve4, reject) => {
35567
- const child = spawn11(spawnSpec.command, spawnSpec.args, {
36010
+ const child = spawn12(spawnSpec.command, spawnSpec.args, {
35568
36011
  stdio: ["ignore", "pipe", "pipe"]
35569
36012
  });
35570
36013
  let stdout2 = "";
@@ -35702,7 +36145,7 @@ var init_config_check = __esm(async () => {
35702
36145
  });
35703
36146
 
35704
36147
  // src/doctor/checks/daemon-check.ts
35705
- import { readdir as readdir6, readFile as readFile15, rm as rm11 } from "node:fs/promises";
36148
+ import { readdir as readdir6, readFile as readFile16, rm as rm12 } from "node:fs/promises";
35706
36149
  import { fileURLToPath as fileURLToPath6 } from "node:url";
35707
36150
  import { homedir as homedir15 } from "node:os";
35708
36151
  import { join as join24 } from "node:path";
@@ -35848,7 +36291,7 @@ async function defaultListConsumerLocks(runtimeDir) {
35848
36291
  }
35849
36292
  async function defaultReadConsumerLock(path17) {
35850
36293
  try {
35851
- const raw = await readFile15(path17, "utf8");
36294
+ const raw = await readFile16(path17, "utf8");
35852
36295
  const parsed = JSON.parse(raw);
35853
36296
  return typeof parsed.pid === "number" ? { pid: parsed.pid } : null;
35854
36297
  } catch {
@@ -35856,7 +36299,7 @@ async function defaultReadConsumerLock(path17) {
35856
36299
  }
35857
36300
  }
35858
36301
  async function defaultRemoveConsumerLock(path17) {
35859
- await rm11(path17, { force: true });
36302
+ await rm12(path17, { force: true });
35860
36303
  }
35861
36304
  function resolveCliEntryPath() {
35862
36305
  return process.argv[1] ?? fileURLToPath6(import.meta.url);
@@ -35872,7 +36315,7 @@ var init_daemon_check = __esm(() => {
35872
36315
 
35873
36316
  // src/doctor/checks/logs-check.ts
35874
36317
  import { stat as stat5, readdir as readdir7 } from "node:fs/promises";
35875
- import { basename as basename4, join as join25 } from "node:path";
36318
+ import { basename as basename5, join as join25 } from "node:path";
35876
36319
  import { homedir as homedir16 } from "node:os";
35877
36320
  async function checkLogs(options = {}) {
35878
36321
  const home = options.home ?? process.env.HOME ?? homedir16();
@@ -35902,7 +36345,7 @@ async function checkLogs(options = {}) {
35902
36345
  `error: ${formatError6(error2)}`
35903
36346
  ]);
35904
36347
  }
35905
- const baseNames = [basename4(paths.appLog), basename4(paths.stdoutLog), basename4(paths.stderrLog)];
36348
+ const baseNames = [basename5(paths.appLog), basename5(paths.stdoutLog), basename5(paths.stderrLog)];
35906
36349
  const tracked = new Set(baseNames);
35907
36350
  const matched = entries.filter((entry) => isTrackedLogName(entry, tracked));
35908
36351
  const files = [];
@@ -36233,7 +36676,7 @@ var init_plugin_check = __esm(async () => {
36233
36676
  // src/doctor/checks/runtime-check.ts
36234
36677
  import { constants } from "node:fs";
36235
36678
  import { access as access4, stat as stat6 } from "node:fs/promises";
36236
- import { dirname as dirname14 } from "node:path";
36679
+ import { dirname as dirname15 } from "node:path";
36237
36680
  import { homedir as homedir18 } from "node:os";
36238
36681
  async function checkRuntime(options = {}) {
36239
36682
  const home = options.home ?? process.env.HOME ?? homedir18();
@@ -36389,7 +36832,7 @@ async function checkFileCreatable(label, path17, probe, platform) {
36389
36832
  detail: `${label}: ${path17} (unusable: ${formatError9(error2)})`
36390
36833
  };
36391
36834
  }
36392
- const parentCheck = await checkCreatableAncestorDirectory(dirname14(path17), probe, platform);
36835
+ const parentCheck = await checkCreatableAncestorDirectory(dirname15(path17), probe, platform);
36393
36836
  if (!parentCheck.ok) {
36394
36837
  return {
36395
36838
  ok: false,
@@ -36425,7 +36868,7 @@ async function checkCreatableAncestorDirectory(path17, probe, platform) {
36425
36868
  blockingPath: path17
36426
36869
  };
36427
36870
  }
36428
- const parent = dirname14(path17);
36871
+ const parent = dirname15(path17);
36429
36872
  if (parent === path17) {
36430
36873
  return {
36431
36874
  ok: false,
@@ -37210,7 +37653,7 @@ var init_doctor2 = __esm(async () => {
37210
37653
  init_core_home();
37211
37654
  import { randomUUID as randomUUID6 } from "node:crypto";
37212
37655
  import { homedir as homedir20 } from "node:os";
37213
- import { dirname as dirname15, join as join27, sep as sep2 } from "node:path";
37656
+ import { dirname as dirname16, join as join27, sep as sep2 } from "node:path";
37214
37657
  import { fileURLToPath as fileURLToPath7 } from "node:url";
37215
37658
 
37216
37659
  // src/runtime/migrate-core-home.ts
@@ -53839,7 +54282,7 @@ function safeDaemonLogPaths() {
53839
54282
  const configPath = resolveConfigPathForCurrentEnv();
53840
54283
  const paths = resolveDaemonPathsForCurrentConfig();
53841
54284
  return {
53842
- appLog: join27(dirname15(configPath), "runtime", "app.log"),
54285
+ appLog: join27(dirname16(configPath), "runtime", "app.log"),
53843
54286
  stderrLog: paths.stderrLog
53844
54287
  };
53845
54288
  } catch {
@@ -48,6 +48,11 @@ export interface TerminalConfig {
48
48
  /** Idle seconds before a terminal PTY is auto-killed. Defaults to 900 (15 min). */
49
49
  idleTimeoutSeconds?: number;
50
50
  }
51
+ export interface FilesConfig {
52
+ /** Default false. When false, all fs write ops (new/rename/delete/copy) are rejected
53
+ * before touching disk. Download is a read and stays available regardless. */
54
+ writeEnabled: boolean;
55
+ }
51
56
  export type LoggingLevel = "error" | "info" | "debug";
52
57
  export interface PerfLogConfig {
53
58
  enabled: boolean;
@@ -110,6 +115,8 @@ export interface AppConfig {
110
115
  later?: LaterConfig;
111
116
  language?: Locale;
112
117
  terminal?: TerminalConfig;
118
+ files?: FilesConfig;
113
119
  }
114
120
  export declare function terminalEnabled(config: AppConfig): boolean;
115
121
  export declare function terminalIdleTimeoutSeconds(config: AppConfig): number;
122
+ export declare function filesWriteEnabled(config: AppConfig): boolean;
@@ -8,7 +8,7 @@ import type { CancelTaskInput, OrchestrationService, OrchestrationTaskFilter } f
8
8
  import type { OrchestrationTaskRecord } from "../orchestration/orchestration-types";
9
9
  import type { ControlEventBus } from "./control-event-bus";
10
10
  import type { AgentCatalogEntry } from "../config/agent-catalog";
11
- import { type DirListing, type FileContent, type SearchResult, type WorkspaceDiff } from "./workspace-fs";
11
+ import { type DirListing, type FileContent, type SearchOptions, type SearchResult, type WorkspaceDiff } from "./workspace-fs";
12
12
  import type { PromptAttachmentRef } from "@ganglion/xacpx-relay-protocol";
13
13
  import type { UploadStore } from "./upload-store.js";
14
14
  export interface ControlSessionInfo {
@@ -79,6 +79,7 @@ export interface ControlServiceDeps {
79
79
  uploadStore: UploadStore;
80
80
  terminal: import("./terminal-service").TerminalService;
81
81
  terminalEnabled: () => boolean;
82
+ filesWriteEnabled: () => boolean;
82
83
  }
83
84
  export interface ControlPromptInput {
84
85
  chatKey: string;
@@ -138,7 +139,25 @@ export declare class ControlService {
138
139
  listDirectory(workspace: string, path?: string): Promise<DirListing>;
139
140
  readWorkspaceFile(workspace: string, path: string): Promise<FileContent>;
140
141
  workspaceGitDiff(workspace: string, path?: string): Promise<WorkspaceDiff>;
141
- searchWorkspace(workspace: string, query: string): Promise<SearchResult>;
142
+ searchWorkspace(workspace: string, opts: SearchOptions): Promise<SearchResult>;
143
+ fsCreate(workspace: string, path: string, kind: "file" | "dir"): Promise<{
144
+ path: string;
145
+ }>;
146
+ fsRename(workspace: string, path: string, newName: string): Promise<{
147
+ path: string;
148
+ }>;
149
+ fsDelete(workspace: string, path: string): Promise<{
150
+ path: string;
151
+ }>;
152
+ fsCopy(workspace: string, path: string): Promise<{
153
+ path: string;
154
+ }>;
155
+ fsDownload(workspace: string, path: string): Promise<{
156
+ path: string;
157
+ base64: string;
158
+ size: number;
159
+ mimeType: string;
160
+ }>;
142
161
  uploadFile(input: {
143
162
  filename: string;
144
163
  content: string;
@@ -2,11 +2,17 @@ export interface FsEntry {
2
2
  name: string;
3
3
  type: "dir" | "file";
4
4
  size?: number;
5
+ /** True when git check-ignore matches this entry (omitted in non-git workspaces). */
6
+ ignored?: boolean;
5
7
  }
6
8
  export interface DirListing {
7
9
  workspace: string;
8
10
  path: string;
9
11
  entries: FsEntry[];
12
+ /** Absolute realpath'd workspace root on the connector host. */
13
+ root: string;
14
+ /** Host path separator, so the client can render an absolute host path. */
15
+ sep: "/" | "\\";
10
16
  }
11
17
  export interface FileContent {
12
18
  workspace: string;
@@ -20,12 +26,31 @@ export interface DiffFile {
20
26
  path: string;
21
27
  status: string;
22
28
  }
29
+ export interface SearchHit {
30
+ path: string;
31
+ line: number;
32
+ text: string;
33
+ }
23
34
  export interface SearchResult {
24
35
  workspace: string;
25
36
  query: string;
26
37
  matches: string[];
38
+ hits: SearchHit[];
27
39
  truncated: boolean;
28
40
  }
41
+ export interface SearchOptions {
42
+ query: string;
43
+ mode?: "name" | "content";
44
+ matchCase?: boolean;
45
+ wholeWord?: boolean;
46
+ regex?: boolean;
47
+ /** Glob to restrict matches (relative to workspace root). */
48
+ include?: string;
49
+ /** Glob to drop matches. */
50
+ exclude?: string;
51
+ /** Base directory (relative) to scope the search; defaults to the whole workspace. */
52
+ path?: string;
53
+ }
29
54
  export interface WorkspaceDiff {
30
55
  workspace: string;
31
56
  files: DiffFile[];
@@ -51,14 +76,57 @@ export declare class WorkspaceFs {
51
76
  /** Resolve `relPath` within a named workspace, symlink-safe and contained. Throws
52
77
  * on unknown workspace, missing target, or any path that escapes the root. */
53
78
  private resolve;
79
+ /** Resolve the PARENT directory of a to-be-created/renamed target. Parent must exist
80
+ * and be contained; the final segment is validated (non-empty, no separators, not
81
+ * "."/".."). Because parent is realpath'd+contained and name has no separator, the
82
+ * join target stays inside the workspace root. */
83
+ private resolveParent;
84
+ createFile(workspace: string, relPath: string): Promise<{
85
+ path: string;
86
+ }>;
87
+ createDir(workspace: string, relPath: string): Promise<{
88
+ path: string;
89
+ }>;
90
+ rename(workspace: string, relPath: string, newName: string): Promise<{
91
+ path: string;
92
+ }>;
93
+ duplicate(workspace: string, relPath: string): Promise<{
94
+ path: string;
95
+ }>;
96
+ remove(workspace: string, relPath: string): Promise<{
97
+ path: string;
98
+ }>;
99
+ readFileBytes(workspace: string, relPath: string): Promise<{
100
+ path: string;
101
+ base64: string;
102
+ size: number;
103
+ mimeType: string;
104
+ }>;
54
105
  listDirectory(workspace: string, relPath?: string): Promise<DirListing>;
106
+ /** Return the subset of the given root-relative paths that git considers ignored.
107
+ * Uses `check-ignore -z --stdin` (bounded by the caller's MAX_ENTRIES listing).
108
+ * Any git error (not a repo, git missing) yields an empty set — callers degrade to
109
+ * "nothing ignored". Never throws. */
110
+ private gitCheckIgnore;
55
111
  readFile(workspace: string, relPath: string): Promise<FileContent>;
56
- /** Find files whose relative path contains `query` (case-insensitive). Walks the
57
- * tree breadth-first, skipping `.git`/`node_modules` and never following symlinks
58
- * (so it stays contained), bounded by a scan budget and a result cap. */
59
- search(workspace: string, query: string): Promise<SearchResult>;
112
+ /** Find files by name (default) or content. Name mode walks relative paths
113
+ * breadth-first, skipping `.git`/`node_modules` and never following symlinks (so it
114
+ * stays contained), applying the query plus any include/exclude globs. Content mode
115
+ * delegates to `contentGrep`. Both are bounded by a scan budget and a result cap. */
116
+ search(workspace: string, opts: SearchOptions): Promise<SearchResult>;
117
+ /** Content grep: `git grep` when the workspace root is a git repo (fast, respects
118
+ * .gitignore), else a bounded manual walk that scans each file line by line. Never
119
+ * throws on git absence/failure or an invalid user regex — both degrade gracefully.
120
+ * `scopeRel` is the already-resolved (canonicalized, containment-checked) scope from
121
+ * `resolve()`; include/exclude are applied as a post-filter on the parsed hits rather
122
+ * than as extra git pathspecs — git ORs multiple positive pathspecs together, so a
123
+ * scope pathspec plus an include pathspec would leak matches from outside the scope. */
124
+ private contentGrep;
60
125
  gitDiff(workspace: string, relPath?: string): Promise<WorkspaceDiff>;
61
126
  /** Branch + worktree context for a repo root. Best-effort: any git hiccup just
62
127
  * omits the fields so the diff itself still returns. */
63
128
  private gitContext;
64
129
  }
130
+ /** First free "NAME copy", "NAME copy 2"… inserting before the extension. Dotfiles
131
+ * (leading dot, no other dot) and extension-less names get no split. */
132
+ export declare function copyName(existing: Set<string>, name: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.17.0-beta.1",
3
+ "version": "0.17.0-beta.2",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",