@agentprojectcontext/apx 1.8.2 → 1.10.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/src/daemon/api.js CHANGED
@@ -43,6 +43,12 @@ import { readAgents } from "../core/parser.js";
43
43
  import { parseSessionFrontmatter } from "../core/parser.js";
44
44
  import { writeAgentFile, ensureAgentDir, regenerateAgentsMd } from "../core/scaffold.js";
45
45
  import { buildAgentSystem } from "../core/agent-system.js";
46
+ import {
47
+ createArtifact,
48
+ listArtifacts,
49
+ readArtifact,
50
+ removeArtifact,
51
+ } from "../core/artifacts-store.js";
46
52
 
47
53
  const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
48
54
 
@@ -518,6 +524,35 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
518
524
  }
519
525
  });
520
526
 
527
+ // POST /projects/:pid/super-agent/chat
528
+ app.post("/projects/:pid/super-agent/chat", async (req, res) => {
529
+ const p = project(req, res);
530
+ if (!p) return;
531
+ const { prompt, contextNote, previousMessages, model } = req.body || {};
532
+ if (!prompt) return res.status(400).json({ error: "prompt required" });
533
+ try {
534
+ const saResult = await runSuperAgent({
535
+ globalConfig: config,
536
+ projects,
537
+ plugins,
538
+ registries,
539
+ prompt,
540
+ contextNote: contextNote || `Context: Project ${p.id} (${p.name}) at ${p.path}`,
541
+ previousMessages: previousMessages || [],
542
+ overrideModel: model,
543
+ });
544
+ projects.rebuild(p.id);
545
+ res.json({
546
+ text: saResult.text,
547
+ usage: saResult.usage,
548
+ name: saResult.name,
549
+ trace: saResult.trace,
550
+ });
551
+ } catch (e) {
552
+ res.status(500).json({ error: e.message });
553
+ }
554
+ });
555
+
521
556
  // GET /projects/:pid/agents/:slug/conversations
