@agentprojectcontext/apx 1.8.2 → 1.9.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 +4 -1
- package/src/cli/commands/artifact.js +45 -0
- package/src/cli/commands/routine.js +15 -1
- package/src/cli/commands/sys.js +325 -0
- package/src/cli/index.js +97 -3
- package/src/cli/terminal-chat/renderer.js +412 -0
- package/src/core/artifacts-store.js +59 -0
- package/src/core/routines-store.js +40 -7
- package/src/daemon/api.js +80 -2
- package/src/daemon/routines.js +141 -13
- package/src/daemon/runtimes/claude-code.js +24 -6
- package/src/daemon/super-agent-tools/index.js +2 -0
- package/src/daemon/super-agent-tools/tools/call-runtime.js +111 -41
- package/src/daemon/super-agent-tools/tools/search-files.js +66 -0
- package/src/daemon/super-agent.js +6 -17
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
|
|
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
|
-
|
|
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;
|
package/src/daemon/routines.js
CHANGED
|
@@ -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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 =
|
|
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
|
-
}
|
|
187
|
-
|
|
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
|
-
|
|
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 {
|
|
@@ -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);
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { readAgents } from "../../../core/parser.js";
|
|
4
|
+
import {
|
|
5
|
+
buildApfHint,
|
|
6
|
+
closeRuntimeSession,
|
|
7
|
+
createRuntimeSession,
|
|
8
|
+
extractApfResult,
|
|
9
|
+
} from "../../apc-runtime-context.js";
|
|
2
10
|
import { getRuntime, RUNTIME_IDS } from "../../runtimes/index.js";
|
|
3
11
|
import { buildAgentSystem, confirmedProperty, resolveProject } from "../helpers.js";
|
|
4
12
|
|
|
@@ -20,18 +28,51 @@ function resolveProjectForAgent(projects, project, slug) {
|
|
|
20
28
|
return resolveProject(projects, project);
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
function projectName(project) {
|
|
32
|
+
try {
|
|
33
|
+
const meta = JSON.parse(fs.readFileSync(path.join(project.path, ".apc", "project.json"), "utf8"));
|
|
34
|
+
if (meta.name) return meta.name;
|
|
35
|
+
} catch {}
|
|
36
|
+
return path.basename(project.path);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildRuntimeSystem(project, agent, runtime, sessionId, caller) {
|
|
40
|
+
const agentSlug = agent?.slug || "apx";
|
|
41
|
+
const hint = buildApfHint({
|
|
42
|
+
projectName: projectName(project),
|
|
43
|
+
projectPath: project.path,
|
|
44
|
+
agentSlug,
|
|
45
|
+
sessionId,
|
|
46
|
+
});
|
|
47
|
+
if (agent) {
|
|
48
|
+
return buildAgentSystem(project, agent, {
|
|
49
|
+
invocation: "runtime",
|
|
50
|
+
runtime,
|
|
51
|
+
caller,
|
|
52
|
+
extraParts: [hint],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
"You are APX running inside an external coding runtime.",
|
|
58
|
+
"No APC agent was explicitly selected for this run.",
|
|
59
|
+
"Use the project context and runtime tools directly. Do not impersonate a project agent.",
|
|
60
|
+
hint,
|
|
61
|
+
].join("\n\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
23
64
|
export default {
|
|
24
65
|
name: "call_runtime",
|
|
25
66
|
schema: {
|
|
26
67
|
type: "function",
|
|
27
68
|
function: {
|
|
28
69
|
name: "call_runtime",
|
|
29
|
-
description: "Spawn an external CLI runtime (Claude Code, Codex, OpenCode, Aider) impersonating an APC agent.",
|
|
70
|
+
description: "Spawn an external CLI runtime (Claude Code, Codex, OpenCode, Aider), optionally impersonating an APC agent.",
|
|
30
71
|
parameters: {
|
|
31
72
|
type: "object",
|
|
32
73
|
properties: {
|
|
33
74
|
project: { type: "string" },
|
|
34
|
-
agent: { type: "string", description: "APC agent slug from AGENTS.md, not runtime name" },
|
|
75
|
+
agent: { type: "string", description: "Optional APC agent slug from AGENTS.md, not runtime name. Omit when the user did not name an agent." },
|
|
35
76
|
runtime: {
|
|
36
77
|
type: "string",
|
|
37
78
|
enum: ["claude-code", "codex", "opencode", "aider"],
|
|
@@ -41,16 +82,16 @@ export default {
|
|
|
41
82
|
timeout_s: { type: "integer", description: "seconds before SIGTERM; default 300" },
|
|
42
83
|
confirmed: confirmedProperty("true only after explicit user confirmation for this exact runtime command"),
|
|
43
84
|
},
|
|
44
|
-
required: ["
|
|
85
|
+
required: ["runtime", "prompt"],
|
|
45
86
|
},
|
|
46
87
|
},
|
|
47
88
|
},
|
|
48
89
|
makeHandler: ({ projects, requirePermission }) => async ({ project, agent: slug, runtime, prompt, timeout_s = 300, confirmed = false }) => {
|
|
49
90
|
requirePermission("call_runtime", { dangerous: true, confirmed });
|
|
50
91
|
|
|
51
|
-
const p = resolveProjectForAgent(projects, project, slug);
|
|
52
|
-
const agent = readAgents(p.path).find((a) => a.slug === slug);
|
|
53
|
-
if (!agent) {
|
|
92
|
+
const p = slug ? resolveProjectForAgent(projects, project, slug) : resolveProject(projects, project);
|
|
93
|
+
const agent = slug ? readAgents(p.path).find((a) => a.slug === slug) : null;
|
|
94
|
+
if (slug && !agent) {
|
|
54
95
|
const directory = projects.list().map((entry) => ({
|
|
55
96
|
project: entry.name,
|
|
56
97
|
kind: entry.id === 0 ? "default" : "project",
|
|
@@ -67,45 +108,74 @@ export default {
|
|
|
67
108
|
return { error: `${e.message}. Available runtimes: ${RUNTIME_IDS.join(", ")}` };
|
|
68
109
|
}
|
|
69
110
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
cwd: p.path,
|
|
78
|
-
timeoutMs: timeout_s * 1000,
|
|
111
|
+
const actor = agent?.slug || "apx";
|
|
112
|
+
const session = createRuntimeSession({
|
|
113
|
+
projectRoot: p.path,
|
|
114
|
+
storageRoot: p.storagePath,
|
|
115
|
+
agentSlug: actor,
|
|
116
|
+
runtime,
|
|
117
|
+
title: `Runtime: ${runtime}${agent ? ` (${agent.slug})` : ""}`,
|
|
79
118
|
});
|
|
80
119
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
try {
|
|
121
|
+
const r = await rt.run({
|
|
122
|
+
system: buildRuntimeSystem(p, agent, runtime, session.id, "super_agent_tool"),
|
|
123
|
+
prompt,
|
|
124
|
+
cwd: p.path,
|
|
125
|
+
timeoutMs: timeout_s * 1000,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = extractApfResult(r.output) || (r.output || "").slice(0, 200);
|
|
129
|
+
closeRuntimeSession({
|
|
130
|
+
filePath: session.path,
|
|
131
|
+
externalSessionPath: r.externalSessionPath || null,
|
|
132
|
+
exitCode: r.exitCode,
|
|
133
|
+
result,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
p.logMessage({
|
|
137
|
+
agent_slug: actor,
|
|
138
|
+
channel: "runtime",
|
|
139
|
+
direction: "in",
|
|
140
|
+
author: "user",
|
|
141
|
+
body: prompt,
|
|
142
|
+
meta: { runtime, invoked_by: "super_agent_tool", apc_session: session.id },
|
|
143
|
+
});
|
|
144
|
+
p.logMessage({
|
|
145
|
+
agent_slug: actor,
|
|
146
|
+
channel: "runtime",
|
|
147
|
+
direction: "out",
|
|
148
|
+
author: actor,
|
|
149
|
+
body: r.output || "",
|
|
150
|
+
meta: {
|
|
151
|
+
runtime,
|
|
152
|
+
exit_code: r.exitCode,
|
|
153
|
+
external_session_path: r.externalSessionPath || null,
|
|
154
|
+
session_id: r.sessionId || null,
|
|
155
|
+
apc_session: session.id,
|
|
156
|
+
invoked_by: "super_agent_tool",
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return {
|
|
96
161
|
runtime,
|
|
162
|
+
agent: agent?.slug || null,
|
|
163
|
+
apc_session: session.id,
|
|
97
164
|
exit_code: r.exitCode,
|
|
165
|
+
output: (r.output || "").slice(0, 4000),
|
|
166
|
+
truncated: (r.output || "").length > 4000,
|
|
98
167
|
external_session_path: r.externalSessionPath || null,
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
168
|
+
session_id: r.sessionId || null,
|
|
169
|
+
};
|
|
170
|
+
} catch (e) {
|
|
171
|
+
try {
|
|
172
|
+
closeRuntimeSession({
|
|
173
|
+
filePath: session.path,
|
|
174
|
+
exitCode: -1,
|
|
175
|
+
result: `error: ${e.message.slice(0, 200)}`,
|
|
176
|
+
});
|
|
177
|
+
} catch {}
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
110
180
|
},
|
|
111
181
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { resolveProject, safePathJoin } from "../helpers.js";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
name: "search_files",
|
|
9
|
+
schema: {
|
|
10
|
+
type: "function",
|
|
11
|
+
function: {
|
|
12
|
+
name: "search_files",
|
|
13
|
+
description: "Search for text patterns inside project files using ripgrep or grep.",
|
|
14
|
+
parameters: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
query: { type: "string", description: "The text or regex pattern to search for." },
|
|
18
|
+
project: { type: "string" },
|
|
19
|
+
path: { type: "string", description: "relative path inside the project to restrict search; default '.'" },
|
|
20
|
+
},
|
|
21
|
+
required: ["query"],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
makeHandler: ({ projects, requirePermission }) => async ({ query, project, path: sub = "." } = {}) => {
|
|
26
|
+
// Optional permission check if it's considered destructive, but search is safe read-only
|
|
27
|
+
await requirePermission("search_files", { query, project, path: sub }, "safe");
|
|
28
|
+
|
|
29
|
+
const p = resolveProject(projects, project);
|
|
30
|
+
const target = safePathJoin(p.path, sub);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const { stdout } = await execFileAsync("rg", ["-n", "--no-heading", "--color=never", query, target], {
|
|
34
|
+
cwd: p.path,
|
|
35
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
36
|
+
});
|
|
37
|
+
return formatResults(stdout);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (e.code === "ENOENT" || e.message.includes("ENOENT")) {
|
|
40
|
+
try {
|
|
41
|
+
const { stdout } = await execFileAsync("grep", ["-rn", query, target], {
|
|
42
|
+
cwd: p.path,
|
|
43
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
44
|
+
});
|
|
45
|
+
return formatResults(stdout);
|
|
46
|
+
} catch (e2) {
|
|
47
|
+
if (e2.code === 1) return { result: "No matches found." };
|
|
48
|
+
throw new Error(`grep failed: ${e2.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (e.code === 1) return { result: "No matches found." };
|
|
52
|
+
return { error: `search failed: ${e.message}` };
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function formatResults(stdout) {
|
|
58
|
+
if (!stdout) return { result: "No matches found." };
|
|
59
|
+
const lines = stdout.split('\n').slice(0, 100);
|
|
60
|
+
const out = lines.join('\n');
|
|
61
|
+
if (lines.length >= 100) {
|
|
62
|
+
return { result: out + '\n...(truncated)' };
|
|
63
|
+
}
|
|
64
|
+
return { result: out };
|
|
65
|
+
}
|
|
66
|
+
|