@ganglion/xacpx 0.17.0-beta.0 → 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) {
33727
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) {
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,17 +34209,106 @@ 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
33891
34310
  import path15 from "node:path";
34311
+ import { randomUUID as randomUUID3 } from "node:crypto";
33892
34312
 
33893
34313
  class ControlService {
33894
34314
  deps;
@@ -33905,8 +34325,31 @@ class ControlService {
33905
34325
  workspaceGitDiff(workspace3, path16) {
33906
34326
  return this.workspaceFs.gitDiff(workspace3, path16);
33907
34327
  }
33908
- searchWorkspace(workspace3, query) {
33909
- 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);
33910
34353
  }
33911
34354
  async uploadFile(input) {
33912
34355
  return this.deps.uploadStore.save(input.filename, input.content, input.mimeType);
@@ -34056,12 +34499,23 @@ class ControlService {
34056
34499
  return task;
34057
34500
  }
34058
34501
  inFlight = new Map;
34502
+ queues = new Map;
34503
+ draining = new Set;
34504
+ emitQueueUpdated(chatKey, sessionAlias) {
34505
+ const items = (this.queues.get(turnKey(chatKey, sessionAlias)) ?? []).map((q) => ({
34506
+ id: q.id,
34507
+ textPreview: q.text.length > QUEUE_PREVIEW_MAX ? q.text.slice(0, QUEUE_PREVIEW_MAX) : q.text,
34508
+ enqueuedAt: q.enqueuedAt
34509
+ }));
34510
+ this.deps.events.emit({ type: "queue-updated", chatKey, sessionAlias, items });
34511
+ }
34059
34512
  async prompt(input) {
34060
34513
  return this.executeTurn({
34061
34514
  chatKey: input.chatKey,
34062
34515
  sessionAlias: input.sessionAlias,
34063
34516
  text: input.text,
34064
34517
  senderId: input.senderId,
34518
+ queueable: true,
34065
34519
  ...input.isOwner !== undefined ? { isOwner: input.isOwner } : {},
34066
34520
  ...input.accountId !== undefined ? { accountId: input.accountId } : {},
34067
34521
  ...input.media !== undefined ? { media: input.media } : {}
@@ -34081,14 +34535,34 @@ class ControlService {
34081
34535
  }
34082
34536
  async executeTurn(params) {
34083
34537
  const key = turnKey(params.chatKey, params.sessionAlias);
34084
- const existing = this.inFlight.get(key);
34085
- if (existing) {
34086
- if (!existing.controller.signal.aborted) {
34538
+ if (!params.drained) {
34539
+ const existing = this.inFlight.get(key);
34540
+ const busy = this.draining.has(key) || existing !== undefined && !existing.controller.signal.aborted;
34541
+ if (busy) {
34542
+ if (params.queueable) {
34543
+ const id = randomUUID3();
34544
+ const item = {
34545
+ id,
34546
+ text: params.text,
34547
+ enqueuedAt: new Date().toISOString(),
34548
+ senderId: params.senderId,
34549
+ ...params.isOwner !== undefined ? { isOwner: params.isOwner } : {},
34550
+ ...params.accountId !== undefined ? { accountId: params.accountId } : {},
34551
+ ...params.media !== undefined ? { media: params.media } : {}
34552
+ };
34553
+ const q = this.queues.get(key) ?? [];
34554
+ q.push(item);
34555
+ this.queues.set(key, q);
34556
+ this.emitQueueUpdated(params.chatKey, params.sessionAlias);
34557
+ return { ok: true, queued: true, queueItemId: id };
34558
+ }
34087
34559
  return { ok: false, errorMessage: "turn-already-running" };
34088
34560
  }
34089
- await raceWithTimeout(existing.settled, CANCEL_DRAIN_TIMEOUT_MS);
34090
- if (this.inFlight.has(key)) {
34091
- return { ok: false, errorMessage: "turn-already-running" };
34561
+ if (existing) {
34562
+ await raceWithTimeout(existing.settled, CANCEL_DRAIN_TIMEOUT_MS);
34563
+ if (this.inFlight.has(key)) {
34564
+ return { ok: false, errorMessage: "turn-already-running" };
34565
+ }
34092
34566
  }
34093
34567
  }
34094
34568
  const controller = new AbortController;
@@ -34103,6 +34577,9 @@ class ControlService {
34103
34577
  resolveSettled = resolve4;
34104
34578
  });
34105
34579
  this.inFlight.set(key, { controller, settled });
34580
+ if (params.drained) {
34581
+ this.draining.delete(key);
34582
+ }
34106
34583
  let internalAlias;
34107
34584
  let wasArchived = false;
34108
34585
  let priorTransportSession;
@@ -34115,8 +34592,8 @@ class ControlService {
34115
34592
  try {
34116
34593
  await this.deps.sessions.useSession(params.chatKey, params.sessionAlias);
34117
34594
  } catch (error2) {
34118
- this.inFlight.delete(key);
34119
34595
  resolveSettled();
34596
+ this.advanceQueue(key, params.chatKey, params.sessionAlias);
34120
34597
  return { ok: false, errorMessage: toErrorMessage(error2) };
34121
34598
  }
34122
34599
  if (wasArchived) {
@@ -34127,7 +34604,8 @@ class ControlService {
34127
34604
  chatKey: params.chatKey,
34128
34605
  sessionAlias: params.sessionAlias,
34129
34606
  ...params.turnStarted?.prompt ? { prompt: params.turnStarted.prompt } : {},
34130
- ...params.turnStarted?.scheduled ? { scheduled: params.turnStarted.scheduled } : {}
34607
+ ...params.turnStarted?.scheduled ? { scheduled: params.turnStarted.scheduled } : {},
34608
+ ...params.turnStarted?.queueItemId ? { queueItemId: params.turnStarted.queueItemId } : {}
34131
34609
  });
34132
34610
  let streamMode = false;
34133
34611
  try {
@@ -34251,8 +34729,9 @@ ${chunk}` : chunk
34251
34729
  });
34252
34730
  return { ok: false, errorMessage };
34253
34731
  } finally {
34254
- this.inFlight.delete(key);
34255
- resolveSettled();
34732
+ if ((this.queues.get(key)?.length ?? 0) > 0) {
34733
+ this.draining.add(key);
34734
+ }
34256
34735
  if (internalAlias && priorTransportSession) {
34257
34736
  try {
34258
34737
  const after = await this.deps.sessions.getSession(internalAlias);
@@ -34261,6 +34740,33 @@ ${chunk}` : chunk
34261
34740
  }
34262
34741
  } catch {}
34263
34742
  }
34743
+ resolveSettled();
34744
+ this.advanceQueue(key, params.chatKey, params.sessionAlias);
34745
+ }
34746
+ }
34747
+ advanceQueue(key, chatKey, sessionAlias) {
34748
+ const q = this.queues.get(key);
34749
+ const next = q?.shift();
34750
+ if (q && q.length === 0)
34751
+ this.queues.delete(key);
34752
+ if (next) {
34753
+ this.draining.add(key);
34754
+ this.emitQueueUpdated(chatKey, sessionAlias);
34755
+ this.executeTurn({
34756
+ chatKey,
34757
+ sessionAlias,
34758
+ text: next.text,
34759
+ senderId: next.senderId,
34760
+ queueable: true,
34761
+ drained: true,
34762
+ ...next.isOwner !== undefined ? { isOwner: next.isOwner } : {},
34763
+ ...next.accountId !== undefined ? { accountId: next.accountId } : {},
34764
+ ...next.media !== undefined ? { media: next.media } : {},
34765
+ turnStarted: { queueItemId: next.id }
34766
+ });
34767
+ } else {
34768
+ this.draining.delete(key);
34769
+ this.inFlight.delete(key);
34264
34770
  }
34265
34771
  }
34266
34772
  cancelTurn(chatKey, sessionAlias) {
@@ -34271,6 +34777,20 @@ ${chunk}` : chunk
34271
34777
  entry.controller.abort();
34272
34778
  return true;
34273
34779
  }
34780
+ cancelQueuedItem(chatKey, sessionAlias, itemId) {
34781
+ const key = turnKey(chatKey, sessionAlias);
34782
+ const q = this.queues.get(key);
34783
+ if (!q)
34784
+ return { cancelled: false };
34785
+ const i = q.findIndex((x) => x.id === itemId);
34786
+ if (i < 0)
34787
+ return { cancelled: false };
34788
+ q.splice(i, 1);
34789
+ if (q.length === 0)
34790
+ this.queues.delete(key);
34791
+ this.emitQueueUpdated(chatKey, sessionAlias);
34792
+ return { cancelled: true };
34793
+ }
34274
34794
  async executeCommand(input) {
34275
34795
  const chunks = [];
34276
34796
  const response = await this.deps.agent.chat({
@@ -34332,7 +34852,7 @@ function buildControlMetadata(senderId, isOwner) {
34332
34852
  ...isOwner === undefined ? {} : { isOwner }
34333
34853
  };
34334
34854
  }
34335
- var CANCEL_DRAIN_TIMEOUT_MS = 5000;
34855
+ var CANCEL_DRAIN_TIMEOUT_MS = 5000, QUEUE_PREVIEW_MAX = 120;
34336
34856
  var init_control_service = __esm(() => {
34337
34857
  init_channel_scope();
34338
34858
  init_native_session_history();
@@ -34340,7 +34860,7 @@ var init_control_service = __esm(() => {
34340
34860
  });
34341
34861
 
34342
34862
  // src/control/terminal-service.ts
34343
- import { randomUUID as randomUUID3 } from "node:crypto";
34863
+ import { randomUUID as randomUUID4 } from "node:crypto";
34344
34864
  import { createRequire as createRequire6 } from "node:module";
34345
34865
  import { spawn as spawnPty2 } from "node-pty";
34346
34866
  function scrubEnv() {
@@ -34367,7 +34887,7 @@ function realPtySpawn(file, args, opts) {
34367
34887
  return spawnPty2(file, args, opts);
34368
34888
  }
34369
34889
  function createTerminalService(deps) {
34370
- const spawn11 = deps.spawn ?? realPtySpawn;
34890
+ const spawn12 = deps.spawn ?? realPtySpawn;
34371
34891
  const platform = deps.platform ?? process.platform;
34372
34892
  const sessions = new Map;
34373
34893
  const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
@@ -34392,8 +34912,8 @@ function createTerminalService(deps) {
34392
34912
  create({ cwd, cols, rows }) {
34393
34913
  if (platform === "win32")
34394
34914
  throw new Error("terminal-unsupported-platform");
34395
- const terminalId = randomUUID3();
34396
- const handle = spawn11(defaultShell(platform), [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34915
+ const terminalId = randomUUID4();
34916
+ const handle = spawn12(defaultShell(platform), [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34397
34917
  const session3 = { handle, seq: 0, idleTimer: null };
34398
34918
  sessions.set(terminalId, session3);
34399
34919
  handle.onData((data) => {
@@ -34467,7 +34987,7 @@ var init_terminal_service = __esm(() => {
34467
34987
  });
34468
34988
 
34469
34989
  // src/control/upload-store.ts
34470
- 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";
34471
34991
  import { homedir as homedir13 } from "node:os";
34472
34992
  import path16 from "node:path";
34473
34993
  function defaultRootDir() {
@@ -34503,11 +35023,11 @@ class UploadStore {
34503
35023
  if (bytes.byteLength > this.maxBytes)
34504
35024
  throw new Error("file-too-large");
34505
35025
  const safeName = sanitizeUploadFilename(filename);
34506
- const { mkdir: mkdir9 } = await import("node:fs/promises");
34507
- await mkdir9(this.rootDir, { recursive: true });
35026
+ const { mkdir: mkdir10 } = await import("node:fs/promises");
35027
+ await mkdir10(this.rootDir, { recursive: true });
34508
35028
  const dir = await mkdtemp2(path16.join(this.rootDir, "u-"));
34509
35029
  const filePath = path16.join(dir, safeName);
34510
- await writeFile8(filePath, bytes);
35030
+ await writeFile9(filePath, bytes);
34511
35031
  return {
34512
35032
  id: path16.basename(dir),
34513
35033
  path: filePath,
@@ -34530,7 +35050,7 @@ class UploadStore {
34530
35050
  try {
34531
35051
  const info = await stat4(dir);
34532
35052
  if (info.mtimeMs < cutoff) {
34533
- await rm10(dir, { recursive: true, force: true });
35053
+ await rm11(dir, { recursive: true, force: true });
34534
35054
  removed += 1;
34535
35055
  }
34536
35056
  } catch {}
@@ -34588,12 +35108,12 @@ var init_agent_catalog = __esm(() => {
34588
35108
 
34589
35109
  // src/config/config-watcher.ts
34590
35110
  import { watch } from "node:fs";
34591
- import { basename as basename3, dirname as dirname12 } from "node:path";
35111
+ import { basename as basename4, dirname as dirname13 } from "node:path";
34592
35112
  function startConfigWatcher(options) {
34593
35113
  const { configPath, onChange, debounceMs = 250, logger: logger2 } = options;
34594
35114
  const watchFactory = options.watchFactory ?? watch;
34595
- const dir = dirname12(configPath);
34596
- const target = basename3(configPath);
35115
+ const dir = dirname13(configPath);
35116
+ const target = basename4(configPath);
34597
35117
  let timer;
34598
35118
  let watcher;
34599
35119
  const fire = () => {
@@ -34608,7 +35128,7 @@ function startConfigWatcher(options) {
34608
35128
  };
34609
35129
  try {
34610
35130
  watcher = watchFactory(dir, { persistent: false }, (_event, filename) => {
34611
- if (filename !== null && basename3(filename.toString()) !== target)
35131
+ if (filename !== null && basename4(filename.toString()) !== target)
34612
35132
  return;
34613
35133
  if (timer)
34614
35134
  clearTimeout(timer);
@@ -34646,9 +35166,9 @@ __export(exports_main, {
34646
35166
  main: () => main,
34647
35167
  buildApp: () => buildApp
34648
35168
  });
34649
- import { randomUUID as randomUUID4 } from "node:crypto";
35169
+ import { randomUUID as randomUUID5 } from "node:crypto";
34650
35170
  import { homedir as homedir14 } from "node:os";
34651
- import { dirname as dirname13, join as join23 } from "node:path";
35171
+ import { dirname as dirname14, join as join23 } from "node:path";
34652
35172
  import { fileURLToPath as fileURLToPath5 } from "node:url";
34653
35173
  function startProgressHeartbeat(orchestration3, config4, logger2, channel) {
34654
35174
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
@@ -35026,7 +35546,7 @@ async function buildApp(paths, deps = {}) {
35026
35546
  };
35027
35547
  orchestration3 = new OrchestrationService({
35028
35548
  now: deps.loggerNow ?? (() => new Date),
35029
- createId: () => randomUUID4(),
35549
+ createId: () => randomUUID5(),
35030
35550
  config: config4,
35031
35551
  loadState: async () => JSON.parse(JSON.stringify(state)),
35032
35552
  saveState: async (nextState) => {
@@ -35193,7 +35713,8 @@ async function buildApp(paths, deps = {}) {
35193
35713
  },
35194
35714
  uploadStore,
35195
35715
  terminal: terminalService,
35196
- terminalEnabled: () => terminalEnabled(config4)
35716
+ terminalEnabled: () => terminalEnabled(config4),
35717
+ filesWriteEnabled: () => filesWriteEnabled(config4)
35197
35718
  });
35198
35719
  const workspaceSignature = (cfg) => JSON.stringify(Object.keys(cfg.workspaces).sort().map((name) => {
35199
35720
  const ws = cfg.workspaces[name];
@@ -35346,7 +35867,7 @@ async function main() {
35346
35867
  }
35347
35868
  }
35348
35869
  async function prepareChannelMedia(configPath, config4) {
35349
- const runtimeDir = join23(dirname13(configPath), "runtime");
35870
+ const runtimeDir = join23(dirname14(configPath), "runtime");
35350
35871
  const mediaRootDir = join23(runtimeDir, "media");
35351
35872
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
35352
35873
  await mediaStore.cleanupExpired().catch((error2) => {
@@ -35361,7 +35882,7 @@ function resolveRuntimePaths() {
35361
35882
  throw new Error("Unable to resolve the current user home directory");
35362
35883
  }
35363
35884
  const configPath = coreEnv("CONFIG") ?? join23(coreHomeDir(home), "config.json");
35364
- const runtimeDir = join23(dirname13(configPath), "runtime");
35885
+ const runtimeDir = join23(dirname14(configPath), "runtime");
35365
35886
  return {
35366
35887
  configPath,
35367
35888
  statePath: coreEnv("STATE") ?? join23(coreHomeDir(home), "state.json"),
@@ -35376,12 +35897,12 @@ function resolveBridgeEntryPath() {
35376
35897
  return fileURLToPath5(new URL("./bridge/bridge-main.ts", import.meta.url));
35377
35898
  }
35378
35899
  function resolveAppLogPath(configPath) {
35379
- const rootDir = dirname13(configPath);
35900
+ const rootDir = dirname14(configPath);
35380
35901
  const runtimeDir = join23(rootDir, "runtime");
35381
35902
  return join23(runtimeDir, "app.log");
35382
35903
  }
35383
35904
  function resolvePerfLogPath(configPath) {
35384
- const rootDir = dirname13(configPath);
35905
+ const rootDir = dirname14(configPath);
35385
35906
  const runtimeDir = join23(rootDir, "runtime");
35386
35907
  return join23(runtimeDir, "perf.log");
35387
35908
  }
@@ -35439,7 +35960,7 @@ var init_main = __esm(async () => {
35439
35960
  });
35440
35961
 
35441
35962
  // src/doctor/checks/acpx-check.ts
35442
- import { spawn as spawn11 } from "node:child_process";
35963
+ import { spawn as spawn12 } from "node:child_process";
35443
35964
  async function checkAcpx(options = {}) {
35444
35965
  const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
35445
35966
  try {
@@ -35486,7 +36007,7 @@ function buildDetails(metadata, version2, verbose) {
35486
36007
  async function defaultRunVersion(command) {
35487
36008
  const spawnSpec = resolveSpawnCommand(command, ["--version"]);
35488
36009
  return await new Promise((resolve4, reject) => {
35489
- const child = spawn11(spawnSpec.command, spawnSpec.args, {
36010
+ const child = spawn12(spawnSpec.command, spawnSpec.args, {
35490
36011
  stdio: ["ignore", "pipe", "pipe"]
35491
36012
  });
35492
36013
  let stdout2 = "";
@@ -35624,7 +36145,7 @@ var init_config_check = __esm(async () => {
35624
36145
  });
35625
36146
 
35626
36147
  // src/doctor/checks/daemon-check.ts
35627
- 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";
35628
36149
  import { fileURLToPath as fileURLToPath6 } from "node:url";
35629
36150
  import { homedir as homedir15 } from "node:os";
35630
36151
  import { join as join24 } from "node:path";
@@ -35770,7 +36291,7 @@ async function defaultListConsumerLocks(runtimeDir) {
35770
36291
  }
35771
36292
  async function defaultReadConsumerLock(path17) {
35772
36293
  try {
35773
- const raw = await readFile15(path17, "utf8");
36294
+ const raw = await readFile16(path17, "utf8");
35774
36295
  const parsed = JSON.parse(raw);
35775
36296
  return typeof parsed.pid === "number" ? { pid: parsed.pid } : null;
35776
36297
  } catch {
@@ -35778,7 +36299,7 @@ async function defaultReadConsumerLock(path17) {
35778
36299
  }
35779
36300
  }
35780
36301
  async function defaultRemoveConsumerLock(path17) {
35781
- await rm11(path17, { force: true });
36302
+ await rm12(path17, { force: true });
35782
36303
  }
35783
36304
  function resolveCliEntryPath() {
35784
36305
  return process.argv[1] ?? fileURLToPath6(import.meta.url);
@@ -35794,7 +36315,7 @@ var init_daemon_check = __esm(() => {
35794
36315
 
35795
36316
  // src/doctor/checks/logs-check.ts
35796
36317
  import { stat as stat5, readdir as readdir7 } from "node:fs/promises";
35797
- import { basename as basename4, join as join25 } from "node:path";
36318
+ import { basename as basename5, join as join25 } from "node:path";
35798
36319
  import { homedir as homedir16 } from "node:os";
35799
36320
  async function checkLogs(options = {}) {
35800
36321
  const home = options.home ?? process.env.HOME ?? homedir16();
@@ -35824,7 +36345,7 @@ async function checkLogs(options = {}) {
35824
36345
  `error: ${formatError6(error2)}`
35825
36346
  ]);
35826
36347
  }
35827
- const baseNames = [basename4(paths.appLog), basename4(paths.stdoutLog), basename4(paths.stderrLog)];
36348
+ const baseNames = [basename5(paths.appLog), basename5(paths.stdoutLog), basename5(paths.stderrLog)];
35828
36349
  const tracked = new Set(baseNames);
35829
36350
  const matched = entries.filter((entry) => isTrackedLogName(entry, tracked));
35830
36351
  const files = [];
@@ -36155,7 +36676,7 @@ var init_plugin_check = __esm(async () => {
36155
36676
  // src/doctor/checks/runtime-check.ts
36156
36677
  import { constants } from "node:fs";
36157
36678
  import { access as access4, stat as stat6 } from "node:fs/promises";
36158
- import { dirname as dirname14 } from "node:path";
36679
+ import { dirname as dirname15 } from "node:path";
36159
36680
  import { homedir as homedir18 } from "node:os";
36160
36681
  async function checkRuntime(options = {}) {
36161
36682
  const home = options.home ?? process.env.HOME ?? homedir18();
@@ -36311,7 +36832,7 @@ async function checkFileCreatable(label, path17, probe, platform) {
36311
36832
  detail: `${label}: ${path17} (unusable: ${formatError9(error2)})`
36312
36833
  };
36313
36834
  }
36314
- const parentCheck = await checkCreatableAncestorDirectory(dirname14(path17), probe, platform);
36835
+ const parentCheck = await checkCreatableAncestorDirectory(dirname15(path17), probe, platform);
36315
36836
  if (!parentCheck.ok) {
36316
36837
  return {
36317
36838
  ok: false,
@@ -36347,7 +36868,7 @@ async function checkCreatableAncestorDirectory(path17, probe, platform) {
36347
36868
  blockingPath: path17
36348
36869
  };
36349
36870
  }
36350
- const parent = dirname14(path17);
36871
+ const parent = dirname15(path17);
36351
36872
  if (parent === path17) {
36352
36873
  return {
36353
36874
  ok: false,
@@ -37130,9 +37651,9 @@ var init_doctor2 = __esm(async () => {
37130
37651
 
37131
37652
  // src/cli.ts
37132
37653
  init_core_home();
37133
- import { randomUUID as randomUUID5 } from "node:crypto";
37654
+ import { randomUUID as randomUUID6 } from "node:crypto";
37134
37655
  import { homedir as homedir20 } from "node:os";
37135
- 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";
37136
37657
  import { fileURLToPath as fileURLToPath7 } from "node:url";
37137
37658
 
37138
37659
  // src/runtime/migrate-core-home.ts
@@ -52806,7 +53327,7 @@ async function prepareMcpCoordinatorStartup(input) {
52806
53327
  return { kind: "external-coordinator" };
52807
53328
  }
52808
53329
  function createMcpStdioIdentityResolver(input) {
52809
- const instanceId = randomUUID5().slice(0, 8);
53330
+ const instanceId = randomUUID6().slice(0, 8);
52810
53331
  return async (context) => {
52811
53332
  const parsedCoordinatorSession = input.parsedCoordinatorSession?.trim() || null;
52812
53333
  const workspace3 = input.workspace?.trim() || null;
@@ -53761,7 +54282,7 @@ function safeDaemonLogPaths() {
53761
54282
  const configPath = resolveConfigPathForCurrentEnv();
53762
54283
  const paths = resolveDaemonPathsForCurrentConfig();
53763
54284
  return {
53764
- appLog: join27(dirname15(configPath), "runtime", "app.log"),
54285
+ appLog: join27(dirname16(configPath), "runtime", "app.log"),
53765
54286
  stderrLog: paths.stderrLog
53766
54287
  };
53767
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;
@@ -6,6 +6,13 @@ export interface ScheduledOrigin {
6
6
  taskId: string;
7
7
  executeAt: string;
8
8
  }
9
+ /** A single pending item in the per-session server-side prompt queue, as surfaced on
10
+ * the wire by `queue-updated`. `textPreview` is truncated server-side (~120 chars). */
11
+ export interface QueuedItemInfo {
12
+ id: string;
13
+ textPreview: string;
14
+ enqueuedAt: string;
15
+ }
9
16
  export type ControlEvent = {
10
17
  type: "turn-output";
11
18
  chatKey: string;
@@ -17,6 +24,12 @@ export type ControlEvent = {
17
24
  sessionAlias: string;
18
25
  prompt?: string;
19
26
  scheduled?: ScheduledOrigin;
27
+ queueItemId?: string;
28
+ } | {
29
+ type: "queue-updated";
30
+ chatKey: string;
31
+ sessionAlias: string;
32
+ items: QueuedItemInfo[];
20
33
  } | {
21
34
  type: "tool-event";
22
35
  chatKey: string;
@@ -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;
@@ -93,6 +94,23 @@ export interface ControlPromptResult {
93
94
  ok: boolean;
94
95
  text?: string;
95
96
  errorMessage?: string;
97
+ /** True when this prompt did not run immediately and was instead appended to the
98
+ * per-session server-side queue (a turn was already in flight). */
99
+ queued?: boolean;
100
+ /** Id of the queued item, present only when `queued` is true. Used to cancel it via
101
+ * `cancelQueuedItem` before it drains. */
102
+ queueItemId?: string;
103
+ }
104
+ /** A prompt held in the per-session server-side queue while a turn is in flight.
105
+ * Runs through the same `executeTurn` machinery as a manual prompt once drained. */
106
+ export interface QueuedPrompt {
107
+ id: string;
108
+ text: string;
109
+ enqueuedAt: string;
110
+ senderId: string;
111
+ isOwner?: boolean;
112
+ accountId?: string;
113
+ media?: PromptAttachmentRef[];
96
114
  }
97
115
  /** A turn started by a fired scheduled task. Runs through the same agent + turn-event
98
116
  * machinery as a normal prompt, so it streams live and persists to history — but it
@@ -121,7 +139,25 @@ export declare class ControlService {
121
139
  listDirectory(workspace: string, path?: string): Promise<DirListing>;
122
140
  readWorkspaceFile(workspace: string, path: string): Promise<FileContent>;
123
141
  workspaceGitDiff(workspace: string, path?: string): Promise<WorkspaceDiff>;
124
- 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
+ }>;
125
161
  uploadFile(input: {
126
162
  filename: string;
127
163
  content: string;
@@ -173,6 +209,9 @@ export declare class ControlService {
173
209
  getOrchestrationTask(taskId: string): Promise<OrchestrationTaskRecord | null>;
174
210
  cancelOrchestrationTask(input: CancelTaskInput): Promise<OrchestrationTaskRecord>;
175
211
  private readonly inFlight;
212
+ private readonly queues;
213
+ private readonly draining;
214
+ private emitQueueUpdated;
176
215
  prompt(input: ControlPromptInput): Promise<ControlPromptResult>;
177
216
  /** Run a fired scheduled task as a real turn through the same machinery as a manual
178
217
  * prompt — so it streams live and persists to history — while tagging turn-started
@@ -180,7 +219,15 @@ export declare class ControlService {
180
219
  * the web can badge it. Owner-authorized: the task was owner-gated at creation. */
181
220
  runScheduledTurn(input: ControlScheduledTurnInput): Promise<ControlPromptResult>;
182
221
  private executeTurn;
222
+ private advanceQueue;
183
223
  cancelTurn(chatKey: string, sessionAlias: string): boolean;
224
+ /** Remove a pending queued prompt (by id) before it drains. No-ops (returns
225
+ * `{ cancelled: false }`) when the queue or the id is absent/already drained —
226
+ * e.g. a race where the item drained into a running turn just before the cancel
227
+ * arrived. Does NOT touch a turn that is already running (use `cancelTurn`). */
228
+ cancelQueuedItem(chatKey: string, sessionAlias: string, itemId: string): {
229
+ cancelled: boolean;
230
+ };
184
231
  executeCommand(input: ControlExecuteCommandInput): Promise<string>;
185
232
  /** Open an interactive terminal in the session's workspace cwd. Rejected when terminal is disabled. */
186
233
  createTerminal(chatKey: string, sessionAlias: string, cols: number, rows: number): Promise<{
@@ -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.0",
3
+ "version": "0.17.0-beta.2",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",