522
557
  app.get("/projects/:pid/agents/:slug/conversations", (req, res) => {
523
558
  const p = project(req, res);
@@ -739,10 +774,14 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
739
774
  if (!p) return;
740
775
  const { id } = req.params;
741
776
 
742
- const agentsDir = path.join(p.path, ".apc", "agents");
777
+ const sessionRoots = [
778
+ path.join(p.storagePath || p.path, "agents"),
779
+ path.join(p.path, ".apc", "agents"),
780
+ ];
743
781
  let sessionFile = null;
744
782
  let agentSlug = null;
745
- if (fs.existsSync(agentsDir)) {
783
+ for (const agentsDir of sessionRoots) {
784
+ if (!fs.existsSync(agentsDir)) continue;
746
785
  for (const slug of fs.readdirSync(agentsDir)) {
747
786
  const f = path.join(agentsDir, slug, "sessions", `${id}.md`);
748
787
  if (fs.existsSync(f)) {
@@ -751,6 +790,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
751
790
  break;
752
791
  }
753
792
  }
793
+ if (sessionFile) break;
754
794
  }
755
795
  if (!sessionFile) return res.status(404).json({ error: `session ${id} not found` });
756
796
 
@@ -812,6 +852,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
812
852
  const p = project(req, res);
813
853
  if (!p) return;
814
854
  try {
855
+ // Pass all fields including pipeline extensions.
815
856
  const r = upsertRoutine(p.storagePath, req.body || {});
816
857
  res.status(201).json(r);
817
858
  } catch (e) {
@@ -819,6 +860,43 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
819
860
  }
820
861
  });
821
862
 
863
+ // ---- Artifacts (managed files in storagePath/artifacts/) ---------
864
+ app.get("/projects/:pid/artifacts", (req, res) => {
865
+ const p = project(req, res);
866
+ if (!p) return;
867
+ res.json(listArtifacts(p.storagePath));
868
+ });
869
+
870
+ app.post("/projects/:pid/artifacts", (req, res) => {
871
+ const p = project(req, res);
872
+ if (!p) return;
873
+ const { name, content = "" } = req.body || {};
874
+ if (!name) return res.status(400).json({ error: "name required" });
875
+ try {
876
+ const filePath = createArtifact(p.storagePath, name, content);
877
+ res.status(201).json({ name, path: filePath });
878
+ } catch (e) {
879
+ res.status(400).json({ error: e.message });
880
+ }
881
+ });
882
+
883
+ app.get("/projects/:pid/artifacts/:name", (req, res) => {
884
+ const p = project(req, res);
885
+ if (!p) return;
886
+ try {
887
+ res.json(readArtifact(p.storagePath, decodeURIComponent(req.params.name)));
888
+ } catch (e) {
889
+ res.status(404).json({ error: e.message });
890
+ }
891
+ });
892
+
893
+ app.delete("/projects/:pid/artifacts/:name", (req, res) => {
894
+ const p = project(req, res);
895
+ if (!p) return;
896
+ const ok = removeArtifact(p.storagePath, decodeURIComponent(req.params.name));
897
+ res.status(ok ? 204 : 404).end();
898
+ });
899
+
822
900
  app.delete("/projects/:pid/routines/:name", (req, res) => {
823
901
  const p = project(req, res);
824
902
  if (!p) return;
@@ -11,6 +11,7 @@ const PROBES = [
11
11
  { id: "aider", binary: "aider", args: ["--version"], category: "runtime" },
12
12
  { id: "gemini-cli", binary: "gemini", args: ["--version"], category: "runtime" },
13
13
  { id: "cursor-agent",binary: "cursor-agent", args: ["--version"], category: "runtime" },
14
+ { id: "qwen-code", binary: "qwen", args: ["--version"], category: "runtime" },
14
15
 
15
16
  // Local LLM runners (engines/)
16
17
  { id: "ollama", binary: "ollama", args: ["--version"], category: "engine" },
@@ -11,10 +11,15 @@
11
11
  // shell — run a shell command. spec: { command, timeout_ms? }
12
12
 
13
13
  import { spawn } from "node:child_process";
14
+ import { execFile } from "node:child_process";
15
+ import os from "node:os";
16
+ import fs from "node:fs";
17
+ import path from "node:path";
14
18
  import { callEngine } from "./engines/index.js";
15
19
  import { runSuperAgent } from "./super-agent.js";
16
20
  import { readAgents } from "../core/parser.js";
17
21
  import { buildAgentSystem } from "../core/agent-system.js";
22
+ import { resolveArtifactRef, ARTIFACTS_SKIP_SIGNAL } from "../core/artifacts-store.js";
18
23
  import {
19
24
  listRoutines,
20
25
  getRoutine,
@@ -169,25 +174,148 @@ const HANDLERS = {
169
174
  shell: handleShell,
170
175
  };
171
176
 
177
+ // --------------------- pipeline: pre/post shell commands --------------------
178
+
179
+ // Run a single shell command. Returns { exitCode, stdout, stderr }.
180
+ function runShellCmd(cmd, env = {}, cwd = os.homedir()) {
181
+ return new Promise((resolve) => {
182
+ const child = spawn("sh", ["-c", cmd], {
183
+ cwd,
184
+ env: { ...process.env, ...env },
185
+ stdio: ["ignore", "pipe", "pipe"],
186
+ });
187
+ let stdout = "";
188
+ let stderr = "";
189
+ child.stdout.on("data", (c) => (stdout += c.toString()));
190
+ child.stderr.on("data", (c) => (stderr += c.toString()));
191
+ child.on("close", (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
192
+ child.on("error", (e) => resolve({ exitCode: 1, stdout: "", stderr: e.message }));
193
+ });
194
+ }
195
+
196
+ // Inject {{pre_output}} into a prompt string.
197
+ function injectPreOutput(prompt, preOutput) {
198
+ if (!prompt || typeof prompt !== "string") return prompt;
199
+ return prompt.replace(/\{\{pre_output\}\}/g, preOutput || "");
200
+ }
201
+
202
+ // Determine whether to skip the LLM call based on skip_prompt_on + pre results.
203
+ function shouldSkipPrompt(routine, preExitCode, preStdout) {
204
+ const mode = routine.skip_prompt_on || "signal";
205
+ if (mode === "always") return true;
206
+ if (mode === "never") return false;
207
+ if (mode === "signal") return preStdout.includes(ARTIFACTS_SKIP_SIGNAL);
208
+ if (mode === "pre_failure") return preExitCode !== 0;
209
+ if (mode === "pre_success") return preExitCode === 0;
210
+ return false;
211
+ }
212
+
172
213
  // --------------------- runtime: run one + loop ------------------------------
173
214
 
174
215
  export async function runRoutineNow(ctx, routine) {
175
- const handler = HANDLERS[routine.kind];
176
- if (!handler) throw new Error(`unknown routine kind: ${routine.kind}`);
177
- let result;
216
+ // Determine the working directory for shell commands.
217
+ const cwd = ctx.project?.path || os.homedir();
218
+ const storagePath = ctx.project?.storagePath || os.homedir();
219
+
220
+ const hasPreCmds = Array.isArray(routine.pre_commands) && routine.pre_commands.length > 0;
221
+ const hasPostCmds = Array.isArray(routine.post_commands) && routine.post_commands.length > 0;
222
+
223
+ let preStdout = "";
224
+ let preExitCode = 0;
225
+ let preOutputFile = null;
226
+
227
+ // ── Phase 1: pre_commands ──────────────────────────────────────────────────
228
+ if (hasPreCmds) {
229
+ const combinedOut = [];
230
+ for (const rawCmd of routine.pre_commands) {
231
+ // Resolve "artifact:<name>" shorthand to its absolute path.
232
+ const cmd = resolveArtifactRef(rawCmd, storagePath);
233
+ const { exitCode, stdout, stderr } = await runShellCmd(cmd, {}, cwd);
234
+ combinedOut.push(stdout);
235
+ if (stderr) combinedOut.push(stderr);
236
+ preExitCode = exitCode;
237
+ if (exitCode !== 0 && (routine.skip_prompt_on === "pre_failure" || routine.skip_prompt_on === "signal")) {
238
+ // Stop running further pre_commands on failure when mode cares about exit code.
239
+ break;
240
+ }
241
+ }
242
+ preStdout = combinedOut.join("");
243
+
244
+ // Write pre output to a temp file so post_commands can reference it via
245
+ // $APX_PRE_OUTPUT_FILE even if the output is large.
246
+ try {
247
+ preOutputFile = path.join(os.tmpdir(), `apx-pre-${routine.name}-${Date.now()}.txt`);
248
+ fs.writeFileSync(preOutputFile, preStdout);
249
+ } catch { preOutputFile = null; }
250
+ }
251
+
252
+ // Env vars injected into post_commands and available in shell pre_commands output.
253
+ const pipelineEnv = {
254
+ APX_PRE_EXIT: String(preExitCode),
255
+ APX_PRE_OUTPUT: preStdout.slice(0, 32_000), // guard against huge outputs
256
+ APX_PRE_OUTPUT_FILE: preOutputFile || "",
257
+ APX_ROUTINE: routine.name,
258
+ };
259
+
260
+ // ── Phase 2: LLM / handler ────────────────────────────────────────────────
261
+ const skip = hasPreCmds && shouldSkipPrompt(routine, preExitCode, preStdout);
262
+
263
+ let result = { status: "ok" };
178
264
  let status = "ok";
179
265
  let errMsg = null;
180
- try {
181
- result = await handler(ctx, routine);
182
- if (result?.status === "error") {
266
+
267
+ if (!skip) {
268
+ // Inject {{pre_output}} into exec_agent and super_agent prompts.
269
+ const enrichedRoutine = (hasPreCmds && preStdout)
270
+ ? {
271
+ ...routine,
272
+ spec: {
273
+ ...routine.spec,
274
+ prompt: injectPreOutput(routine.spec?.prompt, preStdout),
275
+ },
276
+ }
277
+ : routine;
278
+
279
+ const handler = HANDLERS[enrichedRoutine.kind];
280
+ if (!handler) {
183
281
  status = "error";
184
- errMsg = result.error || result.stderr || `routine ${routine.name} returned error status`;
282
+ errMsg = `unknown routine kind: ${enrichedRoutine.kind}`;
283
+ } else {
284
+ try {
285
+ result = await handler(ctx, enrichedRoutine);
286
+ if (result?.status === "error") {
287
+ status = "error";
288
+ errMsg = result.error || result.stderr || `routine ${routine.name} returned error status`;
289
+ }
290
+ } catch (e) {
291
+ status = "error";
292
+ errMsg = e.message;
293
+ result = { status: "error", error: e.message };
294
+ }
185
295
  }
186
- } catch (e) {
187
- status = "error";
188
- errMsg = e.message;
189
- result = { status: "error", error: e.message };
296
+ } else {
297
+ result = { status: "ok", skipped: true, note: "pre_commands signalled skip" };
190
298
  }
299
+
300
+ // ── Phase 3: post_commands ────────────────────────────────────────────────
301
+ if (hasPostCmds) {
302
+ const llmOutput = result?.reply || result?.text || "";
303
+ const postEnv = {
304
+ ...pipelineEnv,
305
+ APX_LLM_OUTPUT: llmOutput.slice(0, 32_000),
306
+ APX_STATUS: status,
307
+ APX_SKIPPED: skip ? "1" : "0",
308
+ };
309
+ for (const rawCmd of routine.post_commands) {
310
+ const cmd = resolveArtifactRef(rawCmd, storagePath);
311
+ await runShellCmd(cmd, postEnv, cwd);
312
+ // Post-command failures are logged but don't change routine status.
313
+ }
314
+ }
315
+
316
+ // Cleanup temp file.
317
+ if (preOutputFile) try { fs.unlinkSync(preOutputFile); } catch {}
318
+
191
319
  const lastRun = nowIso();
192
320
  const next = computeNextRun({ schedule: routine.schedule, last_run_at: lastRun });
193
321
  const isOnce = parseSchedule(routine.schedule).kind === "once";
@@ -205,9 +333,9 @@ export async function runRoutineNow(ctx, routine) {
205
333
  actor_id: "apx:routine",
206
334
  author: "apx",
207
335
  body: status === "ok"
208
- ? `routine ${routine.name} ok`
336
+ ? `routine ${routine.name} ok${skip ? " (skipped LLM)" : ""}`
209
337
  : `routine ${routine.name} error: ${errMsg}`,
210
- meta: { routine: routine.name, status, result },
338
+ meta: { routine: routine.name, status, skipped: skip, result },
211
339
  });
212
340
  return { ...result, last_run_at: lastRun, next_run_at: next };
213
341
  }
@@ -3,8 +3,31 @@
3
3
  // Returns one JSON line with the result and session_id.
4
4
  // Reference: https://docs.claude.com/en/docs/claude-code/headless
5
5
 
6
+ import fs from "node:fs";
7
+ import path from "node:path";
6
8
  import { runProcess } from "./_spawn.js";
7
9
 
10
+ export function encodeClaudeProjectPath(cwd) {
11
+ return String(cwd || process.cwd()).replace(/[^A-Za-z0-9]/g, "-");
12
+ }
13
+
14
+ export function resolveClaudeSessionPath({ cwd, sessionId, home = process.env.HOME || process.env.USERPROFILE || "" }) {
15
+ if (!sessionId || !home) return null;
16
+ const projectsDir = path.join(home, ".claude", "projects");
17
+ const encodedCwd = encodeClaudeProjectPath(cwd);
18
+ const expected = path.join(projectsDir, encodedCwd, `${sessionId}.jsonl`);
19
+ if (fs.existsSync(expected)) return expected;
20
+
21
+ try {
22
+ for (const dir of fs.readdirSync(projectsDir)) {
23
+ const candidate = path.join(projectsDir, dir, `${sessionId}.jsonl`);
24
+ if (fs.existsSync(candidate)) return candidate;
25
+ }
26
+ } catch {}
27
+
28
+ return expected;
29
+ }
30
+
8
31
  export default {
9
32
  id: "claude-code",
10
33
  binary: "claude",
@@ -40,12 +63,7 @@ export default {
40
63
  }
41
64
 
42
65
  if (sessionId) {
43
- // Claude Code's session directory naming: replace BOTH "/" and "_" with
44
- // "-" (verified empirically against ~/.claude/projects/). The trailing
45
- // file is `<sessionId>.jsonl`.
46
- const home = process.env.HOME || process.env.USERPROFILE || "";
47
- const encodedCwd = (cwd || process.cwd()).replace(/[/_]/g, "-");
48
- externalSessionPath = `${home}/.claude/projects/${encodedCwd}/${sessionId}.jsonl`;
66
+ externalSessionPath = resolveClaudeSessionPath({ cwd, sessionId });
49
67
  }
50
68
 
51
69
  return {
@@ -0,0 +1,34 @@
1
+ // Cursor Agent runtime adapter. Uses print mode for non-interactive runs:
2
+ // cursor-agent --print --output-format text --trust --force "<prompt>"
3
+ // Reference: https://docs.cursor.com/en/cli/headless
4
+
5
+ import { runProcess } from "./_spawn.js";
6
+
7
+ export default {
8
+ id: "cursor-agent",
9
+ binary: "cursor-agent",
10
+ versionFlag: "--version",
11
+
12
+ async run({ system, prompt, cwd, env, timeoutMs }) {
13
+ const fullPrompt = system ? `${system}\n\n---\n\n${prompt}` : prompt;
14
+ const r = await runProcess({
15
+ command: "cursor-agent",
16
+ args: [
17
+ "--print",
18
+ "--output-format", "text",
19
+ "--trust",
20
+ "--force",
21
+ fullPrompt,
22
+ ],
23
+ cwd,
24
+ env,
25
+ timeoutMs,
26
+ });
27
+ return {
28
+ exitCode: r.exitCode,
29
+ output: r.stdout.trim(),
30
+ stderr: r.stderr,
31
+ externalSessionPath: null,
32
+ };
33
+ },
34
+ };
@@ -0,0 +1,32 @@
1
+ // Gemini CLI runtime adapter. Uses headless prompt mode:
2
+ // gemini --prompt "<prompt>" --output-format text --approval-mode yolo
3
+ // Reference: https://google-gemini.github.io/gemini-cli/docs/cli/headless.html
4
+
5
+ import { runProcess } from "./_spawn.js";
6
+
7
+ export default {
8
+ id: "gemini-cli",
9
+ binary: "gemini",
10
+ versionFlag: "--version",
11
+
12
+ async run({ system, prompt, cwd, env, timeoutMs }) {
13
+ const fullPrompt = system ? `${system}\n\n---\n\n${prompt}` : prompt;
14
+ const r = await runProcess({
15
+ command: "gemini",
16
+ args: [
17
+ "--prompt", fullPrompt,
18
+ "--output-format", "text",
19
+ "--approval-mode", "yolo",
20
+ ],
21
+ cwd,
22
+ env,
23
+ timeoutMs,
24
+ });
25
+ return {
26
+ exitCode: r.exitCode,
27
+ output: r.stdout.trim(),
28
+ stderr: r.stderr,
29
+ externalSessionPath: null,
30
+ };
31
+ },
32
+ };
@@ -1,5 +1,6 @@
1
1
  // Runtime adapters: spawn external agent CLIs (Claude Code, Codex, OpenCode,
2
- // Aider, ...) with the agent's system prompt + the prompt we want to run, and
2
+ // Aider, Cursor Agent, Gemini CLI, Qwen Code, ...) with the agent's system
3
+ // prompt + the prompt we want to run, and
3
4
  // capture their output. Unlike engines/ — which talk directly to model APIs —
4
5
  // runtimes/ delegate the whole conversation to the external tool. APX only
5
6
  // records the invocation, the prompt, the captured output, and where the tool
@@ -18,12 +19,18 @@ import claudeCode from "./claude-code.js";
18
19
  import codex from "./codex.js";
19
20
  import opencode from "./opencode.js";
20
21
  import aider from "./aider.js";
22
+ import cursorAgent from "./cursor-agent.js";
23
+ import geminiCli from "./gemini-cli.js";
24
+ import qwenCode from "./qwen-code.js";
21
25
 
22
26
  const REGISTRY = {
23
27
  "claude-code": claudeCode,
24
28
  codex,
25
29
  opencode,
26
30
  aider,
31
+ "cursor-agent": cursorAgent,
32
+ "gemini-cli": geminiCli,
33
+ "qwen-code": qwenCode,
27
34
  };
28
35
 
29
36
  export const RUNTIME_IDS = Object.keys(REGISTRY);
@@ -0,0 +1,36 @@
1
+ // Qwen Code runtime adapter. Uses non-interactive mode:
2
+ // qwen --output-format text --approval-mode yolo "<prompt>"
3
+ // Reference: https://qwenlm.github.io/qwen-code-docs/en/cli/index
4
+
5
+ import { runProcess } from "./_spawn.js";
6
+
7
+ export default {
8
+ id: "qwen-code",
9
+ binary: "qwen",
10
+ versionFlag: "--version",
11
+
12
+ async run({ system, prompt, cwd, env, timeoutMs }) {
13
+ const args = [
14
+ "--output-format", "text",
15
+ "--approval-mode", "yolo",
16
+ ];
17
+ if (system) {
18
+ args.push("--append-system-prompt", system);
19
+ }
20
+ args.push(prompt);
21
+
22
+ const r = await runProcess({
23
+ command: "qwen",
24
+ args,
25
+ cwd,
26
+ env,
27
+ timeoutMs,
28
+ });
29
+ return {
30
+ exitCode: r.exitCode,
31
+ output: r.stdout.trim(),
32
+ stderr: r.stderr,
33
+ externalSessionPath: null,
34
+ };
35
+ },
36
+ };
@@ -18,6 +18,7 @@ import callRuntime from "./tools/call-runtime.js";
18
18
  import sendTelegram from "./tools/send-telegram.js";
19
19
  import setIdentity from "./tools/set-identity.js";
20
20
  import setPermissionMode from "./tools/set-permission-mode.js";
21
+ import searchFiles from "./tools/search-files.js";
21
22
  import { createPermissionGuard } from "./helpers.js";
22
23
 
23
24
  const TOOLS = [
@@ -41,6 +42,7 @@ const TOOLS = [
41
42
  sendTelegram,
42
43
  setIdentity,
43
44
  setPermissionMode,
45
+ searchFiles,
44
46
  ];
45
47
 
46
48
  export const TOOL_SCHEMAS = TOOLS.map((tool) => tool.schema);