@agentprojectcontext/apx 1.5.0 → 1.7.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/core/config.js +2 -0
- package/src/core/mascot.js +5 -7
- package/src/daemon/api.js +162 -0
- package/src/daemon/index.js +37 -2
- package/src/daemon/super-agent-tools/helpers.js +119 -0
- package/src/daemon/super-agent-tools/index.js +52 -0
- package/src/daemon/super-agent-tools/tools/add-project.js +36 -0
- package/src/daemon/super-agent-tools/tools/call-agent.js +45 -0
- package/src/daemon/super-agent-tools/tools/call-mcp.js +30 -0
- package/src/daemon/super-agent-tools/tools/call-runtime.js +107 -0
- package/src/daemon/super-agent-tools/tools/edit-file.js +44 -0
- package/src/daemon/super-agent-tools/tools/import-agent.js +48 -0
- package/src/daemon/super-agent-tools/tools/list-agents.js +36 -0
- package/src/daemon/super-agent-tools/tools/list-files.js +38 -0
- package/src/daemon/super-agent-tools/tools/list-mcps.js +48 -0
- package/src/daemon/super-agent-tools/tools/list-projects.js +20 -0
- package/src/daemon/super-agent-tools/tools/list-vault-agents.js +18 -0
- package/src/daemon/super-agent-tools/tools/read-agent-memory.js +28 -0
- package/src/daemon/super-agent-tools/tools/read-file.js +33 -0
- package/src/daemon/super-agent-tools/tools/run-shell.js +64 -0
- package/src/daemon/super-agent-tools/tools/search-messages.js +32 -0
- package/src/daemon/super-agent-tools/tools/send-telegram.js +30 -0
- package/src/daemon/super-agent-tools/tools/set-identity.js +35 -0
- package/src/daemon/super-agent-tools/tools/tail-messages.js +37 -0
- package/src/daemon/super-agent-tools/tools/write-file.js +33 -0
- package/src/daemon/super-agent-tools.js +1 -539
- package/src/daemon/super-agent.js +34 -7
package/package.json
CHANGED
package/src/core/config.js
CHANGED
|
@@ -46,6 +46,8 @@ const DEFAULT_CONFIG = {
|
|
|
46
46
|
name: "apx",
|
|
47
47
|
model: "", // e.g. "ollama:llama3.2:3b"
|
|
48
48
|
system: "", // optional override; defaults baked into super-agent.js
|
|
49
|
+
permission_mode: "automatico", // total | automatico | permiso
|
|
50
|
+
allowed_tools: [], // used by permission_mode="permiso"
|
|
49
51
|
},
|
|
50
52
|
engines: {
|
|
51
53
|
anthropic: { api_key: "" },
|
package/src/core/mascot.js
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
const R = "\x1b[0m";
|
|
5
5
|
const B = "\x1b[1m";
|
|
6
6
|
const W = "\x1b[97m"; // bright white
|
|
7
|
-
const K = "\x1b[30m"; // black
|
|
8
7
|
const BK = "\x1b[40m"; // bg black
|
|
9
|
-
const BW = "\x1b[47m"; // bg white
|
|
10
8
|
const CY = "\x1b[36m";
|
|
11
9
|
const YE = "\x1b[33m";
|
|
12
10
|
const GR = "\x1b[32m";
|
|
@@ -34,10 +32,10 @@ const MOODS = {
|
|
|
34
32
|
wave: {
|
|
35
33
|
color: CY,
|
|
36
34
|
lines: [
|
|
37
|
-
` ${BK}${W} ▄███████▄ ${R}
|
|
38
|
-
` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
|
|
39
|
-
` ${BK}${W} █ ◕ ◕ █ ${R}`,
|
|
40
|
-
` ${BK}${W} █ ╰▽╯ █ ${R}`,
|
|
35
|
+
` ${BK}${W} ▄███████▄ ${R} ${DI}/)${R}`,
|
|
36
|
+
` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R} ${DI}//${R}`,
|
|
37
|
+
` ${BK}${W} █ ◕ ◕ █ ${R} ${DI}//${R}`,
|
|
38
|
+
` ${BK}${W} █ ╰▽╯ █ ${R}${DI}/${R}`,
|
|
41
39
|
` ${BK}${W} ▀███████▀ ${R}`,
|
|
42
40
|
` ${DI} ╱ ╲ ╱ ╲ ${R}`,
|
|
43
41
|
],
|
|
@@ -76,7 +74,7 @@ const MOODS = {
|
|
|
76
74
|
excited: {
|
|
77
75
|
color: BL,
|
|
78
76
|
lines: [
|
|
79
|
-
` ${BK}${W} ▄███████▄ ${R} ${BL}
|
|
77
|
+
` ${BK}${W} ▄███████▄ ${R} ${BL}↑${R}`,
|
|
80
78
|
` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
|
|
81
79
|
` ${BK}${W} █ ★ ★ █ ${R}`,
|
|
82
80
|
` ${BK}${W} █ ╰◡╯ █ ${R}`,
|
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 });
|
package/src/daemon/index.js
CHANGED
|
@@ -44,6 +44,32 @@ function writePid() {
|
|
|
44
44
|
} catch {}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function pidIsAlive(pid) {
|
|
48
|
+
if (!pid || pid === process.pid) return false;
|
|
49
|
+
try {
|
|
50
|
+
process.kill(pid, 0);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function claimSingleton() {
|
|
58
|
+
try {
|
|
59
|
+
if (fs.existsSync(PID_PATH)) {
|
|
60
|
+
const pid = parseInt(fs.readFileSync(PID_PATH, "utf8"), 10);
|
|
61
|
+
if (pidIsAlive(pid)) {
|
|
62
|
+
log(`fatal: apx-daemon already running with pid ${pid}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
fs.unlinkSync(PID_PATH);
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
log(`fatal: cannot claim daemon pid file: ${e.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
function clearPid() {
|
|
48
74
|
try {
|
|
49
75
|
if (fs.existsSync(PID_PATH)) fs.unlinkSync(PID_PATH);
|
|
@@ -71,6 +97,7 @@ class RegistryCache {
|
|
|
71
97
|
|
|
72
98
|
async function main() {
|
|
73
99
|
ensureHome();
|
|
100
|
+
claimSingleton();
|
|
74
101
|
|
|
75
102
|
const cfg = readConfig();
|
|
76
103
|
const host = effectiveHost(cfg);
|
|
@@ -95,7 +122,6 @@ async function main() {
|
|
|
95
122
|
|
|
96
123
|
const plugins = new PluginManager({ projects, config: cfg, log, registries });
|
|
97
124
|
plugins.initAll();
|
|
98
|
-
plugins.startAll();
|
|
99
125
|
|
|
100
126
|
const scheduler = new RoutineScheduler({
|
|
101
127
|
projects,
|
|
@@ -103,7 +129,6 @@ async function main() {
|
|
|
103
129
|
globalConfig: cfg,
|
|
104
130
|
log,
|
|
105
131
|
});
|
|
106
|
-
scheduler.start();
|
|
107
132
|
|
|
108
133
|
const startedAt = Date.now();
|
|
109
134
|
const app = buildApi({
|
|
@@ -130,10 +155,20 @@ async function main() {
|
|
|
130
155
|
writePid();
|
|
131
156
|
log(`apx-daemon ${PKG.version} listening on http://${host}:${port}`);
|
|
132
157
|
log(`projects: ${projects.list().length} | plugins: ${Object.keys(plugins.status()).join(", ") || "(none)"}`);
|
|
158
|
+
plugins.startAll();
|
|
159
|
+
scheduler.start();
|
|
133
160
|
// Fire wake-up message after a short delay so plugins (Telegram) are ready
|
|
134
161
|
setTimeout(() => triggerWakeup(cfg, log), 3000);
|
|
135
162
|
});
|
|
136
163
|
|
|
164
|
+
server.on("error", (e) => {
|
|
165
|
+
log(`fatal: listen ${host}:${port} failed: ${e.message}`);
|
|
166
|
+
plugins.stopAll();
|
|
167
|
+
registries.shutdown();
|
|
168
|
+
clearPid();
|
|
169
|
+
process.exit(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
137
172
|
function shutdown(signal) {
|
|
138
173
|
log(`received ${signal}, shutting down...`);
|
|
139
174
|
scheduler.stop();
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function projectMeta(projects, entry) {
|
|
5
|
+
const meta = projects.list().find((p) => p.id === entry.id);
|
|
6
|
+
return {
|
|
7
|
+
id: entry.id,
|
|
8
|
+
name: meta?.name || path.basename(entry.path),
|
|
9
|
+
path: entry.path,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveProject(projects, target, { allowMulti = false } = {}) {
|
|
14
|
+
if (target === undefined || target === null || target === "") {
|
|
15
|
+
if (allowMulti) return null;
|
|
16
|
+
const defaultProject = projects.get(0);
|
|
17
|
+
if (defaultProject) return defaultProject;
|
|
18
|
+
const all = projects.list();
|
|
19
|
+
if (all.length === 1) return projects.get(all[0].id);
|
|
20
|
+
throw new Error(`multiple projects registered (${all.length}); specify project=<id|name|path>`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tgt = String(target);
|
|
24
|
+
if (tgt.toLowerCase() === "default") {
|
|
25
|
+
const defaultProject = projects.get(0);
|
|
26
|
+
if (!defaultProject) throw new Error("default project not available");
|
|
27
|
+
return defaultProject;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof target === "number" || /^\d+$/.test(tgt)) {
|
|
31
|
+
const entry = projects.get(parseInt(tgt, 10));
|
|
32
|
+
if (!entry) throw new Error(`project id ${target} not found`);
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const all = projects.list();
|
|
37
|
+
const byPath = all.find((p) => p.path === path.resolve(tgt));
|
|
38
|
+
if (byPath) return projects.get(byPath.id);
|
|
39
|
+
|
|
40
|
+
const byName = all.find((p) => p.name === tgt);
|
|
41
|
+
if (byName) return projects.get(byName.id);
|
|
42
|
+
|
|
43
|
+
const tgtLow = tgt.toLowerCase();
|
|
44
|
+
const fuzzy = all.filter(
|
|
45
|
+
(p) => p.name.toLowerCase().includes(tgtLow) || p.path.toLowerCase().includes(tgtLow)
|
|
46
|
+
);
|
|
47
|
+
if (fuzzy.length === 1) return projects.get(fuzzy[0].id);
|
|
48
|
+
if (fuzzy.length > 1) {
|
|
49
|
+
throw new Error(`project "${tgt}" is ambiguous; matches: ${fuzzy.map((p) => p.name).join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`project "${tgt}" not found`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function safePathJoin(root, sub = ".") {
|
|
55
|
+
const target = path.resolve(root, sub || ".");
|
|
56
|
+
const rootResolved = path.resolve(root);
|
|
57
|
+
if (target !== rootResolved && !target.startsWith(rootResolved + path.sep)) {
|
|
58
|
+
throw new Error(`path "${sub}" escapes the project root`);
|
|
59
|
+
}
|
|
60
|
+
return target;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function skillsFromFields(fields = {}) {
|
|
64
|
+
if (Array.isArray(fields.Skills)) return fields.Skills;
|
|
65
|
+
return (fields.Skills || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function agentRow(agent) {
|
|
69
|
+
return {
|
|
70
|
+
slug: agent.slug,
|
|
71
|
+
role: agent.fields.Role || null,
|
|
72
|
+
model: agent.fields.Model || null,
|
|
73
|
+
language: agent.fields.Language || null,
|
|
74
|
+
description: agent.fields.Description || null,
|
|
75
|
+
skills: skillsFromFields(agent.fields),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildAgentSystem(project, agent) {
|
|
80
|
+
const parts = [];
|
|
81
|
+
if (agent.fields.Description) parts.push(agent.fields.Description);
|
|
82
|
+
if (agent.fields.Role) parts.push(`Role: ${agent.fields.Role}`);
|
|
83
|
+
if (agent.fields.Language) parts.push(`Default language: ${agent.fields.Language}`);
|
|
84
|
+
|
|
85
|
+
const memPath = path.join(project.path, ".apc", "agents", agent.slug, "memory.md");
|
|
86
|
+
if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
|
|
87
|
+
|
|
88
|
+
const apxSkill = path.join(project.path, ".apc", "skills", "apx.md");
|
|
89
|
+
if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
|
|
90
|
+
|
|
91
|
+
for (const skill of skillsFromFields(agent.fields)) {
|
|
92
|
+
const skillPath = path.join(project.path, ".apc", "skills", `${skill}.md`);
|
|
93
|
+
if (fs.existsSync(skillPath)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(skillPath, "utf8"));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return parts.join("\n\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createPermissionGuard(globalConfig = {}) {
|
|
100
|
+
const permissionMode = globalConfig.super_agent?.permission_mode || "automatico";
|
|
101
|
+
const allowedTools = new Set(globalConfig.super_agent?.allowed_tools || []);
|
|
102
|
+
|
|
103
|
+
return function requirePermission(tool, { dangerous = false, confirmed = false } = {}) {
|
|
104
|
+
if (permissionMode === "total") return;
|
|
105
|
+
if (permissionMode === "permiso" && !allowedTools.has(tool) && !confirmed) {
|
|
106
|
+
throw new Error(`requires_confirmation: permission_mode=permiso blocks ${tool}`);
|
|
107
|
+
}
|
|
108
|
+
if (permissionMode === "automatico" && dangerous && !confirmed) {
|
|
109
|
+
throw new Error(`requires_confirmation: permission_mode=automatico requires confirmation for ${tool}`);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function confirmedProperty(description) {
|
|
115
|
+
return {
|
|
116
|
+
type: "boolean",
|
|
117
|
+
description: description || "true only after explicit user confirmation for this exact action",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import listProjects from "./tools/list-projects.js";
|
|
2
|
+
import listAgents from "./tools/list-agents.js";
|
|
3
|
+
import listVaultAgents from "./tools/list-vault-agents.js";
|
|
4
|
+
import importAgent from "./tools/import-agent.js";
|
|
5
|
+
import addProject from "./tools/add-project.js";
|
|
6
|
+
import listMcps from "./tools/list-mcps.js";
|
|
7
|
+
import readAgentMemory from "./tools/read-agent-memory.js";
|
|
8
|
+
import listFiles from "./tools/list-files.js";
|
|
9
|
+
import readFile from "./tools/read-file.js";
|
|
10
|
+
import writeFile from "./tools/write-file.js";
|
|
11
|
+
import editFile from "./tools/edit-file.js";
|
|
12
|
+
import runShell from "./tools/run-shell.js";
|
|
13
|
+
import tailMessages from "./tools/tail-messages.js";
|
|
14
|
+
import searchMessages from "./tools/search-messages.js";
|
|
15
|
+
import callAgent from "./tools/call-agent.js";
|
|
16
|
+
import callMcp from "./tools/call-mcp.js";
|
|
17
|
+
import callRuntime from "./tools/call-runtime.js";
|
|
18
|
+
import sendTelegram from "./tools/send-telegram.js";
|
|
19
|
+
import setIdentity from "./tools/set-identity.js";
|
|
20
|
+
import { createPermissionGuard } from "./helpers.js";
|
|
21
|
+
|
|
22
|
+
const TOOLS = [
|
|
23
|
+
listProjects,
|
|
24
|
+
listAgents,
|
|
25
|
+
listVaultAgents,
|
|
26
|
+
importAgent,
|
|
27
|
+
addProject,
|
|
28
|
+
listMcps,
|
|
29
|
+
readAgentMemory,
|
|
30
|
+
listFiles,
|
|
31
|
+
readFile,
|
|
32
|
+
writeFile,
|
|
33
|
+
editFile,
|
|
34
|
+
runShell,
|
|
35
|
+
tailMessages,
|
|
36
|
+
searchMessages,
|
|
37
|
+
callAgent,
|
|
38
|
+
callMcp,
|
|
39
|
+
callRuntime,
|
|
40
|
+
sendTelegram,
|
|
41
|
+
setIdentity,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export const TOOL_SCHEMAS = TOOLS.map((tool) => tool.schema);
|
|
45
|
+
|
|
46
|
+
export function makeToolHandlers(ctx) {
|
|
47
|
+
const toolCtx = {
|
|
48
|
+
...ctx,
|
|
49
|
+
requirePermission: createPermissionGuard(ctx.globalConfig || {}),
|
|
50
|
+
};
|
|
51
|
+
return Object.fromEntries(TOOLS.map((tool) => [tool.name, tool.makeHandler(toolCtx)]));
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readConfig, addProject as addProjectInConfig } from "../../../core/config.js";
|
|
3
|
+
import { confirmedProperty, projectMeta } from "../helpers.js";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: "add_project",
|
|
7
|
+
schema: {
|
|
8
|
+
type: "function",
|
|
9
|
+
function: {
|
|
10
|
+
name: "add_project",
|
|
11
|
+
description: "Register an existing APC project path with the APX daemon. The path must contain AGENTS.md and .apc/project.json.",
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
path: { type: "string", description: "absolute or relative filesystem path to an APC project" },
|
|
16
|
+
confirmed: confirmedProperty("true only after explicit user confirmation for this exact project registration"),
|
|
17
|
+
},
|
|
18
|
+
required: ["path"],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
makeHandler: ({ projects, requirePermission }) => ({ path: projectPath, confirmed = false }) => {
|
|
23
|
+
requirePermission("add_project", { dangerous: true, confirmed });
|
|
24
|
+
if (!projectPath) throw new Error("add_project: path required");
|
|
25
|
+
|
|
26
|
+
const cfg = readConfig();
|
|
27
|
+
const result = addProjectInConfig(cfg, projectPath);
|
|
28
|
+
const p = projects.register(result.project.path);
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
added: result.added,
|
|
32
|
+
project: projectMeta(projects, p),
|
|
33
|
+
normalized_path: path.resolve(projectPath),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { callEngine } from "../../engines/index.js";
|
|
2
|
+
import { readAgents } from "../../../core/parser.js";
|
|
3
|
+
import { buildAgentSystem, resolveProject } from "../helpers.js";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: "call_agent",
|
|
7
|
+
schema: {
|
|
8
|
+
type: "function",
|
|
9
|
+
function: {
|
|
10
|
+
name: "call_agent",
|
|
11
|
+
description: "Run a one-shot prompt through a project agent's configured LLM engine.",
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
project: { type: "string" },
|
|
16
|
+
agent: { type: "string", description: "agent slug" },
|
|
17
|
+
prompt: { type: "string" },
|
|
18
|
+
},
|
|
19
|
+
required: ["agent", "prompt"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
makeHandler: ({ projects, globalConfig }) => async ({ project, agent: slug, prompt }) => {
|
|
24
|
+
const p = resolveProject(projects, project);
|
|
25
|
+
const agent = readAgents(p.path).find((a) => a.slug === slug);
|
|
26
|
+
if (!agent) throw new Error(`agent ${slug} not found`);
|
|
27
|
+
if (!agent.fields.Model) throw new Error(`agent ${slug} has no model`);
|
|
28
|
+
|
|
29
|
+
const result = await callEngine({
|
|
30
|
+
modelId: agent.fields.Model,
|
|
31
|
+
system: buildAgentSystem(p, agent),
|
|
32
|
+
messages: [{ role: "user", content: prompt }],
|
|
33
|
+
config: p.config || globalConfig,
|
|
34
|
+
});
|
|
35
|
+
p.logMessage({
|
|
36
|
+
agent_slug: slug,
|
|
37
|
+
channel: "engine",
|
|
38
|
+
direction: "out",
|
|
39
|
+
author: slug,
|
|
40
|
+
body: result.text,
|
|
41
|
+
meta: { invoked_by: "super_agent_tool", usage: result.usage },
|
|
42
|
+
});
|
|
43
|
+
return { text: result.text, usage: result.usage };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { confirmedProperty, resolveProject } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "call_mcp",
|
|
5
|
+
schema: {
|
|
6
|
+
type: "function",
|
|
7
|
+
function: {
|
|
8
|
+
name: "call_mcp",
|
|
9
|
+
description: "Call a tool on an MCP server registered in default or a project. Args is a JSON object.",
|
|
10
|
+
parameters: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
project: { type: "string" },
|
|
14
|
+
mcp: { type: "string", description: "MCP server name" },
|
|
15
|
+
tool: { type: "string", description: "tool name on that MCP" },
|
|
16
|
+
args: { type: "object", description: "arguments object" },
|
|
17
|
+
confirmed: confirmedProperty("true only after explicit user confirmation for this exact MCP call"),
|
|
18
|
+
},
|
|
19
|
+
required: ["mcp", "tool"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
makeHandler: ({ projects, registries, requirePermission }) => async ({ project, mcp, tool, args = {}, confirmed = false }) => {
|
|
24
|
+
requirePermission("call_mcp", { dangerous: true, confirmed });
|
|
25
|
+
const p = resolveProject(projects, project);
|
|
26
|
+
if (!registries) throw new Error("MCP registry unavailable");
|
|
27
|
+
const registry = registries.for ? registries.for(p) : registries.ensure(p);
|
|
28
|
+
return registry.call(mcp, tool, args);
|
|
29
|
+
},
|
|
30
|
+
};
|