@agentprojectcontext/apx 1.8.1 → 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 +20 -4
- 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 +43 -9
- package/src/daemon/api.js +87 -9
- package/src/daemon/routines.js +143 -15
- 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 +8 -18
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
|
|
|
@@ -797,13 +837,13 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
797
837
|
app.get("/projects/:pid/routines", (req, res) => {
|
|
798
838
|
const p = project(req, res);
|
|
799
839
|
if (!p) return;
|
|
800
|
-
res.json(listRoutines(p.
|
|
840
|
+
res.json(listRoutines(p.storagePath));
|
|
801
841
|
});
|
|
802
842
|
|
|
803
843
|
app.get("/projects/:pid/routines/:name", (req, res) => {
|
|
804
844
|
const p = project(req, res);
|
|
805
845
|
if (!p) return;
|
|
806
|
-
const r = getRoutine(p.
|
|
846
|
+
const r = getRoutine(p.storagePath, req.params.name);
|
|
807
847
|
if (!r) return res.status(404).json({ error: "routine not found" });
|
|
808
848
|
res.json(r);
|
|
809
849
|
});
|
|
@@ -812,38 +852,76 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
812
852
|
const p = project(req, res);
|
|
813
853
|
if (!p) return;
|
|
814
854
|
try {
|
|
815
|
-
|
|
855
|
+
// Pass all fields including pipeline extensions.
|
|
856
|
+
const r = upsertRoutine(p.storagePath, req.body || {});
|
|
816
857
|
res.status(201).json(r);
|
|
817
858
|
} catch (e) {
|
|
818
859
|
res.status(400).json({ error: e.message });
|
|
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;
|
|
825
|
-
const ok = deleteRoutine(p.
|
|
903
|
+
const ok = deleteRoutine(p.storagePath, req.params.name);
|
|
826
904
|
res.status(ok ? 204 : 404).end();
|
|
827
905
|
});
|
|
828
906
|
|
|
829
907
|
app.post("/projects/:pid/routines/:name/enable", (req, res) => {
|
|
830
908
|
const p = project(req, res);
|
|
831
909
|
if (!p) return;
|
|
832
|
-
setRoutineEnabled(p.
|
|
910
|
+
setRoutineEnabled(p.storagePath, req.params.name, true);
|
|
833
911
|
res.json({ ok: true });
|
|
834
912
|
});
|
|
835
913
|
|
|
836
914
|
app.post("/projects/:pid/routines/:name/disable", (req, res) => {
|
|
837
915
|
const p = project(req, res);
|
|
838
916
|
if (!p) return;
|
|
839
|
-
setRoutineEnabled(p.
|
|
917
|
+
setRoutineEnabled(p.storagePath, req.params.name, false);
|
|
840
918
|
res.json({ ok: true });
|
|
841
919
|
});
|
|
842
920
|
|
|
843
921
|
app.post("/projects/:pid/routines/:name/run", async (req, res) => {
|
|
844
922
|
const p = project(req, res);
|
|
845
923
|
if (!p) return;
|
|
846
|
-
const r = getRoutine(p.
|
|
924
|
+
const r = getRoutine(p.storagePath, req.params.name);
|
|
847
925
|
if (!r) return res.status(404).json({ error: "routine not found" });
|
|
848
926
|
try {
|
|
849
927
|
const result = await runRoutineNow({ project: p, projects, plugins, registries, globalConfig: config }, r);
|
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,29 +174,152 @@ 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";
|
|
194
|
-
updateRunState(ctx.project.
|
|
322
|
+
updateRunState(ctx.project.storagePath, routine.name, {
|
|
195
323
|
last_run_at: lastRun,
|
|
196
324
|
last_status: status,
|
|
197
325
|
last_error: errMsg,
|
|
@@ -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
|
}
|
|
@@ -246,7 +374,7 @@ export class RoutineScheduler {
|
|
|
246
374
|
const nowStr = nowIso();
|
|
247
375
|
for (const proj of this.projects.list().map((p) => this.projects.get(p.id))) {
|
|
248
376
|
if (!proj) continue;
|
|
249
|
-
const due = getDueRoutines(proj.
|
|
377
|
+
const due = getDueRoutines(proj.storagePath, nowStr);
|
|
250
378
|
for (const r of due) {
|
|
251
379
|
this.log(`routine ${r.name} (${r.kind}) firing in project #${proj.id}`);
|
|
252
380
|
await runRoutineNow(
|
|
@@ -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
|
};
|