@ganglion/xacpx 0.17.0-beta.1 → 0.17.0-beta.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/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);
@@ -33759,6 +33889,7 @@ class WorkspaceFs {
33759
33889
  path: rel,
33760
33890
  content: binary ? "" : slice.toString("utf8"),
33761
33891
  size: info.size,
33892
+ mtimeMs: info.mtimeMs,
33762
33893
  truncated: info.size > FILE_READ_CAP,
33763
33894
  binary
33764
33895
  };
@@ -33766,16 +33897,45 @@ class WorkspaceFs {
33766
33897
  await fh.close();
33767
33898
  }
33768
33899
  }
33769
- async search(workspace3, query) {
33770
- const { root } = await this.resolve(workspace3, undefined);
33771
- const needle = query.trim().toLowerCase();
33900
+ async writeFile(workspace3, relPath, content, expected) {
33901
+ const { abs, rel } = await this.resolve(workspace3, relPath);
33902
+ const info = await stat3(abs);
33903
+ if (!info.isFile())
33904
+ throw new Error("not-a-file");
33905
+ if (info.size > FILE_READ_CAP)
33906
+ throw new Error("file-too-large");
33907
+ if (content.includes("\x00"))
33908
+ throw new Error("is-binary");
33909
+ if (Buffer.byteLength(content, "utf8") > FILE_READ_CAP)
33910
+ throw new Error("file-too-large");
33911
+ if (info.mtimeMs !== expected.mtimeMs || info.size !== expected.size)
33912
+ throw new Error("stale-write");
33913
+ await import_write_file_atomic3.default(abs, content);
33914
+ const after = await stat3(abs);
33915
+ return { path: rel, mtimeMs: after.mtimeMs, size: after.size };
33916
+ }
33917
+ async search(workspace3, opts) {
33918
+ const { root, abs: base, rel: scopeRel } = await this.resolve(workspace3, opts.path);
33919
+ const query = opts.query.trim();
33920
+ const empty = { workspace: workspace3, query, matches: [], hits: [], truncated: false };
33921
+ if (!query)
33922
+ return empty;
33923
+ if ((opts.mode ?? "name") === "content") {
33924
+ return this.contentGrep(workspace3, root, base, scopeRel, opts, query);
33925
+ }
33926
+ const matcher = buildMatcher(query, opts);
33927
+ const inc = opts.include ? globToRegExp(opts.include) : null;
33928
+ const exc = opts.exclude ? globToRegExp(opts.exclude) : null;
33772
33929
  const matches = [];
33773
- if (!needle)
33774
- return { workspace: workspace3, query, matches, truncated: false };
33775
33930
  let scanned = 0;
33776
33931
  let truncated = false;
33777
- const queue = [root];
33932
+ const deadline = Date.now() + SEARCH_DEADLINE_MS;
33933
+ const queue = [base];
33778
33934
  while (queue.length) {
33935
+ if (Date.now() > deadline) {
33936
+ truncated = true;
33937
+ break;
33938
+ }
33779
33939
  const dir = queue.shift();
33780
33940
  let dirents;
33781
33941
  try {
@@ -33784,6 +33944,10 @@ class WorkspaceFs {
33784
33944
  continue;
33785
33945
  }
33786
33946
  for (const d of dirents) {
33947
+ if (Date.now() > deadline) {
33948
+ truncated = true;
33949
+ break;
33950
+ }
33787
33951
  if (++scanned > SEARCH_MAX_SCAN) {
33788
33952
  truncated = true;
33789
33953
  break;
@@ -33795,7 +33959,11 @@ class WorkspaceFs {
33795
33959
  queue.push(resolve3(dir, d.name));
33796
33960
  } else if (d.isFile()) {
33797
33961
  const rel = relative(root, resolve3(dir, d.name)).split(sep).join("/");
33798
- if (rel.toLowerCase().includes(needle)) {
33962
+ if (inc && !inc.test(rel))
33963
+ continue;
33964
+ if (exc && exc.test(rel))
33965
+ continue;
33966
+ if (matcher(rel)) {
33799
33967
  matches.push(rel);
33800
33968
  if (matches.length >= SEARCH_MAX_RESULTS) {
33801
33969
  truncated = true;
@@ -33808,18 +33976,199 @@ class WorkspaceFs {
33808
33976
  break;
33809
33977
  }
33810
33978
  matches.sort();
33811
- return { workspace: workspace3, query, matches, truncated };
33979
+ return { workspace: workspace3, query, matches, hits: [], truncated };
33980
+ }
33981
+ async contentGrep(workspace3, root, base, scopeRel, opts, query) {
33982
+ const inc = opts.include ? globToRegExp(opts.include) : null;
33983
+ const exc = opts.exclude ? globToRegExp(opts.exclude) : null;
33984
+ 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);
33985
+ if (inGit) {
33986
+ const args = ["-C", root, "grep", "-n", "--column", "-I", "--no-color", "--untracked"];
33987
+ if (!opts.matchCase)
33988
+ args.push("-i");
33989
+ if (opts.wholeWord)
33990
+ args.push("-w");
33991
+ args.push(opts.regex ? "-E" : "-F");
33992
+ args.push("-e", query, "--", scopeRel || ".");
33993
+ let stdout2 = "";
33994
+ let killed = false;
33995
+ try {
33996
+ stdout2 = (await execFileAsync("git", args, { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout;
33997
+ } catch (e) {
33998
+ killed = e.killed === true || e.signal === "SIGKILL";
33999
+ const code = e.code;
34000
+ if (code === 1)
34001
+ stdout2 = "";
34002
+ else {
34003
+ const out = e.stdout;
34004
+ stdout2 = typeof out === "string" ? out : "";
34005
+ }
34006
+ }
34007
+ const hits2 = [];
34008
+ let truncated2 = killed;
34009
+ for (const raw of stdout2.split(`
34010
+ `)) {
34011
+ if (!raw)
34012
+ continue;
34013
+ const m = raw.match(/^(.*?):(\d+):(\d+):(.*)$/);
34014
+ if (!m)
34015
+ continue;
34016
+ const p = m[1].split(sep).join("/");
34017
+ if (inc && !inc.test(p))
34018
+ continue;
34019
+ if (exc && exc.test(p))
34020
+ continue;
34021
+ hits2.push({ path: p, line: Number(m[2]), text: m[4].slice(0, 400) });
34022
+ if (hits2.length >= SEARCH_CONTENT_MAX_HITS) {
34023
+ truncated2 = true;
34024
+ break;
34025
+ }
34026
+ }
34027
+ return { workspace: workspace3, query, matches: [], hits: hits2, truncated: truncated2 };
34028
+ }
34029
+ try {
34030
+ const args = ["grep", "--no-index", "-n", "--column", "-I", "--no-color"];
34031
+ if (!opts.matchCase)
34032
+ args.push("-i");
34033
+ if (opts.wholeWord)
34034
+ args.push("-w");
34035
+ args.push(opts.regex ? "-E" : "-F");
34036
+ args.push("-e", query, "--", ".", ":(exclude)node_modules/", ":(exclude).git/");
34037
+ let stdout2 = "";
34038
+ let killed = false;
34039
+ try {
34040
+ stdout2 = (await execFileAsync("git", args, { cwd: base, maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout;
34041
+ } catch (e) {
34042
+ if (e.code === "ENOENT")
34043
+ throw e;
34044
+ killed = e.killed === true || e.signal === "SIGKILL";
34045
+ const code = e.code;
34046
+ if (code === 1)
34047
+ stdout2 = "";
34048
+ else {
34049
+ const out = e.stdout;
34050
+ stdout2 = typeof out === "string" ? out : "";
34051
+ }
34052
+ }
34053
+ const hits2 = [];
34054
+ let truncated2 = killed;
34055
+ for (const raw of stdout2.split(`
34056
+ `)) {
34057
+ if (!raw)
34058
+ continue;
34059
+ const m = raw.match(/^(.*?):(\d+):(\d+):(.*)$/);
34060
+ if (!m)
34061
+ continue;
34062
+ const relToBase = m[1].split(sep).join("/");
34063
+ const p = scopeRel ? `${scopeRel}/${relToBase}` : relToBase;
34064
+ if (inc && !inc.test(p))
34065
+ continue;
34066
+ if (exc && exc.test(p))
34067
+ continue;
34068
+ hits2.push({ path: p, line: Number(m[2]), text: m[4].slice(0, 400) });
34069
+ if (hits2.length >= SEARCH_CONTENT_MAX_HITS) {
34070
+ truncated2 = true;
34071
+ break;
34072
+ }
34073
+ }
34074
+ return { workspace: workspace3, query, matches: [], hits: hits2, truncated: truncated2 };
34075
+ } catch (e) {
34076
+ if (e.code !== "ENOENT") {
34077
+ return { workspace: workspace3, query, matches: [], hits: [], truncated: false };
34078
+ }
34079
+ }
34080
+ const matcher = buildLineMatcher(query, opts);
34081
+ const hits = [];
34082
+ let scanned = 0;
34083
+ let truncated = false;
34084
+ const deadline = Date.now() + SEARCH_DEADLINE_MS;
34085
+ const queue = [base];
34086
+ while (queue.length) {
34087
+ if (Date.now() > deadline) {
34088
+ truncated = true;
34089
+ break;
34090
+ }
34091
+ const dir = queue.shift();
34092
+ let dirents;
34093
+ try {
34094
+ dirents = await readdir4(dir, { withFileTypes: true });
34095
+ } catch {
34096
+ continue;
34097
+ }
34098
+ for (const d of dirents) {
34099
+ if (Date.now() > deadline) {
34100
+ truncated = true;
34101
+ break;
34102
+ }
34103
+ if (++scanned > SEARCH_MAX_SCAN) {
34104
+ truncated = true;
34105
+ break;
34106
+ }
34107
+ if (d.isSymbolicLink())
34108
+ continue;
34109
+ const full = resolve3(dir, d.name);
34110
+ if (d.isDirectory()) {
34111
+ if (!SEARCH_SKIP_DIRS.has(d.name))
34112
+ queue.push(full);
34113
+ continue;
34114
+ }
34115
+ if (!d.isFile())
34116
+ continue;
34117
+ const rel = relative(root, full).split(sep).join("/");
34118
+ if (inc && !inc.test(rel))
34119
+ continue;
34120
+ if (exc && exc.test(rel))
34121
+ continue;
34122
+ let info;
34123
+ try {
34124
+ info = await stat3(full);
34125
+ } catch {
34126
+ continue;
34127
+ }
34128
+ if (info.size > SEARCH_CONTENT_MAX_FILE)
34129
+ continue;
34130
+ let text;
34131
+ try {
34132
+ text = await readFile15(full, "utf8");
34133
+ } catch {
34134
+ continue;
34135
+ }
34136
+ if (text.includes("\x00"))
34137
+ continue;
34138
+ const lines = text.split(`
34139
+ `);
34140
+ for (let i = 0;i < lines.length; i++) {
34141
+ if (Date.now() > deadline) {
34142
+ truncated = true;
34143
+ break;
34144
+ }
34145
+ const line = lines[i];
34146
+ if (matcher(line.length > LINE_MATCH_CAP ? line.slice(0, LINE_MATCH_CAP) : line)) {
34147
+ hits.push({ path: rel, line: i + 1, text: line.slice(0, 400) });
34148
+ if (hits.length >= SEARCH_CONTENT_MAX_HITS) {
34149
+ truncated = true;
34150
+ break;
34151
+ }
34152
+ }
34153
+ }
34154
+ if (truncated)
34155
+ break;
34156
+ }
34157
+ if (truncated)
34158
+ break;
34159
+ }
34160
+ return { workspace: workspace3, query, matches: [], hits, truncated };
33812
34161
  }
33813
34162
  async gitDiff(workspace3, relPath) {
33814
34163
  const { root, rel } = await this.resolve(workspace3, relPath);
33815
34164
  try {
33816
- await execFileAsync("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { maxBuffer: GIT_MAX_BUFFER });
34165
+ await execFileAsync("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" });
33817
34166
  } catch {
33818
34167
  throw new Error("not-a-git-repo");
33819
34168
  }
33820
34169
  const files = [];
33821
34170
  try {
33822
- const { stdout: stdout2 } = await execFileAsync("git", ["-C", root, "-c", "core.quotePath=false", "status", "--porcelain", "-z"], { maxBuffer: GIT_MAX_BUFFER });
34171
+ 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
34172
  const fields = stdout2.split("\x00");
33824
34173
  for (let i = 0;i < fields.length; i++) {
33825
34174
  const field = fields[i];
@@ -33835,17 +34184,17 @@ class WorkspaceFs {
33835
34184
  const diffArgs = (base) => ["-C", root, ...base, ...rel ? ["--", rel] : []];
33836
34185
  let diff = "";
33837
34186
  try {
33838
- diff = (await execFileAsync("git", diffArgs(["diff", "HEAD"]), { maxBuffer: GIT_MAX_BUFFER })).stdout;
34187
+ diff = (await execFileAsync("git", diffArgs(["diff", "HEAD"]), { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout;
33839
34188
  } catch {
33840
34189
  try {
33841
- diff = (await execFileAsync("git", diffArgs(["diff"]), { maxBuffer: GIT_MAX_BUFFER })).stdout;
34190
+ diff = (await execFileAsync("git", diffArgs(["diff"]), { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout;
33842
34191
  } catch {
33843
34192
  diff = "";
33844
34193
  }
33845
34194
  }
33846
34195
  if (rel && !diff) {
33847
34196
  try {
33848
- diff = (await execFileAsync("git", ["-C", root, "-c", "core.quotePath=false", "diff", "--no-index", "--", "/dev/null", rel], { maxBuffer: GIT_MAX_BUFFER })).stdout;
34197
+ 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
34198
  } catch (e) {
33850
34199
  const out = e.stdout;
33851
34200
  if (typeof out === "string")
@@ -33858,7 +34207,7 @@ class WorkspaceFs {
33858
34207
  async gitContext(root) {
33859
34208
  const run = async (...args) => {
33860
34209
  try {
33861
- return (await execFileAsync("git", ["-C", root, ...args], { maxBuffer: GIT_MAX_BUFFER })).stdout.trim();
34210
+ return (await execFileAsync("git", ["-C", root, ...args], { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" })).stdout.trim();
33862
34211
  } catch {
33863
34212
  return null;
33864
34213
  }
@@ -33878,13 +34227,102 @@ class WorkspaceFs {
33878
34227
  return ctx;
33879
34228
  }
33880
34229
  }
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;
34230
+ function escapeRe(s) {
34231
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34232
+ }
34233
+ function copyName(existing, name) {
34234
+ const dot = name.lastIndexOf(".");
34235
+ const hasExt = dot > 0;
34236
+ const base = hasExt ? name.slice(0, dot) : name;
34237
+ const ext = hasExt ? name.slice(dot) : "";
34238
+ let candidate = `${base} copy${ext}`;
34239
+ let n = 2;
34240
+ while (existing.has(candidate))
34241
+ candidate = `${base} copy ${n++}${ext}`;
34242
+ return candidate;
34243
+ }
34244
+ function mimeForName(name) {
34245
+ const dot = name.lastIndexOf(".");
34246
+ const ext = dot > 0 ? name.slice(dot).toLowerCase() : "";
34247
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
34248
+ }
34249
+ function buildMatcher(query, opts) {
34250
+ return buildLineMatcher(query, opts);
34251
+ }
34252
+ function buildLineMatcher(query, opts) {
34253
+ const flags = opts.matchCase ? "" : "i";
34254
+ let pattern;
34255
+ if (opts.regex)
34256
+ pattern = query;
34257
+ else
34258
+ pattern = escapeRe(query);
34259
+ if (opts.wholeWord)
34260
+ pattern = `\\b${pattern}\\b`;
34261
+ let re = null;
34262
+ try {
34263
+ re = new RegExp(pattern, flags);
34264
+ } catch {
34265
+ re = null;
34266
+ }
34267
+ if (!re) {
34268
+ const needle = opts.matchCase ? query : query.toLowerCase();
34269
+ return (s) => (opts.matchCase ? s : s.toLowerCase()).includes(needle);
34270
+ }
34271
+ return (s) => re.test(s);
34272
+ }
34273
+ function globToRegExp(glob) {
34274
+ let re = "^";
34275
+ for (let i = 0;i < glob.length; i++) {
34276
+ const c = glob[i];
34277
+ if (c === "*") {
34278
+ if (glob[i + 1] === "*") {
34279
+ re += ".*";
34280
+ i++;
34281
+ if (glob[i + 1] === "/")
34282
+ i++;
34283
+ } else
34284
+ re += "[^/]*";
34285
+ } else if (c === "?")
34286
+ re += "[^/]";
34287
+ else
34288
+ re += escapeRe(c);
34289
+ }
34290
+ re += "$";
34291
+ try {
34292
+ return new RegExp(re);
34293
+ } catch {
34294
+ return /$^/;
34295
+ }
34296
+ }
34297
+ var import_write_file_atomic3, 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
34298
  var init_workspace_fs = __esm(() => {
34299
+ import_write_file_atomic3 = __toESM(require_lib(), 1);
33883
34300
  execFileAsync = promisify(execFile);
33884
34301
  FILE_READ_CAP = 256 * 1024;
33885
34302
  DIFF_CAP = 512 * 1024;
33886
34303
  GIT_MAX_BUFFER = 32 * 1024 * 1024;
33887
34304
  SEARCH_SKIP_DIRS = new Set([".git", "node_modules"]);
34305
+ SEARCH_CONTENT_MAX_FILE = 1024 * 1024;
34306
+ DOWNLOAD_MAX = 5 * 1024 * 1024;
34307
+ MIME_BY_EXT = {
34308
+ ".txt": "text/plain",
34309
+ ".md": "text/markdown",
34310
+ ".json": "application/json",
34311
+ ".js": "text/javascript",
34312
+ ".ts": "text/plain",
34313
+ ".html": "text/html",
34314
+ ".css": "text/css",
34315
+ ".csv": "text/csv",
34316
+ ".xml": "application/xml",
34317
+ ".png": "image/png",
34318
+ ".jpg": "image/jpeg",
34319
+ ".jpeg": "image/jpeg",
34320
+ ".gif": "image/gif",
34321
+ ".svg": "image/svg+xml",
34322
+ ".webp": "image/webp",
34323
+ ".pdf": "application/pdf",
34324
+ ".zip": "application/zip"
34325
+ };
33888
34326
  });
33889
34327
 
33890
34328
  // src/control/control-service.ts
@@ -33906,8 +34344,36 @@ class ControlService {
33906
34344
  workspaceGitDiff(workspace3, path16) {
33907
34345
  return this.workspaceFs.gitDiff(workspace3, path16);
33908
34346
  }
33909
- searchWorkspace(workspace3, query) {
33910
- return this.workspaceFs.search(workspace3, query);
34347
+ searchWorkspace(workspace3, opts) {
34348
+ return this.workspaceFs.search(workspace3, opts);
34349
+ }
34350
+ async fsCreate(workspace3, path16, kind) {
34351
+ if (!this.deps.filesWriteEnabled())
34352
+ throw new Error("files-write-disabled");
34353
+ return kind === "dir" ? this.workspaceFs.createDir(workspace3, path16) : this.workspaceFs.createFile(workspace3, path16);
34354
+ }
34355
+ async fsRename(workspace3, path16, newName) {
34356
+ if (!this.deps.filesWriteEnabled())
34357
+ throw new Error("files-write-disabled");
34358
+ return this.workspaceFs.rename(workspace3, path16, newName);
34359
+ }
34360
+ async fsDelete(workspace3, path16) {
34361
+ if (!this.deps.filesWriteEnabled())
34362
+ throw new Error("files-write-disabled");
34363
+ return this.workspaceFs.remove(workspace3, path16);
34364
+ }
34365
+ async fsCopy(workspace3, path16) {
34366
+ if (!this.deps.filesWriteEnabled())
34367
+ throw new Error("files-write-disabled");
34368
+ return this.workspaceFs.duplicate(workspace3, path16);
34369
+ }
34370
+ async fsWrite(workspace3, path16, content, expected) {
34371
+ if (!this.deps.filesWriteEnabled())
34372
+ throw new Error("files-write-disabled");
34373
+ return this.workspaceFs.writeFile(workspace3, path16, content, expected);
34374
+ }
34375
+ async fsDownload(workspace3, path16) {
34376
+ return this.workspaceFs.readFileBytes(workspace3, path16);
33911
34377
  }
33912
34378
  async uploadFile(input) {
33913
34379
  return this.deps.uploadStore.save(input.filename, input.content, input.mimeType);
@@ -34445,7 +34911,7 @@ function realPtySpawn(file, args, opts) {
34445
34911
  return spawnPty2(file, args, opts);
34446
34912
  }
34447
34913
  function createTerminalService(deps) {
34448
- const spawn11 = deps.spawn ?? realPtySpawn;
34914
+ const spawn12 = deps.spawn ?? realPtySpawn;
34449
34915
  const platform = deps.platform ?? process.platform;
34450
34916
  const sessions = new Map;
34451
34917
  const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
@@ -34471,7 +34937,7 @@ function createTerminalService(deps) {
34471
34937
  if (platform === "win32")
34472
34938
  throw new Error("terminal-unsupported-platform");
34473
34939
  const terminalId = randomUUID4();
34474
- const handle = spawn11(defaultShell(platform), [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34940
+ const handle = spawn12(defaultShell(platform), [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34475
34941
  const session3 = { handle, seq: 0, idleTimer: null };
34476
34942
  sessions.set(terminalId, session3);
34477
34943
  handle.onData((data) => {
@@ -34545,7 +35011,7 @@ var init_terminal_service = __esm(() => {
34545
35011
  });
34546
35012
 
34547
35013
  // 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";
35014
+ import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm11, stat as stat4, writeFile as writeFile9 } from "node:fs/promises";
34549
35015
  import { homedir as homedir13 } from "node:os";
34550
35016
  import path16 from "node:path";
34551
35017
  function defaultRootDir() {
@@ -34581,11 +35047,11 @@ class UploadStore {
34581
35047
  if (bytes.byteLength > this.maxBytes)
34582
35048
  throw new Error("file-too-large");
34583
35049
  const safeName = sanitizeUploadFilename(filename);
34584
- const { mkdir: mkdir9 } = await import("node:fs/promises");
34585
- await mkdir9(this.rootDir, { recursive: true });
35050
+ const { mkdir: mkdir10 } = await import("node:fs/promises");
35051
+ await mkdir10(this.rootDir, { recursive: true });
34586
35052
  const dir = await mkdtemp2(path16.join(this.rootDir, "u-"));
34587
35053
  const filePath = path16.join(dir, safeName);
34588
- await writeFile8(filePath, bytes);
35054
+ await writeFile9(filePath, bytes);
34589
35055
  return {
34590
35056
  id: path16.basename(dir),
34591
35057
  path: filePath,
@@ -34608,7 +35074,7 @@ class UploadStore {
34608
35074
  try {
34609
35075
  const info = await stat4(dir);
34610
35076
  if (info.mtimeMs < cutoff) {
34611
- await rm10(dir, { recursive: true, force: true });
35077
+ await rm11(dir, { recursive: true, force: true });
34612
35078
  removed += 1;
34613
35079
  }
34614
35080
  } catch {}
@@ -34666,12 +35132,12 @@ var init_agent_catalog = __esm(() => {
34666
35132
 
34667
35133
  // src/config/config-watcher.ts
34668
35134
  import { watch } from "node:fs";
34669
- import { basename as basename3, dirname as dirname12 } from "node:path";
35135
+ import { basename as basename4, dirname as dirname13 } from "node:path";
34670
35136
  function startConfigWatcher(options) {
34671
35137
  const { configPath, onChange, debounceMs = 250, logger: logger2 } = options;
34672
35138
  const watchFactory = options.watchFactory ?? watch;
34673
- const dir = dirname12(configPath);
34674
- const target = basename3(configPath);
35139
+ const dir = dirname13(configPath);
35140
+ const target = basename4(configPath);
34675
35141
  let timer;
34676
35142
  let watcher;
34677
35143
  const fire = () => {
@@ -34686,7 +35152,7 @@ function startConfigWatcher(options) {
34686
35152
  };
34687
35153
  try {
34688
35154
  watcher = watchFactory(dir, { persistent: false }, (_event, filename) => {
34689
- if (filename !== null && basename3(filename.toString()) !== target)
35155
+ if (filename !== null && basename4(filename.toString()) !== target)
34690
35156
  return;
34691
35157
  if (timer)
34692
35158
  clearTimeout(timer);
@@ -34726,7 +35192,7 @@ __export(exports_main, {
34726
35192
  });
34727
35193
  import { randomUUID as randomUUID5 } from "node:crypto";
34728
35194
  import { homedir as homedir14 } from "node:os";
34729
- import { dirname as dirname13, join as join23 } from "node:path";
35195
+ import { dirname as dirname14, join as join23 } from "node:path";
34730
35196
  import { fileURLToPath as fileURLToPath5 } from "node:url";
34731
35197
  function startProgressHeartbeat(orchestration3, config4, logger2, channel) {
34732
35198
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
@@ -35271,7 +35737,8 @@ async function buildApp(paths, deps = {}) {
35271
35737
  },
35272
35738
  uploadStore,
35273
35739
  terminal: terminalService,
35274
- terminalEnabled: () => terminalEnabled(config4)
35740
+ terminalEnabled: () => terminalEnabled(config4),
35741
+ filesWriteEnabled: () => filesWriteEnabled(config4)
35275
35742
  });
35276
35743
  const workspaceSignature = (cfg) => JSON.stringify(Object.keys(cfg.workspaces).sort().map((name) => {
35277
35744
  const ws = cfg.workspaces[name];
@@ -35424,7 +35891,7 @@ async function main() {
35424
35891
  }
35425
35892
  }
35426
35893
  async function prepareChannelMedia(configPath, config4) {
35427
- const runtimeDir = join23(dirname13(configPath), "runtime");
35894
+ const runtimeDir = join23(dirname14(configPath), "runtime");
35428
35895
  const mediaRootDir = join23(runtimeDir, "media");
35429
35896
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
35430
35897
  await mediaStore.cleanupExpired().catch((error2) => {
@@ -35439,7 +35906,7 @@ function resolveRuntimePaths() {
35439
35906
  throw new Error("Unable to resolve the current user home directory");
35440
35907
  }
35441
35908
  const configPath = coreEnv("CONFIG") ?? join23(coreHomeDir(home), "config.json");
35442
- const runtimeDir = join23(dirname13(configPath), "runtime");
35909
+ const runtimeDir = join23(dirname14(configPath), "runtime");
35443
35910
  return {
35444
35911
  configPath,
35445
35912
  statePath: coreEnv("STATE") ?? join23(coreHomeDir(home), "state.json"),
@@ -35454,12 +35921,12 @@ function resolveBridgeEntryPath() {
35454
35921
  return fileURLToPath5(new URL("./bridge/bridge-main.ts", import.meta.url));
35455
35922
  }
35456
35923
  function resolveAppLogPath(configPath) {
35457
- const rootDir = dirname13(configPath);
35924
+ const rootDir = dirname14(configPath);
35458
35925
  const runtimeDir = join23(rootDir, "runtime");
35459
35926
  return join23(runtimeDir, "app.log");
35460
35927
  }
35461
35928
  function resolvePerfLogPath(configPath) {
35462
- const rootDir = dirname13(configPath);
35929
+ const rootDir = dirname14(configPath);
35463
35930
  const runtimeDir = join23(rootDir, "runtime");
35464
35931
  return join23(runtimeDir, "perf.log");
35465
35932
  }
@@ -35517,7 +35984,7 @@ var init_main = __esm(async () => {
35517
35984
  });
35518
35985
 
35519
35986
  // src/doctor/checks/acpx-check.ts
35520
- import { spawn as spawn11 } from "node:child_process";
35987
+ import { spawn as spawn12 } from "node:child_process";
35521
35988
  async function checkAcpx(options = {}) {
35522
35989
  const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
35523
35990
  try {
@@ -35564,7 +36031,7 @@ function buildDetails(metadata, version2, verbose) {
35564
36031
  async function defaultRunVersion(command) {
35565
36032
  const spawnSpec = resolveSpawnCommand(command, ["--version"]);
35566
36033
  return await new Promise((resolve4, reject) => {
35567
- const child = spawn11(spawnSpec.command, spawnSpec.args, {
36034
+ const child = spawn12(spawnSpec.command, spawnSpec.args, {
35568
36035
  stdio: ["ignore", "pipe", "pipe"]
35569
36036
  });
35570
36037
  let stdout2 = "";
@@ -35702,7 +36169,7 @@ var init_config_check = __esm(async () => {
35702
36169
  });
35703
36170
 
35704
36171
  // src/doctor/checks/daemon-check.ts
35705
- import { readdir as readdir6, readFile as readFile15, rm as rm11 } from "node:fs/promises";
36172
+ import { readdir as readdir6, readFile as readFile16, rm as rm12 } from "node:fs/promises";
35706
36173
  import { fileURLToPath as fileURLToPath6 } from "node:url";
35707
36174
  import { homedir as homedir15 } from "node:os";
35708
36175
  import { join as join24 } from "node:path";
@@ -35848,7 +36315,7 @@ async function defaultListConsumerLocks(runtimeDir) {
35848
36315
  }
35849
36316
  async function defaultReadConsumerLock(path17) {
35850
36317
  try {
35851
- const raw = await readFile15(path17, "utf8");
36318
+ const raw = await readFile16(path17, "utf8");
35852
36319
  const parsed = JSON.parse(raw);
35853
36320
  return typeof parsed.pid === "number" ? { pid: parsed.pid } : null;
35854
36321
  } catch {
@@ -35856,7 +36323,7 @@ async function defaultReadConsumerLock(path17) {
35856
36323
  }
35857
36324
  }
35858
36325
  async function defaultRemoveConsumerLock(path17) {
35859
- await rm11(path17, { force: true });
36326
+ await rm12(path17, { force: true });
35860
36327
  }
35861
36328
  function resolveCliEntryPath() {
35862
36329
  return process.argv[1] ?? fileURLToPath6(import.meta.url);
@@ -35872,7 +36339,7 @@ var init_daemon_check = __esm(() => {
35872
36339
 
35873
36340
  // src/doctor/checks/logs-check.ts
35874
36341
  import { stat as stat5, readdir as readdir7 } from "node:fs/promises";
35875
- import { basename as basename4, join as join25 } from "node:path";
36342
+ import { basename as basename5, join as join25 } from "node:path";
35876
36343
  import { homedir as homedir16 } from "node:os";
35877
36344
  async function checkLogs(options = {}) {
35878
36345
  const home = options.home ?? process.env.HOME ?? homedir16();
@@ -35902,7 +36369,7 @@ async function checkLogs(options = {}) {
35902
36369
  `error: ${formatError6(error2)}`
35903
36370
  ]);
35904
36371
  }
35905
- const baseNames = [basename4(paths.appLog), basename4(paths.stdoutLog), basename4(paths.stderrLog)];
36372
+ const baseNames = [basename5(paths.appLog), basename5(paths.stdoutLog), basename5(paths.stderrLog)];
35906
36373
  const tracked = new Set(baseNames);
35907
36374
  const matched = entries.filter((entry) => isTrackedLogName(entry, tracked));
35908
36375
  const files = [];
@@ -36233,7 +36700,7 @@ var init_plugin_check = __esm(async () => {
36233
36700
  // src/doctor/checks/runtime-check.ts
36234
36701
  import { constants } from "node:fs";
36235
36702
  import { access as access4, stat as stat6 } from "node:fs/promises";
36236
- import { dirname as dirname14 } from "node:path";
36703
+ import { dirname as dirname15 } from "node:path";
36237
36704
  import { homedir as homedir18 } from "node:os";
36238
36705
  async function checkRuntime(options = {}) {
36239
36706
  const home = options.home ?? process.env.HOME ?? homedir18();
@@ -36389,7 +36856,7 @@ async function checkFileCreatable(label, path17, probe, platform) {
36389
36856
  detail: `${label}: ${path17} (unusable: ${formatError9(error2)})`
36390
36857
  };
36391
36858
  }
36392
- const parentCheck = await checkCreatableAncestorDirectory(dirname14(path17), probe, platform);
36859
+ const parentCheck = await checkCreatableAncestorDirectory(dirname15(path17), probe, platform);
36393
36860
  if (!parentCheck.ok) {
36394
36861
  return {
36395
36862
  ok: false,
@@ -36425,7 +36892,7 @@ async function checkCreatableAncestorDirectory(path17, probe, platform) {
36425
36892
  blockingPath: path17
36426
36893
  };
36427
36894
  }
36428
- const parent = dirname14(path17);
36895
+ const parent = dirname15(path17);
36429
36896
  if (parent === path17) {
36430
36897
  return {
36431
36898
  ok: false,
@@ -37210,7 +37677,7 @@ var init_doctor2 = __esm(async () => {
37210
37677
  init_core_home();
37211
37678
  import { randomUUID as randomUUID6 } from "node:crypto";
37212
37679
  import { homedir as homedir20 } from "node:os";
37213
- import { dirname as dirname15, join as join27, sep as sep2 } from "node:path";
37680
+ import { dirname as dirname16, join as join27, sep as sep2 } from "node:path";
37214
37681
  import { fileURLToPath as fileURLToPath7 } from "node:url";
37215
37682
 
37216
37683
  // src/runtime/migrate-core-home.ts
@@ -53839,7 +54306,7 @@ function safeDaemonLogPaths() {
53839
54306
  const configPath = resolveConfigPathForCurrentEnv();
53840
54307
  const paths = resolveDaemonPathsForCurrentConfig();
53841
54308
  return {
53842
- appLog: join27(dirname15(configPath), "runtime", "app.log"),
54309
+ appLog: join27(dirname16(configPath), "runtime", "app.log"),
53843
54310
  stderrLog: paths.stderrLog
53844
54311
  };
53845
54312
  } 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,33 @@ 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
+ fsWrite(workspace: string, path: string, content: string, expected: {
156
+ mtimeMs: number;
157
+ size: number;
158
+ }): Promise<{
159
+ path: string;
160
+ mtimeMs: number;
161
+ size: number;
162
+ }>;
163
+ fsDownload(workspace: string, path: string): Promise<{
164
+ path: string;
165
+ base64: string;
166
+ size: number;
167
+ mimeType: string;
168
+ }>;
142
169
  uploadFile(input: {
143
170
  filename: string;
144
171
  content: string;
@@ -2,17 +2,24 @@ 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;
13
19
  path: string;
14
20
  content: string;
15
21
  size: number;
22
+ mtimeMs: number;
16
23
  truncated: boolean;
17
24
  binary: boolean;
18
25
  }
@@ -20,12 +27,31 @@ export interface DiffFile {
20
27
  path: string;
21
28
  status: string;
22
29
  }
30
+ export interface SearchHit {
31
+ path: string;
32
+ line: number;
33
+ text: string;
34
+ }
23
35
  export interface SearchResult {
24
36
  workspace: string;
25
37
  query: string;
26
38
  matches: string[];
39
+ hits: SearchHit[];
27
40
  truncated: boolean;
28
41
  }
42
+ export interface SearchOptions {
43
+ query: string;
44
+ mode?: "name" | "content";
45
+ matchCase?: boolean;
46
+ wholeWord?: boolean;
47
+ regex?: boolean;
48
+ /** Glob to restrict matches (relative to workspace root). */
49
+ include?: string;
50
+ /** Glob to drop matches. */
51
+ exclude?: string;
52
+ /** Base directory (relative) to scope the search; defaults to the whole workspace. */
53
+ path?: string;
54
+ }
29
55
  export interface WorkspaceDiff {
30
56
  workspace: string;
31
57
  files: DiffFile[];
@@ -51,14 +77,69 @@ export declare class WorkspaceFs {
51
77
  /** Resolve `relPath` within a named workspace, symlink-safe and contained. Throws
52
78
  * on unknown workspace, missing target, or any path that escapes the root. */
53
79
  private resolve;
80
+ /** Resolve the PARENT directory of a to-be-created/renamed target. Parent must exist
81
+ * and be contained; the final segment is validated (non-empty, no separators, not
82
+ * "."/".."). Because parent is realpath'd+contained and name has no separator, the
83
+ * join target stays inside the workspace root. */
84
+ private resolveParent;
85
+ createFile(workspace: string, relPath: string): Promise<{
86
+ path: string;
87
+ }>;
88
+ createDir(workspace: string, relPath: string): Promise<{
89
+ path: string;
90
+ }>;
91
+ rename(workspace: string, relPath: string, newName: string): Promise<{
92
+ path: string;
93
+ }>;
94
+ duplicate(workspace: string, relPath: string): Promise<{
95
+ path: string;
96
+ }>;
97
+ remove(workspace: string, relPath: string): Promise<{
98
+ path: string;
99
+ }>;
100
+ readFileBytes(workspace: string, relPath: string): Promise<{
101
+ path: string;
102
+ base64: string;
103
+ size: number;
104
+ mimeType: string;
105
+ }>;
54
106
  listDirectory(workspace: string, relPath?: string): Promise<DirListing>;
107
+ /** Return the subset of the given root-relative paths that git considers ignored.
108
+ * Uses `check-ignore -z --stdin` (bounded by the caller's MAX_ENTRIES listing).
109
+ * Any git error (not a repo, git missing) yields an empty set — callers degrade to
110
+ * "nothing ignored". Never throws. */
111
+ private gitCheckIgnore;
55
112
  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>;
113
+ /** Overwrite an EXISTING file's content, guarded by a stale-write token. The target must
114
+ * resolve inside the workspace (realpath containment), be a regular file, and match the
115
+ * caller's `{mtimeMs,size}` token — otherwise `stale-write`. Content must be UTF-8 text
116
+ * (no NUL) and within the read cap. Writes atomically. Returns a fresh token. */
117
+ writeFile(workspace: string, relPath: string, content: string, expected: {
118
+ mtimeMs: number;
119
+ size: number;
120
+ }): Promise<{
121
+ path: string;
122
+ mtimeMs: number;
123
+ size: number;
124
+ }>;
125
+ /** Find files by name (default) or content. Name mode walks relative paths
126
+ * breadth-first, skipping `.git`/`node_modules` and never following symlinks (so it
127
+ * stays contained), applying the query plus any include/exclude globs. Content mode
128
+ * delegates to `contentGrep`. Both are bounded by a scan budget and a result cap. */
129
+ search(workspace: string, opts: SearchOptions): Promise<SearchResult>;
130
+ /** Content grep: `git grep` when the workspace root is a git repo (fast, respects
131
+ * .gitignore), else a bounded manual walk that scans each file line by line. Never
132
+ * throws on git absence/failure or an invalid user regex — both degrade gracefully.
133
+ * `scopeRel` is the already-resolved (canonicalized, containment-checked) scope from
134
+ * `resolve()`; include/exclude are applied as a post-filter on the parsed hits rather
135
+ * than as extra git pathspecs — git ORs multiple positive pathspecs together, so a
136
+ * scope pathspec plus an include pathspec would leak matches from outside the scope. */
137
+ private contentGrep;
60
138
  gitDiff(workspace: string, relPath?: string): Promise<WorkspaceDiff>;
61
139
  /** Branch + worktree context for a repo root. Best-effort: any git hiccup just
62
140
  * omits the fields so the diff itself still returns. */
63
141
  private gitContext;
64
142
  }
143
+ /** First free "NAME copy", "NAME copy 2"… inserting before the extension. Dotfiles
144
+ * (leading dot, no other dot) and extension-less names get no split. */
145
+ 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.3",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",