@agentprojectcontext/apx 1.0.3
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/LICENSE +21 -0
- package/README.md +142 -0
- package/package.json +52 -0
- package/skills/apx/SKILL.md +77 -0
- package/src/cli/commands/a2a.js +66 -0
- package/src/cli/commands/agent.js +181 -0
- package/src/cli/commands/chat.js +84 -0
- package/src/cli/commands/command.js +42 -0
- package/src/cli/commands/config.js +56 -0
- package/src/cli/commands/daemon.js +148 -0
- package/src/cli/commands/exec.js +56 -0
- package/src/cli/commands/identity.js +146 -0
- package/src/cli/commands/init.js +23 -0
- package/src/cli/commands/mcp.js +147 -0
- package/src/cli/commands/memory.js +69 -0
- package/src/cli/commands/messages.js +61 -0
- package/src/cli/commands/plugins.js +23 -0
- package/src/cli/commands/project.js +124 -0
- package/src/cli/commands/routine.js +99 -0
- package/src/cli/commands/runtime.js +64 -0
- package/src/cli/commands/session.js +387 -0
- package/src/cli/commands/skills.js +153 -0
- package/src/cli/commands/telegram.js +48 -0
- package/src/cli/http.js +102 -0
- package/src/cli/index.js +481 -0
- package/src/cli/postinstall.js +25 -0
- package/src/core/apc-context-skill.md +150 -0
- package/src/core/apx-skill.md +78 -0
- package/src/core/config.js +129 -0
- package/src/core/identity.js +23 -0
- package/src/core/messages-store.js +421 -0
- package/src/core/parser.js +217 -0
- package/src/core/routines-store.js +144 -0
- package/src/core/scaffold.js +417 -0
- package/src/core/session-store.js +36 -0
- package/src/daemon/apc-runtime-context.js +123 -0
- package/src/daemon/api.js +946 -0
- package/src/daemon/compact.js +140 -0
- package/src/daemon/conversations.js +108 -0
- package/src/daemon/db.js +81 -0
- package/src/daemon/engines/anthropic.js +58 -0
- package/src/daemon/engines/gemini.js +55 -0
- package/src/daemon/engines/index.js +65 -0
- package/src/daemon/engines/mock.js +18 -0
- package/src/daemon/engines/ollama.js +66 -0
- package/src/daemon/engines/openai.js +58 -0
- package/src/daemon/env-detect.js +69 -0
- package/src/daemon/index.js +156 -0
- package/src/daemon/mcp-runner.js +218 -0
- package/src/daemon/mcp-sources.js +114 -0
- package/src/daemon/plugins/index.js +91 -0
- package/src/daemon/plugins/telegram.js +549 -0
- package/src/daemon/project-config.js +98 -0
- package/src/daemon/routines.js +211 -0
- package/src/daemon/runtimes/_spawn.js +44 -0
- package/src/daemon/runtimes/aider.js +32 -0
- package/src/daemon/runtimes/claude-code.js +60 -0
- package/src/daemon/runtimes/codex.js +30 -0
- package/src/daemon/runtimes/index.js +39 -0
- package/src/daemon/runtimes/opencode.js +28 -0
- package/src/daemon/smoke.js +54 -0
- package/src/daemon/super-agent-tools.js +539 -0
- package/src/daemon/super-agent.js +188 -0
- package/src/daemon/thinking.js +45 -0
- package/src/daemon/tool-call-parser.js +116 -0
- package/src/daemon/wakeup.js +92 -0
- package/src/mcp/index.js +220 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Routines: scheduled tasks per project. State persists in .apc/routines.json.
|
|
2
|
+
//
|
|
3
|
+
// Schedule formats:
|
|
4
|
+
// every:60s | every:5m | every:1h | once:<iso-8601>
|
|
5
|
+
//
|
|
6
|
+
// Kinds:
|
|
7
|
+
// heartbeat — log a heartbeat message. spec: { channel?, message? }
|
|
8
|
+
// exec_agent — call an agent engine. spec: { agent: slug, prompt }
|
|
9
|
+
// telegram — send a Telegram message. spec: { channel?, chat_id?, text }
|
|
10
|
+
// shell — run a shell command. spec: { command, timeout_ms? }
|
|
11
|
+
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import { callEngine } from "./engines/index.js";
|
|
16
|
+
import { readAgents } from "../core/parser.js";
|
|
17
|
+
import {
|
|
18
|
+
listRoutines,
|
|
19
|
+
getRoutine,
|
|
20
|
+
upsertRoutine,
|
|
21
|
+
deleteRoutine,
|
|
22
|
+
setEnabled,
|
|
23
|
+
updateRunState,
|
|
24
|
+
getDueRoutines,
|
|
25
|
+
parseSchedule,
|
|
26
|
+
computeNextRun,
|
|
27
|
+
} from "../core/routines-store.js";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
listRoutines,
|
|
31
|
+
getRoutine,
|
|
32
|
+
upsertRoutine,
|
|
33
|
+
deleteRoutine,
|
|
34
|
+
setEnabled,
|
|
35
|
+
parseSchedule,
|
|
36
|
+
computeNextRun,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const TICK_MS = 5_000;
|
|
40
|
+
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
41
|
+
|
|
42
|
+
// --------------------- handlers ---------------------------------------------
|
|
43
|
+
|
|
44
|
+
async function handleHeartbeat(ctx, routine) {
|
|
45
|
+
const { project } = ctx;
|
|
46
|
+
const channel = routine.spec.channel || "heartbeat";
|
|
47
|
+
const message = routine.spec.message || `heartbeat from ${routine.name}`;
|
|
48
|
+
project.logMessage({
|
|
49
|
+
channel,
|
|
50
|
+
direction: "out",
|
|
51
|
+
author: "apx",
|
|
52
|
+
body: message,
|
|
53
|
+
meta: { routine: routine.name },
|
|
54
|
+
});
|
|
55
|
+
return { status: "ok", note: `logged to messages on channel '${channel}'` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function handleExecAgent(ctx, routine) {
|
|
59
|
+
const { project, globalConfig } = ctx;
|
|
60
|
+
const { agent: slug, prompt } = routine.spec;
|
|
61
|
+
if (!slug || !prompt) throw new Error("exec_agent: spec needs { agent, prompt }");
|
|
62
|
+
|
|
63
|
+
const agents = readAgents(project.path);
|
|
64
|
+
const agent = agents.find((a) => a.slug === slug);
|
|
65
|
+
if (!agent) throw new Error(`agent ${slug} not found`);
|
|
66
|
+
const model = agent.fields.Model;
|
|
67
|
+
if (!model) throw new Error(`agent ${slug} has no model`);
|
|
68
|
+
|
|
69
|
+
const f = agent.fields;
|
|
70
|
+
const parts = [];
|
|
71
|
+
if (f.Description) parts.push(f.Description);
|
|
72
|
+
if (f.Role) parts.push(`Role: ${f.Role}`);
|
|
73
|
+
const memPath = path.join(project.path, ".apc", "agents", slug, "memory.md");
|
|
74
|
+
if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
|
|
75
|
+
parts.push(`You were invoked by routine "${routine.name}". Reply briefly, max 4 sentences.`);
|
|
76
|
+
|
|
77
|
+
const result = await callEngine({
|
|
78
|
+
modelId: model,
|
|
79
|
+
system: parts.join("\n\n"),
|
|
80
|
+
messages: [{ role: "user", content: prompt }],
|
|
81
|
+
config: project.config || globalConfig,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
project.logMessage({
|
|
85
|
+
agent_slug: slug,
|
|
86
|
+
channel: "routine",
|
|
87
|
+
direction: "out",
|
|
88
|
+
author: slug,
|
|
89
|
+
body: result.text,
|
|
90
|
+
meta: { routine: routine.name, usage: result.usage },
|
|
91
|
+
});
|
|
92
|
+
return { status: "ok", reply: result.text };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function handleTelegram(ctx, routine) {
|
|
96
|
+
const { plugins } = ctx;
|
|
97
|
+
const tg = plugins?.get("telegram");
|
|
98
|
+
if (!tg) throw new Error("telegram plugin not loaded");
|
|
99
|
+
const { channel, chat_id, text } = routine.spec;
|
|
100
|
+
if (!text) throw new Error("telegram routine needs spec.text");
|
|
101
|
+
await tg.send({ channel, chat_id, text });
|
|
102
|
+
return { status: "ok" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleShell(ctx, routine) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const { command, timeout_ms = 30_000 } = routine.spec;
|
|
108
|
+
if (!command) return reject(new Error("shell routine needs spec.command"));
|
|
109
|
+
const child = spawn("sh", ["-c", command], {
|
|
110
|
+
cwd: ctx.project.path,
|
|
111
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
112
|
+
});
|
|
113
|
+
let stdout = "";
|
|
114
|
+
let stderr = "";
|
|
115
|
+
const timer = setTimeout(() => child.kill("SIGTERM"), timeout_ms);
|
|
116
|
+
child.stdout.on("data", (c) => (stdout += c.toString()));
|
|
117
|
+
child.stderr.on("data", (c) => (stderr += c.toString()));
|
|
118
|
+
child.on("close", (code) => {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
if (code === 0) resolve({ status: "ok", stdout: stdout.trim().slice(0, 4000) });
|
|
121
|
+
else resolve({ status: "error", code, stderr: stderr.trim().slice(0, 2000) });
|
|
122
|
+
});
|
|
123
|
+
child.on("error", (e) => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
reject(e);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const HANDLERS = {
|
|
131
|
+
heartbeat: handleHeartbeat,
|
|
132
|
+
exec_agent: handleExecAgent,
|
|
133
|
+
telegram: handleTelegram,
|
|
134
|
+
shell: handleShell,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// --------------------- runtime: run one + loop ------------------------------
|
|
138
|
+
|
|
139
|
+
export async function runRoutineNow(ctx, routine) {
|
|
140
|
+
const handler = HANDLERS[routine.kind];
|
|
141
|
+
if (!handler) throw new Error(`unknown routine kind: ${routine.kind}`);
|
|
142
|
+
let result;
|
|
143
|
+
let status = "ok";
|
|
144
|
+
let errMsg = null;
|
|
145
|
+
try {
|
|
146
|
+
result = await handler(ctx, routine);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
status = "error";
|
|
149
|
+
errMsg = e.message;
|
|
150
|
+
result = { status: "error", error: e.message };
|
|
151
|
+
}
|
|
152
|
+
const lastRun = nowIso();
|
|
153
|
+
const next = computeNextRun({ schedule: routine.schedule, last_run_at: lastRun });
|
|
154
|
+
const isOnce = parseSchedule(routine.schedule).kind === "once";
|
|
155
|
+
updateRunState(ctx.project.path, routine.name, {
|
|
156
|
+
last_run_at: lastRun,
|
|
157
|
+
last_status: status,
|
|
158
|
+
last_error: errMsg,
|
|
159
|
+
next_run_at: next,
|
|
160
|
+
disable: isOnce,
|
|
161
|
+
});
|
|
162
|
+
return { ...result, last_run_at: lastRun, next_run_at: next };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export class RoutineScheduler {
|
|
166
|
+
constructor({ projects, plugins, globalConfig, log }) {
|
|
167
|
+
this.projects = projects;
|
|
168
|
+
this.plugins = plugins;
|
|
169
|
+
this.globalConfig = globalConfig;
|
|
170
|
+
this.log = log || (() => {});
|
|
171
|
+
this._timer = null;
|
|
172
|
+
this._running = false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
start() {
|
|
176
|
+
if (this._timer) return;
|
|
177
|
+
this._timer = setInterval(
|
|
178
|
+
() => this._tick().catch((e) => this.log(`routines tick error: ${e.message}`)),
|
|
179
|
+
TICK_MS
|
|
180
|
+
);
|
|
181
|
+
this._timer.unref?.();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
stop() {
|
|
185
|
+
if (this._timer) {
|
|
186
|
+
clearInterval(this._timer);
|
|
187
|
+
this._timer = null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async _tick() {
|
|
192
|
+
if (this._running) return;
|
|
193
|
+
this._running = true;
|
|
194
|
+
try {
|
|
195
|
+
const nowStr = nowIso();
|
|
196
|
+
for (const proj of this.projects.list().map((p) => this.projects.get(p.id))) {
|
|
197
|
+
if (!proj) continue;
|
|
198
|
+
const due = getDueRoutines(proj.path, nowStr);
|
|
199
|
+
for (const r of due) {
|
|
200
|
+
this.log(`routine ${r.name} (${r.kind}) firing in project #${proj.id}`);
|
|
201
|
+
await runRoutineNow(
|
|
202
|
+
{ project: proj, plugins: this.plugins, globalConfig: this.globalConfig },
|
|
203
|
+
r
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
this._running = false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Shared spawn helper: runs a command, pipes a string to stdin, captures
|
|
2
|
+
// stdout/stderr, returns when the process exits or the timeout fires.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
|
|
7
|
+
export function runProcess({ command, args = [], stdin = "", cwd, env, timeoutMs = DEFAULT_TIMEOUT }) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const child = spawn(command, args, {
|
|
10
|
+
cwd,
|
|
11
|
+
env: { ...process.env, ...(env || {}) },
|
|
12
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
let stdout = "";
|
|
16
|
+
let stderr = "";
|
|
17
|
+
let killed = false;
|
|
18
|
+
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
killed = true;
|
|
21
|
+
child.kill("SIGTERM");
|
|
22
|
+
}, timeoutMs);
|
|
23
|
+
|
|
24
|
+
child.stdout.setEncoding("utf8");
|
|
25
|
+
child.stderr.setEncoding("utf8");
|
|
26
|
+
child.stdout.on("data", (c) => (stdout += c));
|
|
27
|
+
child.stderr.on("data", (c) => (stderr += c));
|
|
28
|
+
|
|
29
|
+
child.on("error", (err) => {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
resolve({ exitCode: -1, stdout, stderr, error: err.message, killed });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.on("close", (code) => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
resolve({ exitCode: code, stdout, stderr, killed });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (stdin) {
|
|
40
|
+
child.stdin.write(stdin);
|
|
41
|
+
}
|
|
42
|
+
child.stdin.end();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Aider runtime adapter. Uses --message for non-interactive mode.
|
|
2
|
+
// aider --message "<prompt>" --no-auto-commits --yes
|
|
3
|
+
// Reference: https://aider.chat/docs/scripting.html
|
|
4
|
+
|
|
5
|
+
import { runProcess } from "./_spawn.js";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
id: "aider",
|
|
9
|
+
binary: "aider",
|
|
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: "aider",
|
|
16
|
+
args: [
|
|
17
|
+
"--message", fullPrompt,
|
|
18
|
+
"--yes-always",
|
|
19
|
+
"--no-auto-commits",
|
|
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
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Claude Code runtime adapter. Uses the headless `-p` mode:
|
|
2
|
+
// claude -p "<prompt>" --append-system-prompt "<system>" --output-format json
|
|
3
|
+
// Returns one JSON line with the result and session_id.
|
|
4
|
+
// Reference: https://docs.claude.com/en/docs/claude-code/headless
|
|
5
|
+
|
|
6
|
+
import { runProcess } from "./_spawn.js";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
id: "claude-code",
|
|
10
|
+
binary: "claude",
|
|
11
|
+
versionFlag: "--version",
|
|
12
|
+
|
|
13
|
+
async run({ system, prompt, cwd, env, timeoutMs }) {
|
|
14
|
+
const args = ["-p", prompt, "--output-format", "json"];
|
|
15
|
+
if (system) {
|
|
16
|
+
args.push("--append-system-prompt", system);
|
|
17
|
+
}
|
|
18
|
+
const r = await runProcess({
|
|
19
|
+
command: "claude",
|
|
20
|
+
args,
|
|
21
|
+
cwd,
|
|
22
|
+
env,
|
|
23
|
+
timeoutMs,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let output = r.stdout.trim();
|
|
27
|
+
let sessionId = null;
|
|
28
|
+
let externalSessionPath = null;
|
|
29
|
+
let parsed = null;
|
|
30
|
+
|
|
31
|
+
if (output) {
|
|
32
|
+
try {
|
|
33
|
+
// headless --output-format json emits a single-line JSON result
|
|
34
|
+
parsed = JSON.parse(output);
|
|
35
|
+
if (parsed.result) output = parsed.result;
|
|
36
|
+
sessionId = parsed.session_id || null;
|
|
37
|
+
} catch {
|
|
38
|
+
// not JSON — keep raw stdout
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
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`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
exitCode: r.exitCode,
|
|
53
|
+
output,
|
|
54
|
+
stderr: r.stderr,
|
|
55
|
+
externalSessionPath,
|
|
56
|
+
sessionId,
|
|
57
|
+
raw: parsed,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// OpenAI Codex CLI runtime adapter.
|
|
2
|
+
// codex exec "<prompt>"
|
|
3
|
+
// System prompt is prepended to the prompt body since Codex doesn't have a
|
|
4
|
+
// dedicated --system flag in `exec` mode.
|
|
5
|
+
// Reference: https://github.com/openai/codex
|
|
6
|
+
|
|
7
|
+
import { runProcess } from "./_spawn.js";
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
id: "codex",
|
|
11
|
+
binary: "codex",
|
|
12
|
+
versionFlag: "--version",
|
|
13
|
+
|
|
14
|
+
async run({ system, prompt, cwd, env, timeoutMs }) {
|
|
15
|
+
const fullPrompt = system ? `${system}\n\n---\n\n${prompt}` : prompt;
|
|
16
|
+
const r = await runProcess({
|
|
17
|
+
command: "codex",
|
|
18
|
+
args: ["exec", fullPrompt],
|
|
19
|
+
cwd,
|
|
20
|
+
env,
|
|
21
|
+
timeoutMs,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
exitCode: r.exitCode,
|
|
25
|
+
output: r.stdout.trim(),
|
|
26
|
+
stderr: r.stderr,
|
|
27
|
+
externalSessionPath: null,
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
|
3
|
+
// capture their output. Unlike engines/ — which talk directly to model APIs —
|
|
4
|
+
// runtimes/ delegate the whole conversation to the external tool. APX only
|
|
5
|
+
// records the invocation, the prompt, the captured output, and where the tool
|
|
6
|
+
// stored its own session (if it tells us).
|
|
7
|
+
//
|
|
8
|
+
// Each runtime exports:
|
|
9
|
+
// {
|
|
10
|
+
// id,
|
|
11
|
+
// binary, executable name to look for in PATH
|
|
12
|
+
// versionFlag, flag to print the version
|
|
13
|
+
// async run({ system, prompt, cwd, env, timeoutMs })
|
|
14
|
+
// → { exitCode, output, externalSessionPath?, raw? }
|
|
15
|
+
// }
|
|
16
|
+
|
|
17
|
+
import claudeCode from "./claude-code.js";
|
|
18
|
+
import codex from "./codex.js";
|
|
19
|
+
import opencode from "./opencode.js";
|
|
20
|
+
import aider from "./aider.js";
|
|
21
|
+
|
|
22
|
+
const REGISTRY = {
|
|
23
|
+
"claude-code": claudeCode,
|
|
24
|
+
codex,
|
|
25
|
+
opencode,
|
|
26
|
+
aider,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const RUNTIME_IDS = Object.keys(REGISTRY);
|
|
30
|
+
|
|
31
|
+
export function getRuntime(id) {
|
|
32
|
+
const r = REGISTRY[id];
|
|
33
|
+
if (!r) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`unknown runtime "${id}". Known: ${RUNTIME_IDS.join(", ")}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return r;
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// OpenCode runtime adapter. Uses headless run:
|
|
2
|
+
// opencode run "<prompt>"
|
|
3
|
+
// Reference: https://opencode.ai/docs/
|
|
4
|
+
|
|
5
|
+
import { runProcess } from "./_spawn.js";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
id: "opencode",
|
|
9
|
+
binary: "opencode",
|
|
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: "opencode",
|
|
16
|
+
args: ["run", fullPrompt],
|
|
17
|
+
cwd,
|
|
18
|
+
env,
|
|
19
|
+
timeoutMs,
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
exitCode: r.exitCode,
|
|
23
|
+
output: r.stdout.trim(),
|
|
24
|
+
stderr: r.stderr,
|
|
25
|
+
externalSessionPath: null,
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Smoke test: register the example project and verify agents, sessions, MCPs
|
|
2
|
+
// are readable from the filesystem without any SQLite.
|
|
3
|
+
//
|
|
4
|
+
// node src/smoke.js
|
|
5
|
+
//
|
|
6
|
+
// Exits non-zero on failure.
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { ProjectManager } from "./db.js";
|
|
11
|
+
import { McpRegistry } from "./mcp-runner.js";
|
|
12
|
+
import { readAgents } from "../core/parser.js";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
const EXAMPLE = path.resolve(__dirname, "..", "..", "examples", "my-first-project");
|
|
18
|
+
|
|
19
|
+
function assert(cond, msg) {
|
|
20
|
+
if (!cond) {
|
|
21
|
+
console.error("FAIL:", msg);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const projects = new ProjectManager();
|
|
27
|
+
const entry = projects.register(EXAMPLE);
|
|
28
|
+
console.log("registered project", entry.id, entry.path);
|
|
29
|
+
|
|
30
|
+
const agents = readAgents(entry.path);
|
|
31
|
+
console.log("agents:", agents.map((a) => `${a.slug} (${a.fields.Role || "-"}, ${a.fields.Model || "-"})`));
|
|
32
|
+
assert(agents.length === 2, `expected 2 agents, got ${agents.length}`);
|
|
33
|
+
assert(agents.find((a) => a.slug === "sofia"), "sofia missing");
|
|
34
|
+
assert(agents.find((a) => a.slug === "martin"), "martin missing");
|
|
35
|
+
|
|
36
|
+
// Sessions: scan .apc/agents/sofia/sessions/
|
|
37
|
+
const sofiaSessions = (() => {
|
|
38
|
+
const dir = path.join(entry.path, ".apc", "agents", "sofia", "sessions");
|
|
39
|
+
if (!fs.existsSync(dir)) return [];
|
|
40
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
41
|
+
})();
|
|
42
|
+
console.log("sofia sessions:", sofiaSessions);
|
|
43
|
+
assert(sofiaSessions.length >= 1, "expected at least one sofia session");
|
|
44
|
+
|
|
45
|
+
const reg = new McpRegistry(entry.path);
|
|
46
|
+
const list = reg.list();
|
|
47
|
+
console.log("mcps:", list.map((m) => `${m.name} (${m.source})`));
|
|
48
|
+
assert(list.find((m) => m.name === "filesystem" && m.source === "apc"), "filesystem MCP missing");
|
|
49
|
+
assert(list.find((m) => m.name === "brave" && m.source === "apc"), "brave MCP missing");
|
|
50
|
+
const conflicts = reg.conflicts();
|
|
51
|
+
console.log("conflicts:", conflicts);
|
|
52
|
+
reg.shutdown();
|
|
53
|
+
|
|
54
|
+
console.log("OK");
|