@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,69 @@
|
|
|
1
|
+
// Best-effort detection of installed agent CLIs and LLM runners.
|
|
2
|
+
// We just probe the binary with `--version` (or equivalent) and don't fail if
|
|
3
|
+
// it isn't there — caller decides what to do with absence.
|
|
4
|
+
import { runProcess } from "./runtimes/_spawn.js";
|
|
5
|
+
|
|
6
|
+
const PROBES = [
|
|
7
|
+
// Coding-agent CLIs (runtimes/)
|
|
8
|
+
{ id: "claude-code", binary: "claude", args: ["--version"], category: "runtime" },
|
|
9
|
+
{ id: "codex", binary: "codex", args: ["--version"], category: "runtime" },
|
|
10
|
+
{ id: "opencode", binary: "opencode", args: ["--version"], category: "runtime" },
|
|
11
|
+
{ id: "aider", binary: "aider", args: ["--version"], category: "runtime" },
|
|
12
|
+
{ id: "gemini-cli", binary: "gemini", args: ["--version"], category: "runtime" },
|
|
13
|
+
{ id: "cursor-agent",binary: "cursor-agent", args: ["--version"], category: "runtime" },
|
|
14
|
+
|
|
15
|
+
// Local LLM runners (engines/)
|
|
16
|
+
{ id: "ollama", binary: "ollama", args: ["--version"], category: "engine" },
|
|
17
|
+
{ id: "llama-cpp", binary: "llama", args: ["--version"], category: "engine" },
|
|
18
|
+
|
|
19
|
+
// Tooling
|
|
20
|
+
{ id: "node", binary: "node", args: ["--version"], category: "tool" },
|
|
21
|
+
{ id: "python3", binary: "python3", args: ["--version"], category: "tool" },
|
|
22
|
+
{ id: "uv", binary: "uv", args: ["--version"], category: "tool" },
|
|
23
|
+
{ id: "git", binary: "git", args: ["--version"], category: "tool" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export async function detectAll() {
|
|
27
|
+
const results = [];
|
|
28
|
+
for (const p of PROBES) {
|
|
29
|
+
const r = await probe(p);
|
|
30
|
+
results.push(r);
|
|
31
|
+
}
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function probe(p) {
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
try {
|
|
38
|
+
const r = await runProcess({
|
|
39
|
+
command: p.binary,
|
|
40
|
+
args: p.args,
|
|
41
|
+
timeoutMs: 3000,
|
|
42
|
+
});
|
|
43
|
+
if (r.exitCode === 0 || (r.stdout && r.stdout.trim())) {
|
|
44
|
+
return {
|
|
45
|
+
id: p.id,
|
|
46
|
+
binary: p.binary,
|
|
47
|
+
category: p.category,
|
|
48
|
+
installed: true,
|
|
49
|
+
version: r.stdout.trim().split("\n")[0] || r.stderr.trim().split("\n")[0] || "unknown",
|
|
50
|
+
latency_ms: Date.now() - start,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
id: p.id,
|
|
55
|
+
binary: p.binary,
|
|
56
|
+
category: p.category,
|
|
57
|
+
installed: false,
|
|
58
|
+
reason: r.error || `exit ${r.exitCode}`,
|
|
59
|
+
};
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return {
|
|
62
|
+
id: p.id,
|
|
63
|
+
binary: p.binary,
|
|
64
|
+
category: p.category,
|
|
65
|
+
installed: false,
|
|
66
|
+
reason: e.message,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// APX daemon entry point. Boots config + projects + Express + plugins.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
readConfig,
|
|
8
|
+
writeConfig,
|
|
9
|
+
effectiveHost,
|
|
10
|
+
effectivePort,
|
|
11
|
+
addProject as addProjectInConfig,
|
|
12
|
+
PID_PATH,
|
|
13
|
+
LOG_PATH,
|
|
14
|
+
APX_HOME,
|
|
15
|
+
} from "../core/config.js";
|
|
16
|
+
import { ProjectManager } from "./db.js";
|
|
17
|
+
import { McpRegistry } from "./mcp-runner.js";
|
|
18
|
+
import { PluginManager } from "./plugins/index.js";
|
|
19
|
+
import { RoutineScheduler } from "./routines.js";
|
|
20
|
+
import { buildApi } from "./api.js";
|
|
21
|
+
import { triggerWakeup } from "./wakeup.js";
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = path.dirname(__filename);
|
|
25
|
+
const PKG = JSON.parse(
|
|
26
|
+
fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8")
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// When the daemon is spawned detached by the CLI, stdout is already redirected
|
|
30
|
+
// to ~/.apx/daemon.log via `stdio: ["ignore", out, out]`. So a single
|
|
31
|
+
// process.stdout.write reaches the file once. In foreground (npm start), it
|
|
32
|
+
// still prints to the console. No double-append.
|
|
33
|
+
const log = (msg) => {
|
|
34
|
+
process.stdout.write(`[${new Date().toISOString()}] ${msg}\n`);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function ensureHome() {
|
|
38
|
+
fs.mkdirSync(APX_HOME, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writePid() {
|
|
42
|
+
try {
|
|
43
|
+
fs.writeFileSync(PID_PATH, String(process.pid));
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clearPid() {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(PID_PATH)) fs.unlinkSync(PID_PATH);
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class RegistryCache {
|
|
54
|
+
constructor() {
|
|
55
|
+
this.byProjectId = new Map();
|
|
56
|
+
}
|
|
57
|
+
ensure(projectEntry) {
|
|
58
|
+
if (!this.byProjectId.has(projectEntry.id)) {
|
|
59
|
+
this.byProjectId.set(projectEntry.id, new McpRegistry(projectEntry.path));
|
|
60
|
+
}
|
|
61
|
+
return this.byProjectId.get(projectEntry.id);
|
|
62
|
+
}
|
|
63
|
+
for(projectEntry) {
|
|
64
|
+
return this.ensure(projectEntry);
|
|
65
|
+
}
|
|
66
|
+
shutdown() {
|
|
67
|
+
for (const r of this.byProjectId.values()) r.shutdown();
|
|
68
|
+
this.byProjectId.clear();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
ensureHome();
|
|
74
|
+
|
|
75
|
+
const cfg = readConfig();
|
|
76
|
+
const host = effectiveHost(cfg);
|
|
77
|
+
const port = effectivePort(cfg);
|
|
78
|
+
|
|
79
|
+
const projects = new ProjectManager(cfg);
|
|
80
|
+
const registries = new RegistryCache();
|
|
81
|
+
|
|
82
|
+
// Load registered projects from config.
|
|
83
|
+
for (const entry of cfg.projects) {
|
|
84
|
+
try {
|
|
85
|
+
const p = projects.register(entry.path);
|
|
86
|
+
registries.ensure(p);
|
|
87
|
+
log(`loaded project #${p.id} ${p.path}`);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
log(`skipping project ${entry.path}: ${e.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const plugins = new PluginManager({ projects, config: cfg, log, registries });
|
|
94
|
+
plugins.initAll();
|
|
95
|
+
plugins.startAll();
|
|
96
|
+
|
|
97
|
+
const scheduler = new RoutineScheduler({
|
|
98
|
+
projects,
|
|
99
|
+
plugins,
|
|
100
|
+
globalConfig: cfg,
|
|
101
|
+
log,
|
|
102
|
+
});
|
|
103
|
+
scheduler.start();
|
|
104
|
+
|
|
105
|
+
const startedAt = Date.now();
|
|
106
|
+
const app = buildApi({
|
|
107
|
+
projects,
|
|
108
|
+
registries,
|
|
109
|
+
plugins,
|
|
110
|
+
scheduler,
|
|
111
|
+
config: cfg,
|
|
112
|
+
version: PKG.version,
|
|
113
|
+
startedAt,
|
|
114
|
+
addProjectGlobally: (absPath) => {
|
|
115
|
+
try {
|
|
116
|
+
const fresh = readConfig();
|
|
117
|
+
addProjectInConfig(fresh, absPath);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
log(`could not persist project to global config: ${e.message}`);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
plugins.installRoutes(app);
|
|
125
|
+
|
|
126
|
+
const server = app.listen(port, host, () => {
|
|
127
|
+
writePid();
|
|
128
|
+
log(`apx-daemon ${PKG.version} listening on http://${host}:${port}`);
|
|
129
|
+
log(`projects: ${projects.list().length} | plugins: ${Object.keys(plugins.status()).join(", ") || "(none)"}`);
|
|
130
|
+
// Fire wake-up message after a short delay so plugins (Telegram) are ready
|
|
131
|
+
setTimeout(() => triggerWakeup(cfg, log), 3000);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function shutdown(signal) {
|
|
135
|
+
log(`received ${signal}, shutting down...`);
|
|
136
|
+
scheduler.stop();
|
|
137
|
+
plugins.stopAll();
|
|
138
|
+
registries.shutdown();
|
|
139
|
+
server.close(() => {
|
|
140
|
+
clearPid();
|
|
141
|
+
process.exit(0);
|
|
142
|
+
});
|
|
143
|
+
setTimeout(() => process.exit(1), 5000).unref();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
147
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
148
|
+
process.on("uncaughtException", (e) => {
|
|
149
|
+
log(`uncaughtException: ${e.stack || e.message}`);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main().catch((e) => {
|
|
154
|
+
log(`fatal: ${e.stack || e.message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// MCP runner: spawn child MCP processes and proxy JSON-RPC tools/call.
|
|
2
|
+
// Speaks the stdio transport: newline-delimited JSON-RPC 2.0 messages.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { loadAll } from "./mcp-sources.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
|
|
8
|
+
class McpProcess {
|
|
9
|
+
constructor({ name, command, args = [], env = {} }) {
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.command = command;
|
|
12
|
+
this.args = args;
|
|
13
|
+
this.env = env;
|
|
14
|
+
this.proc = null;
|
|
15
|
+
this.buffer = "";
|
|
16
|
+
this.pending = new Map(); // id -> { resolve, reject, timer }
|
|
17
|
+
this._nextId = 1;
|
|
18
|
+
this._initPromise = null;
|
|
19
|
+
this._initialized = false;
|
|
20
|
+
this._stderrBuf = "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
start() {
|
|
24
|
+
if (this.proc) return;
|
|
25
|
+
this.proc = spawn(this.command, this.args, {
|
|
26
|
+
env: { ...process.env, ...this.env },
|
|
27
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
this.proc.stdout.setEncoding("utf8");
|
|
31
|
+
this.proc.stderr.setEncoding("utf8");
|
|
32
|
+
|
|
33
|
+
this.proc.stdout.on("data", (chunk) => this._onStdout(chunk));
|
|
34
|
+
this.proc.stderr.on("data", (chunk) => {
|
|
35
|
+
this._stderrBuf += chunk;
|
|
36
|
+
if (this._stderrBuf.length > 4096) {
|
|
37
|
+
this._stderrBuf = this._stderrBuf.slice(-4096);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.proc.on("exit", (code) => {
|
|
42
|
+
const err = new Error(
|
|
43
|
+
`MCP "${this.name}" exited with code ${code}. stderr: ${this._stderrBuf.trim()}`
|
|
44
|
+
);
|
|
45
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
reject(err);
|
|
48
|
+
}
|
|
49
|
+
this.pending.clear();
|
|
50
|
+
this.proc = null;
|
|
51
|
+
this._initialized = false;
|
|
52
|
+
this._initPromise = null;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_onStdout(chunk) {
|
|
57
|
+
this.buffer += chunk;
|
|
58
|
+
let idx;
|
|
59
|
+
while ((idx = this.buffer.indexOf("\n")) !== -1) {
|
|
60
|
+
const line = this.buffer.slice(0, idx).trim();
|
|
61
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
62
|
+
if (!line) continue;
|
|
63
|
+
let msg;
|
|
64
|
+
try {
|
|
65
|
+
msg = JSON.parse(line);
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
70
|
+
const { resolve, reject, timer } = this.pending.get(msg.id);
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
this.pending.delete(msg.id);
|
|
73
|
+
if (msg.error) reject(new Error(msg.error.message || "MCP error"));
|
|
74
|
+
else resolve(msg.result);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_send(method, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
80
|
+
this.start();
|
|
81
|
+
const id = this._nextId++;
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const timer = setTimeout(() => {
|
|
84
|
+
this.pending.delete(id);
|
|
85
|
+
reject(new Error(`MCP "${this.name}" call ${method} timed out`));
|
|
86
|
+
}, timeoutMs);
|
|
87
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
88
|
+
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
89
|
+
try {
|
|
90
|
+
this.proc.stdin.write(payload + "\n");
|
|
91
|
+
} catch (e) {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
this.pending.delete(id);
|
|
94
|
+
reject(e);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async _ensureInitialized() {
|
|
100
|
+
if (this._initialized) return;
|
|
101
|
+
if (!this._initPromise) {
|
|
102
|
+
this._initPromise = (async () => {
|
|
103
|
+
await this._send(
|
|
104
|
+
"initialize",
|
|
105
|
+
{
|
|
106
|
+
protocolVersion: "2024-11-05",
|
|
107
|
+
capabilities: {},
|
|
108
|
+
clientInfo: { name: "apx-daemon", version: "0.1.0" },
|
|
109
|
+
},
|
|
110
|
+
10_000
|
|
111
|
+
);
|
|
112
|
+
try {
|
|
113
|
+
this.proc.stdin.write(
|
|
114
|
+
JSON.stringify({
|
|
115
|
+
jsonrpc: "2.0",
|
|
116
|
+
method: "notifications/initialized",
|
|
117
|
+
}) + "\n"
|
|
118
|
+
);
|
|
119
|
+
} catch {}
|
|
120
|
+
this._initialized = true;
|
|
121
|
+
})();
|
|
122
|
+
}
|
|
123
|
+
return this._initPromise;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async listTools() {
|
|
127
|
+
await this._ensureInitialized();
|
|
128
|
+
return this._send("tools/list", {});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async callTool(name, args) {
|
|
132
|
+
await this._ensureInitialized();
|
|
133
|
+
return this._send("tools/call", { name, arguments: args || {} });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
stop() {
|
|
137
|
+
if (this.proc) {
|
|
138
|
+
try {
|
|
139
|
+
this.proc.kill();
|
|
140
|
+
} catch {}
|
|
141
|
+
this.proc = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function entryToMeta(e) {
|
|
147
|
+
return {
|
|
148
|
+
name: e.name,
|
|
149
|
+
command: e.command,
|
|
150
|
+
args: e.args,
|
|
151
|
+
env: e.env,
|
|
152
|
+
url: e.url,
|
|
153
|
+
headers: e.headers || {},
|
|
154
|
+
transport: e.transport || "stdio",
|
|
155
|
+
source: e.source,
|
|
156
|
+
enabled: e.enabled,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class McpRegistry {
|
|
161
|
+
constructor(projectPath) {
|
|
162
|
+
this.projectPath = projectPath;
|
|
163
|
+
this.processes = new Map(); // mcp name -> McpProcess
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
list() {
|
|
167
|
+
return loadAll(this.projectPath).entries.map(entryToMeta);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
conflicts() {
|
|
171
|
+
return loadAll(this.projectPath).conflicts;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
evict(name) {
|
|
175
|
+
const proc = this.processes.get(name);
|
|
176
|
+
if (proc) {
|
|
177
|
+
proc.stop();
|
|
178
|
+
this.processes.delete(name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getByName(name) {
|
|
183
|
+
const e = loadAll(this.projectPath).entries.find((x) => x.name === name);
|
|
184
|
+
return e ? entryToMeta(e) : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_ensureProcess(name) {
|
|
188
|
+
let proc = this.processes.get(name);
|
|
189
|
+
if (proc && proc.proc) return proc;
|
|
190
|
+
const meta = this.getByName(name);
|
|
191
|
+
if (!meta) throw new Error(`MCP "${name}" not registered`);
|
|
192
|
+
if (!meta.enabled) throw new Error(`MCP "${name}" is disabled`);
|
|
193
|
+
if (meta.transport === "http" || meta.url) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`MCP "${name}" uses HTTP transport (url=${meta.url}); HTTP/SSE transport arrives in v0.2. Use a stdio MCP for now.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (!meta.command) throw new Error(`MCP "${name}" has no command — invalid registration`);
|
|
199
|
+
proc = new McpProcess(meta);
|
|
200
|
+
this.processes.set(name, proc);
|
|
201
|
+
return proc;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async call(name, tool, args) {
|
|
205
|
+
const proc = this._ensureProcess(name);
|
|
206
|
+
return proc.callTool(tool, args);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async listTools(name) {
|
|
210
|
+
const proc = this._ensureProcess(name);
|
|
211
|
+
return proc.listTools();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
shutdown() {
|
|
215
|
+
for (const p of this.processes.values()) p.stop();
|
|
216
|
+
this.processes.clear();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Multi-source MCP discovery: read .apc/mcps.json plus configs from coexisting
|
|
2
|
+
// AI tools (Cursor, Claude Code, VS Code, Roo, etc.) and merge them with APC
|
|
3
|
+
// taking precedence. Read-only — never modifies any external file.
|
|
4
|
+
//
|
|
5
|
+
// References (formats validated against official docs):
|
|
6
|
+
// .apc/mcps.json APC (this project) key: mcpServers
|
|
7
|
+
// .mcp.json Claude Code (project scope) key: mcpServers
|
|
8
|
+
// .cursor/mcp.json Cursor key: mcpServers
|
|
9
|
+
// .vscode/mcp.json VS Code / Copilot key: servers (different!)
|
|
10
|
+
// .roo/mcp.json Roo Code key: mcpServers
|
|
11
|
+
// .gemini/settings.json Gemini CLI key: mcpServers
|
|
12
|
+
//
|
|
13
|
+
// Priority (first wins by name):
|
|
14
|
+
// 1. apc — .apc/mcps.json
|
|
15
|
+
// 2. claude — .mcp.json
|
|
16
|
+
// 3. cursor — .cursor/mcp.json
|
|
17
|
+
// 4. vscode — .vscode/mcp.json
|
|
18
|
+
// 5. roo — .roo/mcp.json
|
|
19
|
+
// 6. gemini — .gemini/settings.json
|
|
20
|
+
//
|
|
21
|
+
// Each entry returns:
|
|
22
|
+
// { name, source, command?, args?, env?, url?, headers?, enabled, raw }
|
|
23
|
+
// `enabled` is APC's extension. Falls back to !disabled if the source uses that
|
|
24
|
+
// instead, then to true.
|
|
25
|
+
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
|
|
29
|
+
export const SOURCES = [
|
|
30
|
+
{ id: "apc", file: ".apc/mcps.json", key: "mcpServers" },
|
|
31
|
+
{ id: "claude", file: ".mcp.json", key: "mcpServers" },
|
|
32
|
+
{ id: "cursor", file: ".cursor/mcp.json", key: "mcpServers" },
|
|
33
|
+
{ id: "vscode", file: ".vscode/mcp.json", key: "servers" },
|
|
34
|
+
{ id: "roo", file: ".roo/mcp.json", key: "mcpServers" },
|
|
35
|
+
{ id: "gemini", file: ".gemini/settings.json", key: "mcpServers" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export function loadAll(projectRoot) {
|
|
39
|
+
const merged = new Map(); // name -> entry (first writer wins)
|
|
40
|
+
const conflicts = []; // [{name, winner, loser, sources}]
|
|
41
|
+
const sourceMap = {}; // sourceId -> count
|
|
42
|
+
|
|
43
|
+
for (const src of SOURCES) {
|
|
44
|
+
const abs = path.join(projectRoot, src.file);
|
|
45
|
+
if (!fs.existsSync(abs)) continue;
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
49
|
+
} catch (e) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const dict = raw[src.key] || {};
|
|
53
|
+
sourceMap[src.id] = 0;
|
|
54
|
+
for (const [name, server] of Object.entries(dict)) {
|
|
55
|
+
sourceMap[src.id]++;
|
|
56
|
+
const entry = normalize(name, server, src.id);
|
|
57
|
+
if (merged.has(name)) {
|
|
58
|
+
const winner = merged.get(name);
|
|
59
|
+
conflicts.push({
|
|
60
|
+
name,
|
|
61
|
+
winner: winner.source,
|
|
62
|
+
loser: src.id,
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
merged.set(name, entry);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
entries: Array.from(merged.values()),
|
|
71
|
+
conflicts,
|
|
72
|
+
sourceCounts: sourceMap,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalize(name, server, sourceId) {
|
|
77
|
+
const enabled =
|
|
78
|
+
server.enabled === false
|
|
79
|
+
? false
|
|
80
|
+
: server.disabled === true
|
|
81
|
+
? false
|
|
82
|
+
: true;
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
source: sourceId,
|
|
86
|
+
command: server.command || null,
|
|
87
|
+
args: server.args || [],
|
|
88
|
+
env: server.env || {},
|
|
89
|
+
url: server.url || null,
|
|
90
|
+
headers: server.headers || null,
|
|
91
|
+
transport: server.url ? "http" : "stdio",
|
|
92
|
+
enabled,
|
|
93
|
+
raw: server,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Read just .apc/mcps.json and return the parsed object (for editing).
|
|
98
|
+
export function readApfMcps(projectRoot) {
|
|
99
|
+
const p = path.join(projectRoot, ".apc", "mcps.json");
|
|
100
|
+
if (!fs.existsSync(p)) return { mcpServers: {} };
|
|
101
|
+
try {
|
|
102
|
+
const json = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
103
|
+
if (!json.mcpServers) json.mcpServers = {};
|
|
104
|
+
return json;
|
|
105
|
+
} catch {
|
|
106
|
+
return { mcpServers: {} };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function writeApfMcps(projectRoot, json) {
|
|
111
|
+
const p = path.join(projectRoot, ".apc", "mcps.json");
|
|
112
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
113
|
+
fs.writeFileSync(p, JSON.stringify(json, null, 2) + "\n");
|
|
114
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Plugin registry. Each plugin exports a default object:
|
|
2
|
+
//
|
|
3
|
+
// {
|
|
4
|
+
// id, // unique identifier (e.g. "telegram")
|
|
5
|
+
// init(ctx) → instance, // called once on daemon boot. ctx provides:
|
|
6
|
+
// // projects: ProjectManager
|
|
7
|
+
// // config: global cfg from ~/.apx/config.json
|
|
8
|
+
// // log: log function
|
|
9
|
+
// // instance shape:
|
|
10
|
+
// // { start(), stop(), status() → object, routes?(app) }
|
|
11
|
+
// }
|
|
12
|
+
//
|
|
13
|
+
// Plugins are discovered by static import here. Adding a new plugin = importing
|
|
14
|
+
// it and pushing into PLUGINS.
|
|
15
|
+
import telegramPlugin from "./telegram.js";
|
|
16
|
+
|
|
17
|
+
export const PLUGINS = [telegramPlugin];
|
|
18
|
+
|
|
19
|
+
export class PluginManager {
|
|
20
|
+
constructor({ projects, config, log, registries }) {
|
|
21
|
+
this.projects = projects;
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.log = log;
|
|
24
|
+
this.registries = registries;
|
|
25
|
+
this.instances = new Map();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
initAll() {
|
|
29
|
+
for (const p of PLUGINS) {
|
|
30
|
+
try {
|
|
31
|
+
const inst = p.init({
|
|
32
|
+
projects: this.projects,
|
|
33
|
+
config: this.config,
|
|
34
|
+
log: this.log,
|
|
35
|
+
plugins: this, // self-reference so plugins can call siblings
|
|
36
|
+
registries: this.registries,
|
|
37
|
+
});
|
|
38
|
+
this.instances.set(p.id, inst);
|
|
39
|
+
this.log(`plugin ${p.id} initialized`);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
this.log(`plugin ${p.id} init failed: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
startAll() {
|
|
47
|
+
for (const [id, inst] of this.instances) {
|
|
48
|
+
try {
|
|
49
|
+
inst.start?.();
|
|
50
|
+
} catch (e) {
|
|
51
|
+
this.log(`plugin ${id} start failed: ${e.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
stopAll() {
|
|
57
|
+
for (const [, inst] of this.instances) {
|
|
58
|
+
try {
|
|
59
|
+
inst.stop?.();
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get(id) {
|
|
65
|
+
return this.instances.get(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
status() {
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const [id, inst] of this.instances) {
|
|
71
|
+
try {
|
|
72
|
+
out[id] = inst.status?.() || { running: !!inst };
|
|
73
|
+
} catch (e) {
|
|
74
|
+
out[id] = { error: e.message };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
installRoutes(app) {
|
|
81
|
+
for (const [id, inst] of this.instances) {
|
|
82
|
+
if (typeof inst.routes === "function") {
|
|
83
|
+
try {
|
|
84
|
+
inst.routes(app);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
this.log(`plugin ${id} route install failed: ${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|