@n1creator/openacp-cli 2026.712.12 → 2026.713.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -291,12 +291,12 @@ function createSessionLogger(sessionId, parentLogger) {
291
291
  async function closeSessionLogger(logger) {
292
292
  const dest = logger.__sessionDest;
293
293
  if (!dest) return;
294
- await new Promise((resolve7) => {
294
+ await new Promise((resolve8) => {
295
295
  let settled = false;
296
296
  const done = () => {
297
297
  if (!settled) {
298
298
  settled = true;
299
- resolve7();
299
+ resolve8();
300
300
  }
301
301
  };
302
302
  const timeout = setTimeout(done, 3e3);
@@ -321,11 +321,11 @@ async function shutdownLogger() {
321
321
  logDir = void 0;
322
322
  initialized = false;
323
323
  if (transport) {
324
- await new Promise((resolve7) => {
325
- const timeout = setTimeout(resolve7, 3e3);
324
+ await new Promise((resolve8) => {
325
+ const timeout = setTimeout(resolve8, 3e3);
326
326
  transport.on("close", () => {
327
327
  clearTimeout(timeout);
328
- resolve7();
328
+ resolve8();
329
329
  });
330
330
  transport.end();
331
331
  });
@@ -458,22 +458,22 @@ __export(config_registry_exports, {
458
458
  resolveOptions: () => resolveOptions,
459
459
  setFieldValueAsync: () => setFieldValueAsync
460
460
  });
461
- function getFieldDef(path38) {
462
- return CONFIG_REGISTRY.find((f) => f.path === path38);
461
+ function getFieldDef(path39) {
462
+ return CONFIG_REGISTRY.find((f) => f.path === path39);
463
463
  }
464
464
  function getSafeFields() {
465
465
  return CONFIG_REGISTRY.filter((f) => f.scope === "safe");
466
466
  }
467
- function isHotReloadable(path38) {
468
- const def = getFieldDef(path38);
467
+ function isHotReloadable(path39) {
468
+ const def = getFieldDef(path39);
469
469
  return def?.hotReload ?? false;
470
470
  }
471
471
  function resolveOptions(def, config) {
472
472
  if (!def.options) return void 0;
473
473
  return typeof def.options === "function" ? def.options(config) : def.options;
474
474
  }
475
- function getConfigValue(config, path38) {
476
- const parts = path38.split(".");
475
+ function getConfigValue(config, path39) {
476
+ const parts = path39.split(".");
477
477
  let current = config;
478
478
  for (const part of parts) {
479
479
  if (current && typeof current === "object" && part in current) {
@@ -1682,6 +1682,996 @@ var init_agent_installer = __esm({
1682
1682
  }
1683
1683
  });
1684
1684
 
1685
+ // src/core/plugin/plugin-installer.ts
1686
+ var plugin_installer_exports = {};
1687
+ __export(plugin_installer_exports, {
1688
+ PluginInstallCrashSimulation: () => PluginInstallCrashSimulation,
1689
+ PluginInstallError: () => PluginInstallError,
1690
+ PluginInstallJournalController: () => PluginInstallJournalController,
1691
+ acquirePluginInstallLock: () => acquirePluginInstallLock,
1692
+ importFromDir: () => importFromDir,
1693
+ installNpmPlugin: () => installNpmPlugin,
1694
+ parseNpmPackageSpec: () => parseNpmPackageSpec,
1695
+ pluginMutationLockHeld: () => pluginMutationLockHeld,
1696
+ recoverPluginInstallTransaction: () => recoverPluginInstallTransaction,
1697
+ stageNpmPlugin: () => stageNpmPlugin,
1698
+ validateOpenACPPluginModule: () => validateOpenACPPluginModule,
1699
+ withHeldPluginMutationLock: () => withHeldPluginMutationLock,
1700
+ withPluginMutationLock: () => withPluginMutationLock
1701
+ });
1702
+ import { execFile } from "child_process";
1703
+ import { promisify } from "util";
1704
+ import * as fs15 from "fs/promises";
1705
+ import fsSync from "fs";
1706
+ import * as path14 from "path";
1707
+ import { pathToFileURL } from "url";
1708
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
1709
+ import { AsyncLocalStorage } from "async_hooks";
1710
+ async function importFromDir(packageName, dir) {
1711
+ const pkgDir = path14.join(dir, "node_modules", ...packageName.split("/"));
1712
+ const pkgJsonPath = path14.join(pkgDir, "package.json");
1713
+ let pkgJson;
1714
+ try {
1715
+ pkgJson = JSON.parse(await fs15.readFile(pkgJsonPath, "utf-8"));
1716
+ } catch (err) {
1717
+ throw new Error(`Cannot read package.json for "${packageName}" at ${pkgJsonPath}: ${err.message}`);
1718
+ }
1719
+ let entry;
1720
+ const exportsMain = pkgJson.exports?.["."];
1721
+ if (typeof exportsMain === "string") {
1722
+ entry = exportsMain;
1723
+ } else if (exportsMain?.import) {
1724
+ entry = exportsMain.import;
1725
+ } else {
1726
+ entry = pkgJson.main ?? "index.js";
1727
+ }
1728
+ const entryPath = path14.join(pkgDir, entry);
1729
+ try {
1730
+ await fs15.access(entryPath);
1731
+ } catch {
1732
+ throw new Error(`Entry point "${entry}" not found for "${packageName}" at ${entryPath}`);
1733
+ }
1734
+ return import(pathToFileURL(entryPath).href);
1735
+ }
1736
+ function parseNpmPackageSpec(spec) {
1737
+ if (!VALID_NPM_NAME.test(spec)) throw new PluginInstallError("PLUGIN_PACKAGE_SPEC_INVALID", "Plugin package spec is invalid.");
1738
+ if (spec.startsWith("@")) {
1739
+ const slash = spec.indexOf("/");
1740
+ const versionAt2 = spec.indexOf("@", slash + 1);
1741
+ return { packageName: versionAt2 === -1 ? spec : spec.slice(0, versionAt2), spec };
1742
+ }
1743
+ const versionAt = spec.lastIndexOf("@");
1744
+ return { packageName: versionAt > 0 ? spec.slice(0, versionAt) : spec, spec };
1745
+ }
1746
+ function validateOpenACPPluginModule(module, manifest, packageName) {
1747
+ const plugin = module?.default;
1748
+ if (!plugin || typeof plugin !== "object" || plugin.name !== packageName || manifest.name !== packageName || typeof manifest.version !== "string" || plugin.version !== manifest.version || typeof plugin.setup !== "function" || typeof plugin.install !== "function") {
1749
+ throw new PluginInstallError(
1750
+ "PLUGIN_CONTRACT_INVALID",
1751
+ "Package is not an installable OpenACP plugin: default export, matching name/version, setup(), and explicit install() are required."
1752
+ );
1753
+ }
1754
+ return plugin;
1755
+ }
1756
+ function assertDirectoryBoundary(target, label) {
1757
+ const stat = fsSync.lstatSync(target);
1758
+ if (stat.isSymbolicLink() || !stat.isDirectory() || fsSync.realpathSync(target) !== path14.resolve(target)) {
1759
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `${label} must be a real directory without symbolic-link ancestors.`);
1760
+ }
1761
+ }
1762
+ function assertInstanceBoundaries(instanceRoot) {
1763
+ assertDirectoryBoundary(instanceRoot, "Plugin instance root");
1764
+ const pluginsDir = path14.join(instanceRoot, "plugins");
1765
+ if (fsSync.existsSync(pluginsDir)) assertDirectoryBoundary(pluginsDir, "Plugin package root");
1766
+ }
1767
+ function assertContainedPath(instanceRoot, target, label) {
1768
+ const root = path14.resolve(instanceRoot);
1769
+ const resolved = path14.resolve(target);
1770
+ const relative2 = path14.relative(root, resolved);
1771
+ if (relative2.startsWith("..") || path14.isAbsolute(relative2)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `${label} escapes the trusted instance root.`);
1772
+ let current = root;
1773
+ for (const segment of relative2 ? relative2.split(path14.sep) : []) {
1774
+ current = path14.join(current, segment);
1775
+ if (!fsSync.existsSync(current)) break;
1776
+ const stat = fsSync.lstatSync(current);
1777
+ if (stat.isSymbolicLink()) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `${label} contains a symbolic-link boundary.`);
1778
+ const real = fsSync.realpathSync(current);
1779
+ if (real !== root && !real.startsWith(`${root}${path14.sep}`)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `${label} resolves outside the trusted instance root.`);
1780
+ }
1781
+ }
1782
+ function atomicJson(file, value) {
1783
+ const directory = path14.dirname(file);
1784
+ fsSync.mkdirSync(directory, { recursive: true, mode: 448 });
1785
+ const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
1786
+ try {
1787
+ fsSync.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
1788
+ `, { flag: "wx", mode: 384 });
1789
+ fsSync.chmodSync(temporary, 384);
1790
+ const fd = fsSync.openSync(temporary, "r");
1791
+ fsSync.fsyncSync(fd);
1792
+ fsSync.closeSync(fd);
1793
+ fsSync.renameSync(temporary, file);
1794
+ fsSync.chmodSync(file, 384);
1795
+ try {
1796
+ const dir = fsSync.openSync(directory, "r");
1797
+ fsSync.fsyncSync(dir);
1798
+ fsSync.closeSync(dir);
1799
+ } catch {
1800
+ }
1801
+ } finally {
1802
+ try {
1803
+ if (fsSync.existsSync(temporary)) fsSync.unlinkSync(temporary);
1804
+ } catch {
1805
+ }
1806
+ }
1807
+ }
1808
+ function processAlive(pid) {
1809
+ try {
1810
+ process.kill(pid, 0);
1811
+ return true;
1812
+ } catch (error) {
1813
+ return error.code !== "ESRCH";
1814
+ }
1815
+ }
1816
+ async function acquirePluginInstallLock(instanceRoot, options = {}) {
1817
+ const lockPath = path14.join(instanceRoot, LOCK_FILE);
1818
+ const reclaimPrefix = `${LOCK_FILE}.reclaim-`;
1819
+ const timeoutMs = options.timeoutMs ?? 5e3;
1820
+ const staleMs = options.staleMs ?? 3e4;
1821
+ const transactionId = options.transactionId ?? randomUUID2();
1822
+ fsSync.mkdirSync(instanceRoot, { recursive: true, mode: 448 });
1823
+ const createOwnedLock = () => {
1824
+ const fd = fsSync.openSync(lockPath, "wx", 384);
1825
+ try {
1826
+ fsSync.writeFileSync(fd, JSON.stringify({ version: 1, transactionId, pid: process.pid, createdAt: Date.now() }));
1827
+ fsSync.fsyncSync(fd);
1828
+ } finally {
1829
+ fsSync.closeSync(fd);
1830
+ }
1831
+ fsSync.chmodSync(lockPath, 384);
1832
+ let released = false;
1833
+ return { transactionId, release: () => {
1834
+ if (released) return;
1835
+ released = true;
1836
+ try {
1837
+ const owner = JSON.parse(fsSync.readFileSync(lockPath, "utf8"));
1838
+ if (owner.transactionId === transactionId) fsSync.unlinkSync(lockPath);
1839
+ } catch {
1840
+ }
1841
+ } };
1842
+ };
1843
+ const started = Date.now();
1844
+ while (Date.now() - started <= timeoutMs) {
1845
+ try {
1846
+ const reclaims = fsSync.readdirSync(instanceRoot).filter((name) => name.startsWith(reclaimPrefix));
1847
+ for (const name of reclaims) {
1848
+ const ownerId = name.slice(reclaimPrefix.length);
1849
+ const reclaimPath = path14.join(instanceRoot, name);
1850
+ const stat = fsSync.lstatSync(reclaimPath);
1851
+ if (!TRANSACTION_ID.test(ownerId) || stat.isSymbolicLink() || !stat.isFile() || stat.size > 4096) {
1852
+ throw new PluginInstallError("PLUGIN_INSTALL_BUSY", "Malformed stale-lock reclaim evidence requires operator inspection.");
1853
+ }
1854
+ const owner = JSON.parse(fsSync.readFileSync(reclaimPath, "utf8"));
1855
+ const reclaimable = owner.transactionId === ownerId && Number.isInteger(owner.pid) && Number.isFinite(owner.createdAt) && Date.now() - Number(owner.createdAt) > staleMs && !processAlive(Number(owner.pid));
1856
+ if (!reclaimable) throw new PluginInstallError("PLUGIN_INSTALL_BUSY", "A plugin lock reclaim is active or cannot be proven stale.");
1857
+ if (!fsSync.existsSync(lockPath)) {
1858
+ const lock = createOwnedLock();
1859
+ fsSync.unlinkSync(reclaimPath);
1860
+ return lock;
1861
+ }
1862
+ fsSync.unlinkSync(reclaimPath);
1863
+ }
1864
+ return createOwnedLock();
1865
+ } catch (error) {
1866
+ if (error instanceof PluginInstallError) throw error;
1867
+ if (error.code !== "EEXIST") throw error;
1868
+ try {
1869
+ const owner = JSON.parse(fsSync.readFileSync(lockPath, "utf8"));
1870
+ const oldEnough = TRANSACTION_ID.test(owner.transactionId ?? "") && Number.isFinite(owner.createdAt) && Date.now() - Number(owner.createdAt) > staleMs;
1871
+ const definitelyDead = Number.isInteger(owner.pid) && !processAlive(Number(owner.pid));
1872
+ if (oldEnough && definitelyDead) {
1873
+ const reclaimPath = path14.join(instanceRoot, `${reclaimPrefix}${owner.transactionId}`);
1874
+ try {
1875
+ if (fsSync.existsSync(reclaimPath)) continue;
1876
+ fsSync.renameSync(lockPath, reclaimPath);
1877
+ const renamed = JSON.parse(fsSync.readFileSync(reclaimPath, "utf8"));
1878
+ if (renamed.transactionId !== owner.transactionId || renamed.pid !== owner.pid || renamed.createdAt !== owner.createdAt) {
1879
+ if (!fsSync.existsSync(lockPath)) fsSync.renameSync(reclaimPath, lockPath);
1880
+ continue;
1881
+ }
1882
+ } catch (reclaimError) {
1883
+ if (reclaimError.code !== "ENOENT") throw reclaimError;
1884
+ }
1885
+ continue;
1886
+ }
1887
+ } catch {
1888
+ }
1889
+ await new Promise((resolve8) => setTimeout(resolve8, 20));
1890
+ }
1891
+ }
1892
+ throw new PluginInstallError("PLUGIN_INSTALL_BUSY", "Another plugin package transaction owns the install lock; retry after it finishes.");
1893
+ }
1894
+ function treeRecords(root, excludeMarker = true) {
1895
+ if (!fsSync.existsSync(root)) return [];
1896
+ const rootStat = fsSync.lstatSync(root);
1897
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin data snapshot root must be a real directory.");
1898
+ const records = [];
1899
+ const walk = (directory, relative2 = "") => {
1900
+ for (const entry of fsSync.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
1901
+ if (excludeMarker && !relative2 && entry.name === ".snapshot-complete.json") continue;
1902
+ const childRelative = path14.join(relative2, entry.name);
1903
+ const child = path14.join(directory, entry.name);
1904
+ const stat = fsSync.lstatSync(child);
1905
+ if (entry.isSymbolicLink()) throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin data snapshot refuses symbolic links.");
1906
+ if (entry.isDirectory()) {
1907
+ records.push({ path: childRelative, mode: stat.mode & 511, type: "directory" });
1908
+ walk(child, childRelative);
1909
+ } else if (entry.isFile()) records.push({ path: childRelative, mode: stat.mode & 511, type: "file", sha256: createHash2("sha256").update(fsSync.readFileSync(child)).digest("hex") });
1910
+ else throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin data snapshot contains an unsupported file type.");
1911
+ }
1912
+ };
1913
+ walk(root);
1914
+ return records;
1915
+ }
1916
+ function treeDigest(root) {
1917
+ return createHash2("sha256").update(JSON.stringify({
1918
+ records: treeRecords(root).map(({ path: recordPath, type, sha256 }) => ({ path: recordPath, type, sha256 }))
1919
+ })).digest("hex");
1920
+ }
1921
+ function treeModeDigest(root, excludeMarker = false) {
1922
+ const records = treeRecords(root, excludeMarker);
1923
+ return createHash2("sha256").update(JSON.stringify({ rootMode: fsSync.lstatSync(root).mode & 511, records })).digest("hex");
1924
+ }
1925
+ function boundaryDigest(target) {
1926
+ const records = [];
1927
+ const walk = (current, relative2) => {
1928
+ const stat = fsSync.lstatSync(current);
1929
+ if (stat.isSymbolicLink()) records.push({ path: relative2, type: "symlink", mode: stat.mode & 511, link: fsSync.readlinkSync(current) });
1930
+ else if (stat.isFile()) records.push({ path: relative2, type: "file", mode: stat.mode & 511, sha256: createHash2("sha256").update(fsSync.readFileSync(current)).digest("hex") });
1931
+ else if (stat.isDirectory()) {
1932
+ records.push({ path: relative2, type: "directory", mode: stat.mode & 511 });
1933
+ for (const name of fsSync.readdirSync(current).sort()) walk(path14.join(current, name), relative2 ? path14.join(relative2, name) : name);
1934
+ } else throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin package evidence contains an unsupported boundary type.");
1935
+ };
1936
+ walk(target, "");
1937
+ return createHash2("sha256").update(JSON.stringify(records)).digest("hex");
1938
+ }
1939
+ function fsyncBoundary(target) {
1940
+ const stat = fsSync.lstatSync(target);
1941
+ if (stat.isFile()) {
1942
+ const fd = fsSync.openSync(target, "r");
1943
+ fsSync.fsyncSync(fd);
1944
+ fsSync.closeSync(fd);
1945
+ return;
1946
+ }
1947
+ if (stat.isSymbolicLink()) return;
1948
+ for (const name of fsSync.readdirSync(target)) fsyncBoundary(path14.join(target, name));
1949
+ try {
1950
+ const fd = fsSync.openSync(target, "r");
1951
+ fsSync.fsyncSync(fd);
1952
+ fsSync.closeSync(fd);
1953
+ } catch {
1954
+ }
1955
+ }
1956
+ function fsyncDirectory(directory) {
1957
+ try {
1958
+ const fd = fsSync.openSync(directory, "r");
1959
+ fsSync.fsyncSync(fd);
1960
+ fsSync.closeSync(fd);
1961
+ } catch {
1962
+ }
1963
+ }
1964
+ function secureSnapshotTree(root) {
1965
+ fsSync.chmodSync(root, 448);
1966
+ for (const record of treeRecords(root)) fsSync.chmodSync(path14.join(root, record.path), record.type === "directory" ? 448 : 384);
1967
+ }
1968
+ function fsyncTree(root) {
1969
+ for (const record of treeRecords(root)) {
1970
+ const target = path14.join(root, record.path);
1971
+ if (record.type === "file") {
1972
+ const fd = fsSync.openSync(target, "r");
1973
+ fsSync.fsyncSync(fd);
1974
+ fsSync.closeSync(fd);
1975
+ }
1976
+ }
1977
+ const directories = [root, ...treeRecords(root).filter((item) => item.type === "directory").map((item) => path14.join(root, item.path))].reverse();
1978
+ for (const directory of directories) {
1979
+ try {
1980
+ const fd = fsSync.openSync(directory, "r");
1981
+ fsSync.fsyncSync(fd);
1982
+ fsSync.closeSync(fd);
1983
+ } catch {
1984
+ }
1985
+ }
1986
+ }
1987
+ function copySnapshotContents(snapshot, destination) {
1988
+ fsSync.mkdirSync(destination, { recursive: true });
1989
+ for (const entry of fsSync.readdirSync(snapshot, { withFileTypes: true })) {
1990
+ if (entry.name === ".snapshot-complete.json") continue;
1991
+ fsSync.cpSync(path14.join(snapshot, entry.name), path14.join(destination, entry.name), { recursive: true, preserveTimestamps: true });
1992
+ }
1993
+ fsSync.chmodSync(destination, fsSync.lstatSync(snapshot).mode & 511);
1994
+ for (const record of treeRecords(snapshot)) {
1995
+ fsSync.chmodSync(path14.join(destination, record.path), record.mode);
1996
+ }
1997
+ }
1998
+ function describeTreeMismatch(expectedRoot, actualRoot) {
1999
+ try {
2000
+ const expectedRootMode = fsSync.lstatSync(expectedRoot).mode & 511;
2001
+ const actualRootMode = fsSync.lstatSync(actualRoot).mode & 511;
2002
+ if (expectedRootMode !== actualRootMode) return `root mode expected ${expectedRootMode.toString(8)}, actual ${actualRootMode.toString(8)}`;
2003
+ const expected = treeRecords(expectedRoot);
2004
+ const actual = new Map(treeRecords(actualRoot).map((record) => [record.path, record]));
2005
+ for (const record of expected) {
2006
+ const copy = actual.get(record.path);
2007
+ if (!copy) return `missing relative record ${JSON.stringify(record.path)}`;
2008
+ if (copy.type !== record.type) return `relative record ${JSON.stringify(record.path)} type mismatch`;
2009
+ if (copy.mode !== record.mode) return `relative record ${JSON.stringify(record.path)} mode expected ${record.mode.toString(8)}, actual ${copy.mode.toString(8)}`;
2010
+ if (copy.sha256 !== record.sha256) return `relative record ${JSON.stringify(record.path)} content digest mismatch`;
2011
+ actual.delete(record.path);
2012
+ }
2013
+ const extra = actual.keys().next().value;
2014
+ if (extra !== void 0) return `unexpected relative record ${JSON.stringify(extra)}`;
2015
+ } catch {
2016
+ return "tree comparison unavailable";
2017
+ }
2018
+ return "aggregate digest mismatch without a record-level difference";
2019
+ }
2020
+ function validateJournal(instanceRoot, journal) {
2021
+ assertInstanceBoundaries(instanceRoot);
2022
+ if (!journal || typeof journal !== "object" || !journal.data || typeof journal.data !== "object" || !journal.registry || typeof journal.registry !== "object" || !Array.isArray(journal.items) || typeof journal.pluginName !== "string" || typeof journal.transactionId !== "string" || typeof journal.phase !== "string") {
2023
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal structure is invalid.");
2024
+ }
2025
+ const exactKeys = (value, allowed) => Object.keys(value).every((key) => allowed.includes(key)) && allowed.filter((key) => !["contentBase64", "commitEvidence", "existed", "snapshotDir", "digest", "modeDigest", "hookStarted", "originalDigest"].includes(key)).every((key) => key in value);
2026
+ if (!exactKeys(journal, ["version", "transactionId", "pluginName", "phase", "stageDir", "rollbackDir", "items", "data", "registry"]) || !exactKeys(journal.data, ["directory", "existed", "snapshotDir", "digest", "modeDigest", "hookStarted"]) || !exactKeys(journal.registry, ["path", "existed", "contentBase64", "mode", "commitEvidence"]) || journal.items.some((item) => !item || typeof item !== "object" || !exactKeys(item, ["name", "hadLive", "state", "originalDigest"]))) {
2027
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal contains unknown or missing fields.");
2028
+ }
2029
+ const pluginsDir = path14.join(instanceRoot, "plugins");
2030
+ const validPhases = /* @__PURE__ */ new Set([
2031
+ "initialized",
2032
+ "staged",
2033
+ "snapshot-pending",
2034
+ "hook-pending",
2035
+ "hook-running",
2036
+ "hook-complete",
2037
+ "packages-activated",
2038
+ "registry-committing",
2039
+ "registry-committed",
2040
+ "committed",
2041
+ ...PACKAGE_ITEMS.flatMap((name) => [`backup:${name}`, `activate:${name}`])
2042
+ ]);
2043
+ let parsedName;
2044
+ try {
2045
+ parsedName = parseNpmPackageSpec(journal.pluginName);
2046
+ } catch {
2047
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal package name is invalid.");
2048
+ }
2049
+ const expectedData = path14.join(pluginsDir, "data", ...journal.pluginName.split("/"));
2050
+ const itemNames = Array.isArray(journal.items) ? journal.items.map((item) => item?.name) : [];
2051
+ const exactItems = itemNames.length === PACKAGE_ITEMS.length && new Set(itemNames).size === PACKAGE_ITEMS.length && PACKAGE_ITEMS.every((name) => itemNames.includes(name));
2052
+ if (journal.version !== 3 || !TRANSACTION_ID.test(journal.transactionId) || parsedName.packageName !== journal.pluginName || parsedName.spec !== journal.pluginName || !validPhases.has(journal.phase) || journal.registry.path !== path14.join(instanceRoot, "plugins.json") || journal.stageDir !== path14.join(pluginsDir, `.stage-${journal.transactionId}`) || journal.rollbackDir !== path14.join(pluginsDir, `.rollback-${journal.transactionId}`) || journal.data.directory !== expectedData || !exactItems) {
2053
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal ownership validation failed.");
2054
+ }
2055
+ if (journal.items.some((item) => typeof item.hadLive !== "boolean" || !["untouched", "live-backed-up-complete", "new-activated"].includes(item.state))) {
2056
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal item state is invalid.");
2057
+ }
2058
+ if (journal.items.some((item) => item.hadLive ? !/^[0-9a-f]{64}$/.test(item.originalDigest ?? "") : item.originalDigest !== void 0)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction original package evidence is invalid.");
2059
+ if (typeof journal.registry.existed !== "boolean" || !Number.isInteger(journal.registry.mode) || journal.registry.mode < 0 || journal.registry.mode > 511 || journal.registry.existed && typeof journal.registry.contentBase64 !== "string" || !journal.registry.existed && journal.registry.contentBase64 !== void 0 || journal.registry.contentBase64 && Buffer.byteLength(journal.registry.contentBase64, "base64") > 4 * 1024 * 1024) {
2060
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal registry snapshot is invalid.");
2061
+ }
2062
+ if (journal.registry.contentBase64 !== void 0 && (!/^[A-Za-z0-9+/]*={0,2}$/.test(journal.registry.contentBase64) || Buffer.from(journal.registry.contentBase64, "base64").toString("base64") !== journal.registry.contentBase64)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin registry snapshot encoding is invalid.");
2063
+ const committedPhase = journal.phase === "registry-committed" || journal.phase === "committed";
2064
+ const evidence = journal.registry.commitEvidence;
2065
+ if (committedPhase !== Boolean(evidence)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin registry commit evidence is missing or present before its committed phase.");
2066
+ if (evidence) {
2067
+ if (!exactKeys(evidence, ["contentBase64", "digest", "mode"]) || !/^[A-Za-z0-9+/]*={0,2}$/.test(evidence.contentBase64) || Buffer.from(evidence.contentBase64, "base64").toString("base64") !== evidence.contentBase64 || Buffer.byteLength(evidence.contentBase64, "base64") > 4 * 1024 * 1024 || !/^[0-9a-f]{64}$/.test(evidence.digest) || createHash2("sha256").update(Buffer.from(evidence.contentBase64, "base64")).digest("hex") !== evidence.digest || !Number.isInteger(evidence.mode) || evidence.mode < 0 || evidence.mode > 511) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin registry commit evidence is invalid.");
2068
+ if (!fsSync.existsSync(journal.registry.path) || !fsSync.readFileSync(journal.registry.path).equals(Buffer.from(evidence.contentBase64, "base64")) || (fsSync.statSync(journal.registry.path).mode & 511) !== evidence.mode) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Persisted plugin registry does not match committed evidence; cleanup is quarantined.");
2069
+ }
2070
+ const expectedSnapshot = path14.join(pluginsDir, `.data-snapshot-${journal.transactionId}.complete`);
2071
+ if (journal.data.snapshotDir && journal.data.snapshotDir !== expectedSnapshot) {
2072
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data snapshot provenance validation failed.");
2073
+ }
2074
+ if (journal.data.existed !== void 0 && typeof journal.data.existed !== "boolean") throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data snapshot state is invalid.");
2075
+ if (journal.data.existed === true && (journal.data.snapshotDir !== expectedSnapshot || !/^[0-9a-f]{64}$/.test(journal.data.digest ?? "") || !/^[0-9a-f]{64}$/.test(journal.data.modeDigest ?? ""))) {
2076
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data snapshot completion state is invalid.");
2077
+ }
2078
+ if (journal.data.existed === false && (journal.data.snapshotDir !== void 0 || journal.data.digest !== void 0 || journal.data.modeDigest !== void 0)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data absence state is invalid.");
2079
+ for (const [target, label] of [
2080
+ [pluginsDir, "Plugin package root"],
2081
+ [path14.join(pluginsDir, "data"), "Plugin data root"],
2082
+ [expectedData, "Plugin-owned data path"],
2083
+ [journal.stageDir, "Plugin stage path"],
2084
+ [journal.rollbackDir, "Plugin rollback path"],
2085
+ [path14.join(pluginsDir, `.data-snapshot-${journal.transactionId}.pending`), "Plugin pending snapshot"],
2086
+ [path14.join(pluginsDir, `.data-snapshot-${journal.transactionId}.complete`), "Plugin complete snapshot"],
2087
+ [journal.registry.path, "Plugin registry path"]
2088
+ ]) assertContainedPath(instanceRoot, target, label);
2089
+ for (const name of PACKAGE_ITEMS) {
2090
+ assertContainedPath(instanceRoot, path14.join(pluginsDir, name), `Live package boundary ${name}`);
2091
+ assertContainedPath(instanceRoot, path14.join(journal.stageDir, name), `Staged package boundary ${name}`);
2092
+ assertContainedPath(instanceRoot, path14.join(journal.rollbackDir, name), `Backup package boundary ${name}`);
2093
+ }
2094
+ validateJournalStateMachine(journal);
2095
+ }
2096
+ function validateJournalStateMachine(journal) {
2097
+ const byName = new Map(journal.items.map((item) => [item.name, item]));
2098
+ const states = PACKAGE_ITEMS.map((name) => byName.get(name));
2099
+ const allUntouched = states.every((item) => item.state === "untouched");
2100
+ const allActivated = states.every((item) => item.state === "new-activated");
2101
+ const prePackage = /* @__PURE__ */ new Set(["initialized", "staged", "snapshot-pending", "hook-pending", "hook-running", "hook-complete"]);
2102
+ let packageStateValid = true;
2103
+ if (prePackage.has(journal.phase)) packageStateValid = allUntouched;
2104
+ else if (journal.phase.startsWith("backup:") || journal.phase.startsWith("activate:")) {
2105
+ const [kind, name] = journal.phase.split(":");
2106
+ const index = PACKAGE_ITEMS.indexOf(name);
2107
+ packageStateValid = index >= 0 && states.slice(0, index).every((item) => item.state === "new-activated") && states.slice(index + 1).every((item) => item.state === "untouched");
2108
+ const current = states[index];
2109
+ if (packageStateValid && kind === "backup") {
2110
+ packageStateValid = current.state === "untouched" || current.hadLive && current.state === "live-backed-up-complete";
2111
+ } else if (packageStateValid) {
2112
+ packageStateValid = current.state === "new-activated" || (current.hadLive ? current.state === "live-backed-up-complete" : current.state === "untouched");
2113
+ }
2114
+ } else if (["packages-activated", "registry-committing", "registry-committed", "committed"].includes(journal.phase)) packageStateValid = allActivated;
2115
+ if (!packageStateValid) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction phase and package item states are inconsistent.");
2116
+ const noSnapshotYet = journal.data.existed === void 0 && journal.data.snapshotDir === void 0 && journal.data.digest === void 0 && journal.data.modeDigest === void 0;
2117
+ const snapshotComplete = typeof journal.data.existed === "boolean" && (journal.data.existed === false || typeof journal.data.snapshotDir === "string" && typeof journal.data.digest === "string" && typeof journal.data.modeDigest === "string");
2118
+ const beforeSnapshot = ["initialized", "staged", "snapshot-pending"].includes(journal.phase);
2119
+ const beforeHook = journal.phase === "hook-pending";
2120
+ const afterHookStart = !beforeSnapshot && !beforeHook;
2121
+ const dataStateValid = beforeSnapshot && noSnapshotYet && journal.data.hookStarted === void 0 || beforeHook && snapshotComplete && journal.data.hookStarted !== true || afterHookStart && snapshotComplete && journal.data.hookStarted === true;
2122
+ if (!dataStateValid) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction phase and data snapshot state are inconsistent.");
2123
+ }
2124
+ function validateCompleteSnapshot(journal) {
2125
+ if (journal.data.existed !== true) return void 0;
2126
+ const snapshot = journal.data.snapshotDir;
2127
+ if (!snapshot || !journal.data.digest || !journal.data.modeDigest || !fsSync.existsSync(snapshot)) return void 0;
2128
+ let marker;
2129
+ try {
2130
+ marker = JSON.parse(fsSync.readFileSync(path14.join(snapshot, ".snapshot-complete.json"), "utf8"));
2131
+ } catch {
2132
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data snapshot completion marker is unavailable; original data was not deleted.");
2133
+ }
2134
+ if (marker.transactionId !== journal.transactionId || marker.digest !== journal.data.digest || marker.modeDigest !== journal.data.modeDigest || treeDigest(snapshot) !== journal.data.digest || treeModeDigest(snapshot, true) !== journal.data.modeDigest) {
2135
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin data snapshot integrity validation failed; original data was not deleted.");
2136
+ }
2137
+ return snapshot;
2138
+ }
2139
+ function restoreJournal(instanceRoot, journal, boundary) {
2140
+ validateJournal(instanceRoot, journal);
2141
+ const completeSnapshot = validateCompleteSnapshot(journal);
2142
+ const pluginsDir = path14.join(instanceRoot, "plugins");
2143
+ for (const item of journal.items) {
2144
+ const live = path14.join(pluginsDir, item.name);
2145
+ const backup = path14.join(journal.rollbackDir, item.name);
2146
+ const staged = path14.join(journal.stageDir, item.name);
2147
+ const activated = item.state === "new-activated" || (journal.phase === `activate:${item.name}` || journal.phase === "packages-activated" || journal.phase === "registry-committing") && fsSync.existsSync(live) && !fsSync.existsSync(staged) && (fsSync.existsSync(backup) || !item.hadLive);
2148
+ if (item.hadLive) {
2149
+ const expected = item.originalDigest;
2150
+ if (!fsSync.existsSync(live) || boundaryDigest(live) !== expected) {
2151
+ if (!fsSync.existsSync(backup) || boundaryDigest(backup) !== expected) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `Original package evidence is unavailable for ${item.name}; live state was not removed.`);
2152
+ const restoreTemp = path14.join(pluginsDir, `.restore-${journal.transactionId}-${item.name}`);
2153
+ fsSync.rmSync(restoreTemp, { recursive: true, force: true });
2154
+ fsSync.cpSync(backup, restoreTemp, { recursive: true, preserveTimestamps: true, dereference: false });
2155
+ if (boundaryDigest(restoreTemp) !== expected) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `Prepared package restore did not verify for ${item.name}.`);
2156
+ fsyncBoundary(restoreTemp);
2157
+ boundary?.(`package:${item.name}:prepared`);
2158
+ fsSync.rmSync(live, { recursive: true, force: true });
2159
+ boundary?.(`package:${item.name}:live-removed`);
2160
+ fsSync.renameSync(restoreTemp, live);
2161
+ try {
2162
+ const fd = fsSync.openSync(pluginsDir, "r");
2163
+ fsSync.fsyncSync(fd);
2164
+ fsSync.closeSync(fd);
2165
+ } catch {
2166
+ }
2167
+ boundary?.(`package:${item.name}:restored`);
2168
+ }
2169
+ if (!fsSync.existsSync(live) || boundaryDigest(live) !== expected) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `Restored package verification failed for ${item.name}.`);
2170
+ boundary?.(`package:${item.name}:verified`);
2171
+ } else if (activated && fsSync.existsSync(live)) {
2172
+ fsSync.rmSync(live, { recursive: true, force: true });
2173
+ fsyncDirectory(pluginsDir);
2174
+ boundary?.(`package:${item.name}:absent`);
2175
+ }
2176
+ }
2177
+ if (journal.data.existed === true) {
2178
+ const dataVerified = fsSync.existsSync(journal.data.directory) && treeDigest(journal.data.directory) === journal.data.digest && treeModeDigest(journal.data.directory) === journal.data.modeDigest;
2179
+ if (!dataVerified) {
2180
+ if (!completeSnapshot) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Complete plugin data evidence is unavailable; live data was not removed.");
2181
+ const restoreTemp = path14.join(pluginsDir, `.data-restore-${journal.transactionId}.pending`);
2182
+ fsSync.rmSync(restoreTemp, { recursive: true, force: true });
2183
+ copySnapshotContents(completeSnapshot, restoreTemp);
2184
+ if (treeDigest(restoreTemp) !== journal.data.digest || treeModeDigest(restoreTemp) !== journal.data.modeDigest) {
2185
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `Prepared plugin data rollback verification failed: ${describeTreeMismatch(completeSnapshot, restoreTemp)}.`);
2186
+ }
2187
+ fsyncTree(restoreTemp);
2188
+ boundary?.("data:prepared");
2189
+ fsSync.rmSync(journal.data.directory, { recursive: true, force: true });
2190
+ boundary?.("data:live-removed");
2191
+ fsSync.renameSync(restoreTemp, journal.data.directory);
2192
+ try {
2193
+ const fd = fsSync.openSync(path14.dirname(journal.data.directory), "r");
2194
+ fsSync.fsyncSync(fd);
2195
+ fsSync.closeSync(fd);
2196
+ } catch {
2197
+ }
2198
+ boundary?.("data:restored");
2199
+ }
2200
+ if (!fsSync.existsSync(journal.data.directory) || treeDigest(journal.data.directory) !== journal.data.digest || treeModeDigest(journal.data.directory) !== journal.data.modeDigest) {
2201
+ const detail = completeSnapshot ? describeTreeMismatch(completeSnapshot, journal.data.directory) : "durable snapshot unavailable";
2202
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", `Plugin data rollback verification failed: ${detail}.`);
2203
+ }
2204
+ boundary?.("data:verified");
2205
+ } else if (journal.data.existed === false && journal.data.hookStarted === true) {
2206
+ fsSync.rmSync(journal.data.directory, { recursive: true, force: true });
2207
+ fsyncDirectory(path14.dirname(journal.data.directory));
2208
+ boundary?.("data:absent");
2209
+ }
2210
+ if (journal.registry.existed && journal.registry.contentBase64 !== void 0) {
2211
+ const expected = Buffer.from(journal.registry.contentBase64, "base64");
2212
+ const alreadyVerified = fsSync.existsSync(journal.registry.path) && fsSync.readFileSync(journal.registry.path).equals(expected) && (fsSync.statSync(journal.registry.path).mode & 511) === journal.registry.mode;
2213
+ if (!alreadyVerified) {
2214
+ const tmp = `${journal.registry.path}.${journal.transactionId}.recovery`;
2215
+ fsSync.rmSync(tmp, { force: true });
2216
+ try {
2217
+ fsSync.writeFileSync(tmp, expected, { flag: "wx", mode: journal.registry.mode });
2218
+ const fd = fsSync.openSync(tmp, "r");
2219
+ fsSync.fsyncSync(fd);
2220
+ fsSync.closeSync(fd);
2221
+ fsSync.renameSync(tmp, journal.registry.path);
2222
+ fsSync.chmodSync(journal.registry.path, journal.registry.mode);
2223
+ try {
2224
+ const dir = fsSync.openSync(path14.dirname(journal.registry.path), "r");
2225
+ fsSync.fsyncSync(dir);
2226
+ fsSync.closeSync(dir);
2227
+ } catch {
2228
+ }
2229
+ } finally {
2230
+ fsSync.rmSync(tmp, { force: true });
2231
+ }
2232
+ boundary?.("registry:restored");
2233
+ }
2234
+ if (!fsSync.readFileSync(journal.registry.path).equals(expected) || (fsSync.statSync(journal.registry.path).mode & 511) !== journal.registry.mode) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin registry rollback verification failed.");
2235
+ } else if (journal.phase === "registry-committing") {
2236
+ fsSync.rmSync(journal.registry.path, { force: true });
2237
+ fsyncDirectory(path14.dirname(journal.registry.path));
2238
+ } else if (fsSync.existsSync(journal.registry.path)) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Unexpected registry appeared before its mutation boundary; it was not removed.");
2239
+ boundary?.("all:verified");
2240
+ }
2241
+ function cleanJournalArtifacts(instanceRoot, journal, removePath = (target) => fsSync.rmSync(target, { recursive: true, force: true })) {
2242
+ validateJournal(instanceRoot, journal);
2243
+ const pluginsDir = path14.join(instanceRoot, "plugins");
2244
+ const pendingSnapshot = path14.join(pluginsDir, `.data-snapshot-${journal.transactionId}.pending`);
2245
+ const completeSnapshot = path14.join(pluginsDir, `.data-snapshot-${journal.transactionId}.complete`);
2246
+ const restorePaths = PACKAGE_ITEMS.map((name) => path14.join(pluginsDir, `.restore-${journal.transactionId}-${name}`));
2247
+ const dataRestore = path14.join(pluginsDir, `.data-restore-${journal.transactionId}.pending`);
2248
+ for (const owned of [journal.stageDir, journal.rollbackDir, journal.data.snapshotDir, pendingSnapshot, completeSnapshot, ...restorePaths, dataRestore]) if (owned) removePath(owned);
2249
+ removePath(`${journal.registry.path}.${journal.transactionId}.recovery`);
2250
+ removePath(path14.join(instanceRoot, JOURNAL_FILE));
2251
+ fsyncDirectory(pluginsDir);
2252
+ fsyncDirectory(instanceRoot);
2253
+ }
2254
+ function recoverLocked(instanceRoot) {
2255
+ assertInstanceBoundaries(instanceRoot);
2256
+ const journalPath = path14.join(instanceRoot, JOURNAL_FILE);
2257
+ if (!fsSync.existsSync(journalPath)) return;
2258
+ let journal;
2259
+ try {
2260
+ if (fsSync.lstatSync(journalPath).isSymbolicLink() || fsSync.statSync(journalPath).size > MAX_JOURNAL_BYTES) throw new Error("invalid journal file");
2261
+ journal = JSON.parse(fsSync.readFileSync(journalPath, "utf8"));
2262
+ } catch {
2263
+ throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal is unreadable; community plugins remain quarantined.");
2264
+ }
2265
+ validateJournal(instanceRoot, journal);
2266
+ if (journal.phase !== "registry-committed" && journal.phase !== "committed") restoreJournal(instanceRoot, journal);
2267
+ cleanJournalArtifacts(instanceRoot, journal);
2268
+ }
2269
+ async function recoverPluginInstallTransaction(instanceRoot, options = {}) {
2270
+ const lock = await acquirePluginInstallLock(instanceRoot, options);
2271
+ try {
2272
+ recoverLocked(instanceRoot);
2273
+ } finally {
2274
+ lock.release();
2275
+ }
2276
+ }
2277
+ function pluginMutationLockHeld(instanceRoot) {
2278
+ return mutationContext.getStore()?.instanceRoot === instanceRoot;
2279
+ }
2280
+ async function withHeldPluginMutationLock(instanceRoot, lock, callback) {
2281
+ return mutationContext.run({ instanceRoot, transactionId: lock.transactionId }, callback);
2282
+ }
2283
+ async function withPluginMutationLock(instanceRoot, callback) {
2284
+ if (pluginMutationLockHeld(instanceRoot)) return callback();
2285
+ const lock = await acquirePluginInstallLock(instanceRoot);
2286
+ try {
2287
+ recoverLocked(instanceRoot);
2288
+ return await withHeldPluginMutationLock(instanceRoot, lock, callback);
2289
+ } finally {
2290
+ lock.release();
2291
+ }
2292
+ }
2293
+ async function pathExists(file) {
2294
+ try {
2295
+ await fs15.access(file);
2296
+ return true;
2297
+ } catch {
2298
+ return false;
2299
+ }
2300
+ }
2301
+ async function stageNpmPlugin(packageSpec, pluginsDir, childEnv, options = {}) {
2302
+ const { packageName } = parseNpmPackageSpec(packageSpec);
2303
+ await fs15.mkdir(pluginsDir, { recursive: true });
2304
+ const liveManifest = path14.join(pluginsDir, "package.json");
2305
+ const liveModules = path14.join(pluginsDir, "node_modules");
2306
+ if (await pathExists(liveModules) && !await pathExists(liveManifest)) {
2307
+ throw new PluginInstallError("PLUGIN_INSTALL_STATE_INVALID", "Plugin directory has node_modules without package.json; no changes were made.");
2308
+ }
2309
+ const transactionId = options.transactionId ?? randomUUID2();
2310
+ const stageDir = path14.join(pluginsDir, `.stage-${transactionId}`);
2311
+ const rollbackDir = path14.join(pluginsDir, `.rollback-${transactionId}`);
2312
+ await fs15.mkdir(stageDir, { recursive: true });
2313
+ try {
2314
+ for (const name of ["package.json", "package-lock.json"]) {
2315
+ const source = path14.join(pluginsDir, name);
2316
+ if (await pathExists(source)) await fs15.copyFile(source, path14.join(stageDir, name));
2317
+ }
2318
+ if (!await pathExists(path14.join(stageDir, "package.json"))) {
2319
+ await fs15.writeFile(path14.join(stageDir, "package.json"), '{"private":true}\n', { mode: 384 });
2320
+ }
2321
+ try {
2322
+ if (options.runNpm) await options.runNpm(stageDir, packageSpec);
2323
+ else await execFileAsync("npm", ["install", packageSpec, "--prefix", stageDir, "--save", "--ignore-scripts"], {
2324
+ timeout: 6e4,
2325
+ env: childEnv
2326
+ });
2327
+ } catch {
2328
+ throw new PluginInstallError("PLUGIN_STAGE_FAILED", "npm could not stage the plugin package; live plugins were not changed.");
2329
+ }
2330
+ const packagePath = path14.join(stageDir, "node_modules", ...packageName.split("/"), "package.json");
2331
+ let manifest;
2332
+ try {
2333
+ manifest = JSON.parse(await fs15.readFile(packagePath, "utf8"));
2334
+ } catch {
2335
+ throw new PluginInstallError("PLUGIN_CONTRACT_INVALID", "Staged plugin package metadata is unavailable or invalid.");
2336
+ }
2337
+ let module;
2338
+ try {
2339
+ module = await importFromDir(packageName, stageDir);
2340
+ } catch {
2341
+ throw new PluginInstallError("PLUGIN_CONTRACT_INVALID", "Staged plugin entry point could not be loaded.");
2342
+ }
2343
+ const plugin = validateOpenACPPluginModule(module, manifest, packageName);
2344
+ let activated = false;
2345
+ let finished = false;
2346
+ const rollback = async () => {
2347
+ if (finished) return;
2348
+ for (const name of ["node_modules", "package.json", "package-lock.json"]) {
2349
+ const live = path14.join(pluginsDir, name);
2350
+ const backup = path14.join(rollbackDir, name);
2351
+ if (await pathExists(live)) await fs15.rm(live, { recursive: true, force: true });
2352
+ if (await pathExists(backup)) await fs15.rename(backup, live);
2353
+ }
2354
+ finished = true;
2355
+ await fs15.rm(rollbackDir, { recursive: true, force: true });
2356
+ await fs15.rm(stageDir, { recursive: true, force: true });
2357
+ };
2358
+ return {
2359
+ packageName,
2360
+ manifest,
2361
+ plugin,
2362
+ stageDir,
2363
+ rollbackDir,
2364
+ activationItems: async () => Promise.all(PACKAGE_ITEMS.map(async (name) => ({ name, hadLive: await pathExists(path14.join(pluginsDir, name)), state: "untouched" }))),
2365
+ discard: async () => {
2366
+ if (activated) await rollback();
2367
+ else await fs15.rm(stageDir, { recursive: true, force: true });
2368
+ },
2369
+ activate: async (hooks) => {
2370
+ if (activated) throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Staged plugin was already activated.");
2371
+ await fs15.mkdir(rollbackDir, { recursive: true });
2372
+ try {
2373
+ const roots = [pluginsDir, stageDir, rollbackDir];
2374
+ for (const root of roots) assertDirectoryBoundary(root, "Plugin activation boundary");
2375
+ const devices = roots.map((root) => fsSync.statSync(root).dev);
2376
+ if (new Set(devices).size !== 1) throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Plugin activation boundaries must share one filesystem.");
2377
+ for (const name of PACKAGE_ITEMS) {
2378
+ const live = path14.join(pluginsDir, name);
2379
+ const stagedItem = path14.join(stageDir, name);
2380
+ const backup = path14.join(rollbackDir, name);
2381
+ if (fsSync.existsSync(backup)) throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Plugin rollback boundary must be empty before activation.");
2382
+ for (const boundary of [live, stagedItem]) {
2383
+ if (!fsSync.existsSync(boundary)) continue;
2384
+ const stat = fsSync.lstatSync(boundary);
2385
+ if (stat.isSymbolicLink() || stat.dev !== devices[0]) throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Plugin activation boundary is a symlink or crosses filesystems.");
2386
+ if (fsSync.realpathSync(path14.dirname(boundary)) !== path14.resolve(path14.dirname(boundary))) throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Plugin activation boundary has a symbolic-link ancestor.");
2387
+ }
2388
+ }
2389
+ await hooks?.beforeActivation();
2390
+ for (const name of ["node_modules", "package.json", "package-lock.json"]) {
2391
+ const live = path14.join(pluginsDir, name);
2392
+ const staged = path14.join(stageDir, name);
2393
+ const backup = path14.join(rollbackDir, name);
2394
+ await hooks?.beforeBackup(name);
2395
+ if (await pathExists(live)) {
2396
+ if (options.renamePath) options.renamePath(live, backup);
2397
+ else await fs15.rename(live, backup);
2398
+ }
2399
+ await hooks?.afterBackup(name);
2400
+ await hooks?.beforeActivate(name);
2401
+ if (await pathExists(staged)) {
2402
+ if (options.renamePath) options.renamePath(staged, live);
2403
+ else await fs15.rename(staged, live);
2404
+ }
2405
+ await hooks?.afterActivate(name);
2406
+ }
2407
+ activated = true;
2408
+ await hooks?.afterPackagesActivated();
2409
+ } catch (error) {
2410
+ if (error instanceof PluginInstallCrashSimulation || hooks) throw error;
2411
+ await rollback();
2412
+ throw new PluginInstallError("PLUGIN_ACTIVATION_FAILED", "Plugin activation failed; previous packages were restored.");
2413
+ }
2414
+ return {
2415
+ rollback,
2416
+ commit: async () => {
2417
+ if (finished) return;
2418
+ finished = true;
2419
+ await Promise.allSettled([
2420
+ fs15.rm(rollbackDir, { recursive: true, force: true }),
2421
+ fs15.rm(stageDir, { recursive: true, force: true })
2422
+ ]);
2423
+ }
2424
+ };
2425
+ }
2426
+ };
2427
+ } catch (error) {
2428
+ await fs15.rm(stageDir, { recursive: true, force: true });
2429
+ throw error;
2430
+ }
2431
+ }
2432
+ async function installNpmPlugin(packageName, pluginsDir, childEnv) {
2433
+ if (!VALID_NPM_NAME.test(packageName)) {
2434
+ throw new Error(`Invalid package name: "${packageName}". Must be a valid npm package name.`);
2435
+ }
2436
+ const dir = pluginsDir;
2437
+ try {
2438
+ return await importFromDir(packageName, dir);
2439
+ } catch {
2440
+ }
2441
+ void childEnv;
2442
+ throw new PluginInstallError(
2443
+ "PLUGIN_INSTALL_STATE_INVALID",
2444
+ `Runtime package installation is not safe while OpenACP may be loaded. Run "openacp plugin install ${packageName}" and restart OpenACP.`
2445
+ );
2446
+ }
2447
+ var execFileAsync, VALID_NPM_NAME, PluginInstallError, PluginInstallCrashSimulation, JOURNAL_FILE, LOCK_FILE, PACKAGE_ITEMS, MAX_JOURNAL_BYTES, TRANSACTION_ID, mutationContext, PluginInstallJournalController;
2448
+ var init_plugin_installer = __esm({
2449
+ "src/core/plugin/plugin-installer.ts"() {
2450
+ "use strict";
2451
+ execFileAsync = promisify(execFile);
2452
+ VALID_NPM_NAME = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*(@[\w.^~>=<|-]+)?$/i;
2453
+ PluginInstallError = class extends Error {
2454
+ constructor(code, message) {
2455
+ super(message);
2456
+ this.code = code;
2457
+ this.name = "PluginInstallError";
2458
+ }
2459
+ code;
2460
+ };
2461
+ PluginInstallCrashSimulation = class extends Error {
2462
+ constructor(phase) {
2463
+ super(`Simulated process crash at ${phase}`);
2464
+ this.phase = phase;
2465
+ this.name = "PluginInstallCrashSimulation";
2466
+ }
2467
+ phase;
2468
+ };
2469
+ JOURNAL_FILE = "plugin-install.journal.json";
2470
+ LOCK_FILE = "plugin-install.lock";
2471
+ PACKAGE_ITEMS = ["node_modules", "package.json", "package-lock.json"];
2472
+ MAX_JOURNAL_BYTES = 8 * 1024 * 1024;
2473
+ TRANSACTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2474
+ mutationContext = new AsyncLocalStorage();
2475
+ PluginInstallJournalController = class {
2476
+ constructor(instanceRoot, lock, hooks = {}) {
2477
+ this.instanceRoot = instanceRoot;
2478
+ this.lock = lock;
2479
+ this.hooks = hooks;
2480
+ this.transactionId = lock.transactionId;
2481
+ this.journalPath = path14.join(instanceRoot, JOURNAL_FILE);
2482
+ }
2483
+ instanceRoot;
2484
+ lock;
2485
+ hooks;
2486
+ journal;
2487
+ journalPath;
2488
+ transactionId;
2489
+ recoverExisting() {
2490
+ recoverLocked(this.instanceRoot);
2491
+ }
2492
+ initialize(pluginName, dataDirectory, registryPath) {
2493
+ if (this.journal) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal is already initialized.");
2494
+ const pluginsDir = path14.join(this.instanceRoot, "plugins");
2495
+ assertInstanceBoundaries(this.instanceRoot);
2496
+ const expectedData = path14.join(pluginsDir, "data", ...pluginName.split("/"));
2497
+ if (dataDirectory !== expectedData || registryPath !== path14.join(this.instanceRoot, "plugins.json")) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction intent paths are invalid.");
2498
+ const existed = fsSync.existsSync(registryPath);
2499
+ this.journal = {
2500
+ version: 3,
2501
+ transactionId: this.transactionId,
2502
+ pluginName,
2503
+ phase: "initialized",
2504
+ stageDir: path14.join(pluginsDir, `.stage-${this.transactionId}`),
2505
+ rollbackDir: path14.join(pluginsDir, `.rollback-${this.transactionId}`),
2506
+ items: PACKAGE_ITEMS.map((name) => {
2507
+ const live = path14.join(pluginsDir, name);
2508
+ const hadLive = fsSync.existsSync(live);
2509
+ return { name, hadLive, state: "untouched", originalDigest: hadLive ? boundaryDigest(live) : void 0 };
2510
+ }),
2511
+ data: { directory: dataDirectory },
2512
+ registry: { path: registryPath, existed, contentBase64: existed ? fsSync.readFileSync(registryPath).toString("base64") : void 0, mode: existed ? fsSync.statSync(registryPath).mode & 511 : 384 }
2513
+ };
2514
+ validateJournal(this.instanceRoot, this.journal);
2515
+ this.persist();
2516
+ this.hooks.crashAt?.("initialized");
2517
+ }
2518
+ snapshotData(directory) {
2519
+ if (!this.journal || this.journal.data.directory !== directory) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction was not prepared for this data directory.");
2520
+ validateJournal(this.instanceRoot, this.journal);
2521
+ if (!fsSync.existsSync(directory)) {
2522
+ const result2 = { directory, existed: false };
2523
+ this.journal.data = result2;
2524
+ this.transition("hook-pending");
2525
+ return result2;
2526
+ }
2527
+ const pluginsDir = path14.join(this.instanceRoot, "plugins");
2528
+ const pending = path14.join(pluginsDir, `.data-snapshot-${this.transactionId}.pending`);
2529
+ const complete = path14.join(pluginsDir, `.data-snapshot-${this.transactionId}.complete`);
2530
+ let result;
2531
+ try {
2532
+ fsSync.rmSync(pending, { recursive: true, force: true });
2533
+ fsSync.rmSync(complete, { recursive: true, force: true });
2534
+ (this.hooks.copyTree ?? ((source, destination) => fsSync.cpSync(source, destination, { recursive: true, preserveTimestamps: true })))(directory, pending);
2535
+ const digest = treeDigest(directory);
2536
+ if (treeDigest(pending) !== digest) throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin data snapshot verification failed.");
2537
+ secureSnapshotTree(pending);
2538
+ const modeDigest = treeModeDigest(pending, true);
2539
+ atomicJson(path14.join(pending, ".snapshot-complete.json"), { version: 1, transactionId: this.transactionId, digest, modeDigest });
2540
+ fsyncTree(pending);
2541
+ const deviceForPath = this.hooks.deviceForPath ?? ((target) => fsSync.statSync(target).dev);
2542
+ if (deviceForPath(pluginsDir) !== deviceForPath(pending)) throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin transaction paths must share one filesystem.");
2543
+ fsSync.renameSync(pending, complete);
2544
+ try {
2545
+ const fd = fsSync.openSync(pluginsDir, "r");
2546
+ fsSync.fsyncSync(fd);
2547
+ fsSync.closeSync(fd);
2548
+ } catch {
2549
+ }
2550
+ this.hooks.crashAfterSnapshotRename?.();
2551
+ result = { directory, existed: true, snapshotDir: complete, digest, modeDigest };
2552
+ } catch (error) {
2553
+ if (error instanceof PluginInstallCrashSimulation) throw error;
2554
+ fsSync.rmSync(pending, { recursive: true, force: true });
2555
+ fsSync.rmSync(complete, { recursive: true, force: true });
2556
+ if (error instanceof PluginInstallError) throw error;
2557
+ throw new PluginInstallError("PLUGIN_SNAPSHOT_FAILED", "Plugin data snapshot could not be completed; original data was not changed.");
2558
+ }
2559
+ this.journal.data = result;
2560
+ try {
2561
+ this.transition("hook-pending");
2562
+ } catch (error) {
2563
+ if (error instanceof PluginInstallCrashSimulation) throw error;
2564
+ fsSync.rmSync(complete, { recursive: true, force: true });
2565
+ throw error;
2566
+ }
2567
+ return result;
2568
+ }
2569
+ async prepare(pluginName, staged, dataDirectory, registryPath) {
2570
+ const pluginsDir = path14.join(this.instanceRoot, "plugins");
2571
+ const deviceForPath = this.hooks.deviceForPath ?? ((target) => fsSync.statSync(target).dev);
2572
+ if (deviceForPath(pluginsDir) !== deviceForPath(staged.stageDir)) throw new PluginInstallError("PLUGIN_STAGE_FAILED", "Plugin transaction paths must share one filesystem.");
2573
+ if (!this.journal) this.initialize(pluginName, dataDirectory, registryPath);
2574
+ const journal = this.journal;
2575
+ if (journal.pluginName !== pluginName || journal.stageDir !== staged.stageDir || journal.rollbackDir !== staged.rollbackDir) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Staged plugin does not match durable transaction intent.");
2576
+ this.transition("staged");
2577
+ this.transition("snapshot-pending");
2578
+ return {
2579
+ beforeActivation: async () => {
2580
+ },
2581
+ beforeBackup: async (name) => this.transition(`backup:${name}`),
2582
+ afterBackup: async (name) => {
2583
+ const item = this.journal.items.find((entry) => entry.name === name);
2584
+ if (item.hadLive) item.state = "live-backed-up-complete";
2585
+ this.persist();
2586
+ },
2587
+ beforeActivate: async (name) => this.transition(`activate:${name}`),
2588
+ afterActivate: async (name) => {
2589
+ const item = this.journal.items.find((entry) => entry.name === name);
2590
+ item.state = "new-activated";
2591
+ this.persist();
2592
+ },
2593
+ afterPackagesActivated: async () => this.transition("packages-activated")
2594
+ };
2595
+ }
2596
+ transition(phase) {
2597
+ if (!this.journal) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Plugin transaction journal was not prepared.");
2598
+ const previous = this.journal.phase;
2599
+ const previousHookStarted = this.journal.data.hookStarted;
2600
+ this.journal.phase = phase;
2601
+ if (phase === "hook-running") this.journal.data.hookStarted = true;
2602
+ try {
2603
+ this.hooks.failPersistAt?.(phase);
2604
+ this.persist();
2605
+ } catch (error) {
2606
+ this.journal.phase = previous;
2607
+ this.journal.data.hookStarted = previousHookStarted;
2608
+ throw error;
2609
+ }
2610
+ this.hooks.crashAt?.(phase);
2611
+ }
2612
+ markRegistryCommitted(expectedContent, expectedMode) {
2613
+ if (!this.journal || this.journal.phase !== "registry-committing") throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Registry commit marker requires the registry-committing phase.");
2614
+ const actual = fsSync.readFileSync(this.journal.registry.path);
2615
+ const actualMode = fsSync.statSync(this.journal.registry.path).mode & 511;
2616
+ if (!actual.equals(expectedContent) || actualMode !== expectedMode) throw new PluginInstallError("PLUGIN_RECOVERY_FAILED", "Persisted registry does not match the intended committed state.");
2617
+ const previousPhase = this.journal.phase;
2618
+ this.journal.registry.commitEvidence = {
2619
+ contentBase64: expectedContent.toString("base64"),
2620
+ digest: createHash2("sha256").update(expectedContent).digest("hex"),
2621
+ mode: expectedMode
2622
+ };
2623
+ this.journal.phase = "registry-committed";
2624
+ try {
2625
+ this.hooks.failPersistAt?.("registry-committed");
2626
+ this.persist();
2627
+ } catch (error) {
2628
+ this.journal.phase = previousPhase;
2629
+ delete this.journal.registry.commitEvidence;
2630
+ throw error;
2631
+ }
2632
+ this.hooks.crashAt?.("registry-committed");
2633
+ }
2634
+ rollback() {
2635
+ if (!this.journal) return;
2636
+ if (this.journal.phase === "registry-committed" || this.journal.phase === "committed") {
2637
+ cleanJournalArtifacts(this.instanceRoot, this.journal, this.hooks.removePath);
2638
+ this.journal = void 0;
2639
+ return;
2640
+ }
2641
+ restoreJournal(this.instanceRoot, this.journal, this.hooks.rollbackBoundary);
2642
+ cleanJournalArtifacts(this.instanceRoot, this.journal, this.hooks.removePath);
2643
+ this.journal = void 0;
2644
+ }
2645
+ commit() {
2646
+ try {
2647
+ this.transition("committed");
2648
+ } catch (error) {
2649
+ if (error instanceof PluginInstallCrashSimulation) throw error;
2650
+ return false;
2651
+ }
2652
+ try {
2653
+ cleanJournalArtifacts(this.instanceRoot, this.journal, this.hooks.removePath);
2654
+ this.journal = void 0;
2655
+ return true;
2656
+ } catch (error) {
2657
+ if (error instanceof PluginInstallCrashSimulation) throw error;
2658
+ return false;
2659
+ }
2660
+ }
2661
+ get isCommitted() {
2662
+ return this.journal?.phase === "registry-committed" || this.journal?.phase === "committed";
2663
+ }
2664
+ release() {
2665
+ this.lock.release();
2666
+ }
2667
+ persist() {
2668
+ validateJournal(this.instanceRoot, this.journal);
2669
+ atomicJson(this.journalPath, this.journal);
2670
+ }
2671
+ };
2672
+ }
2673
+ });
2674
+
1685
2675
  // src/plugins/speech/providers/groq.ts
1686
2676
  function mimeToExt(mimeType) {
1687
2677
  const map = {
@@ -1700,16 +2690,18 @@ var init_groq = __esm({
1700
2690
  "use strict";
1701
2691
  GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
1702
2692
  GroqSTT = class {
1703
- constructor(apiKey, defaultModel = "whisper-large-v3-turbo", scopedFetch = globalThis.fetch, getScopedFetch) {
2693
+ constructor(apiKey, defaultModel = "whisper-large-v3-turbo", scopedFetch = globalThis.fetch, getScopedFetch, endpoint = GROQ_API_URL) {
1704
2694
  this.apiKey = apiKey;
1705
2695
  this.defaultModel = defaultModel;
1706
2696
  this.scopedFetch = scopedFetch;
1707
2697
  this.getScopedFetch = getScopedFetch;
2698
+ this.endpoint = endpoint;
1708
2699
  }
1709
2700
  apiKey;
1710
2701
  defaultModel;
1711
2702
  scopedFetch;
1712
2703
  getScopedFetch;
2704
+ endpoint;
1713
2705
  name = "groq";
1714
2706
  /**
1715
2707
  * Transcribes audio using the Groq Whisper API.
@@ -1726,7 +2718,7 @@ var init_groq = __esm({
1726
2718
  if (options?.language) {
1727
2719
  form.append("language", options.language);
1728
2720
  }
1729
- const resp = await (this.getScopedFetch?.() ?? this.scopedFetch)(GROQ_API_URL, {
2721
+ const resp = await (this.getScopedFetch?.() ?? this.scopedFetch)(this.endpoint, {
1730
2722
  method: "POST",
1731
2723
  headers: { Authorization: `Bearer ${this.apiKey}` },
1732
2724
  body: form
@@ -1756,26 +2748,26 @@ var init_groq = __esm({
1756
2748
  });
1757
2749
 
1758
2750
  // src/plugins/speech/providers/local-whisper.ts
1759
- import { execFile } from "child_process";
2751
+ import { execFile as execFile2 } from "child_process";
1760
2752
  import { existsSync as existsSync6 } from "fs";
1761
- import { mkdtemp, rm, writeFile } from "fs/promises";
2753
+ import { mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1762
2754
  import { tmpdir } from "os";
1763
- import path14 from "path";
2755
+ import path16 from "path";
1764
2756
  import { fileURLToPath } from "url";
1765
- import { promisify } from "util";
2757
+ import { promisify as promisify2 } from "util";
1766
2758
  function resolveLocalWhisperScriptPath() {
1767
- const moduleDir = path14.dirname(fileURLToPath(import.meta.url));
2759
+ const moduleDir = path16.dirname(fileURLToPath(import.meta.url));
1768
2760
  const candidates = [
1769
- path14.resolve(moduleDir, "../scripts/transcribe_audio.sh"),
1770
- path14.resolve(moduleDir, "speech/transcribe_audio.sh")
2761
+ path16.resolve(moduleDir, "../scripts/transcribe_audio.sh"),
2762
+ path16.resolve(moduleDir, "speech/transcribe_audio.sh")
1771
2763
  ];
1772
2764
  return candidates.find(existsSync6) ?? candidates[0];
1773
2765
  }
1774
- var execFileAsync, LOCAL_WHISPER_PROVIDER, LOCAL_WHISPER_DEFAULTS;
2766
+ var execFileAsync2, LOCAL_WHISPER_PROVIDER, LOCAL_WHISPER_DEFAULTS;
1775
2767
  var init_local_whisper = __esm({
1776
2768
  "src/plugins/speech/providers/local-whisper.ts"() {
1777
2769
  "use strict";
1778
- execFileAsync = promisify(execFile);
2770
+ execFileAsync2 = promisify2(execFile2);
1779
2771
  LOCAL_WHISPER_PROVIDER = "local-whisper";
1780
2772
  LOCAL_WHISPER_DEFAULTS = {
1781
2773
  language: "ru",
@@ -1805,7 +2797,7 @@ function readLocalWhisperSettings(raw) {
1805
2797
  function buildSpeechServiceConfig(raw) {
1806
2798
  const groqApiKey = readOptionalString(raw.groqApiKey);
1807
2799
  const requestedProvider = readOptionalString(raw.sttProvider);
1808
- const provider = requestedProvider === LOCAL_WHISPER_PROVIDER ? LOCAL_WHISPER_PROVIDER : groqApiKey ? "groq" : null;
2800
+ const provider = requestedProvider === LOCAL_WHISPER_PROVIDER ? LOCAL_WHISPER_PROVIDER : requestedProvider === "groq" && groqApiKey ? "groq" : raw.sttProvider === void 0 && groqApiKey ? "groq" : null;
1809
2801
  const providers = {};
1810
2802
  if (groqApiKey) providers.groq = { apiKey: groqApiKey };
1811
2803
  if (provider === LOCAL_WHISPER_PROVIDER) {
@@ -1856,23 +2848,23 @@ var init_proxy_types = __esm({
1856
2848
  });
1857
2849
 
1858
2850
  // src/core/network/proxy-store.ts
1859
- import fs15 from "fs";
1860
- import path15 from "path";
2851
+ import fs17 from "fs";
2852
+ import path17 from "path";
1861
2853
  import net from "net";
1862
2854
  function atomicWrite(file, value, mode) {
1863
- fs15.mkdirSync(path15.dirname(file), { recursive: true, mode: 448 });
2855
+ fs17.mkdirSync(path17.dirname(file), { recursive: true, mode: 448 });
1864
2856
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
1865
- fs15.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
2857
+ fs17.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
1866
2858
  `, { mode, flag: "wx" });
1867
- fs15.chmodSync(tmp, mode);
1868
- const fd = fs15.openSync(tmp, "r");
1869
- fs15.fsyncSync(fd);
1870
- fs15.closeSync(fd);
1871
- fs15.renameSync(tmp, file);
2859
+ fs17.chmodSync(tmp, mode);
2860
+ const fd = fs17.openSync(tmp, "r");
2861
+ fs17.fsyncSync(fd);
2862
+ fs17.closeSync(fd);
2863
+ fs17.renameSync(tmp, file);
1872
2864
  try {
1873
- const dir = fs15.openSync(path15.dirname(file), "r");
1874
- fs15.fsyncSync(dir);
1875
- fs15.closeSync(dir);
2865
+ const dir = fs17.openSync(path17.dirname(file), "r");
2866
+ fs17.fsyncSync(dir);
2867
+ fs17.closeSync(dir);
1876
2868
  } catch {
1877
2869
  }
1878
2870
  }
@@ -1964,7 +2956,7 @@ var init_proxy_store = __esm({
1964
2956
  };
1965
2957
  ProxyStoreCorruptError = class extends ProxyStoreError {
1966
2958
  constructor(file, reason, quarantine, lkgAvailable = false) {
1967
- super("PROXY_STORE_CORRUPT", `Proxy policy store is invalid (${path15.basename(file)}): ${reason}`, { file, quarantine, lkgAvailable });
2959
+ super("PROXY_STORE_CORRUPT", `Proxy policy store is invalid (${path17.basename(file)}): ${reason}`, { file, quarantine, lkgAvailable });
1968
2960
  this.name = "ProxyStoreCorruptError";
1969
2961
  }
1970
2962
  };
@@ -1981,11 +2973,11 @@ var init_proxy_store = __esm({
1981
2973
  lkgPath;
1982
2974
  lockPath;
1983
2975
  constructor(instanceRoot) {
1984
- this.configPath = path15.join(instanceRoot, "proxy.json");
1985
- this.secretsPath = path15.join(instanceRoot, "proxy-secrets.json");
1986
- this.journalPath = path15.join(instanceRoot, "proxy-transaction.json");
1987
- this.lkgPath = path15.join(instanceRoot, "proxy-lkg.json");
1988
- this.lockPath = path15.join(instanceRoot, "proxy.lock");
2976
+ this.configPath = path17.join(instanceRoot, "proxy.json");
2977
+ this.secretsPath = path17.join(instanceRoot, "proxy-secrets.json");
2978
+ this.journalPath = path17.join(instanceRoot, "proxy-transaction.json");
2979
+ this.lkgPath = path17.join(instanceRoot, "proxy-lkg.json");
2980
+ this.lockPath = path17.join(instanceRoot, "proxy.lock");
1989
2981
  const lock = this.acquireLock();
1990
2982
  try {
1991
2983
  this.cleanupOrphans(instanceRoot);
@@ -1996,18 +2988,18 @@ var init_proxy_store = __esm({
1996
2988
  }
1997
2989
  cleanupOrphans(root) {
1998
2990
  try {
1999
- const names = fs15.readdirSync(root);
2000
- for (const name of names) if (/^proxy.*\.tmp$/.test(name)) fs15.rmSync(path15.join(root, name), { force: true });
2991
+ const names = fs17.readdirSync(root);
2992
+ for (const name of names) if (/^proxy.*\.tmp$/.test(name)) fs17.rmSync(path17.join(root, name), { force: true });
2001
2993
  const quarantines = names.filter((n) => n.includes(".corrupt.")).sort().reverse();
2002
- for (const name of quarantines.slice(3)) fs15.rmSync(path15.join(root, name), { force: true });
2994
+ for (const name of quarantines.slice(3)) fs17.rmSync(path17.join(root, name), { force: true });
2003
2995
  } catch {
2004
2996
  }
2005
2997
  }
2006
2998
  quarantine(file) {
2007
2999
  try {
2008
3000
  const target = `${file}.corrupt.${Date.now()}`;
2009
- fs15.copyFileSync(file, target, fs15.constants.COPYFILE_EXCL);
2010
- fs15.chmodSync(target, 384);
3001
+ fs17.copyFileSync(file, target, fs17.constants.COPYFILE_EXCL);
3002
+ fs17.chmodSync(target, 384);
2011
3003
  return target;
2012
3004
  } catch {
2013
3005
  return void 0;
@@ -2015,28 +3007,28 @@ var init_proxy_store = __esm({
2015
3007
  }
2016
3008
  readConfigFile(file = this.configPath) {
2017
3009
  try {
2018
- return validateConfig(JSON.parse(fs15.readFileSync(file, "utf8")));
3010
+ return validateConfig(JSON.parse(fs17.readFileSync(file, "utf8")));
2019
3011
  } catch (error) {
2020
3012
  if (error.code === "ENOENT" && file === this.configPath) return structuredClone(DEFAULT_CONFIG2);
2021
3013
  const q = file === this.configPath ? this.quarantine(file) : void 0;
2022
- throw new ProxyStoreCorruptError(file, error.message, q, fs15.existsSync(this.lkgPath));
3014
+ throw new ProxyStoreCorruptError(file, error.message, q, fs17.existsSync(this.lkgPath));
2023
3015
  }
2024
3016
  }
2025
3017
  readSecretsFile() {
2026
3018
  try {
2027
- const stat = fs15.statSync(this.secretsPath);
3019
+ const stat = fs17.statSync(this.secretsPath);
2028
3020
  if ((stat.mode & 63) !== 0) throw new Error("secret file mode must be 0600");
2029
- return validateSecrets(JSON.parse(fs15.readFileSync(this.secretsPath, "utf8")));
3021
+ return validateSecrets(JSON.parse(fs17.readFileSync(this.secretsPath, "utf8")));
2030
3022
  } catch (error) {
2031
3023
  if (error.code === "ENOENT") return {};
2032
3024
  const q = this.quarantine(this.secretsPath);
2033
- throw new ProxyStoreCorruptError(this.secretsPath, error.message, q, fs15.existsSync(this.lkgPath));
3025
+ throw new ProxyStoreCorruptError(this.secretsPath, error.message, q, fs17.existsSync(this.lkgPath));
2034
3026
  }
2035
3027
  }
2036
3028
  recoverJournal() {
2037
- if (!fs15.existsSync(this.journalPath)) return;
3029
+ if (!fs17.existsSync(this.journalPath)) return;
2038
3030
  try {
2039
- const tx = JSON.parse(fs15.readFileSync(this.journalPath, "utf8"));
3031
+ const tx = JSON.parse(fs17.readFileSync(this.journalPath, "utf8"));
2040
3032
  if (tx.version !== 1) throw new Error("unsupported transaction version");
2041
3033
  const config = validateConfig(tx.config);
2042
3034
  const secrets = validateSecrets(tx.secrets);
@@ -2044,9 +3036,9 @@ var init_proxy_store = __esm({
2044
3036
  atomicWrite(this.configPath, config, 384);
2045
3037
  atomicWrite(this.secretsPath, secrets, 384);
2046
3038
  atomicWrite(this.lkgPath, tx, 384);
2047
- fs15.unlinkSync(this.journalPath);
3039
+ fs17.unlinkSync(this.journalPath);
2048
3040
  } catch (error) {
2049
- throw new ProxyStoreCorruptError(this.journalPath, error.message, this.quarantine(this.journalPath), fs15.existsSync(this.lkgPath));
3041
+ throw new ProxyStoreCorruptError(this.journalPath, error.message, this.quarantine(this.journalPath), fs17.existsSync(this.lkgPath));
2050
3042
  }
2051
3043
  }
2052
3044
  load() {
@@ -2055,7 +3047,7 @@ var init_proxy_store = __esm({
2055
3047
  validateCredentialConsistency(config, this.readSecretsFile());
2056
3048
  } catch (error) {
2057
3049
  if (error instanceof ProxyStoreCorruptError) throw error;
2058
- throw new ProxyStoreCorruptError(this.secretsPath, error.message, this.quarantine(this.secretsPath), fs15.existsSync(this.lkgPath));
3050
+ throw new ProxyStoreCorruptError(this.secretsPath, error.message, this.quarantine(this.secretsPath), fs17.existsSync(this.lkgPath));
2059
3051
  }
2060
3052
  return config;
2061
3053
  }
@@ -2065,7 +3057,7 @@ var init_proxy_store = __esm({
2065
3057
  try {
2066
3058
  validateCredentialConsistency(config, secrets);
2067
3059
  } catch (error) {
2068
- throw new ProxyStoreCorruptError(this.secretsPath, error.message, this.quarantine(this.secretsPath), fs15.existsSync(this.lkgPath));
3060
+ throw new ProxyStoreCorruptError(this.secretsPath, error.message, this.quarantine(this.secretsPath), fs17.existsSync(this.lkgPath));
2069
3061
  }
2070
3062
  return secrets;
2071
3063
  }
@@ -2086,7 +3078,7 @@ var init_proxy_store = __esm({
2086
3078
  atomicWrite(this.configPath, next, 384);
2087
3079
  atomicWrite(this.secretsPath, cleanSecrets, 384);
2088
3080
  atomicWrite(this.lkgPath, tx, 384);
2089
- fs15.unlinkSync(this.journalPath);
3081
+ fs17.unlinkSync(this.journalPath);
2090
3082
  return next;
2091
3083
  } finally {
2092
3084
  this.releaseLock(lock);
@@ -2094,21 +3086,21 @@ var init_proxy_store = __esm({
2094
3086
  }
2095
3087
  releaseLock(fd) {
2096
3088
  try {
2097
- fs15.closeSync(fd);
3089
+ fs17.closeSync(fd);
2098
3090
  } catch {
2099
3091
  }
2100
3092
  try {
2101
- fs15.unlinkSync(this.lockPath);
3093
+ fs17.unlinkSync(this.lockPath);
2102
3094
  } catch {
2103
3095
  }
2104
3096
  }
2105
3097
  acquireLock() {
2106
- fs15.mkdirSync(path15.dirname(this.lockPath), { recursive: true, mode: 448 });
3098
+ fs17.mkdirSync(path17.dirname(this.lockPath), { recursive: true, mode: 448 });
2107
3099
  for (let attempt = 0; attempt < 200; attempt++) {
2108
3100
  try {
2109
- const fd = fs15.openSync(this.lockPath, "wx", 384);
2110
- fs15.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: Date.now() }));
2111
- fs15.fsyncSync(fd);
3101
+ const fd = fs17.openSync(this.lockPath, "wx", 384);
3102
+ fs17.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: Date.now() }));
3103
+ fs17.fsyncSync(fd);
2112
3104
  return fd;
2113
3105
  } catch (error) {
2114
3106
  if (error.code !== "EEXIST") throw error;
@@ -2120,7 +3112,7 @@ var init_proxy_store = __esm({
2120
3112
  }
2121
3113
  removeStaleLock() {
2122
3114
  try {
2123
- const lock = JSON.parse(fs15.readFileSync(this.lockPath, "utf8"));
3115
+ const lock = JSON.parse(fs17.readFileSync(this.lockPath, "utf8"));
2124
3116
  const old = !Number.isFinite(lock.createdAt) || Date.now() - Number(lock.createdAt) > 3e4;
2125
3117
  let dead = false;
2126
3118
  if (Number.isInteger(lock.pid)) {
@@ -2131,13 +3123,13 @@ var init_proxy_store = __esm({
2131
3123
  }
2132
3124
  }
2133
3125
  if (dead || old && !Number.isInteger(lock.pid)) {
2134
- fs15.unlinkSync(this.lockPath);
3126
+ fs17.unlinkSync(this.lockPath);
2135
3127
  return true;
2136
3128
  }
2137
3129
  } catch {
2138
3130
  try {
2139
- if (Date.now() - fs15.statSync(this.lockPath).mtimeMs > 3e4) {
2140
- fs15.unlinkSync(this.lockPath);
3131
+ if (Date.now() - fs17.statSync(this.lockPath).mtimeMs > 3e4) {
3132
+ fs17.unlinkSync(this.lockPath);
2141
3133
  return true;
2142
3134
  }
2143
3135
  } catch {
@@ -2147,7 +3139,7 @@ var init_proxy_store = __esm({
2147
3139
  }
2148
3140
  loadLastKnownGood() {
2149
3141
  try {
2150
- return validateConfig(JSON.parse(fs15.readFileSync(this.lkgPath, "utf8")).config);
3142
+ return validateConfig(JSON.parse(fs17.readFileSync(this.lkgPath, "utf8")).config);
2151
3143
  } catch {
2152
3144
  return void 0;
2153
3145
  }
@@ -2159,7 +3151,7 @@ var init_proxy_store = __esm({
2159
3151
  // src/core/network/proxy-service.ts
2160
3152
  import { ProxyAgent } from "proxy-agent";
2161
3153
  import nodeFetch from "node-fetch";
2162
- import fs16 from "fs";
3154
+ import fs18 from "fs";
2163
3155
  import net2 from "net";
2164
3156
  import { Readable } from "stream";
2165
3157
  import debug from "debug";
@@ -2331,6 +3323,7 @@ var init_proxy_service = __esm({
2331
3323
  "services.npmUpdate",
2332
3324
  "services.agentRegistry",
2333
3325
  "services.pluginInstaller",
3326
+ "services.speech",
2334
3327
  "services.speechDownloads",
2335
3328
  "plugins.default"
2336
3329
  ]);
@@ -2534,11 +3527,11 @@ var init_proxy_service = __esm({
2534
3527
  return this.saveProfile(this.parseEnvFile(id, envFile, name));
2535
3528
  }
2536
3529
  parseEnvFile(id, envFile, name) {
2537
- const stat = fs16.statSync(envFile);
3530
+ const stat = fs18.statSync(envFile);
2538
3531
  if (!stat.isFile()) throw new Error("Proxy env path is not a file");
2539
3532
  if ((stat.mode & 63) !== 0) throw new Error("Proxy env file must have mode 0600");
2540
3533
  const values = {};
2541
- for (const rawLine of fs16.readFileSync(envFile, "utf8").split(/\r?\n/)) {
3534
+ for (const rawLine of fs18.readFileSync(envFile, "utf8").split(/\r?\n/)) {
2542
3535
  const line = rawLine.trim();
2543
3536
  if (!line || line.startsWith("#")) continue;
2544
3537
  const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
@@ -3040,7 +4033,7 @@ var init_proxy_service = __esm({
3040
4033
  });
3041
4034
 
3042
4035
  // src/core/doctor/checks/config.ts
3043
- import * as fs17 from "fs";
4036
+ import * as fs19 from "fs";
3044
4037
  var configCheck;
3045
4038
  var init_config2 = __esm({
3046
4039
  "src/core/doctor/checks/config.ts"() {
@@ -3052,14 +4045,14 @@ var init_config2 = __esm({
3052
4045
  order: 1,
3053
4046
  async run(ctx) {
3054
4047
  const results = [];
3055
- if (!fs17.existsSync(ctx.configPath)) {
4048
+ if (!fs19.existsSync(ctx.configPath)) {
3056
4049
  results.push({ status: "fail", message: "Config file not found" });
3057
4050
  return results;
3058
4051
  }
3059
4052
  results.push({ status: "pass", message: "Config file exists" });
3060
4053
  let raw;
3061
4054
  try {
3062
- raw = JSON.parse(fs17.readFileSync(ctx.configPath, "utf-8"));
4055
+ raw = JSON.parse(fs19.readFileSync(ctx.configPath, "utf-8"));
3063
4056
  } catch (err) {
3064
4057
  results.push({
3065
4058
  status: "fail",
@@ -3078,7 +4071,7 @@ var init_config2 = __esm({
3078
4071
  fixRisk: "safe",
3079
4072
  fix: async () => {
3080
4073
  applyMigrations(raw);
3081
- fs17.writeFileSync(ctx.configPath, JSON.stringify(raw, null, 2));
4074
+ fs19.writeFileSync(ctx.configPath, JSON.stringify(raw, null, 2));
3082
4075
  return { success: true, message: "applied migrations" };
3083
4076
  }
3084
4077
  });
@@ -3102,8 +4095,8 @@ var init_config2 = __esm({
3102
4095
 
3103
4096
  // src/core/doctor/checks/agents.ts
3104
4097
  import { execFileSync as execFileSync3 } from "child_process";
3105
- import * as fs18 from "fs";
3106
- import * as path17 from "path";
4098
+ import * as fs20 from "fs";
4099
+ import * as path19 from "path";
3107
4100
  function commandExists2(cmd) {
3108
4101
  try {
3109
4102
  execFileSync3("which", [cmd], { stdio: "pipe" });
@@ -3112,9 +4105,9 @@ function commandExists2(cmd) {
3112
4105
  }
3113
4106
  let dir = process.cwd();
3114
4107
  while (true) {
3115
- const binPath = path17.join(dir, "node_modules", ".bin", cmd);
3116
- if (fs18.existsSync(binPath)) return true;
3117
- const parent = path17.dirname(dir);
4108
+ const binPath = path19.join(dir, "node_modules", ".bin", cmd);
4109
+ if (fs20.existsSync(binPath)) return true;
4110
+ const parent = path19.dirname(dir);
3118
4111
  if (parent === dir) break;
3119
4112
  dir = parent;
3120
4113
  }
@@ -3136,9 +4129,9 @@ var init_agents = __esm({
3136
4129
  const defaultAgent = ctx.config.defaultAgent;
3137
4130
  let agents = {};
3138
4131
  try {
3139
- const agentsPath = path17.join(ctx.dataDir, "agents.json");
3140
- if (fs18.existsSync(agentsPath)) {
3141
- const data = JSON.parse(fs18.readFileSync(agentsPath, "utf-8"));
4132
+ const agentsPath = path19.join(ctx.dataDir, "agents.json");
4133
+ if (fs20.existsSync(agentsPath)) {
4134
+ const data = JSON.parse(fs20.readFileSync(agentsPath, "utf-8"));
3142
4135
  agents = data.installed ?? {};
3143
4136
  }
3144
4137
  } catch {
@@ -3189,8 +4182,61 @@ var settings_manager_exports = {};
3189
4182
  __export(settings_manager_exports, {
3190
4183
  SettingsManager: () => SettingsManager
3191
4184
  });
3192
- import fs19 from "fs";
3193
- import path18 from "path";
4185
+ import fs21 from "fs";
4186
+ import path20 from "path";
4187
+ import { randomUUID as randomUUID4 } from "crypto";
4188
+ function secureSettingsDirectory(directory) {
4189
+ fs21.mkdirSync(directory, { recursive: true, mode: 448 });
4190
+ fs21.chmodSync(directory, 448);
4191
+ }
4192
+ function secureSettingsTree(basePath, settingsPath, create) {
4193
+ const target = path20.dirname(settingsPath);
4194
+ const relative2 = path20.relative(basePath, target);
4195
+ if (relative2.startsWith("..") || path20.isAbsolute(relative2)) throw new Error("Settings path escapes its base directory");
4196
+ secureSettingsDirectory(basePath);
4197
+ let current = basePath;
4198
+ for (const segment of relative2 ? relative2.split(path20.sep) : []) {
4199
+ current = path20.join(current, segment);
4200
+ if (create || fs21.existsSync(current)) secureSettingsDirectory(current);
4201
+ }
4202
+ }
4203
+ function repairSettingsPermissions(settingsPath, basePath) {
4204
+ if (!fs21.existsSync(settingsPath)) return;
4205
+ secureSettingsTree(basePath, settingsPath, false);
4206
+ fs21.chmodSync(settingsPath, 384);
4207
+ }
4208
+ function atomicSettingsWrite(settingsPath, basePath, data) {
4209
+ const directory = path20.dirname(settingsPath);
4210
+ secureSettingsTree(basePath, settingsPath, true);
4211
+ const temporary = `${settingsPath}.${process.pid}.${randomUUID4()}.tmp`;
4212
+ try {
4213
+ fs21.writeFileSync(temporary, `${JSON.stringify(data, null, 2)}
4214
+ `, { mode: 384, flag: "wx" });
4215
+ fs21.chmodSync(temporary, 384);
4216
+ const fd = fs21.openSync(temporary, "r");
4217
+ try {
4218
+ fs21.fsyncSync(fd);
4219
+ } finally {
4220
+ fs21.closeSync(fd);
4221
+ }
4222
+ fs21.renameSync(temporary, settingsPath);
4223
+ fs21.chmodSync(settingsPath, 384);
4224
+ try {
4225
+ const directoryFd = fs21.openSync(directory, "r");
4226
+ try {
4227
+ fs21.fsyncSync(directoryFd);
4228
+ } finally {
4229
+ fs21.closeSync(directoryFd);
4230
+ }
4231
+ } catch {
4232
+ }
4233
+ } finally {
4234
+ try {
4235
+ if (fs21.existsSync(temporary)) fs21.unlinkSync(temporary);
4236
+ } catch {
4237
+ }
4238
+ }
4239
+ }
3194
4240
  var SettingsManager, SettingsAPIImpl;
3195
4241
  var init_settings_manager = __esm({
3196
4242
  "src/core/plugin/settings-manager.ts"() {
@@ -3198,6 +4244,7 @@ var init_settings_manager = __esm({
3198
4244
  SettingsManager = class {
3199
4245
  constructor(basePath) {
3200
4246
  this.basePath = basePath;
4247
+ secureSettingsDirectory(basePath);
3201
4248
  }
3202
4249
  basePath;
3203
4250
  /** Returns the base path for all plugin settings directories. */
@@ -3207,13 +4254,16 @@ var init_settings_manager = __esm({
3207
4254
  /** Create a SettingsAPI instance scoped to a specific plugin. */
3208
4255
  createAPI(pluginName) {
3209
4256
  const settingsPath = this.getSettingsPath(pluginName);
3210
- return new SettingsAPIImpl(settingsPath);
4257
+ return new SettingsAPIImpl(settingsPath, this.basePath);
3211
4258
  }
3212
4259
  /** Load a plugin's settings from disk. Returns empty object if file doesn't exist. */
3213
4260
  async loadSettings(pluginName) {
3214
4261
  const settingsPath = this.getSettingsPath(pluginName);
4262
+ secureSettingsDirectory(this.basePath);
4263
+ if (!fs21.existsSync(settingsPath)) return {};
4264
+ repairSettingsPermissions(settingsPath, this.basePath);
3215
4265
  try {
3216
- const content = fs19.readFileSync(settingsPath, "utf-8");
4266
+ const content = fs21.readFileSync(settingsPath, "utf-8");
3217
4267
  return JSON.parse(content);
3218
4268
  } catch {
3219
4269
  return {};
@@ -3233,7 +4283,7 @@ var init_settings_manager = __esm({
3233
4283
  }
3234
4284
  /** Resolve the absolute path to a plugin's settings.json file. */
3235
4285
  getSettingsPath(pluginName) {
3236
- return path18.join(this.basePath, pluginName, "settings.json");
4286
+ return path20.join(this.basePath, pluginName, "settings.json");
3237
4287
  }
3238
4288
  async getPluginSettings(pluginName) {
3239
4289
  return this.loadSettings(pluginName);
@@ -3246,15 +4296,19 @@ var init_settings_manager = __esm({
3246
4296
  }
3247
4297
  };
3248
4298
  SettingsAPIImpl = class {
3249
- constructor(settingsPath) {
4299
+ constructor(settingsPath, basePath) {
3250
4300
  this.settingsPath = settingsPath;
4301
+ this.basePath = basePath;
3251
4302
  }
3252
4303
  settingsPath;
4304
+ basePath;
3253
4305
  cache = null;
3254
4306
  readFile() {
3255
4307
  if (this.cache !== null) return this.cache;
4308
+ secureSettingsDirectory(this.basePath);
4309
+ if (fs21.existsSync(this.settingsPath)) repairSettingsPermissions(this.settingsPath, this.basePath);
3256
4310
  try {
3257
- const content = fs19.readFileSync(this.settingsPath, "utf-8");
4311
+ const content = fs21.readFileSync(this.settingsPath, "utf-8");
3258
4312
  this.cache = JSON.parse(content);
3259
4313
  return this.cache;
3260
4314
  } catch {
@@ -3263,9 +4317,7 @@ var init_settings_manager = __esm({
3263
4317
  }
3264
4318
  }
3265
4319
  writeFile(data) {
3266
- const dir = path18.dirname(this.settingsPath);
3267
- fs19.mkdirSync(dir, { recursive: true });
3268
- fs19.writeFileSync(this.settingsPath, JSON.stringify(data, null, 2));
4320
+ atomicSettingsWrite(this.settingsPath, this.basePath, data);
3269
4321
  this.cache = data;
3270
4322
  }
3271
4323
  async get(key) {
@@ -3307,20 +4359,20 @@ __export(command_ownership_store_exports, {
3307
4359
  telegramCommandHostId: () => telegramCommandHostId,
3308
4360
  telegramCommandInstanceKey: () => telegramCommandInstanceKey
3309
4361
  });
3310
- import fs20 from "fs";
3311
- import path19 from "path";
4362
+ import fs22 from "fs";
4363
+ import path21 from "path";
3312
4364
  import os5 from "os";
3313
- import { createHash as createHash2 } from "crypto";
4365
+ import { createHash as createHash4 } from "crypto";
3314
4366
  function telegramCommandHostId() {
3315
4367
  let source = `${os5.hostname()}|${typeof process.getuid === "function" ? process.getuid() : "unknown"}`;
3316
4368
  try {
3317
- source = fs20.readFileSync("/etc/machine-id", "utf8").trim() || source;
4369
+ source = fs22.readFileSync("/etc/machine-id", "utf8").trim() || source;
3318
4370
  } catch {
3319
4371
  }
3320
- return createHash2("sha256").update(source).digest("hex");
4372
+ return createHash4("sha256").update(source).digest("hex");
3321
4373
  }
3322
4374
  function telegramCommandInstanceKey(instanceRoot) {
3323
- return createHash2("sha256").update(path19.resolve(instanceRoot)).digest("hex");
4375
+ return createHash4("sha256").update(path21.resolve(instanceRoot)).digest("hex");
3324
4376
  }
3325
4377
  function emptyLedger() {
3326
4378
  return { version: STORE_VERSION, bots: {} };
@@ -3368,21 +4420,21 @@ var init_command_ownership_store = __esm({
3368
4420
  file;
3369
4421
  lockFile;
3370
4422
  constructor(instanceRoot) {
3371
- this.file = path19.join(instanceRoot, "telegram-command-ownership.json");
4423
+ this.file = path21.join(instanceRoot, "telegram-command-ownership.json");
3372
4424
  this.lockFile = `${this.file}.lock`;
3373
4425
  }
3374
4426
  async withLock(operation) {
3375
- fs20.mkdirSync(path19.dirname(this.file), { recursive: true, mode: 448 });
4427
+ fs22.mkdirSync(path21.dirname(this.file), { recursive: true, mode: 448 });
3376
4428
  const deadline = Date.now() + 1e4;
3377
4429
  let fd;
3378
4430
  while (fd === void 0) {
3379
4431
  try {
3380
- fd = fs20.openSync(this.lockFile, "wx", 384);
3381
- fs20.writeFileSync(fd, String(process.pid));
4432
+ fd = fs22.openSync(this.lockFile, "wx", 384);
4433
+ fs22.writeFileSync(fd, String(process.pid));
3382
4434
  } catch (error) {
3383
4435
  if (error.code !== "EEXIST") throw error;
3384
4436
  try {
3385
- const owner = Number(fs20.readFileSync(this.lockFile, "utf8"));
4437
+ const owner = Number(fs22.readFileSync(this.lockFile, "utf8"));
3386
4438
  let alive = Number.isSafeInteger(owner) && owner > 0;
3387
4439
  if (alive) {
3388
4440
  try {
@@ -3391,34 +4443,34 @@ var init_command_ownership_store = __esm({
3391
4443
  alive = false;
3392
4444
  }
3393
4445
  }
3394
- if (!alive) fs20.unlinkSync(this.lockFile);
4446
+ if (!alive) fs22.unlinkSync(this.lockFile);
3395
4447
  } catch {
3396
4448
  }
3397
4449
  if (Date.now() >= deadline) throw new Error("Timed out waiting for Telegram command ownership lock");
3398
- await new Promise((resolve7) => setTimeout(resolve7, 50));
4450
+ await new Promise((resolve8) => setTimeout(resolve8, 50));
3399
4451
  }
3400
4452
  }
3401
4453
  try {
3402
4454
  return await operation(this.load());
3403
4455
  } finally {
3404
4456
  try {
3405
- fs20.closeSync(fd);
4457
+ fs22.closeSync(fd);
3406
4458
  } catch {
3407
4459
  }
3408
4460
  try {
3409
- fs20.unlinkSync(this.lockFile);
4461
+ fs22.unlinkSync(this.lockFile);
3410
4462
  } catch {
3411
4463
  }
3412
4464
  }
3413
4465
  }
3414
4466
  load() {
3415
- if (!fs20.existsSync(this.file)) return { ledger: emptyLedger(), conservative: false };
4467
+ if (!fs22.existsSync(this.file)) return { ledger: emptyLedger(), conservative: false };
3416
4468
  try {
3417
- return { ledger: validateLedger(JSON.parse(fs20.readFileSync(this.file, "utf8"))), conservative: false };
4469
+ return { ledger: validateLedger(JSON.parse(fs22.readFileSync(this.file, "utf8"))), conservative: false };
3418
4470
  } catch {
3419
4471
  const quarantine = `${this.file}.corrupt.${Date.now()}`;
3420
4472
  try {
3421
- fs20.renameSync(this.file, quarantine);
4473
+ fs22.renameSync(this.file, quarantine);
3422
4474
  } catch {
3423
4475
  }
3424
4476
  return { ledger: emptyLedger(), conservative: true };
@@ -3499,19 +4551,19 @@ var init_command_ownership_store = __esm({
3499
4551
  }
3500
4552
  save(ledger) {
3501
4553
  const clean = validateLedger(ledger);
3502
- fs20.mkdirSync(path19.dirname(this.file), { recursive: true, mode: 448 });
4554
+ fs22.mkdirSync(path21.dirname(this.file), { recursive: true, mode: 448 });
3503
4555
  const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
3504
- fs20.writeFileSync(tmp, `${JSON.stringify(clean, null, 2)}
4556
+ fs22.writeFileSync(tmp, `${JSON.stringify(clean, null, 2)}
3505
4557
  `, { mode: 384, flag: "wx" });
3506
- fs20.chmodSync(tmp, 384);
3507
- const fd = fs20.openSync(tmp, "r");
3508
- fs20.fsyncSync(fd);
3509
- fs20.closeSync(fd);
3510
- fs20.renameSync(tmp, this.file);
4558
+ fs22.chmodSync(tmp, 384);
4559
+ const fd = fs22.openSync(tmp, "r");
4560
+ fs22.fsyncSync(fd);
4561
+ fs22.closeSync(fd);
4562
+ fs22.renameSync(tmp, this.file);
3511
4563
  try {
3512
- const dir = fs20.openSync(path19.dirname(this.file), "r");
3513
- fs20.fsyncSync(dir);
3514
- fs20.closeSync(dir);
4564
+ const dir = fs22.openSync(path21.dirname(this.file), "r");
4565
+ fs22.fsyncSync(dir);
4566
+ fs22.closeSync(dir);
3515
4567
  } catch {
3516
4568
  }
3517
4569
  }
@@ -3528,8 +4580,8 @@ __export(instance_context_exports, {
3528
4580
  resolveInstanceRoot: () => resolveInstanceRoot,
3529
4581
  resolveRunningInstance: () => resolveRunningInstance
3530
4582
  });
3531
- import path20 from "path";
3532
- import fs21 from "fs";
4583
+ import path22 from "path";
4584
+ import fs23 from "fs";
3533
4585
  import os6 from "os";
3534
4586
  function createInstanceContext(opts) {
3535
4587
  const { id, root } = opts;
@@ -3538,22 +4590,22 @@ function createInstanceContext(opts) {
3538
4590
  id,
3539
4591
  root,
3540
4592
  paths: {
3541
- config: path20.join(root, "config.json"),
3542
- sessions: path20.join(root, "sessions.json"),
3543
- agents: path20.join(root, "agents.json"),
3544
- registryCache: path20.join(globalRoot, "cache", "registry-cache.json"),
3545
- plugins: path20.join(root, "plugins"),
3546
- pluginsData: path20.join(root, "plugins", "data"),
3547
- pluginRegistry: path20.join(root, "plugins.json"),
3548
- logs: path20.join(root, "logs"),
3549
- pid: path20.join(root, "openacp.pid"),
3550
- running: path20.join(root, "running"),
3551
- apiPort: path20.join(root, "api.port"),
3552
- apiSecret: path20.join(root, "api-secret"),
3553
- bin: path20.join(globalRoot, "bin"),
3554
- cache: path20.join(root, "cache"),
3555
- tunnels: path20.join(root, "tunnels.json"),
3556
- agentsDir: path20.join(globalRoot, "agents")
4593
+ config: path22.join(root, "config.json"),
4594
+ sessions: path22.join(root, "sessions.json"),
4595
+ agents: path22.join(root, "agents.json"),
4596
+ registryCache: path22.join(globalRoot, "cache", "registry-cache.json"),
4597
+ plugins: path22.join(root, "plugins"),
4598
+ pluginsData: path22.join(root, "plugins", "data"),
4599
+ pluginRegistry: path22.join(root, "plugins.json"),
4600
+ logs: path22.join(root, "logs"),
4601
+ pid: path22.join(root, "openacp.pid"),
4602
+ running: path22.join(root, "running"),
4603
+ apiPort: path22.join(root, "api.port"),
4604
+ apiSecret: path22.join(root, "api-secret"),
4605
+ bin: path22.join(globalRoot, "bin"),
4606
+ cache: path22.join(root, "cache"),
4607
+ tunnels: path22.join(root, "tunnels.json"),
4608
+ agentsDir: path22.join(globalRoot, "agents")
3557
4609
  }
3558
4610
  };
3559
4611
  }
@@ -3562,50 +4614,50 @@ function generateSlug(name) {
3562
4614
  return slug || "openacp";
3563
4615
  }
3564
4616
  function expandHome3(p) {
3565
- if (p.startsWith("~")) return path20.join(os6.homedir(), p.slice(1));
4617
+ if (p.startsWith("~")) return path22.join(os6.homedir(), p.slice(1));
3566
4618
  return p;
3567
4619
  }
3568
4620
  function resolveInstanceRoot(opts) {
3569
4621
  const cwd = opts.cwd ?? process.cwd();
3570
4622
  const home = os6.homedir();
3571
4623
  const globalRoot = getGlobalRoot();
3572
- if (opts.dir) return path20.join(expandHome3(opts.dir), ".openacp");
3573
- if (opts.local) return path20.join(cwd, ".openacp");
3574
- const cwdRoot = path20.join(cwd, ".openacp");
3575
- if (fs21.existsSync(path20.join(cwdRoot, "config.json"))) return cwdRoot;
3576
- let dir = path20.resolve(cwd);
4624
+ if (opts.dir) return path22.join(expandHome3(opts.dir), ".openacp");
4625
+ if (opts.local) return path22.join(cwd, ".openacp");
4626
+ const cwdRoot = path22.join(cwd, ".openacp");
4627
+ if (fs23.existsSync(path22.join(cwdRoot, "config.json"))) return cwdRoot;
4628
+ let dir = path22.resolve(cwd);
3577
4629
  while (true) {
3578
- const parent = path20.dirname(dir);
4630
+ const parent = path22.dirname(dir);
3579
4631
  if (parent === dir) break;
3580
4632
  dir = parent;
3581
- const candidate = path20.join(dir, ".openacp");
4633
+ const candidate = path22.join(dir, ".openacp");
3582
4634
  if (candidate === globalRoot) {
3583
4635
  if (dir === home) break;
3584
4636
  continue;
3585
4637
  }
3586
- if (fs21.existsSync(path20.join(candidate, "config.json"))) return candidate;
4638
+ if (fs23.existsSync(path22.join(candidate, "config.json"))) return candidate;
3587
4639
  if (dir === home) break;
3588
4640
  }
3589
- if (path20.resolve(cwd) === path20.resolve(home)) {
3590
- const defaultWs = path20.join(home, "openacp-workspace", ".openacp");
3591
- if (fs21.existsSync(path20.join(defaultWs, "config.json"))) return defaultWs;
4641
+ if (path22.resolve(cwd) === path22.resolve(home)) {
4642
+ const defaultWs = path22.join(home, "openacp-workspace", ".openacp");
4643
+ if (fs23.existsSync(path22.join(defaultWs, "config.json"))) return defaultWs;
3592
4644
  }
3593
4645
  if (process.env.OPENACP_INSTANCE_ROOT) return process.env.OPENACP_INSTANCE_ROOT;
3594
4646
  return null;
3595
4647
  }
3596
4648
  function getGlobalRoot() {
3597
- return path20.join(os6.homedir(), ".openacp");
4649
+ return path22.join(os6.homedir(), ".openacp");
3598
4650
  }
3599
4651
  async function resolveRunningInstance(cwd) {
3600
4652
  const globalRoot = getGlobalRoot();
3601
4653
  const home = os6.homedir();
3602
- let dir = path20.resolve(cwd);
4654
+ let dir = path22.resolve(cwd);
3603
4655
  while (true) {
3604
- const candidate = path20.join(dir, ".openacp");
3605
- if (candidate !== globalRoot && fs21.existsSync(candidate)) {
4656
+ const candidate = path22.join(dir, ".openacp");
4657
+ if (candidate !== globalRoot && fs23.existsSync(candidate)) {
3606
4658
  if (await isInstanceRunning(candidate)) return candidate;
3607
4659
  }
3608
- const parent = path20.dirname(dir);
4660
+ const parent = path22.dirname(dir);
3609
4661
  if (parent === dir) break;
3610
4662
  if (dir === home) break;
3611
4663
  dir = parent;
@@ -3613,9 +4665,9 @@ async function resolveRunningInstance(cwd) {
3613
4665
  return null;
3614
4666
  }
3615
4667
  async function isInstanceRunning(instanceRoot) {
3616
- const portFile = path20.join(instanceRoot, "api.port");
4668
+ const portFile = path22.join(instanceRoot, "api.port");
3617
4669
  try {
3618
- const content = fs21.readFileSync(portFile, "utf-8").trim();
4670
+ const content = fs23.readFileSync(portFile, "utf-8").trim();
3619
4671
  const port = parseInt(content, 10);
3620
4672
  if (isNaN(port)) return false;
3621
4673
  const controller = new AbortController();
@@ -3636,7 +4688,7 @@ var init_instance_context = __esm({
3636
4688
  });
3637
4689
 
3638
4690
  // src/core/doctor/checks/telegram.ts
3639
- import * as path21 from "path";
4691
+ import * as path23 from "path";
3640
4692
  var BOT_TOKEN_REGEX, telegramCheck;
3641
4693
  var init_telegram = __esm({
3642
4694
  "src/core/doctor/checks/telegram.ts"() {
@@ -3654,7 +4706,7 @@ var init_telegram = __esm({
3654
4706
  return results;
3655
4707
  }
3656
4708
  const { SettingsManager: SettingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), settings_manager_exports));
3657
- const sm = new SettingsManager2(path21.join(ctx.pluginsDir, "data"));
4709
+ const sm = new SettingsManager2(path23.join(ctx.pluginsDir, "data"));
3658
4710
  const ps = await sm.loadSettings("@openacp/telegram");
3659
4711
  const botToken = ps.botToken;
3660
4712
  const chatId = ps.chatId;
@@ -3814,7 +4866,7 @@ var init_telegram = __esm({
3814
4866
  });
3815
4867
 
3816
4868
  // src/core/doctor/checks/storage.ts
3817
- import * as fs22 from "fs";
4869
+ import * as fs24 from "fs";
3818
4870
  var storageCheck;
3819
4871
  var init_storage = __esm({
3820
4872
  "src/core/doctor/checks/storage.ts"() {
@@ -3824,28 +4876,28 @@ var init_storage = __esm({
3824
4876
  order: 4,
3825
4877
  async run(ctx) {
3826
4878
  const results = [];
3827
- if (!fs22.existsSync(ctx.dataDir)) {
4879
+ if (!fs24.existsSync(ctx.dataDir)) {
3828
4880
  results.push({
3829
4881
  status: "fail",
3830
4882
  message: "Data directory ~/.openacp does not exist",
3831
4883
  fixable: true,
3832
4884
  fixRisk: "safe",
3833
4885
  fix: async () => {
3834
- fs22.mkdirSync(ctx.dataDir, { recursive: true });
4886
+ fs24.mkdirSync(ctx.dataDir, { recursive: true });
3835
4887
  return { success: true, message: "created directory" };
3836
4888
  }
3837
4889
  });
3838
4890
  } else {
3839
4891
  try {
3840
- fs22.accessSync(ctx.dataDir, fs22.constants.W_OK);
4892
+ fs24.accessSync(ctx.dataDir, fs24.constants.W_OK);
3841
4893
  results.push({ status: "pass", message: "Data directory exists and writable" });
3842
4894
  } catch {
3843
4895
  results.push({ status: "fail", message: "Data directory not writable" });
3844
4896
  }
3845
4897
  }
3846
- if (fs22.existsSync(ctx.sessionsPath)) {
4898
+ if (fs24.existsSync(ctx.sessionsPath)) {
3847
4899
  try {
3848
- const content = fs22.readFileSync(ctx.sessionsPath, "utf-8");
4900
+ const content = fs24.readFileSync(ctx.sessionsPath, "utf-8");
3849
4901
  const data = JSON.parse(content);
3850
4902
  if (typeof data === "object" && data !== null && "sessions" in data) {
3851
4903
  results.push({ status: "pass", message: "Sessions file valid" });
@@ -3856,7 +4908,7 @@ var init_storage = __esm({
3856
4908
  fixable: true,
3857
4909
  fixRisk: "risky",
3858
4910
  fix: async () => {
3859
- fs22.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
4911
+ fs24.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
3860
4912
  return { success: true, message: "reset sessions file" };
3861
4913
  }
3862
4914
  });
@@ -3868,7 +4920,7 @@ var init_storage = __esm({
3868
4920
  fixable: true,
3869
4921
  fixRisk: "risky",
3870
4922
  fix: async () => {
3871
- fs22.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
4923
+ fs24.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
3872
4924
  return { success: true, message: "reset sessions file" };
3873
4925
  }
3874
4926
  });
@@ -3876,20 +4928,20 @@ var init_storage = __esm({
3876
4928
  } else {
3877
4929
  results.push({ status: "pass", message: "Sessions file not present yet (created on first session)" });
3878
4930
  }
3879
- if (!fs22.existsSync(ctx.logsDir)) {
4931
+ if (!fs24.existsSync(ctx.logsDir)) {
3880
4932
  results.push({
3881
4933
  status: "warn",
3882
4934
  message: "Log directory does not exist",
3883
4935
  fixable: true,
3884
4936
  fixRisk: "safe",
3885
4937
  fix: async () => {
3886
- fs22.mkdirSync(ctx.logsDir, { recursive: true });
4938
+ fs24.mkdirSync(ctx.logsDir, { recursive: true });
3887
4939
  return { success: true, message: "created log directory" };
3888
4940
  }
3889
4941
  });
3890
4942
  } else {
3891
4943
  try {
3892
- fs22.accessSync(ctx.logsDir, fs22.constants.W_OK);
4944
+ fs24.accessSync(ctx.logsDir, fs24.constants.W_OK);
3893
4945
  results.push({ status: "pass", message: "Log directory exists and writable" });
3894
4946
  } catch {
3895
4947
  results.push({ status: "fail", message: "Log directory not writable" });
@@ -3902,8 +4954,8 @@ var init_storage = __esm({
3902
4954
  });
3903
4955
 
3904
4956
  // src/core/doctor/checks/workspace.ts
3905
- import * as fs23 from "fs";
3906
- import * as path22 from "path";
4957
+ import * as fs25 from "fs";
4958
+ import * as path24 from "path";
3907
4959
  var workspaceCheck;
3908
4960
  var init_workspace = __esm({
3909
4961
  "src/core/doctor/checks/workspace.ts"() {
@@ -3913,21 +4965,21 @@ var init_workspace = __esm({
3913
4965
  order: 5,
3914
4966
  async run(ctx) {
3915
4967
  const results = [];
3916
- const workspace = path22.dirname(ctx.dataDir);
3917
- if (!fs23.existsSync(workspace)) {
4968
+ const workspace = path24.dirname(ctx.dataDir);
4969
+ if (!fs25.existsSync(workspace)) {
3918
4970
  results.push({
3919
4971
  status: "warn",
3920
4972
  message: `Workspace directory does not exist: ${workspace}`,
3921
4973
  fixable: true,
3922
4974
  fixRisk: "safe",
3923
4975
  fix: async () => {
3924
- fs23.mkdirSync(workspace, { recursive: true });
4976
+ fs25.mkdirSync(workspace, { recursive: true });
3925
4977
  return { success: true, message: "created directory" };
3926
4978
  }
3927
4979
  });
3928
4980
  } else {
3929
4981
  try {
3930
- fs23.accessSync(workspace, fs23.constants.W_OK);
4982
+ fs25.accessSync(workspace, fs25.constants.W_OK);
3931
4983
  results.push({ status: "pass", message: `Workspace directory exists: ${workspace}` });
3932
4984
  } catch {
3933
4985
  results.push({ status: "fail", message: `Workspace directory not writable: ${workspace}` });
@@ -3940,8 +4992,8 @@ var init_workspace = __esm({
3940
4992
  });
3941
4993
 
3942
4994
  // src/core/doctor/checks/plugins.ts
3943
- import * as fs24 from "fs";
3944
- import * as path23 from "path";
4995
+ import * as fs26 from "fs";
4996
+ import * as path25 from "path";
3945
4997
  var pluginsCheck;
3946
4998
  var init_plugins = __esm({
3947
4999
  "src/core/doctor/checks/plugins.ts"() {
@@ -3951,16 +5003,16 @@ var init_plugins = __esm({
3951
5003
  order: 6,
3952
5004
  async run(ctx) {
3953
5005
  const results = [];
3954
- if (!fs24.existsSync(ctx.pluginsDir)) {
5006
+ if (!fs26.existsSync(ctx.pluginsDir)) {
3955
5007
  results.push({
3956
5008
  status: "warn",
3957
5009
  message: "Plugins directory does not exist",
3958
5010
  fixable: true,
3959
5011
  fixRisk: "safe",
3960
5012
  fix: async () => {
3961
- fs24.mkdirSync(ctx.pluginsDir, { recursive: true });
3962
- fs24.writeFileSync(
3963
- path23.join(ctx.pluginsDir, "package.json"),
5013
+ fs26.mkdirSync(ctx.pluginsDir, { recursive: true });
5014
+ fs26.writeFileSync(
5015
+ path25.join(ctx.pluginsDir, "package.json"),
3964
5016
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
3965
5017
  );
3966
5018
  return { success: true, message: "initialized plugins directory" };
@@ -3969,15 +5021,15 @@ var init_plugins = __esm({
3969
5021
  return results;
3970
5022
  }
3971
5023
  results.push({ status: "pass", message: "Plugins directory exists" });
3972
- const pkgPath = path23.join(ctx.pluginsDir, "package.json");
3973
- if (!fs24.existsSync(pkgPath)) {
5024
+ const pkgPath = path25.join(ctx.pluginsDir, "package.json");
5025
+ if (!fs26.existsSync(pkgPath)) {
3974
5026
  results.push({
3975
5027
  status: "warn",
3976
5028
  message: "Plugins package.json missing",
3977
5029
  fixable: true,
3978
5030
  fixRisk: "safe",
3979
5031
  fix: async () => {
3980
- fs24.writeFileSync(
5032
+ fs26.writeFileSync(
3981
5033
  pkgPath,
3982
5034
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
3983
5035
  );
@@ -3987,7 +5039,7 @@ var init_plugins = __esm({
3987
5039
  return results;
3988
5040
  }
3989
5041
  try {
3990
- const pkg = JSON.parse(fs24.readFileSync(pkgPath, "utf-8"));
5042
+ const pkg = JSON.parse(fs26.readFileSync(pkgPath, "utf-8"));
3991
5043
  const deps = pkg.dependencies || {};
3992
5044
  const count = Object.keys(deps).length;
3993
5045
  results.push({ status: "pass", message: `Plugins package.json valid (${count} plugins)` });
@@ -3998,7 +5050,7 @@ var init_plugins = __esm({
3998
5050
  fixable: true,
3999
5051
  fixRisk: "risky",
4000
5052
  fix: async () => {
4001
- fs24.writeFileSync(
5053
+ fs26.writeFileSync(
4002
5054
  pkgPath,
4003
5055
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
4004
5056
  );
@@ -4013,7 +5065,7 @@ var init_plugins = __esm({
4013
5065
  });
4014
5066
 
4015
5067
  // src/core/doctor/checks/daemon.ts
4016
- import * as fs25 from "fs";
5068
+ import * as fs27 from "fs";
4017
5069
  import * as net3 from "net";
4018
5070
  function isProcessAlive(pid) {
4019
5071
  try {
@@ -4024,12 +5076,12 @@ function isProcessAlive(pid) {
4024
5076
  }
4025
5077
  }
4026
5078
  function checkPortInUse(port) {
4027
- return new Promise((resolve7) => {
5079
+ return new Promise((resolve8) => {
4028
5080
  const server = net3.createServer();
4029
- server.once("error", () => resolve7(true));
5081
+ server.once("error", () => resolve8(true));
4030
5082
  server.once("listening", () => {
4031
5083
  server.close();
4032
- resolve7(false);
5084
+ resolve8(false);
4033
5085
  });
4034
5086
  server.listen(port, "127.0.0.1");
4035
5087
  });
@@ -4043,8 +5095,8 @@ var init_daemon = __esm({
4043
5095
  order: 7,
4044
5096
  async run(ctx) {
4045
5097
  const results = [];
4046
- if (fs25.existsSync(ctx.pidPath)) {
4047
- const content = fs25.readFileSync(ctx.pidPath, "utf-8").trim();
5098
+ if (fs27.existsSync(ctx.pidPath)) {
5099
+ const content = fs27.readFileSync(ctx.pidPath, "utf-8").trim();
4048
5100
  const pid = parseInt(content, 10);
4049
5101
  if (isNaN(pid)) {
4050
5102
  results.push({
@@ -4053,7 +5105,7 @@ var init_daemon = __esm({
4053
5105
  fixable: true,
4054
5106
  fixRisk: "safe",
4055
5107
  fix: async () => {
4056
- fs25.unlinkSync(ctx.pidPath);
5108
+ fs27.unlinkSync(ctx.pidPath);
4057
5109
  return { success: true, message: "removed invalid PID file" };
4058
5110
  }
4059
5111
  });
@@ -4064,7 +5116,7 @@ var init_daemon = __esm({
4064
5116
  fixable: true,
4065
5117
  fixRisk: "safe",
4066
5118
  fix: async () => {
4067
- fs25.unlinkSync(ctx.pidPath);
5119
+ fs27.unlinkSync(ctx.pidPath);
4068
5120
  return { success: true, message: "removed stale PID file" };
4069
5121
  }
4070
5122
  });
@@ -4072,8 +5124,8 @@ var init_daemon = __esm({
4072
5124
  results.push({ status: "pass", message: `Daemon running (PID ${pid})` });
4073
5125
  }
4074
5126
  }
4075
- if (fs25.existsSync(ctx.portFilePath)) {
4076
- const content = fs25.readFileSync(ctx.portFilePath, "utf-8").trim();
5127
+ if (fs27.existsSync(ctx.portFilePath)) {
5128
+ const content = fs27.readFileSync(ctx.portFilePath, "utf-8").trim();
4077
5129
  const port = parseInt(content, 10);
4078
5130
  if (isNaN(port)) {
4079
5131
  results.push({
@@ -4082,7 +5134,7 @@ var init_daemon = __esm({
4082
5134
  fixable: true,
4083
5135
  fixRisk: "safe",
4084
5136
  fix: async () => {
4085
- fs25.unlinkSync(ctx.portFilePath);
5137
+ fs27.unlinkSync(ctx.portFilePath);
4086
5138
  return { success: true, message: "removed invalid port file" };
4087
5139
  }
4088
5140
  });
@@ -4094,8 +5146,8 @@ var init_daemon = __esm({
4094
5146
  const apiPort = 21420;
4095
5147
  const inUse = await checkPortInUse(apiPort);
4096
5148
  if (inUse) {
4097
- if (fs25.existsSync(ctx.pidPath)) {
4098
- const pid = parseInt(fs25.readFileSync(ctx.pidPath, "utf-8").trim(), 10);
5149
+ if (fs27.existsSync(ctx.pidPath)) {
5150
+ const pid = parseInt(fs27.readFileSync(ctx.pidPath, "utf-8").trim(), 10);
4099
5151
  if (!isNaN(pid) && isProcessAlive(pid)) {
4100
5152
  results.push({ status: "pass", message: `API port ${apiPort} in use by OpenACP daemon` });
4101
5153
  } else {
@@ -4115,17 +5167,17 @@ var init_daemon = __esm({
4115
5167
  });
4116
5168
 
4117
5169
  // src/core/utils/install-binary.ts
4118
- import fs26 from "fs";
4119
- import path24 from "path";
5170
+ import fs28 from "fs";
5171
+ import path26 from "path";
4120
5172
  import https from "https";
4121
5173
  import os7 from "os";
4122
5174
  import { execFileSync as execFileSync4 } from "child_process";
4123
5175
  function downloadFile(url, dest, maxRedirects = 10) {
4124
- return new Promise((resolve7, reject) => {
4125
- const file = fs26.createWriteStream(dest);
5176
+ return new Promise((resolve8, reject) => {
5177
+ const file = fs28.createWriteStream(dest);
4126
5178
  const cleanup = () => {
4127
5179
  try {
4128
- if (fs26.existsSync(dest)) fs26.unlinkSync(dest);
5180
+ if (fs28.existsSync(dest)) fs28.unlinkSync(dest);
4129
5181
  } catch {
4130
5182
  }
4131
5183
  };
@@ -4140,7 +5192,7 @@ function downloadFile(url, dest, maxRedirects = 10) {
4140
5192
  }
4141
5193
  file.close(() => {
4142
5194
  cleanup();
4143
- downloadFile(response.headers.location, dest, maxRedirects - 1).then(resolve7).catch(reject);
5195
+ downloadFile(response.headers.location, dest, maxRedirects - 1).then(resolve8).catch(reject);
4144
5196
  });
4145
5197
  return;
4146
5198
  }
@@ -4164,7 +5216,7 @@ function downloadFile(url, dest, maxRedirects = 10) {
4164
5216
  }
4165
5217
  });
4166
5218
  response.pipe(file);
4167
- file.on("finish", () => file.close(() => resolve7(dest)));
5219
+ file.on("finish", () => file.close(() => resolve8(dest)));
4168
5220
  file.on("error", (err) => {
4169
5221
  file.close(() => {
4170
5222
  cleanup();
@@ -4191,36 +5243,36 @@ function getDownloadUrl(spec) {
4191
5243
  async function ensureBinary(spec, binDir) {
4192
5244
  const resolvedBinDir = binDir ?? DEFAULT_BIN_DIR;
4193
5245
  const binName = IS_WINDOWS ? `${spec.name}.exe` : spec.name;
4194
- const binPath = path24.join(resolvedBinDir, binName);
5246
+ const binPath = path26.join(resolvedBinDir, binName);
4195
5247
  if (commandExists(spec.name)) {
4196
5248
  log19.debug({ name: spec.name }, "Found in PATH");
4197
5249
  return spec.name;
4198
5250
  }
4199
- if (fs26.existsSync(binPath)) {
4200
- if (!IS_WINDOWS) fs26.chmodSync(binPath, "755");
5251
+ if (fs28.existsSync(binPath)) {
5252
+ if (!IS_WINDOWS) fs28.chmodSync(binPath, "755");
4201
5253
  log19.debug({ name: spec.name, path: binPath }, "Found in ~/.openacp/bin");
4202
5254
  return binPath;
4203
5255
  }
4204
5256
  log19.info({ name: spec.name }, "Not found, downloading from GitHub...");
4205
- fs26.mkdirSync(resolvedBinDir, { recursive: true });
5257
+ fs28.mkdirSync(resolvedBinDir, { recursive: true });
4206
5258
  const url = getDownloadUrl(spec);
4207
5259
  const isArchive = spec.isArchive?.(url) ?? false;
4208
- const downloadDest = isArchive ? path24.join(resolvedBinDir, `${spec.name}.tgz`) : binPath;
5260
+ const downloadDest = isArchive ? path26.join(resolvedBinDir, `${spec.name}.tgz`) : binPath;
4209
5261
  await downloadFile(url, downloadDest);
4210
5262
  if (isArchive) {
4211
5263
  const listing = execFileSync4("tar", ["-tf", downloadDest], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
4212
5264
  validateTarContents(listing, resolvedBinDir);
4213
5265
  execFileSync4("tar", ["-xzf", downloadDest, "-C", resolvedBinDir], { stdio: "pipe" });
4214
5266
  try {
4215
- fs26.unlinkSync(downloadDest);
5267
+ fs28.unlinkSync(downloadDest);
4216
5268
  } catch {
4217
5269
  }
4218
5270
  }
4219
- if (!fs26.existsSync(binPath)) {
5271
+ if (!fs28.existsSync(binPath)) {
4220
5272
  throw new Error(`${spec.name}: binary not found at ${binPath} after download/extraction. The archive structure may have changed.`);
4221
5273
  }
4222
5274
  if (!IS_WINDOWS) {
4223
- fs26.chmodSync(binPath, "755");
5275
+ fs28.chmodSync(binPath, "755");
4224
5276
  }
4225
5277
  log19.info({ name: spec.name, path: binPath }, "Installed successfully");
4226
5278
  return binPath;
@@ -4233,7 +5285,7 @@ var init_install_binary = __esm({
4233
5285
  init_agent_dependencies();
4234
5286
  init_agent_installer();
4235
5287
  log19 = createChildLogger({ module: "binary-installer" });
4236
- DEFAULT_BIN_DIR = path24.join(os7.homedir(), ".openacp", "bin");
5288
+ DEFAULT_BIN_DIR = path26.join(os7.homedir(), ".openacp", "bin");
4237
5289
  IS_WINDOWS = os7.platform() === "win32";
4238
5290
  }
4239
5291
  });
@@ -4274,8 +5326,8 @@ var init_install_cloudflared = __esm({
4274
5326
  });
4275
5327
 
4276
5328
  // src/core/doctor/checks/tunnel.ts
4277
- import * as fs27 from "fs";
4278
- import * as path25 from "path";
5329
+ import * as fs29 from "fs";
5330
+ import * as path27 from "path";
4279
5331
  import * as os8 from "os";
4280
5332
  import { execFileSync as execFileSync5 } from "child_process";
4281
5333
  var tunnelCheck;
@@ -4292,7 +5344,7 @@ var init_tunnel = __esm({
4292
5344
  return results;
4293
5345
  }
4294
5346
  const { SettingsManager: SettingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), settings_manager_exports));
4295
- const sm = new SettingsManager2(path25.join(ctx.pluginsDir, "data"));
5347
+ const sm = new SettingsManager2(path27.join(ctx.pluginsDir, "data"));
4296
5348
  const tunnelSettings = await sm.loadSettings("@openacp/tunnel");
4297
5349
  const tunnelEnabled = tunnelSettings.enabled ?? false;
4298
5350
  const provider = tunnelSettings.provider ?? "cloudflare";
@@ -4304,9 +5356,9 @@ var init_tunnel = __esm({
4304
5356
  results.push({ status: "pass", message: `Tunnel provider: ${provider}` });
4305
5357
  if (provider === "cloudflare") {
4306
5358
  const binName = os8.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
4307
- const binPath = path25.join(ctx.dataDir, "bin", binName);
5359
+ const binPath = path27.join(ctx.dataDir, "bin", binName);
4308
5360
  let found = false;
4309
- if (fs27.existsSync(binPath)) {
5361
+ if (fs29.existsSync(binPath)) {
4310
5362
  found = true;
4311
5363
  } else {
4312
5364
  try {
@@ -4380,8 +5432,8 @@ var init_proxy = __esm({
4380
5432
  });
4381
5433
 
4382
5434
  // src/core/doctor/index.ts
4383
- import * as fs28 from "fs";
4384
- import * as path26 from "path";
5435
+ import * as fs30 from "fs";
5436
+ import * as path28 from "path";
4385
5437
  var ALL_CHECKS, CHECK_TIMEOUT_MS, DoctorEngine;
4386
5438
  var init_doctor = __esm({
4387
5439
  "src/core/doctor/index.ts"() {
@@ -4474,27 +5526,27 @@ var init_doctor = __esm({
4474
5526
  /** Constructs the shared context used by all checks — loads config if available. */
4475
5527
  async buildContext() {
4476
5528
  const dataDir = this.dataDir;
4477
- const configPath = process.env.OPENACP_CONFIG_PATH || path26.join(dataDir, "config.json");
5529
+ const configPath = process.env.OPENACP_CONFIG_PATH || path28.join(dataDir, "config.json");
4478
5530
  let config = null;
4479
5531
  let rawConfig = null;
4480
5532
  try {
4481
- const content = fs28.readFileSync(configPath, "utf-8");
5533
+ const content = fs30.readFileSync(configPath, "utf-8");
4482
5534
  rawConfig = JSON.parse(content);
4483
5535
  const cm = new ConfigManager(configPath);
4484
5536
  await cm.load();
4485
5537
  config = cm.get();
4486
5538
  } catch {
4487
5539
  }
4488
- const logsDir = config ? expandHome2(config.logging.logDir) : path26.join(dataDir, "logs");
5540
+ const logsDir = config ? expandHome2(config.logging.logDir) : path28.join(dataDir, "logs");
4489
5541
  return {
4490
5542
  config,
4491
5543
  rawConfig,
4492
5544
  configPath,
4493
5545
  dataDir,
4494
- sessionsPath: path26.join(dataDir, "sessions.json"),
4495
- pidPath: path26.join(dataDir, "openacp.pid"),
4496
- portFilePath: path26.join(dataDir, "api.port"),
4497
- pluginsDir: path26.join(dataDir, "plugins"),
5546
+ sessionsPath: path28.join(dataDir, "sessions.json"),
5547
+ pidPath: path28.join(dataDir, "openacp.pid"),
5548
+ portFilePath: path28.join(dataDir, "api.port"),
5549
+ pluginsDir: path28.join(dataDir, "plugins"),
4498
5550
  logsDir,
4499
5551
  fetchForScope: (scope) => this.proxyService.createFetch(scope)
4500
5552
  };
@@ -4504,12 +5556,12 @@ var init_doctor = __esm({
4504
5556
  });
4505
5557
 
4506
5558
  // src/core/instance/instance-registry.ts
4507
- import fs30 from "fs";
4508
- import path28 from "path";
4509
- import { randomUUID as randomUUID2 } from "crypto";
5559
+ import fs32 from "fs";
5560
+ import path30 from "path";
5561
+ import { randomUUID as randomUUID5 } from "crypto";
4510
5562
  function readIdFromConfig(instanceRoot) {
4511
5563
  try {
4512
- const raw = JSON.parse(fs30.readFileSync(path28.join(instanceRoot, "config.json"), "utf-8"));
5564
+ const raw = JSON.parse(fs32.readFileSync(path30.join(instanceRoot, "config.json"), "utf-8"));
4513
5565
  return typeof raw.id === "string" && raw.id ? raw.id : null;
4514
5566
  } catch {
4515
5567
  return null;
@@ -4528,7 +5580,7 @@ var init_instance_registry = __esm({
4528
5580
  /** Load the registry from disk. If the file is missing or corrupt, starts fresh. */
4529
5581
  load() {
4530
5582
  try {
4531
- const raw = fs30.readFileSync(this.registryPath, "utf-8");
5583
+ const raw = fs32.readFileSync(this.registryPath, "utf-8");
4532
5584
  const parsed = JSON.parse(raw);
4533
5585
  if (parsed.version === 1 && parsed.instances) {
4534
5586
  this.data = parsed;
@@ -4555,9 +5607,9 @@ var init_instance_registry = __esm({
4555
5607
  }
4556
5608
  /** Persist the registry to disk, creating parent directories if needed. */
4557
5609
  save() {
4558
- const dir = path28.dirname(this.registryPath);
4559
- fs30.mkdirSync(dir, { recursive: true });
4560
- fs30.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
5610
+ const dir = path30.dirname(this.registryPath);
5611
+ fs32.mkdirSync(dir, { recursive: true });
5612
+ fs32.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
4561
5613
  }
4562
5614
  /** Add or update an instance entry in the registry. Does not persist — call save() after. */
4563
5615
  register(id, root) {
@@ -4605,7 +5657,7 @@ var init_instance_registry = __esm({
4605
5657
  this.register(configId, instanceRoot);
4606
5658
  return { id: configId, registryUpdated: true };
4607
5659
  }
4608
- const id = configId ?? entry?.id ?? randomUUID2();
5660
+ const id = configId ?? entry?.id ?? randomUUID5();
4609
5661
  if (!this.getByRoot(instanceRoot)) {
4610
5662
  this.register(id, instanceRoot);
4611
5663
  return { id, registryUpdated: true };
@@ -4616,67 +5668,6 @@ var init_instance_registry = __esm({
4616
5668
  }
4617
5669
  });
4618
5670
 
4619
- // src/core/plugin/plugin-installer.ts
4620
- var plugin_installer_exports = {};
4621
- __export(plugin_installer_exports, {
4622
- importFromDir: () => importFromDir,
4623
- installNpmPlugin: () => installNpmPlugin
4624
- });
4625
- import { execFile as execFile2 } from "child_process";
4626
- import { promisify as promisify2 } from "util";
4627
- import * as fs32 from "fs/promises";
4628
- import * as path30 from "path";
4629
- import { pathToFileURL } from "url";
4630
- async function importFromDir(packageName, dir) {
4631
- const pkgDir = path30.join(dir, "node_modules", ...packageName.split("/"));
4632
- const pkgJsonPath = path30.join(pkgDir, "package.json");
4633
- let pkgJson;
4634
- try {
4635
- pkgJson = JSON.parse(await fs32.readFile(pkgJsonPath, "utf-8"));
4636
- } catch (err) {
4637
- throw new Error(`Cannot read package.json for "${packageName}" at ${pkgJsonPath}: ${err.message}`);
4638
- }
4639
- let entry;
4640
- const exportsMain = pkgJson.exports?.["."];
4641
- if (typeof exportsMain === "string") {
4642
- entry = exportsMain;
4643
- } else if (exportsMain?.import) {
4644
- entry = exportsMain.import;
4645
- } else {
4646
- entry = pkgJson.main ?? "index.js";
4647
- }
4648
- const entryPath = path30.join(pkgDir, entry);
4649
- try {
4650
- await fs32.access(entryPath);
4651
- } catch {
4652
- throw new Error(`Entry point "${entry}" not found for "${packageName}" at ${entryPath}`);
4653
- }
4654
- return import(pathToFileURL(entryPath).href);
4655
- }
4656
- async function installNpmPlugin(packageName, pluginsDir, childEnv) {
4657
- if (!VALID_NPM_NAME.test(packageName)) {
4658
- throw new Error(`Invalid package name: "${packageName}". Must be a valid npm package name.`);
4659
- }
4660
- const dir = pluginsDir;
4661
- try {
4662
- return await importFromDir(packageName, dir);
4663
- } catch {
4664
- }
4665
- await execFileAsync2("npm", ["install", packageName, "--prefix", dir, "--save", "--ignore-scripts"], {
4666
- timeout: 6e4,
4667
- env: childEnv
4668
- });
4669
- return await importFromDir(packageName, dir);
4670
- }
4671
- var execFileAsync2, VALID_NPM_NAME;
4672
- var init_plugin_installer = __esm({
4673
- "src/core/plugin/plugin-installer.ts"() {
4674
- "use strict";
4675
- execFileAsync2 = promisify2(execFile2);
4676
- VALID_NPM_NAME = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*(@[\w.^~>=<|-]+)?$/i;
4677
- }
4678
- });
4679
-
4680
5671
  // src/core/plugin/terminal-io.ts
4681
5672
  import * as clack from "@clack/prompts";
4682
5673
  function isCancel(value) {
@@ -4740,10 +5731,10 @@ var install_context_exports = {};
4740
5731
  __export(install_context_exports, {
4741
5732
  createInstallContext: () => createInstallContext
4742
5733
  });
4743
- import path31 from "path";
5734
+ import path32 from "path";
4744
5735
  function createInstallContext(opts) {
4745
5736
  const { pluginName, settingsManager, basePath, instanceRoot } = opts;
4746
- const dataDir = path31.join(basePath, pluginName, "data");
5737
+ const dataDir = path32.join(basePath, pluginName, "data");
4747
5738
  return {
4748
5739
  pluginName,
4749
5740
  terminal: createTerminalIO(),
@@ -4771,14 +5762,14 @@ __export(api_client_exports, {
4771
5762
  waitForApiReady: () => waitForApiReady,
4772
5763
  waitForPortFile: () => waitForPortFile
4773
5764
  });
4774
- import * as fs33 from "fs";
4775
- import * as path32 from "path";
5765
+ import * as fs34 from "fs";
5766
+ import * as path33 from "path";
4776
5767
  import { setTimeout as sleep } from "timers/promises";
4777
5768
  function defaultPortFile(root) {
4778
- return path32.join(root, "api.port");
5769
+ return path33.join(root, "api.port");
4779
5770
  }
4780
5771
  function defaultSecretFile(root) {
4781
- return path32.join(root, "api-secret");
5772
+ return path33.join(root, "api-secret");
4782
5773
  }
4783
5774
  async function waitForPortFile(portFilePath, timeoutMs = 5e3, intervalMs = 100) {
4784
5775
  const deadline = Date.now() + timeoutMs;
@@ -4812,7 +5803,7 @@ async function waitForApiReady(instanceRoot, expectedInstanceId, timeoutMs = 1e4
4812
5803
  function readApiPort(portFilePath, instanceRoot) {
4813
5804
  const filePath = portFilePath ?? defaultPortFile(instanceRoot);
4814
5805
  try {
4815
- const content = fs33.readFileSync(filePath, "utf-8").trim();
5806
+ const content = fs34.readFileSync(filePath, "utf-8").trim();
4816
5807
  const port = parseInt(content, 10);
4817
5808
  return isNaN(port) ? null : port;
4818
5809
  } catch {
@@ -4822,7 +5813,7 @@ function readApiPort(portFilePath, instanceRoot) {
4822
5813
  function readApiSecret(secretFilePath, instanceRoot) {
4823
5814
  const filePath = secretFilePath ?? defaultSecretFile(instanceRoot);
4824
5815
  try {
4825
- const content = fs33.readFileSync(filePath, "utf-8").trim();
5816
+ const content = fs34.readFileSync(filePath, "utf-8").trim();
4826
5817
  return content || null;
4827
5818
  } catch {
4828
5819
  return null;
@@ -4831,7 +5822,7 @@ function readApiSecret(secretFilePath, instanceRoot) {
4831
5822
  function removeStalePortFile(portFilePath, instanceRoot) {
4832
5823
  const filePath = portFilePath ?? defaultPortFile(instanceRoot);
4833
5824
  try {
4834
- fs33.unlinkSync(filePath);
5825
+ fs34.unlinkSync(filePath);
4835
5826
  } catch {
4836
5827
  }
4837
5828
  }
@@ -4968,8 +5959,8 @@ var init_notification = __esm({
4968
5959
  });
4969
5960
 
4970
5961
  // src/plugins/file-service/file-service.ts
4971
- import fs35 from "fs";
4972
- import path35 from "path";
5962
+ import fs36 from "fs";
5963
+ import path36 from "path";
4973
5964
  import { OggOpusDecoder } from "ogg-opus-decoder";
4974
5965
  import wav from "node-wav";
4975
5966
  function classifyMime(mimeType) {
@@ -5026,14 +6017,14 @@ var init_file_service = __esm({
5026
6017
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
5027
6018
  let removed = 0;
5028
6019
  try {
5029
- const entries = await fs35.promises.readdir(this.baseDir, { withFileTypes: true });
6020
+ const entries = await fs36.promises.readdir(this.baseDir, { withFileTypes: true });
5030
6021
  for (const entry of entries) {
5031
6022
  if (!entry.isDirectory()) continue;
5032
- const dirPath = path35.join(this.baseDir, entry.name);
6023
+ const dirPath = path36.join(this.baseDir, entry.name);
5033
6024
  try {
5034
- const stat = await fs35.promises.stat(dirPath);
6025
+ const stat = await fs36.promises.stat(dirPath);
5035
6026
  if (stat.mtimeMs < cutoff) {
5036
- await fs35.promises.rm(dirPath, { recursive: true, force: true });
6027
+ await fs36.promises.rm(dirPath, { recursive: true, force: true });
5037
6028
  removed++;
5038
6029
  }
5039
6030
  } catch {
@@ -5052,11 +6043,11 @@ var init_file_service = __esm({
5052
6043
  * so the user-facing name is not lost.
5053
6044
  */
5054
6045
  async saveFile(sessionId, fileName, data, mimeType) {
5055
- const sessionDir = path35.join(this.baseDir, sessionId);
5056
- await fs35.promises.mkdir(sessionDir, { recursive: true });
6046
+ const sessionDir = path36.join(this.baseDir, sessionId);
6047
+ await fs36.promises.mkdir(sessionDir, { recursive: true });
5057
6048
  const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
5058
- const filePath = path35.join(sessionDir, safeName);
5059
- await fs35.promises.writeFile(filePath, data);
6049
+ const filePath = path36.join(sessionDir, safeName);
6050
+ await fs36.promises.writeFile(filePath, data);
5060
6051
  return {
5061
6052
  type: classifyMime(mimeType),
5062
6053
  filePath,
@@ -5071,14 +6062,14 @@ var init_file_service = __esm({
5071
6062
  */
5072
6063
  async resolveFile(filePath) {
5073
6064
  try {
5074
- const stat = await fs35.promises.stat(filePath);
6065
+ const stat = await fs36.promises.stat(filePath);
5075
6066
  if (!stat.isFile()) return null;
5076
- const ext = path35.extname(filePath).toLowerCase();
6067
+ const ext = path36.extname(filePath).toLowerCase();
5077
6068
  const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
5078
6069
  return {
5079
6070
  type: classifyMime(mimeType),
5080
6071
  filePath,
5081
- fileName: path35.basename(filePath),
6072
+ fileName: path36.basename(filePath),
5082
6073
  mimeType,
5083
6074
  size: stat.size
5084
6075
  };
@@ -5721,8 +6712,8 @@ data: ${JSON.stringify(data)}
5721
6712
  });
5722
6713
 
5723
6714
  // src/plugins/api-server/static-server.ts
5724
- import * as fs36 from "fs";
5725
- import * as path36 from "path";
6715
+ import * as fs37 from "fs";
6716
+ import * as path37 from "path";
5726
6717
  import { fileURLToPath as fileURLToPath2 } from "url";
5727
6718
  var MIME_TYPES, StaticServer;
5728
6719
  var init_static_server = __esm({
@@ -5746,16 +6737,16 @@ var init_static_server = __esm({
5746
6737
  this.uiDir = uiDir;
5747
6738
  if (!this.uiDir) {
5748
6739
  const __filename = fileURLToPath2(import.meta.url);
5749
- const candidate = path36.resolve(path36.dirname(__filename), "../../ui/dist");
5750
- if (fs36.existsSync(path36.join(candidate, "index.html"))) {
6740
+ const candidate = path37.resolve(path37.dirname(__filename), "../../ui/dist");
6741
+ if (fs37.existsSync(path37.join(candidate, "index.html"))) {
5751
6742
  this.uiDir = candidate;
5752
6743
  }
5753
6744
  if (!this.uiDir) {
5754
- const publishCandidate = path36.resolve(
5755
- path36.dirname(__filename),
6745
+ const publishCandidate = path37.resolve(
6746
+ path37.dirname(__filename),
5756
6747
  "../ui"
5757
6748
  );
5758
- if (fs36.existsSync(path36.join(publishCandidate, "index.html"))) {
6749
+ if (fs37.existsSync(path37.join(publishCandidate, "index.html"))) {
5759
6750
  this.uiDir = publishCandidate;
5760
6751
  }
5761
6752
  }
@@ -5773,23 +6764,23 @@ var init_static_server = __esm({
5773
6764
  serve(req, res) {
5774
6765
  if (!this.uiDir) return false;
5775
6766
  const urlPath = (req.url || "/").split("?")[0];
5776
- const safePath = path36.normalize(urlPath);
5777
- const filePath = path36.join(this.uiDir, safePath);
5778
- if (!filePath.startsWith(this.uiDir + path36.sep) && filePath !== this.uiDir)
6767
+ const safePath = path37.normalize(urlPath);
6768
+ const filePath = path37.join(this.uiDir, safePath);
6769
+ if (!filePath.startsWith(this.uiDir + path37.sep) && filePath !== this.uiDir)
5779
6770
  return false;
5780
6771
  let realFilePath;
5781
6772
  try {
5782
- realFilePath = fs36.realpathSync(filePath);
6773
+ realFilePath = fs37.realpathSync(filePath);
5783
6774
  } catch {
5784
6775
  realFilePath = null;
5785
6776
  }
5786
6777
  if (realFilePath !== null) {
5787
- const realUiDir = fs36.realpathSync(this.uiDir);
5788
- if (!realFilePath.startsWith(realUiDir + path36.sep) && realFilePath !== realUiDir)
6778
+ const realUiDir = fs37.realpathSync(this.uiDir);
6779
+ if (!realFilePath.startsWith(realUiDir + path37.sep) && realFilePath !== realUiDir)
5789
6780
  return false;
5790
6781
  }
5791
- if (realFilePath !== null && fs36.existsSync(realFilePath) && fs36.statSync(realFilePath).isFile()) {
5792
- const ext = path36.extname(filePath);
6782
+ if (realFilePath !== null && fs37.existsSync(realFilePath) && fs37.statSync(realFilePath).isFile()) {
6783
+ const ext = path37.extname(filePath);
5793
6784
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
5794
6785
  const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
5795
6786
  const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
@@ -5797,16 +6788,16 @@ var init_static_server = __esm({
5797
6788
  "Content-Type": contentType,
5798
6789
  "Cache-Control": cacheControl
5799
6790
  });
5800
- fs36.createReadStream(realFilePath).pipe(res);
6791
+ fs37.createReadStream(realFilePath).pipe(res);
5801
6792
  return true;
5802
6793
  }
5803
- const indexPath = path36.join(this.uiDir, "index.html");
5804
- if (fs36.existsSync(indexPath)) {
6794
+ const indexPath = path37.join(this.uiDir, "index.html");
6795
+ if (fs37.existsSync(indexPath)) {
5805
6796
  res.writeHead(200, {
5806
6797
  "Content-Type": "text/html; charset=utf-8",
5807
6798
  "Cache-Control": "no-cache"
5808
6799
  });
5809
- fs36.createReadStream(indexPath).pipe(res);
6800
+ fs37.createReadStream(indexPath).pipe(res);
5810
6801
  return true;
5811
6802
  }
5812
6803
  return false;
@@ -5828,6 +6819,8 @@ var init_speech_service = __esm({
5828
6819
  sttProviders = /* @__PURE__ */ new Map();
5829
6820
  ttsProviders = /* @__PURE__ */ new Map();
5830
6821
  providerFactory;
6822
+ factoryOwnedSTT = /* @__PURE__ */ new Set();
6823
+ factoryOwnedTTS = /* @__PURE__ */ new Set();
5831
6824
  /** Set a factory function that can recreate providers from config (for hot-reload) */
5832
6825
  setProviderFactory(factory) {
5833
6826
  this.providerFactory = factory;
@@ -5835,10 +6828,12 @@ var init_speech_service = __esm({
5835
6828
  /** Register an STT provider by name. Overwrites any existing provider with the same name. */
5836
6829
  registerSTTProvider(name, provider) {
5837
6830
  this.sttProviders.set(name, provider);
6831
+ this.factoryOwnedSTT.delete(name);
5838
6832
  }
5839
6833
  /** Register a TTS provider by name. Called by external TTS plugins (e.g. msedge-tts-plugin). */
5840
6834
  registerTTSProvider(name, provider) {
5841
6835
  this.ttsProviders.set(name, provider);
6836
+ this.factoryOwnedTTS.delete(name);
5842
6837
  }
5843
6838
  /** Remove a TTS provider — called by external plugins on teardown. */
5844
6839
  unregisterTTSProvider(name) {
@@ -5903,16 +6898,22 @@ var init_speech_service = __esm({
5903
6898
  * (e.g. from `@openacp/msedge-tts-plugin`) are preserved rather than discarded.
5904
6899
  */
5905
6900
  refreshProviders(newConfig) {
5906
- this.config = newConfig;
5907
- if (this.providerFactory) {
5908
- const { stt, tts } = this.providerFactory(newConfig);
5909
- for (const [name, provider] of stt) {
5910
- this.sttProviders.set(name, provider);
5911
- }
5912
- for (const [name, provider] of tts) {
5913
- this.ttsProviders.set(name, provider);
5914
- }
6901
+ if (!this.providerFactory) {
6902
+ this.config = newConfig;
6903
+ return;
5915
6904
  }
6905
+ const built = this.providerFactory(newConfig);
6906
+ const nextSTT = new Map(this.sttProviders);
6907
+ const nextTTS = new Map(this.ttsProviders);
6908
+ for (const name of this.factoryOwnedSTT) nextSTT.delete(name);
6909
+ for (const name of this.factoryOwnedTTS) nextTTS.delete(name);
6910
+ for (const [name, provider] of built.stt) nextSTT.set(name, provider);
6911
+ for (const [name, provider] of built.tts) nextTTS.set(name, provider);
6912
+ this.config = newConfig;
6913
+ this.sttProviders = nextSTT;
6914
+ this.ttsProviders = nextTTS;
6915
+ this.factoryOwnedSTT = new Set(built.stt.keys());
6916
+ this.factoryOwnedTTS = new Set(built.tts.keys());
5916
6917
  }
5917
6918
  };
5918
6919
  }
@@ -5929,8 +6930,8 @@ var init_exports = __esm({
5929
6930
  });
5930
6931
 
5931
6932
  // src/plugins/context/context-cache.ts
5932
- import * as fs37 from "fs";
5933
- import * as path37 from "path";
6933
+ import * as fs38 from "fs";
6934
+ import * as path38 from "path";
5934
6935
  import * as crypto2 from "crypto";
5935
6936
  var DEFAULT_TTL_MS, ContextCache;
5936
6937
  var init_context_cache = __esm({
@@ -5941,7 +6942,7 @@ var init_context_cache = __esm({
5941
6942
  constructor(cacheDir, ttlMs = DEFAULT_TTL_MS) {
5942
6943
  this.cacheDir = cacheDir;
5943
6944
  this.ttlMs = ttlMs;
5944
- fs37.mkdirSync(cacheDir, { recursive: true });
6945
+ fs38.mkdirSync(cacheDir, { recursive: true });
5945
6946
  }
5946
6947
  cacheDir;
5947
6948
  ttlMs;
@@ -5949,23 +6950,23 @@ var init_context_cache = __esm({
5949
6950
  return crypto2.createHash("sha256").update(`${repoPath}:${queryKey}`).digest("hex").slice(0, 16);
5950
6951
  }
5951
6952
  filePath(repoPath, queryKey) {
5952
- return path37.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
6953
+ return path38.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
5953
6954
  }
5954
6955
  get(repoPath, queryKey) {
5955
6956
  const fp = this.filePath(repoPath, queryKey);
5956
6957
  try {
5957
- const stat = fs37.statSync(fp);
6958
+ const stat = fs38.statSync(fp);
5958
6959
  if (Date.now() - stat.mtimeMs > this.ttlMs) {
5959
- fs37.unlinkSync(fp);
6960
+ fs38.unlinkSync(fp);
5960
6961
  return null;
5961
6962
  }
5962
- return JSON.parse(fs37.readFileSync(fp, "utf-8"));
6963
+ return JSON.parse(fs38.readFileSync(fp, "utf-8"));
5963
6964
  } catch {
5964
6965
  return null;
5965
6966
  }
5966
6967
  }
5967
6968
  set(repoPath, queryKey, result) {
5968
- fs37.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
6969
+ fs38.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
5969
6970
  }
5970
6971
  };
5971
6972
  }
@@ -6939,8 +7940,8 @@ function formatToolSummary(name, rawInput, displaySummary) {
6939
7940
  }
6940
7941
  if (lowerName === "grep") {
6941
7942
  const pattern = args.pattern ?? "";
6942
- const path38 = args.path ?? "";
6943
- return pattern ? `\u{1F50D} Grep "${pattern}"${path38 ? ` in ${path38}` : ""}` : `\u{1F527} ${name}`;
7943
+ const path39 = args.path ?? "";
7944
+ return pattern ? `\u{1F50D} Grep "${pattern}"${path39 ? ` in ${path39}` : ""}` : `\u{1F527} ${name}`;
6944
7945
  }
6945
7946
  if (lowerName === "glob") {
6946
7947
  const pattern = args.pattern ?? "";
@@ -6976,8 +7977,8 @@ function formatToolTitle(name, rawInput, displayTitle) {
6976
7977
  }
6977
7978
  if (lowerName === "grep") {
6978
7979
  const pattern = args.pattern ?? "";
6979
- const path38 = args.path ?? "";
6980
- return pattern ? `"${pattern}"${path38 ? ` in ${path38}` : ""}` : name;
7980
+ const path39 = args.path ?? "";
7981
+ return pattern ? `"${pattern}"${path39 ? ` in ${path39}` : ""}` : name;
6981
7982
  }
6982
7983
  if (lowerName === "glob") {
6983
7984
  return String(args.pattern ?? name);
@@ -7398,10 +8399,10 @@ var init_send_queue = __esm({
7398
8399
  const type = opts?.type ?? "other";
7399
8400
  const key = opts?.key;
7400
8401
  const category = opts?.category;
7401
- let resolve7;
8402
+ let resolve8;
7402
8403
  let reject;
7403
8404
  const promise = new Promise((res, rej) => {
7404
- resolve7 = res;
8405
+ resolve8 = res;
7405
8406
  reject = rej;
7406
8407
  });
7407
8408
  promise.catch(() => {
@@ -7412,12 +8413,12 @@ var init_send_queue = __esm({
7412
8413
  );
7413
8414
  if (idx !== -1) {
7414
8415
  this.items[idx].resolve(void 0);
7415
- this.items[idx] = { fn, type, key, category, resolve: resolve7, reject, promise };
8416
+ this.items[idx] = { fn, type, key, category, resolve: resolve8, reject, promise };
7416
8417
  this.scheduleProcess();
7417
8418
  return promise;
7418
8419
  }
7419
8420
  }
7420
- this.items.push({ fn, type, key, category, resolve: resolve7, reject, promise });
8421
+ this.items.push({ fn, type, key, category, resolve: resolve8, reject, promise });
7421
8422
  this.scheduleProcess();
7422
8423
  return promise;
7423
8424
  }
@@ -8153,10 +9154,10 @@ function shortenTitle(title, kind, workingDirectory) {
8153
9154
  const relativized = normalizedPathPart.split(", ").map((segment) => segment.startsWith(prefix) ? segment.slice(prefix.length) : segment).join(", ");
8154
9155
  if (relativized !== normalizedPathPart) return relativized + rangePart;
8155
9156
  }
8156
- if (FILE_KINDS.has(kind)) return basename(pathPart) + rangePart;
9157
+ if (FILE_KINDS.has(kind)) return basename2(pathPart) + rangePart;
8157
9158
  return title;
8158
9159
  }
8159
- function basename(pathLike) {
9160
+ function basename2(pathLike) {
8160
9161
  return pathLike.replace(/\\/g, "/").split("/").pop() || pathLike;
8161
9162
  }
8162
9163
  function renderSpecSection(spec) {
@@ -8193,7 +9194,7 @@ function renderSpecSection(spec) {
8193
9194
  }
8194
9195
  if (spec.viewerLinks?.file || spec.viewerLinks?.diff || spec.outputViewerLink) {
8195
9196
  const linkParts = [];
8196
- const linkName = basename(displayTitle || kindLabel || spec.kind);
9197
+ const linkName = basename2(displayTitle || kindLabel || spec.kind);
8197
9198
  if (spec.viewerLinks?.file)
8198
9199
  linkParts.push(`<a href="${escapeHtml(spec.viewerLinks.file)}">View ${escapeHtml(linkName)}</a>`);
8199
9200
  if (spec.viewerLinks?.diff)
@@ -8292,7 +9293,7 @@ __export(version_exports, {
8292
9293
  runUpdate: () => runUpdate
8293
9294
  });
8294
9295
  import { fileURLToPath as fileURLToPath3 } from "url";
8295
- import { dirname as dirname12, join as join16, resolve as resolve6 } from "path";
9296
+ import { dirname as dirname13, join as join16, resolve as resolve7 } from "path";
8296
9297
  import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
8297
9298
  function getUpdateNetwork(instanceRoot) {
8298
9299
  if (!instanceRoot) return {};
@@ -8306,11 +9307,11 @@ function getUpdateNetwork(instanceRoot) {
8306
9307
  };
8307
9308
  }
8308
9309
  function findPackageJson() {
8309
- let dir = dirname12(fileURLToPath3(import.meta.url));
9310
+ let dir = dirname13(fileURLToPath3(import.meta.url));
8310
9311
  for (let i = 0; i < 5; i++) {
8311
9312
  const candidate = join16(dir, "package.json");
8312
9313
  if (existsSync17(candidate)) return candidate;
8313
- const parent = resolve6(dir, "..");
9314
+ const parent = resolve7(dir, "..");
8314
9315
  if (parent === dir) break;
8315
9316
  dir = parent;
8316
9317
  }
@@ -8349,7 +9350,7 @@ function compareVersions(current, latest) {
8349
9350
  }
8350
9351
  async function runUpdate(environment) {
8351
9352
  const { spawn: spawn4 } = await import("child_process");
8352
- return new Promise((resolve7) => {
9353
+ return new Promise((resolve8) => {
8353
9354
  const child = spawn4("npm", ["install", "-g", `${NPM_PACKAGE}@latest`], {
8354
9355
  stdio: "inherit",
8355
9356
  shell: false,
@@ -8357,14 +9358,14 @@ async function runUpdate(environment) {
8357
9358
  });
8358
9359
  const onSignal = () => {
8359
9360
  child.kill("SIGTERM");
8360
- resolve7(false);
9361
+ resolve8(false);
8361
9362
  };
8362
9363
  process.on("SIGINT", onSignal);
8363
9364
  process.on("SIGTERM", onSignal);
8364
9365
  child.on("close", (code) => {
8365
9366
  process.off("SIGINT", onSignal);
8366
9367
  process.off("SIGTERM", onSignal);
8367
- resolve7(code === 0);
9368
+ resolve8(code === 0);
8368
9369
  });
8369
9370
  });
8370
9371
  }
@@ -9196,7 +10197,7 @@ __export(integrate_exports, {
9196
10197
  uninstallIntegration: () => uninstallIntegration
9197
10198
  });
9198
10199
  import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7, chmodSync, rmdirSync } from "fs";
9199
- import { join as join17, dirname as dirname13 } from "path";
10200
+ import { join as join17, dirname as dirname14 } from "path";
9200
10201
  import { homedir as homedir4 } from "os";
9201
10202
  function isHooksIntegrationSpec(spec) {
9202
10203
  return spec.strategy === "hooks";
@@ -9374,7 +10375,7 @@ function mergeSettingsJson(settingsPath, hookEvent, hookScriptPath) {
9374
10375
  hooks: [{ type: "command", command: hookScriptPath }]
9375
10376
  });
9376
10377
  }
9377
- mkdirSync11(dirname13(fullPath), { recursive: true });
10378
+ mkdirSync11(dirname14(fullPath), { recursive: true });
9378
10379
  writeFileSync11(fullPath, JSON.stringify(settings, null, 2) + "\n");
9379
10380
  }
9380
10381
  function mergeHooksJson(settingsPath, hookEvent, hookScriptPath) {
@@ -9393,7 +10394,7 @@ function mergeHooksJson(settingsPath, hookEvent, hookScriptPath) {
9393
10394
  if (!alreadyInstalled) {
9394
10395
  eventHooks.push({ command: hookScriptPath });
9395
10396
  }
9396
- mkdirSync11(dirname13(fullPath), { recursive: true });
10397
+ mkdirSync11(dirname14(fullPath), { recursive: true });
9397
10398
  writeFileSync11(fullPath, JSON.stringify(config, null, 2) + "\n");
9398
10399
  }
9399
10400
  function removeFromSettingsJson(settingsPath, hookEvent) {
@@ -9619,7 +10620,7 @@ function buildTunnelItem(spec) {
9619
10620
  const logs = [];
9620
10621
  try {
9621
10622
  const skillPath = getTunnelPath();
9622
- mkdirSync11(dirname13(skillPath), { recursive: true });
10623
+ mkdirSync11(dirname14(skillPath), { recursive: true });
9623
10624
  writeFileSync11(skillPath, generateTunnelCommand());
9624
10625
  logs.push(`Created ${skillPath}`);
9625
10626
  return { success: true, logs };
@@ -9635,7 +10636,7 @@ function buildTunnelItem(spec) {
9635
10636
  if (existsSync18(skillPath)) {
9636
10637
  unlinkSync7(skillPath);
9637
10638
  try {
9638
- rmdirSync(dirname13(skillPath));
10639
+ rmdirSync(dirname14(skillPath));
9639
10640
  } catch {
9640
10641
  }
9641
10642
  logs.push(`Removed ${skillPath}`);
@@ -10168,6 +11169,7 @@ async function buildSettingsKeyboard(core) {
10168
11169
  }
10169
11170
  }
10170
11171
  kb.text("\u{1F310} Proxy Management", settingsCommandCallback("/proxy")).row();
11172
+ kb.text("\u{1F399} Speech-to-Text", settingsCommandCallback("/speech")).row();
10171
11173
  kb.text("\u25C0\uFE0F Back to Menu", "s:back");
10172
11174
  return kb;
10173
11175
  }
@@ -10255,33 +11257,6 @@ Select a value:`, {
10255
11257
  const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
10256
11258
  if (!fieldDef) return;
10257
11259
  try {
10258
- if (fieldPath === "speech.stt.provider") {
10259
- const sm = core.settingsManager;
10260
- let hasApiKey = false;
10261
- if (sm) {
10262
- const speechSettings = await sm.loadSettings("@openacp/speech");
10263
- hasApiKey = !!speechSettings.groqApiKey;
10264
- } else {
10265
- hasApiKey = false;
10266
- }
10267
- if (!hasApiKey) {
10268
- const assistant = getAssistantSession();
10269
- if (assistant) {
10270
- try {
10271
- await ctx.answerCallbackQuery({ text: `\u{1F511} API key needed \u2014 check Assistant topic` });
10272
- } catch {
10273
- }
10274
- const prompt = `User wants to enable ${newValue} as Speech-to-Text provider, but no API key is configured yet. Guide them to get a ${newValue} API key and set it up. After they provide the key, run both commands: \`openacp config set speech.stt.providers.${newValue}.apiKey <key>\` and \`openacp config set speech.stt.provider ${newValue}\``;
10275
- await assistant.enqueuePrompt(prompt);
10276
- return;
10277
- }
10278
- try {
10279
- await ctx.answerCallbackQuery({ text: `\u26A0\uFE0F Set API key first: openacp config set speech.stt.providers.${newValue}.apiKey <key>` });
10280
- } catch {
10281
- }
10282
- return;
10283
- }
10284
- }
10285
11260
  await setFieldValueAsync(fieldDef, newValue, core.configManager);
10286
11261
  try {
10287
11262
  await ctx.answerCallbackQuery({ text: `\u2705 ${fieldPath} = ${newValue}` });
@@ -11978,7 +12953,7 @@ var init_types = __esm({
11978
12953
  });
11979
12954
 
11980
12955
  // src/core/commands/proxy.ts
11981
- import { randomUUID as randomUUID3 } from "crypto";
12956
+ import { randomUUID as randomUUID6 } from "crypto";
11982
12957
  function clearProxyDraftsForChannel(channelId) {
11983
12958
  const prefix = `${channelId}:`;
11984
12959
  for (const [id, draft] of drafts) if (draft.owner.startsWith(prefix)) drafts.delete(id);
@@ -11997,7 +12972,7 @@ var init_proxy2 = __esm({
11997
12972
  });
11998
12973
 
11999
12974
  // src/plugins/telegram/command-sync.ts
12000
- import { createHash as createHash4 } from "crypto";
12975
+ import { createHash as createHash6 } from "crypto";
12001
12976
  function validDescription(description) {
12002
12977
  if (typeof description !== "string") return false;
12003
12978
  const trimmed = description.trim();
@@ -12066,7 +13041,7 @@ function localeLabel(locale) {
12066
13041
  async function synchronizeTelegramCommands(api, chatId, desired, options) {
12067
13042
  if (!/^\d{1,20}$/.test(options.botId)) throw new Error("Telegram bot identity is invalid");
12068
13043
  assertTelegramCommandBoundary(desired);
12069
- const chatKey = createHash4("sha256").update(String(chatId)).digest("hex").slice(0, 16);
13044
+ const chatKey = createHash6("sha256").update(String(chatId)).digest("hex").slice(0, 16);
12070
13045
  const scopes = [
12071
13046
  { name: "default", scope: { type: "default" }, ledgerName: "default" },
12072
13047
  { name: "chat", scope: { type: "chat", chat_id: chatId }, ledgerName: `chat:${chatKey}` },
@@ -13063,11 +14038,11 @@ ${p}` : p;
13063
14038
  { error: safeError2, attempt, maxAttempts, delayMs, operation: "telegram-command-sync" },
13064
14039
  "Telegram command menu sync failed, retrying"
13065
14040
  );
13066
- await new Promise((resolve7) => {
13067
- const timer = setTimeout(resolve7, delayMs);
14041
+ await new Promise((resolve8) => {
14042
+ const timer = setTimeout(resolve8, delayMs);
13068
14043
  signal?.addEventListener("abort", () => {
13069
14044
  clearTimeout(timer);
13070
- resolve7();
14045
+ resolve8();
13071
14046
  }, { once: true });
13072
14047
  });
13073
14048
  }
@@ -14361,15 +15336,15 @@ var ChannelAdapter = class {
14361
15336
  function nodeToWebWritable(nodeStream) {
14362
15337
  return new WritableStream({
14363
15338
  write(chunk) {
14364
- return new Promise((resolve7, reject) => {
15339
+ return new Promise((resolve8, reject) => {
14365
15340
  const ok2 = nodeStream.write(chunk);
14366
15341
  if (ok2) {
14367
- resolve7();
15342
+ resolve8();
14368
15343
  return;
14369
15344
  }
14370
15345
  const onDrain = () => {
14371
15346
  nodeStream.removeListener("error", onError);
14372
- resolve7();
15347
+ resolve8();
14373
15348
  };
14374
15349
  const onError = (err) => {
14375
15350
  nodeStream.removeListener("drain", onDrain);
@@ -14861,12 +15836,12 @@ var TerminalManager = class {
14861
15836
  signal: state.exitStatus.signal
14862
15837
  };
14863
15838
  }
14864
- return new Promise((resolve7) => {
15839
+ return new Promise((resolve8) => {
14865
15840
  state.process.on("exit", (code, signal) => {
14866
- resolve7({ exitCode: code, signal });
15841
+ resolve8({ exitCode: code, signal });
14867
15842
  });
14868
15843
  if (state.exitStatus !== null) {
14869
- resolve7({
15844
+ resolve8({
14870
15845
  exitCode: state.exitStatus.exitCode,
14871
15846
  signal: state.exitStatus.signal
14872
15847
  });
@@ -15148,7 +16123,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
15148
16123
  env: environment ?? filterEnv(process.env, agentDef.env)
15149
16124
  }
15150
16125
  );
15151
- await new Promise((resolve7, reject) => {
16126
+ await new Promise((resolve8, reject) => {
15152
16127
  instance.child.on("error", (err) => {
15153
16128
  reject(
15154
16129
  new Error(
@@ -15156,7 +16131,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
15156
16131
  )
15157
16132
  );
15158
16133
  });
15159
- instance.child.on("spawn", () => resolve7());
16134
+ instance.child.on("spawn", () => resolve8());
15160
16135
  });
15161
16136
  instance.stderrCapture = new StderrCapture(50);
15162
16137
  instance.child.stderr.on("data", (chunk) => {
@@ -15783,15 +16758,15 @@ ${skipNote}`;
15783
16758
  this._destroying = true;
15784
16759
  this.terminalManager.destroyAll();
15785
16760
  if (this.child.exitCode !== null) return;
15786
- await new Promise((resolve7) => {
16761
+ await new Promise((resolve8) => {
15787
16762
  this.child.on("exit", () => {
15788
16763
  clearTimeout(forceKillTimer);
15789
- resolve7();
16764
+ resolve8();
15790
16765
  });
15791
16766
  this.child.kill("SIGTERM");
15792
16767
  const forceKillTimer = setTimeout(() => {
15793
16768
  if (this.child.exitCode === null) this.child.kill("SIGKILL");
15794
- resolve7();
16769
+ resolve8();
15795
16770
  }, 1e4);
15796
16771
  if (typeof forceKillTimer === "object" && forceKillTimer !== null && "unref" in forceKillTimer) {
15797
16772
  forceKillTimer.unref();
@@ -15802,7 +16777,7 @@ ${skipNote}`;
15802
16777
  var AGENT_INIT_TIMEOUT_MS = 3e4;
15803
16778
  function withAgentTimeout(promise, agentName, op, timeoutMs) {
15804
16779
  const timeout = timeoutMs ?? AGENT_INIT_TIMEOUT_MS;
15805
- return new Promise((resolve7, reject) => {
16780
+ return new Promise((resolve8, reject) => {
15806
16781
  const timer = setTimeout(() => {
15807
16782
  reject(new Error(
15808
16783
  `Agent "${agentName}" did not respond to ${op} within ${timeout / 1e3}s. The agent process may have hung during initialization.`
@@ -15811,7 +16786,7 @@ function withAgentTimeout(promise, agentName, op, timeoutMs) {
15811
16786
  promise.then(
15812
16787
  (val) => {
15813
16788
  clearTimeout(timer);
15814
- resolve7(val);
16789
+ resolve8(val);
15815
16790
  },
15816
16791
  (err) => {
15817
16792
  clearTimeout(timer);
@@ -16075,8 +17050,8 @@ var PromptQueue = class {
16075
17050
  if (this.processing) {
16076
17051
  const position = this.queue.length + 1;
16077
17052
  this.onActuallyQueued?.(turnId, position, routing);
16078
- return new Promise((resolve7) => {
16079
- this.queue.push({ text: text3, userPrompt, attachments, routing, turnId, meta, resolve: resolve7 });
17053
+ return new Promise((resolve8) => {
17054
+ this.queue.push({ text: text3, userPrompt, attachments, routing, turnId, meta, resolve: resolve8 });
16080
17055
  });
16081
17056
  }
16082
17057
  await this.process(text3, userPrompt, attachments, routing, turnId, meta);
@@ -16206,8 +17181,8 @@ var PermissionGate = class {
16206
17181
  this.request = request;
16207
17182
  this.settled = false;
16208
17183
  this.clearTimeout();
16209
- return new Promise((resolve7, reject) => {
16210
- this.resolveFn = resolve7;
17184
+ return new Promise((resolve8, reject) => {
17185
+ this.resolveFn = resolve8;
16211
17186
  this.rejectFn = reject;
16212
17187
  this.timeoutTimer = setTimeout(() => {
16213
17188
  this.reject("Permission request timed out (no response received)");
@@ -18123,12 +19098,12 @@ var SessionBridge = class {
18123
19098
  break;
18124
19099
  case "image_content": {
18125
19100
  if (this.deps.fileService) {
18126
- const fs38 = this.deps.fileService;
19101
+ const fs39 = this.deps.fileService;
18127
19102
  const sid = this.session.id;
18128
19103
  const { data, mimeType } = event;
18129
19104
  const buffer = Buffer.from(data, "base64");
18130
- const ext = fs38.extensionFromMime(mimeType);
18131
- fs38.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
19105
+ const ext = fs39.extensionFromMime(mimeType);
19106
+ fs39.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
18132
19107
  this.sendMessage(sid, {
18133
19108
  type: "attachment",
18134
19109
  text: "",
@@ -18140,12 +19115,12 @@ var SessionBridge = class {
18140
19115
  }
18141
19116
  case "audio_content": {
18142
19117
  if (this.deps.fileService) {
18143
- const fs38 = this.deps.fileService;
19118
+ const fs39 = this.deps.fileService;
18144
19119
  const sid = this.session.id;
18145
19120
  const { data, mimeType } = event;
18146
19121
  const buffer = Buffer.from(data, "base64");
18147
- const ext = fs38.extensionFromMime(mimeType);
18148
- fs38.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
19122
+ const ext = fs39.extensionFromMime(mimeType);
19123
+ fs39.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
18149
19124
  this.sendMessage(sid, {
18150
19125
  type: "attachment",
18151
19126
  text: "",
@@ -18757,7 +19732,7 @@ var SessionFactory = class {
18757
19732
  };
18758
19733
 
18759
19734
  // src/core/core.ts
18760
- import path16 from "path";
19735
+ import path18 from "path";
18761
19736
  import { nanoid as nanoid3 } from "nanoid";
18762
19737
 
18763
19738
  // src/core/sessions/session-store.ts
@@ -20192,16 +21167,51 @@ function createPluginContext(opts) {
20192
21167
 
20193
21168
  // src/core/plugin/lifecycle-manager.ts
20194
21169
  init_events();
21170
+ init_plugin_installer();
21171
+ import fs16 from "fs";
21172
+ import path15 from "path";
21173
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
20195
21174
  var SETUP_TIMEOUT_MS = 3e4;
20196
21175
  var TEARDOWN_TIMEOUT_MS = 1e4;
21176
+ function migrationGuardPath(instanceRoot, pluginName) {
21177
+ return path15.join(instanceRoot, "plugin-migration-quarantine", `${createHash3("sha256").update(pluginName).digest("hex")}.json`);
21178
+ }
21179
+ function writeMigrationGuard(instanceRoot, pluginName, fromVersion, toVersion) {
21180
+ const guardPath = migrationGuardPath(instanceRoot, pluginName);
21181
+ const directory = path15.dirname(guardPath);
21182
+ fs16.mkdirSync(directory, { recursive: true, mode: 448 });
21183
+ fs16.chmodSync(directory, 448);
21184
+ const temporary = `${guardPath}.${process.pid}.${randomUUID3()}.tmp`;
21185
+ try {
21186
+ fs16.writeFileSync(temporary, `${JSON.stringify({ version: 1, pluginName, fromVersion, toVersion, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
21187
+ `, { flag: "wx", mode: 384 });
21188
+ const fd = fs16.openSync(temporary, "r");
21189
+ fs16.fsyncSync(fd);
21190
+ fs16.closeSync(fd);
21191
+ fs16.renameSync(temporary, guardPath);
21192
+ fs16.chmodSync(guardPath, 384);
21193
+ try {
21194
+ const dir = fs16.openSync(directory, "r");
21195
+ fs16.fsyncSync(dir);
21196
+ fs16.closeSync(dir);
21197
+ } catch {
21198
+ }
21199
+ } finally {
21200
+ try {
21201
+ if (fs16.existsSync(temporary)) fs16.unlinkSync(temporary);
21202
+ } catch {
21203
+ }
21204
+ }
21205
+ return guardPath;
21206
+ }
20197
21207
  function withTimeout(promise, ms, label) {
20198
- return new Promise((resolve7, reject) => {
21208
+ return new Promise((resolve8, reject) => {
20199
21209
  const timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
20200
21210
  if (typeof timer === "object" && timer !== null && "unref" in timer) {
20201
21211
  ;
20202
21212
  timer.unref();
20203
21213
  }
20204
- promise.then(resolve7, reject).finally(() => clearTimeout(timer));
21214
+ promise.then(resolve8, reject).finally(() => clearTimeout(timer));
20205
21215
  });
20206
21216
  }
20207
21217
  function resolvePluginConfig(pluginName, configManager) {
@@ -20246,6 +21256,8 @@ var LifecycleManager = class {
20246
21256
  settingsManager;
20247
21257
  pluginRegistry;
20248
21258
  _instanceRoot;
21259
+ installRecoveryChecked = false;
21260
+ communityRecoveryError;
20249
21261
  contexts = /* @__PURE__ */ new Map();
20250
21262
  loadOrder = [];
20251
21263
  _loaded = /* @__PURE__ */ new Set();
@@ -20312,6 +21324,17 @@ var LifecycleManager = class {
20312
21324
  * Already-loaded plugins are included in dependency resolution but not re-booted.
20313
21325
  */
20314
21326
  async boot(plugins) {
21327
+ if (!this.installRecoveryChecked && this._instanceRoot && this.pluginRegistry) {
21328
+ this.installRecoveryChecked = true;
21329
+ try {
21330
+ await recoverPluginInstallTransaction(this._instanceRoot);
21331
+ await this.pluginRegistry.load();
21332
+ } catch (error) {
21333
+ const code = error instanceof PluginInstallError ? error.code : "PLUGIN_RECOVERY_FAILED";
21334
+ this.communityRecoveryError = `${code}: Community plugin transaction recovery did not complete; community plugins are quarantined until recovery succeeds.`;
21335
+ this.log?.error(this.communityRecoveryError);
21336
+ }
21337
+ }
20315
21338
  const newNames = new Set(plugins.map((p) => p.name));
20316
21339
  const allForResolution = [...this.loadOrder.filter((p) => !newNames.has(p.name)), ...plugins];
20317
21340
  let sorted;
@@ -20345,13 +21368,27 @@ var LifecycleManager = class {
20345
21368
  }
20346
21369
  }
20347
21370
  const registryEntry = this.pluginRegistry?.get(plugin.name);
21371
+ if (registryEntry && registryEntry.source !== "builtin" && this.communityRecoveryError) {
21372
+ this._failed.add(plugin.name);
21373
+ this.eventBus?.emit(BusEvent.PLUGIN_FAILED, { name: plugin.name, code: "PLUGIN_RECOVERY_FAILED", error: this.communityRecoveryError });
21374
+ continue;
21375
+ }
21376
+ if (this._instanceRoot && fs16.existsSync(migrationGuardPath(this._instanceRoot, plugin.name))) {
21377
+ const status = `PLUGIN_MIGRATION_ROLLBACK_FAILED: ${plugin.name} has a durable migration quarantine marker. Repair or restore its settings and registry entry, then remove the marker before re-enabling it.`;
21378
+ this._failed.add(plugin.name);
21379
+ this.getPluginLogger(plugin.name).error(status);
21380
+ this.eventBus?.emit(BusEvent.PLUGIN_FAILED, { name: plugin.name, code: "PLUGIN_MIGRATION_ROLLBACK_FAILED", error: status });
21381
+ continue;
21382
+ }
20348
21383
  if (registryEntry && registryEntry.enabled === false) {
20349
21384
  this.eventBus?.emit(BusEvent.PLUGIN_DISABLED, { name: plugin.name });
20350
21385
  continue;
20351
21386
  }
20352
21387
  if (registryEntry && plugin.migrate && registryEntry.version !== plugin.version && this.settingsManager) {
21388
+ const oldSettings = structuredClone(await this.settingsManager.loadSettings(plugin.name));
21389
+ const oldRegistryEntry = structuredClone(registryEntry);
21390
+ const migrationGuard = this._instanceRoot ? writeMigrationGuard(this._instanceRoot, plugin.name, registryEntry.version, plugin.version) : void 0;
20353
21391
  try {
20354
- const oldSettings = await this.settingsManager.loadSettings(plugin.name);
20355
21392
  const pluginLog = this.getPluginLogger(plugin.name);
20356
21393
  const migrateCtx = {
20357
21394
  pluginName: plugin.name,
@@ -20363,13 +21400,38 @@ var LifecycleManager = class {
20363
21400
  SETUP_TIMEOUT_MS,
20364
21401
  `${plugin.name}.migrate()`
20365
21402
  );
20366
- if (newSettings && typeof newSettings === "object") {
20367
- await migrateCtx.settings.setAll(newSettings);
21403
+ const migratedSettings = newSettings && typeof newSettings === "object" ? newSettings : await migrateCtx.settings.getAll();
21404
+ if (plugin.settingsSchema) {
21405
+ const validation = this.settingsManager.validateSettings(plugin.name, migratedSettings, plugin.settingsSchema);
21406
+ if (!validation.valid) throw new Error(`migrated settings invalid: ${validation.errors?.join("; ")}`);
20368
21407
  }
21408
+ await migrateCtx.settings.setAll(migratedSettings);
20369
21409
  this.pluginRegistry.updateVersion(plugin.name, plugin.version);
20370
21410
  await this.pluginRegistry.save();
20371
- } catch (err) {
20372
- this.getPluginLogger(plugin.name).warn(`Migration failed, continuing with old settings: ${err}`);
21411
+ if (migrationGuard) fs16.rmSync(migrationGuard, { force: true });
21412
+ } catch {
21413
+ let settingsRestored = true;
21414
+ try {
21415
+ await this.settingsManager.createAPI(plugin.name).setAll(oldSettings);
21416
+ } catch {
21417
+ settingsRestored = false;
21418
+ }
21419
+ this.pluginRegistry.restore(plugin.name, oldRegistryEntry);
21420
+ let registryRestored = true;
21421
+ try {
21422
+ await this.pluginRegistry.save();
21423
+ } catch {
21424
+ registryRestored = false;
21425
+ }
21426
+ if (settingsRestored && registryRestored && migrationGuard) fs16.rmSync(migrationGuard, { force: true });
21427
+ this._failed.add(plugin.name);
21428
+ const rollbackComplete = settingsRestored && registryRestored;
21429
+ const code = rollbackComplete ? "PLUGIN_MIGRATION_FAILED" : "PLUGIN_MIGRATION_ROLLBACK_FAILED";
21430
+ const recovery = rollbackComplete ? "the exact previous registry entry and settings were restored" : "rollback persistence was incomplete and the durable migration quarantine remains in place";
21431
+ const status = `${code}: ${plugin.name} migration ${oldRegistryEntry.version} \u2192 ${plugin.version} failed; ${recovery}. Fix the migration/settings before retrying.`;
21432
+ this.getPluginLogger(plugin.name).error(status);
21433
+ this.eventBus?.emit(BusEvent.PLUGIN_FAILED, { name: plugin.name, code, error: status });
21434
+ continue;
20373
21435
  }
20374
21436
  }
20375
21437
  let pluginConfig;
@@ -21049,7 +22111,7 @@ var OpenACPCore = class {
21049
22111
  }
21050
22112
  );
21051
22113
  registerCoreMenuItems(this.menuRegistry);
21052
- this.assistantRegistry.setInstanceRoot(path16.dirname(ctx.root));
22114
+ this.assistantRegistry.setInstanceRoot(path18.dirname(ctx.root));
21053
22115
  this.assistantRegistry.register(createSessionsSection(this));
21054
22116
  this.assistantRegistry.register(createAgentsSection(this));
21055
22117
  this.assistantRegistry.register(createConfigSection(this));
@@ -21869,33 +22931,33 @@ init_doctor();
21869
22931
  init_config_registry();
21870
22932
 
21871
22933
  // src/core/config/config-editor.ts
21872
- import * as path33 from "path";
22934
+ import * as path34 from "path";
21873
22935
  import * as clack2 from "@clack/prompts";
21874
22936
 
21875
22937
  // src/cli/autostart.ts
21876
22938
  init_log();
21877
22939
  import { execFileSync as execFileSync6 } from "child_process";
21878
- import * as fs29 from "fs";
21879
- import * as path27 from "path";
22940
+ import * as fs31 from "fs";
22941
+ import * as path29 from "path";
21880
22942
  import * as os9 from "os";
21881
22943
  var log20 = createChildLogger({ module: "autostart" });
21882
- var LEGACY_LAUNCHD_PLIST_PATH = path27.join(os9.homedir(), "Library", "LaunchAgents", "com.openacp.daemon.plist");
21883
- var LEGACY_SYSTEMD_SERVICE_PATH = path27.join(os9.homedir(), ".config", "systemd", "user", "openacp.service");
22944
+ var LEGACY_LAUNCHD_PLIST_PATH = path29.join(os9.homedir(), "Library", "LaunchAgents", "com.openacp.daemon.plist");
22945
+ var LEGACY_SYSTEMD_SERVICE_PATH = path29.join(os9.homedir(), ".config", "systemd", "user", "openacp.service");
21884
22946
  function getLaunchdLabel(instanceId) {
21885
22947
  return `com.openacp.daemon.${instanceId}`;
21886
22948
  }
21887
22949
  function getLaunchdPlistPath(instanceId) {
21888
- return path27.join(os9.homedir(), "Library", "LaunchAgents", `${getLaunchdLabel(instanceId)}.plist`);
22950
+ return path29.join(os9.homedir(), "Library", "LaunchAgents", `${getLaunchdLabel(instanceId)}.plist`);
21889
22951
  }
21890
22952
  function getSystemdServiceName(instanceId) {
21891
22953
  return `openacp-${instanceId}`;
21892
22954
  }
21893
22955
  function getSystemdServicePath(instanceId) {
21894
- return path27.join(os9.homedir(), ".config", "systemd", "user", `${getSystemdServiceName(instanceId)}.service`);
22956
+ return path29.join(os9.homedir(), ".config", "systemd", "user", `${getSystemdServiceName(instanceId)}.service`);
21895
22957
  }
21896
22958
  function getAutoStartState(instanceId) {
21897
22959
  if (process.platform === "linux") {
21898
- const installed = fs29.existsSync(getSystemdServicePath(instanceId));
22960
+ const installed = fs31.existsSync(getSystemdServicePath(instanceId));
21899
22961
  if (!installed) return { installed: false, manager: null, active: false };
21900
22962
  try {
21901
22963
  const output = execFileSync6("systemctl", [
@@ -21917,7 +22979,7 @@ function getAutoStartState(instanceId) {
21917
22979
  }
21918
22980
  }
21919
22981
  if (process.platform === "darwin") {
21920
- const installed = fs29.existsSync(getLaunchdPlistPath(instanceId));
22982
+ const installed = fs31.existsSync(getLaunchdPlistPath(instanceId));
21921
22983
  if (!installed) return { installed: false, manager: null, active: false };
21922
22984
  try {
21923
22985
  const uid = process.getuid();
@@ -21947,7 +23009,7 @@ function escapeSystemdValue(str) {
21947
23009
  }
21948
23010
  function generateLaunchdPlist(nodePath, cliPath, logDir2, instanceRoot, instanceId) {
21949
23011
  const label = getLaunchdLabel(instanceId);
21950
- const logFile = path27.join(logDir2, "openacp.log");
23012
+ const logFile = path29.join(logDir2, "openacp.log");
21951
23013
  return `<?xml version="1.0" encoding="UTF-8"?>
21952
23014
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
21953
23015
  <plist version="1.0">
@@ -21999,25 +23061,25 @@ WantedBy=default.target
21999
23061
  `;
22000
23062
  }
22001
23063
  function migrateLegacy() {
22002
- if (process.platform === "darwin" && fs29.existsSync(LEGACY_LAUNCHD_PLIST_PATH)) {
23064
+ if (process.platform === "darwin" && fs31.existsSync(LEGACY_LAUNCHD_PLIST_PATH)) {
22003
23065
  try {
22004
23066
  const uid = process.getuid();
22005
23067
  execFileSync6("launchctl", ["bootout", `gui/${uid}`, "com.openacp.daemon"], { stdio: "pipe" });
22006
23068
  } catch {
22007
23069
  }
22008
23070
  try {
22009
- fs29.unlinkSync(LEGACY_LAUNCHD_PLIST_PATH);
23071
+ fs31.unlinkSync(LEGACY_LAUNCHD_PLIST_PATH);
22010
23072
  } catch {
22011
23073
  }
22012
23074
  log20.info("Removed legacy single-instance LaunchAgent");
22013
23075
  }
22014
- if (process.platform === "linux" && fs29.existsSync(LEGACY_SYSTEMD_SERVICE_PATH)) {
23076
+ if (process.platform === "linux" && fs31.existsSync(LEGACY_SYSTEMD_SERVICE_PATH)) {
22015
23077
  try {
22016
23078
  execFileSync6("systemctl", ["--user", "disable", "openacp"], { stdio: "pipe" });
22017
23079
  } catch {
22018
23080
  }
22019
23081
  try {
22020
- fs29.unlinkSync(LEGACY_SYSTEMD_SERVICE_PATH);
23082
+ fs31.unlinkSync(LEGACY_SYSTEMD_SERVICE_PATH);
22021
23083
  } catch {
22022
23084
  }
22023
23085
  try {
@@ -22032,26 +23094,26 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
22032
23094
  return { success: false, error: "Auto-start not supported on this platform" };
22033
23095
  }
22034
23096
  const nodePath = process.execPath;
22035
- const cliPath = path27.resolve(process.argv[1]);
22036
- const resolvedLogDir = logDir2.startsWith("~") ? path27.join(os9.homedir(), logDir2.slice(1)) : logDir2;
23097
+ const cliPath = path29.resolve(process.argv[1]);
23098
+ const resolvedLogDir = logDir2.startsWith("~") ? path29.join(os9.homedir(), logDir2.slice(1)) : logDir2;
22037
23099
  try {
22038
23100
  migrateLegacy();
22039
23101
  if (process.platform === "darwin") {
22040
23102
  const plistPath = getLaunchdPlistPath(instanceId);
22041
23103
  const plist = generateLaunchdPlist(nodePath, cliPath, resolvedLogDir, instanceRoot, instanceId);
22042
- const dir = path27.dirname(plistPath);
22043
- fs29.mkdirSync(dir, { recursive: true });
23104
+ const dir = path29.dirname(plistPath);
23105
+ fs31.mkdirSync(dir, { recursive: true });
22044
23106
  const uid = process.getuid();
22045
23107
  const domain = `gui/${uid}`;
22046
- const previous = fs29.existsSync(plistPath) ? fs29.readFileSync(plistPath, "utf8") : void 0;
23108
+ const previous = fs31.existsSync(plistPath) ? fs31.readFileSync(plistPath, "utf8") : void 0;
22047
23109
  const previousState = getAutoStartState(instanceId);
22048
23110
  const tmp = `${plistPath}.${process.pid}.tmp`;
22049
23111
  let replaced = false;
22050
23112
  let bootedOut = false;
22051
23113
  try {
22052
- fs29.writeFileSync(tmp, plist, { mode: 384 });
23114
+ fs31.writeFileSync(tmp, plist, { mode: 384 });
22053
23115
  execFileSync6("plutil", ["-lint", tmp], { stdio: "pipe" });
22054
- fs29.renameSync(tmp, plistPath);
23116
+ fs31.renameSync(tmp, plistPath);
22055
23117
  replaced = true;
22056
23118
  if (previousState.active) {
22057
23119
  execFileSync6("launchctl", ["bootout", domain, plistPath], { stdio: "pipe" });
@@ -22060,7 +23122,7 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
22060
23122
  execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
22061
23123
  } catch (error) {
22062
23124
  try {
22063
- fs29.rmSync(tmp, { force: true });
23125
+ fs31.rmSync(tmp, { force: true });
22064
23126
  } catch {
22065
23127
  }
22066
23128
  if (replaced) {
@@ -22070,11 +23132,11 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
22070
23132
  rollbackBootedOut = true;
22071
23133
  } catch {
22072
23134
  }
22073
- if (previous === void 0) fs29.rmSync(plistPath, { force: true });
23135
+ if (previous === void 0) fs31.rmSync(plistPath, { force: true });
22074
23136
  else {
22075
23137
  const rollbackTmp = `${plistPath}.${process.pid}.rollback.tmp`;
22076
- fs29.writeFileSync(rollbackTmp, previous, { mode: 384 });
22077
- fs29.renameSync(rollbackTmp, plistPath);
23138
+ fs31.writeFileSync(rollbackTmp, previous, { mode: 384 });
23139
+ fs31.renameSync(rollbackTmp, plistPath);
22078
23140
  if (previousState.active || bootedOut || rollbackBootedOut) execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
22079
23141
  }
22080
23142
  }
@@ -22087,18 +23149,18 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
22087
23149
  const servicePath = getSystemdServicePath(instanceId);
22088
23150
  const serviceName = getSystemdServiceName(instanceId);
22089
23151
  const unit = generateSystemdUnit(nodePath, cliPath, instanceRoot, instanceId);
22090
- const dir = path27.dirname(servicePath);
22091
- fs29.mkdirSync(dir, { recursive: true });
22092
- const previous = fs29.existsSync(servicePath) ? fs29.readFileSync(servicePath, "utf8") : void 0;
23152
+ const dir = path29.dirname(servicePath);
23153
+ fs31.mkdirSync(dir, { recursive: true });
23154
+ const previous = fs31.existsSync(servicePath) ? fs31.readFileSync(servicePath, "utf8") : void 0;
22093
23155
  const tmp = `${servicePath}.${process.pid}.tmp`;
22094
- fs29.writeFileSync(tmp, unit, { mode: 384 });
22095
- fs29.renameSync(tmp, servicePath);
23156
+ fs31.writeFileSync(tmp, unit, { mode: 384 });
23157
+ fs31.renameSync(tmp, servicePath);
22096
23158
  try {
22097
23159
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
22098
23160
  execFileSync6("systemctl", ["--user", "enable", serviceName], { stdio: "pipe" });
22099
23161
  } catch (error) {
22100
- if (previous === void 0) fs29.unlinkSync(servicePath);
22101
- else fs29.writeFileSync(servicePath, previous);
23162
+ if (previous === void 0) fs31.unlinkSync(servicePath);
23163
+ else fs31.writeFileSync(servicePath, previous);
22102
23164
  try {
22103
23165
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
22104
23166
  } catch {
@@ -22122,13 +23184,13 @@ function uninstallAutoStart(instanceId) {
22122
23184
  try {
22123
23185
  if (process.platform === "darwin") {
22124
23186
  const plistPath = getLaunchdPlistPath(instanceId);
22125
- if (fs29.existsSync(plistPath)) {
23187
+ if (fs31.existsSync(plistPath)) {
22126
23188
  const uid = process.getuid();
22127
23189
  try {
22128
23190
  execFileSync6("launchctl", ["bootout", `gui/${uid}`, plistPath], { stdio: "pipe" });
22129
23191
  } catch {
22130
23192
  }
22131
- fs29.unlinkSync(plistPath);
23193
+ fs31.unlinkSync(plistPath);
22132
23194
  log20.info({ instanceId }, "LaunchAgent removed");
22133
23195
  }
22134
23196
  return { success: true };
@@ -22136,12 +23198,12 @@ function uninstallAutoStart(instanceId) {
22136
23198
  if (process.platform === "linux") {
22137
23199
  const servicePath = getSystemdServicePath(instanceId);
22138
23200
  const serviceName = getSystemdServiceName(instanceId);
22139
- if (fs29.existsSync(servicePath)) {
23201
+ if (fs31.existsSync(servicePath)) {
22140
23202
  try {
22141
23203
  execFileSync6("systemctl", ["--user", "disable", serviceName], { stdio: "pipe" });
22142
23204
  } catch {
22143
23205
  }
22144
- fs29.unlinkSync(servicePath);
23206
+ fs31.unlinkSync(servicePath);
22145
23207
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
22146
23208
  log20.info({ instanceId }, "systemd user service removed");
22147
23209
  }
@@ -22156,10 +23218,10 @@ function uninstallAutoStart(instanceId) {
22156
23218
  }
22157
23219
  function isAutoStartInstalled(instanceId) {
22158
23220
  if (process.platform === "darwin") {
22159
- return fs29.existsSync(getLaunchdPlistPath(instanceId));
23221
+ return fs31.existsSync(getLaunchdPlistPath(instanceId));
22160
23222
  }
22161
23223
  if (process.platform === "linux") {
22162
- return fs29.existsSync(getSystemdServicePath(instanceId));
23224
+ return fs31.existsSync(getSystemdServicePath(instanceId));
22163
23225
  }
22164
23226
  return false;
22165
23227
  }
@@ -22168,25 +23230,25 @@ function isAutoStartInstalled(instanceId) {
22168
23230
  init_instance_context();
22169
23231
  init_instance_registry();
22170
23232
  init_log();
22171
- import fs31 from "fs";
22172
- import path29 from "path";
23233
+ import fs33 from "fs";
23234
+ import path31 from "path";
22173
23235
  var log21 = createChildLogger({ module: "resolve-instance-id" });
22174
23236
  function resolveInstanceId(instanceRoot) {
22175
23237
  try {
22176
- const configPath = path29.join(instanceRoot, "config.json");
22177
- const raw = JSON.parse(fs31.readFileSync(configPath, "utf-8"));
23238
+ const configPath = path31.join(instanceRoot, "config.json");
23239
+ const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
22178
23240
  if (raw.id && typeof raw.id === "string") return raw.id;
22179
23241
  } catch {
22180
23242
  }
22181
23243
  try {
22182
- const reg = new InstanceRegistry(path29.join(getGlobalRoot(), "instances.json"));
23244
+ const reg = new InstanceRegistry(path31.join(getGlobalRoot(), "instances.json"));
22183
23245
  reg.load();
22184
23246
  const entry = reg.getByRoot(instanceRoot);
22185
23247
  if (entry?.id) return entry.id;
22186
23248
  } catch (err) {
22187
23249
  log21.debug({ err: err.message, instanceRoot }, "Could not read instance registry, using fallback id");
22188
23250
  }
22189
- return path29.basename(path29.dirname(instanceRoot)).replace(/[^a-zA-Z0-9-]/g, "-") || "default";
23251
+ return path31.basename(path31.dirname(instanceRoot)).replace(/[^a-zA-Z0-9-]/g, "-") || "default";
22190
23252
  }
22191
23253
 
22192
23254
  // src/core/config/config-editor.ts
@@ -22758,7 +23820,7 @@ async function runConfigEditor(configManager, mode = "file", apiPort, settingsMa
22758
23820
  await configManager.load();
22759
23821
  const config = configManager.get();
22760
23822
  const updates = {};
22761
- const instanceRoot = path33.dirname(configManager.getConfigPath());
23823
+ const instanceRoot = path34.dirname(configManager.getConfigPath());
22762
23824
  console.log(`
22763
23825
  ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
22764
23826
  console.log(dim(`Config: ${configManager.getConfigPath()}`));
@@ -22815,17 +23877,17 @@ ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
22815
23877
  async function sendConfigViaApi(port, updates) {
22816
23878
  const { apiCall: call } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
22817
23879
  const paths = flattenToPaths(updates);
22818
- for (const { path: path38, value } of paths) {
23880
+ for (const { path: path39, value } of paths) {
22819
23881
  const res = await call(port, "/api/config", {
22820
23882
  method: "PATCH",
22821
23883
  headers: { "Content-Type": "application/json" },
22822
- body: JSON.stringify({ path: path38, value })
23884
+ body: JSON.stringify({ path: path39, value })
22823
23885
  });
22824
23886
  const data = await res.json();
22825
23887
  if (!res.ok) {
22826
- console.log(warn(`Failed to update ${path38}: ${data.error}`));
23888
+ console.log(warn(`Failed to update ${path39}: ${data.error}`));
22827
23889
  } else if (data.needsRestart) {
22828
- console.log(warn(`${path38} updated \u2014 restart required`));
23890
+ console.log(warn(`${path39} updated \u2014 restart required`));
22829
23891
  }
22830
23892
  }
22831
23893
  }
@@ -22845,25 +23907,25 @@ function flattenToPaths(obj, prefix = "") {
22845
23907
  // src/cli/daemon.ts
22846
23908
  init_config();
22847
23909
  import { spawn as spawn3 } from "child_process";
22848
- import * as fs34 from "fs";
22849
- import * as path34 from "path";
23910
+ import * as fs35 from "fs";
23911
+ import * as path35 from "path";
22850
23912
  function getPidPath(root) {
22851
- return path34.join(root, "openacp.pid");
23913
+ return path35.join(root, "openacp.pid");
22852
23914
  }
22853
23915
  function getLogDir(root) {
22854
- return path34.join(root, "logs");
23916
+ return path35.join(root, "logs");
22855
23917
  }
22856
23918
  function getRunningMarker(root) {
22857
- return path34.join(root, "running");
23919
+ return path35.join(root, "running");
22858
23920
  }
22859
23921
  function writePidFile(pidPath, pid) {
22860
- const dir = path34.dirname(pidPath);
22861
- fs34.mkdirSync(dir, { recursive: true });
22862
- fs34.writeFileSync(pidPath, String(pid));
23922
+ const dir = path35.dirname(pidPath);
23923
+ fs35.mkdirSync(dir, { recursive: true });
23924
+ fs35.writeFileSync(pidPath, String(pid));
22863
23925
  }
22864
23926
  function readPidFile(pidPath) {
22865
23927
  try {
22866
- const content = fs34.readFileSync(pidPath, "utf-8").trim();
23928
+ const content = fs35.readFileSync(pidPath, "utf-8").trim();
22867
23929
  const pid = parseInt(content, 10);
22868
23930
  return isNaN(pid) ? null : pid;
22869
23931
  } catch {
@@ -22872,7 +23934,7 @@ function readPidFile(pidPath) {
22872
23934
  }
22873
23935
  function removePidFile(pidPath) {
22874
23936
  try {
22875
- fs34.unlinkSync(pidPath);
23937
+ fs35.unlinkSync(pidPath);
22876
23938
  } catch {
22877
23939
  }
22878
23940
  }
@@ -22905,12 +23967,12 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
22905
23967
  return { error: `Already running (PID ${pid})` };
22906
23968
  }
22907
23969
  const resolvedLogDir = logDir2 ? expandHome2(logDir2) : getLogDir(instanceRoot);
22908
- fs34.mkdirSync(resolvedLogDir, { recursive: true });
22909
- const logFile = path34.join(resolvedLogDir, "openacp.log");
22910
- const cliPath = path34.resolve(process.argv[1]);
23970
+ fs35.mkdirSync(resolvedLogDir, { recursive: true });
23971
+ const logFile = path35.join(resolvedLogDir, "openacp.log");
23972
+ const cliPath = path35.resolve(process.argv[1]);
22911
23973
  const nodePath = process.execPath;
22912
- const out = fs34.openSync(logFile, "a");
22913
- const err = fs34.openSync(logFile, "a");
23974
+ const out = fs35.openSync(logFile, "a");
23975
+ const err = fs35.openSync(logFile, "a");
22914
23976
  const child = spawn3(nodePath, [cliPath, "--daemon-child"], {
22915
23977
  detached: true,
22916
23978
  stdio: ["ignore", out, err],
@@ -22919,8 +23981,8 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
22919
23981
  ...instanceRoot ? { OPENACP_INSTANCE_ROOT: instanceRoot } : {}
22920
23982
  }
22921
23983
  });
22922
- fs34.closeSync(out);
22923
- fs34.closeSync(err);
23984
+ fs35.closeSync(out);
23985
+ fs35.closeSync(err);
22924
23986
  if (!child.pid) {
22925
23987
  return { error: "Failed to spawn daemon process" };
22926
23988
  }
@@ -22929,7 +23991,7 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
22929
23991
  return { pid: child.pid };
22930
23992
  }
22931
23993
  function sleep2(ms) {
22932
- return new Promise((resolve7) => setTimeout(resolve7, ms));
23994
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
22933
23995
  }
22934
23996
  function isProcessAlive2(pid) {
22935
23997
  try {
@@ -22991,12 +24053,12 @@ async function stopDaemon(pidPath, instanceRoot) {
22991
24053
  }
22992
24054
  function markRunning(root) {
22993
24055
  const marker = getRunningMarker(root);
22994
- fs34.mkdirSync(path34.dirname(marker), { recursive: true });
22995
- fs34.writeFileSync(marker, "");
24056
+ fs35.mkdirSync(path35.dirname(marker), { recursive: true });
24057
+ fs35.writeFileSync(marker, "");
22996
24058
  }
22997
24059
  function clearRunning(root) {
22998
24060
  try {
22999
- fs34.unlinkSync(getRunningMarker(root));
24061
+ fs35.unlinkSync(getRunningMarker(root));
23000
24062
  } catch {
23001
24063
  }
23002
24064
  }