@agentprojectcontext/apx 1.5.0 → 1.6.0
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/package.json +1 -1
- package/src/daemon/api.js +162 -0
package/package.json
CHANGED
package/src/daemon/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Express REST API for APX. See APC docs reference/apx-daemon.
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
4
5
|
import express from "express";
|
|
5
6
|
import { readApfMcps, writeApfMcps, SOURCES } from "./mcp-sources.js";
|
|
6
7
|
import { callEngine, ENGINE_IDS } from "./engines/index.js";
|
|
@@ -887,6 +888,167 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
887
888
|
res.json({ ok: true, project_only: cfg });
|
|
888
889
|
});
|
|
889
890
|
|
|
891
|
+
// ---- Run (bash execution) -----------------------------------------
|
|
892
|
+
// POST /run { cmd, cwd?, project?, timeout_ms? }
|
|
893
|
+
// Executes a shell command and returns stdout + stderr.
|
|
894
|
+
// `cwd` defaults to the project path (by id or first registered), or process.cwd().
|
|
895
|
+
app.post("/run", (req, res) => {
|
|
896
|
+
const { cmd, cwd: cwdOverride, project: projectRef, timeout_ms = 30000 } = req.body || {};
|
|
897
|
+
if (!cmd) return res.status(400).json({ error: "cmd required" });
|
|
898
|
+
|
|
899
|
+
// Resolve working directory
|
|
900
|
+
let cwd = cwdOverride || null;
|
|
901
|
+
if (!cwd) {
|
|
902
|
+
let entry = null;
|
|
903
|
+
if (projectRef !== undefined && projectRef !== null) {
|
|
904
|
+
const all = projects.list();
|
|
905
|
+
const ref = String(projectRef);
|
|
906
|
+
entry = all.find((p) => String(p.id) === ref || p.path === path.resolve(ref));
|
|
907
|
+
}
|
|
908
|
+
if (!entry) {
|
|
909
|
+
const all = projects.list().filter((p) => p.id !== 0);
|
|
910
|
+
entry = all[0] || projects.get(0);
|
|
911
|
+
}
|
|
912
|
+
cwd = entry ? entry.path : process.cwd();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const timeout = Math.min(Math.max(parseInt(timeout_ms, 10) || 30000, 1000), 300000);
|
|
916
|
+
|
|
917
|
+
execFile("bash", ["-c", cmd], { cwd, timeout, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
918
|
+
const exit_code = err?.code ?? (err ? 1 : 0);
|
|
919
|
+
res.json({
|
|
920
|
+
ok: !err || exit_code === 0,
|
|
921
|
+
exit_code,
|
|
922
|
+
stdout: stdout || "",
|
|
923
|
+
stderr: stderr || "",
|
|
924
|
+
cwd,
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// ---- Top-level memory shortcuts -----------------------------------
|
|
930
|
+
// GET /memory?project=<id> → reads default agent memory.md
|
|
931
|
+
// POST /memory?project=<id> { body } → writes it
|
|
932
|
+
//
|
|
933
|
+
// Targets the *first non-default agent* of the resolved project,
|
|
934
|
+
// or falls back to a bare memory.md in .apc/ root.
|
|
935
|
+
|
|
936
|
+
function resolveTopProject(query) {
|
|
937
|
+
const ref = query?.project;
|
|
938
|
+
if (ref !== undefined && ref !== null) {
|
|
939
|
+
const all = projects.list();
|
|
940
|
+
const r = String(ref);
|
|
941
|
+
return projects.get(all.find((p) => String(p.id) === r || p.path === path.resolve(r))?.id);
|
|
942
|
+
}
|
|
943
|
+
const all = projects.list().filter((p) => p.id !== 0);
|
|
944
|
+
return all.length ? projects.get(all[0].id) : projects.get(0);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function resolveMemoryPath(p) {
|
|
948
|
+
const agentsDir = path.join(p.path, ".apc", "agents");
|
|
949
|
+
if (fs.existsSync(agentsDir)) {
|
|
950
|
+
const slugs = fs.readdirSync(agentsDir).filter((s) => {
|
|
951
|
+
const mp = path.join(agentsDir, s, "memory.md");
|
|
952
|
+
return fs.statSync(path.join(agentsDir, s)).isDirectory();
|
|
953
|
+
});
|
|
954
|
+
if (slugs.length) return path.join(agentsDir, slugs[0], "memory.md");
|
|
955
|
+
}
|
|
956
|
+
return path.join(p.path, ".apc", "memory.md");
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
app.get("/memory", (req, res) => {
|
|
960
|
+
const p = resolveTopProject(req.query);
|
|
961
|
+
if (!p) return res.status(404).json({ error: "no project registered" });
|
|
962
|
+
const memPath = resolveMemoryPath(p);
|
|
963
|
+
const body = fs.existsSync(memPath) ? fs.readFileSync(memPath, "utf8") : "";
|
|
964
|
+
res.json({ project_id: p.id, path: memPath, body });
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
app.post("/memory", (req, res) => {
|
|
968
|
+
const p = resolveTopProject(req.query);
|
|
969
|
+
if (!p) return res.status(404).json({ error: "no project registered" });
|
|
970
|
+
const { body } = req.body || {};
|
|
971
|
+
if (typeof body !== "string") return res.status(400).json({ error: "body must be string" });
|
|
972
|
+
const memPath = resolveMemoryPath(p);
|
|
973
|
+
fs.mkdirSync(path.dirname(memPath), { recursive: true });
|
|
974
|
+
fs.writeFileSync(memPath, body);
|
|
975
|
+
try { projects.rebuild(p.id); } catch {}
|
|
976
|
+
res.json({ ok: true, path: memPath, bytes: Buffer.byteLength(body, "utf8") });
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
// ---- Top-level file shortcuts -------------------------------------
|
|
980
|
+
// GET /files?path=<rel>&project=<id> → read file contents
|
|
981
|
+
// POST /files?project=<id> { path, content } → write file
|
|
982
|
+
|
|
983
|
+
app.get("/files", (req, res) => {
|
|
984
|
+
const p = resolveTopProject(req.query);
|
|
985
|
+
if (!p) return res.status(404).json({ error: "no project registered" });
|
|
986
|
+
const rel = req.query.path;
|
|
987
|
+
if (!rel) {
|
|
988
|
+
// List top-level files of the project
|
|
989
|
+
try {
|
|
990
|
+
const entries = fs.readdirSync(p.path).map((name) => {
|
|
991
|
+
const full = path.join(p.path, name);
|
|
992
|
+
const stat = fs.statSync(full);
|
|
993
|
+
return { name, type: stat.isDirectory() ? "dir" : "file", size: stat.isDirectory() ? null : stat.size };
|
|
994
|
+
});
|
|
995
|
+
return res.json({ project_id: p.id, cwd: p.path, entries });
|
|
996
|
+
} catch (e) {
|
|
997
|
+
return res.status(500).json({ error: e.message });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const abs = path.resolve(p.path, rel);
|
|
1001
|
+
if (!abs.startsWith(path.resolve(p.path))) return res.status(403).json({ error: "path escapes project root" });
|
|
1002
|
+
if (!fs.existsSync(abs)) return res.status(404).json({ error: "not found" });
|
|
1003
|
+
const stat = fs.statSync(abs);
|
|
1004
|
+
if (stat.isDirectory()) {
|
|
1005
|
+
const entries = fs.readdirSync(abs).map((name) => {
|
|
1006
|
+
const s = fs.statSync(path.join(abs, name));
|
|
1007
|
+
return { name, type: s.isDirectory() ? "dir" : "file", size: s.isDirectory() ? null : s.size };
|
|
1008
|
+
});
|
|
1009
|
+
return res.json({ project_id: p.id, path: rel, type: "dir", entries });
|
|
1010
|
+
}
|
|
1011
|
+
const content = fs.readFileSync(abs, "utf8");
|
|
1012
|
+
res.json({ project_id: p.id, path: rel, type: "file", size: stat.size, content });
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
app.post("/files", (req, res) => {
|
|
1016
|
+
const p = resolveTopProject(req.query);
|
|
1017
|
+
if (!p) return res.status(404).json({ error: "no project registered" });
|
|
1018
|
+
const { path: rel, content } = req.body || {};
|
|
1019
|
+
if (!rel) return res.status(400).json({ error: "path required" });
|
|
1020
|
+
if (typeof content !== "string") return res.status(400).json({ error: "content must be string" });
|
|
1021
|
+
const abs = path.resolve(p.path, rel);
|
|
1022
|
+
if (!abs.startsWith(path.resolve(p.path))) return res.status(403).json({ error: "path escapes project root" });
|
|
1023
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
1024
|
+
fs.writeFileSync(abs, content);
|
|
1025
|
+
res.json({ ok: true, path: rel, bytes: Buffer.byteLength(content, "utf8") });
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// ---- Top-level MCP shortcuts --------------------------------------
|
|
1029
|
+
// GET /mcp?project=<id> → list MCPs
|
|
1030
|
+
// POST /mcp/run { project?, name, tool, params } → call MCP tool
|
|
1031
|
+
|
|
1032
|
+
app.get("/mcp", (req, res) => {
|
|
1033
|
+
const p = resolveTopProject(req.query);
|
|
1034
|
+
if (!p) return res.status(404).json({ error: "no project registered" });
|
|
1035
|
+
res.json(registries.for(p).list());
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
app.post("/mcp/run", async (req, res) => {
|
|
1039
|
+
const { project: projectRef, name, tool, params } = req.body || {};
|
|
1040
|
+
if (!name) return res.status(400).json({ error: "name required" });
|
|
1041
|
+
if (!tool) return res.status(400).json({ error: "tool required" });
|
|
1042
|
+
const p = resolveTopProject({ project: projectRef });
|
|
1043
|
+
if (!p) return res.status(404).json({ error: "no project registered" });
|
|
1044
|
+
try {
|
|
1045
|
+
const result = await registries.for(p).call(name, tool, params);
|
|
1046
|
+
res.json({ ok: true, result });
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
res.status(500).json({ error: e.message });
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
|
|
890
1052
|
// ---- Admin --------------------------------------------------------
|
|
891
1053
|
app.post("/admin/shutdown", (_req, res) => {
|
|
892
1054
|
res.json({ ok: true });
|