@agentprojectcontext/apx 1.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -281,14 +281,15 @@ export async function cmdSetup() {
281
281
  // The prompt is in English so the model knows to reply in the user's language.
282
282
  async function sendTelegramWakeup({ botToken, chatId, language, model }) {
283
283
  const prompt =
284
- `You are APX, an AI agent assistant that just came online. ` +
285
- `Send a short, fun, enthusiastic wake-up message to the user. ` +
286
- `Be playful and creative like a friendly AI that just woke up. ` +
287
- `Keep it under 3 sentences. ` +
288
- `IMPORTANT: respond in ${language}. ` +
289
- `Do not mention that you were configured or set up.`;
290
-
291
- // Ask the daemon's super-agent (give it a second attempt window)
284
+ `You are APX, an AI agent assistant that just came online for the first time. ` +
285
+ `Write a short, enthusiastic wake-up message in ${language}. ` +
286
+ `Structure it in exactly 3 short lines: ` +
287
+ `1) An energetic line announcing you are online (use ⚡ emoji). ` +
288
+ `2) Say you don't have a name yet and ask the user what they'd like to call you. ` +
289
+ `3) Ask the user for their own name or what you should call them. ` +
290
+ `Be warm and playful. Do NOT mention configuration or setup.`;
291
+
292
+ // Ask the daemon's super-agent
292
293
  let text;
293
294
  try {
294
295
  const res = await fetchJson("http://127.0.0.1:7430/super-agent/ask", 8000);
@@ -323,10 +324,15 @@ async function sendTelegramWakeup({ botToken, chatId, language, model }) {
323
324
  // Minimal fallback messages per common language (used only if daemon can't respond)
324
325
  function languageFallback(lang) {
325
326
  const l = lang.toLowerCase();
326
- if (/espa[ñn]|spanish|arg|lat/i.test(l)) return "⚡ ¡Despierto y listo para trabajar! APX online.";
327
- if (/portugu|brasil/i.test(l)) return "⚡ Acordei e pronto para trabalhar! APX online.";
328
- if (/franc|french/i.test(l)) return "⚡ Réveillé et prêt à travailler ! APX en ligne.";
329
- if (/deutsch|german/i.test(l)) return "⚡ Aufgewacht und bereit! APX ist online.";
330
- if (/ital/i.test(l)) return "⚡ Sveglio e pronto a lavorare! APX online.";
331
- return "⚡ I'm awake and ready to go! APX is online.";
327
+ if (/espa[ñn]|spanish|arg|lat/i.test(l))
328
+ return "⚡ ¡Despierto y listo para trabajar! APX online.\nAún no tengo nombre, ¿cómo te gustaría llamarme?\nY vos, ¿cómo te llamas o cómo puedo llamarte?";
329
+ if (/portugu|brasil/i.test(l))
330
+ return "⚡ Acordei e pronto para trabalhar! APX online.\nAinda não tenho nome, como você gostaria de me chamar?\nE você, como posso te chamar?";
331
+ if (/franc|french/i.test(l))
332
+ return "⚡ Réveillé et prêt à travailler ! APX en ligne.\nJe n'ai pas encore de nom, comment souhaitez-vous m'appeler ?\nEt vous, comment puis-je vous appeler ?";
333
+ if (/deutsch|german/i.test(l))
334
+ return "⚡ Aufgewacht und bereit! APX ist online.\nIch habe noch keinen Namen — wie möchtest du mich nennen?\nUnd du, wie kann ich dich nennen?";
335
+ if (/ital/i.test(l))
336
+ return "⚡ Sveglio e pronto a lavorare! APX online.\nNon ho ancora un nome, come vorresti chiamarmi?\nE tu, come posso chiamarti?";
337
+ return "⚡ I'm awake and ready to go! APX is online.\nI don't have a name yet — what would you like to call me?\nAnd you, what's your name or what should I call you?";
332
338
  }
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 });