@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,56 @@
|
|
|
1
|
+
import { http } from "../http.js";
|
|
2
|
+
import { resolveProjectId } from "./project.js";
|
|
3
|
+
|
|
4
|
+
function parseValue(raw) {
|
|
5
|
+
// best-effort: try JSON first (covers numbers, bools, objects, arrays, null,
|
|
6
|
+
// and quoted strings). If that fails, treat as a literal string.
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(raw);
|
|
9
|
+
} catch {
|
|
10
|
+
return raw;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function cmdConfigShow(args) {
|
|
15
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
16
|
+
const data = await http.get(`/projects/${pid}/config`);
|
|
17
|
+
if (args.flags.effective) {
|
|
18
|
+
process.stdout.write(JSON.stringify(data.effective, null, 2) + "\n");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// `--only-overrides` shows just .apc/config.json contents.
|
|
22
|
+
// (Was previously `--project` but that collided with the global --project
|
|
23
|
+
// selector flag.)
|
|
24
|
+
if (args.flags["only-overrides"]) {
|
|
25
|
+
process.stdout.write(JSON.stringify(data.project_only, null, 2) + "\n");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(`# .apc/config.json (project-only overrides)`);
|
|
29
|
+
console.log(`# path: ${data.project_config_path}`);
|
|
30
|
+
console.log("");
|
|
31
|
+
console.log(JSON.stringify(data.project_only, null, 2));
|
|
32
|
+
console.log("");
|
|
33
|
+
console.log(`# effective (global merged with project)`);
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log(JSON.stringify(data.effective, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function cmdConfigSet(args) {
|
|
39
|
+
const key = args._[0];
|
|
40
|
+
const valueRaw = args._.slice(1).join(" ");
|
|
41
|
+
if (!key || !valueRaw) {
|
|
42
|
+
throw new Error('apx config set: usage: apx config set <key.path> <value>');
|
|
43
|
+
}
|
|
44
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
45
|
+
const value = parseValue(valueRaw);
|
|
46
|
+
await http.patch(`/projects/${pid}/config`, { set: { [key]: value } });
|
|
47
|
+
console.log(`set ${key} = ${JSON.stringify(value)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function cmdConfigUnset(args) {
|
|
51
|
+
const key = args._[0];
|
|
52
|
+
if (!key) throw new Error("apx config unset: usage: apx config unset <key.path>");
|
|
53
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
54
|
+
await http.patch(`/projects/${pid}/config`, { unset: [key] });
|
|
55
|
+
console.log(`unset ${key}`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensureDaemon, http } from "../http.js";
|
|
5
|
+
|
|
6
|
+
const PID_PATH = path.join(os.homedir(), ".apx", "daemon.pid");
|
|
7
|
+
const LOG_PATH = path.join(os.homedir(), ".apx", "daemon.log");
|
|
8
|
+
|
|
9
|
+
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
10
|
+
const c = {
|
|
11
|
+
reset: "\x1b[0m",
|
|
12
|
+
bold: "\x1b[1m",
|
|
13
|
+
dim: "\x1b[2m",
|
|
14
|
+
green: "\x1b[32m",
|
|
15
|
+
red: "\x1b[31m",
|
|
16
|
+
yellow: "\x1b[33m",
|
|
17
|
+
cyan: "\x1b[36m",
|
|
18
|
+
white: "\x1b[97m",
|
|
19
|
+
gray: "\x1b[90m",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const fmt = {
|
|
23
|
+
bold: (s) => `${c.bold}${s}${c.reset}`,
|
|
24
|
+
dim: (s) => `${c.dim}${s}${c.reset}`,
|
|
25
|
+
green: (s) => `${c.green}${s}${c.reset}`,
|
|
26
|
+
red: (s) => `${c.red}${s}${c.reset}`,
|
|
27
|
+
yellow: (s) => `${c.yellow}${s}${c.reset}`,
|
|
28
|
+
cyan: (s) => `${c.cyan}${s}${c.reset}`,
|
|
29
|
+
gray: (s) => `${c.gray}${s}${c.reset}`,
|
|
30
|
+
kv: (k, v) => ` ${c.gray}${k.padEnd(10)}${c.reset} ${v}`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function uptime(s) {
|
|
34
|
+
if (s < 60) return `${s}s`;
|
|
35
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
36
|
+
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function debugRaw(label, data) {
|
|
40
|
+
console.log(fmt.gray(`\n── debug: ${label} ──`));
|
|
41
|
+
console.log(fmt.gray(JSON.stringify(data, null, 2)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export async function cmdDaemonStart(args = {}) {
|
|
47
|
+
const debug = args.flags?.debug;
|
|
48
|
+
await ensureDaemon();
|
|
49
|
+
const status = await http.get("/health");
|
|
50
|
+
if (debug) debugRaw("/health", status);
|
|
51
|
+
console.log(
|
|
52
|
+
`\n ${fmt.green("●")} ${fmt.bold("apx daemon")} ${fmt.dim("v" + status.version)}` +
|
|
53
|
+
` ${fmt.gray("·")} ${fmt.cyan("port")} ${status.port ?? (process.env.APX_PORT || 7430)}` +
|
|
54
|
+
` ${fmt.gray("·")} ${fmt.cyan("uptime")} ${uptime(status.uptime_s)}\n`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function cmdDaemonStatus(args = {}) {
|
|
59
|
+
const debug = args.flags?.debug;
|
|
60
|
+
const port = process.env.APX_PORT || 7430;
|
|
61
|
+
|
|
62
|
+
if (!(await http.ping())) {
|
|
63
|
+
console.log(
|
|
64
|
+
`\n ${fmt.red("○")} ${fmt.bold("apx daemon")} ${fmt.dim("stopped")}` +
|
|
65
|
+
` ${fmt.gray("(no process on port " + port + ")")}\n`
|
|
66
|
+
);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const h = await http.get("/health", { autoStart: false });
|
|
71
|
+
const projects = await http.get("/projects", { autoStart: false });
|
|
72
|
+
const pid = fs.existsSync(PID_PATH) ? fs.readFileSync(PID_PATH, "utf8").trim() : "?";
|
|
73
|
+
|
|
74
|
+
if (debug) {
|
|
75
|
+
debugRaw("/health", h);
|
|
76
|
+
debugRaw("/projects", projects);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(
|
|
80
|
+
`\n ${fmt.green("●")} ${fmt.bold("apx daemon")} ${fmt.dim("v" + h.version)} ${fmt.green("running")}`
|
|
81
|
+
);
|
|
82
|
+
console.log(fmt.kv("pid", fmt.cyan(pid)));
|
|
83
|
+
console.log(fmt.kv("port", fmt.cyan(port)));
|
|
84
|
+
console.log(fmt.kv("uptime", fmt.cyan(uptime(h.uptime_s))));
|
|
85
|
+
|
|
86
|
+
if (projects.length === 0) {
|
|
87
|
+
console.log(fmt.kv("projects", fmt.dim("none registered")));
|
|
88
|
+
} else {
|
|
89
|
+
console.log(fmt.kv("projects", ""));
|
|
90
|
+
for (const p of projects) {
|
|
91
|
+
const agents = `${p.agents} agent${p.agents !== 1 ? "s" : ""}`;
|
|
92
|
+
console.log(
|
|
93
|
+
` ${fmt.cyan("·")} ${fmt.bold(p.path)} ${fmt.gray(agents)}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function cmdDaemonStop(args = {}) {
|
|
101
|
+
const debug = args.flags?.debug;
|
|
102
|
+
|
|
103
|
+
if (!(await http.ping())) {
|
|
104
|
+
console.log(`\n ${fmt.yellow("○")} ${fmt.bold("apx daemon")} ${fmt.dim("not running")}\n`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const res = await http.post("/admin/shutdown", undefined, { autoStart: false });
|
|
110
|
+
if (debug) debugRaw("/admin/shutdown", res);
|
|
111
|
+
console.log(`\n ${fmt.red("○")} ${fmt.bold("apx daemon")} ${fmt.dim("stopped")}\n`);
|
|
112
|
+
} catch {
|
|
113
|
+
if (fs.existsSync(PID_PATH)) {
|
|
114
|
+
try {
|
|
115
|
+
const pid = parseInt(fs.readFileSync(PID_PATH, "utf8"), 10);
|
|
116
|
+
if (pid) {
|
|
117
|
+
process.kill(pid);
|
|
118
|
+
console.log(`\n ${fmt.red("○")} ${fmt.bold("apx daemon")} ${fmt.dim(`stopped (pid ${pid})`)}\n`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
console.log(`\n ${fmt.yellow("○")} ${fmt.bold("apx daemon")} ${fmt.dim("not running")}\n`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function cmdDaemonLogs(args) {
|
|
128
|
+
const debug = args.flags?.debug;
|
|
129
|
+
if (!fs.existsSync(LOG_PATH)) {
|
|
130
|
+
console.log(fmt.gray(` (no log file at ${LOG_PATH})`));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const tail = args.flags?.tail ? parseInt(args.flags.tail, 10) : 50;
|
|
134
|
+
const lines = fs.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
135
|
+
const slice = lines.slice(-tail - 1).filter(Boolean);
|
|
136
|
+
|
|
137
|
+
if (debug) console.log(fmt.gray(` log: ${LOG_PATH} (last ${tail} lines)\n`));
|
|
138
|
+
|
|
139
|
+
for (const line of slice) {
|
|
140
|
+
// dim timestamps, highlight ERROR/WARN
|
|
141
|
+
const colored = line
|
|
142
|
+
.replace(/^(\d{4}-\d\d-\d\dT[\d:.Z]+)/, (m) => fmt.gray(m))
|
|
143
|
+
.replace(/\bERROR\b/g, fmt.red("ERROR"))
|
|
144
|
+
.replace(/\bWARN\b/g, fmt.yellow("WARN"))
|
|
145
|
+
.replace(/\bINFO\b/g, fmt.cyan("INFO"));
|
|
146
|
+
console.log(colored);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { http } from "../http.js";
|
|
2
|
+
import { resolveProjectId } from "./project.js";
|
|
3
|
+
|
|
4
|
+
function readStdinSync() {
|
|
5
|
+
if (process.stdin.isTTY) return "";
|
|
6
|
+
const chunks = [];
|
|
7
|
+
const buf = Buffer.alloc(65536);
|
|
8
|
+
try {
|
|
9
|
+
while (true) {
|
|
10
|
+
const n = require("node:fs").readSync(0, buf, 0, buf.length);
|
|
11
|
+
if (!n) break;
|
|
12
|
+
chunks.push(buf.slice(0, n).toString("utf8"));
|
|
13
|
+
}
|
|
14
|
+
} catch {}
|
|
15
|
+
return chunks.join("");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function cmdExec(args) {
|
|
19
|
+
const slug = args._[0];
|
|
20
|
+
if (!slug) throw new Error("apx exec: usage: apx exec <agent> \"<prompt>\" [--model <id>]");
|
|
21
|
+
let prompt = args._.slice(1).join(" ").trim();
|
|
22
|
+
if (!prompt || prompt === "-") {
|
|
23
|
+
const fs = await import("node:fs");
|
|
24
|
+
if (!process.stdin.isTTY) {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
const buf = Buffer.alloc(65536);
|
|
27
|
+
try {
|
|
28
|
+
while (true) {
|
|
29
|
+
const n = fs.readSync(0, buf, 0, buf.length);
|
|
30
|
+
if (!n) break;
|
|
31
|
+
chunks.push(buf.slice(0, n).toString("utf8"));
|
|
32
|
+
}
|
|
33
|
+
} catch {}
|
|
34
|
+
prompt = chunks.join("").trim();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!prompt) throw new Error("apx exec: prompt is empty (pass as args or via stdin)");
|
|
38
|
+
|
|
39
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
40
|
+
const body = { prompt };
|
|
41
|
+
if (args.flags.model && args.flags.model !== true) body.model = args.flags.model;
|
|
42
|
+
if (args.flags.temperature) body.temperature = parseFloat(args.flags.temperature);
|
|
43
|
+
if (args.flags["max-tokens"]) body.maxTokens = parseInt(args.flags["max-tokens"], 10);
|
|
44
|
+
|
|
45
|
+
const result = await http.post(
|
|
46
|
+
`/projects/${pid}/agents/${slug}/exec`,
|
|
47
|
+
body
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
process.stdout.write(result.text + "\n");
|
|
51
|
+
if (process.stderr.isTTY || args.flags.verbose) {
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
`\n— ${result.engine} | in=${result.usage?.input_tokens || "?"} out=${result.usage?.output_tokens || "?"} | conv=${result.conversation.id}\n`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { readIdentity, writeIdentity } from "../../core/identity.js";
|
|
6
|
+
|
|
7
|
+
function ask(rl, question, defaultVal) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const hint = defaultVal ? ` (${defaultVal})` : "";
|
|
10
|
+
rl.question(`${question}${hint}: `, (ans) => {
|
|
11
|
+
resolve(ans.trim() || defaultVal || "");
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function askYN(rl, question, defaultYes = false) {
|
|
17
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
rl.question(`${question} [${hint}]: `, (ans) => {
|
|
20
|
+
const a = ans.trim().toLowerCase();
|
|
21
|
+
if (!a) return resolve(defaultYes);
|
|
22
|
+
resolve(a === "y" || a === "yes" || a === "s" || a === "si" || a === "sí");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
28
|
+
|
|
29
|
+
function setupClaudePermissions() {
|
|
30
|
+
try {
|
|
31
|
+
let existing = {};
|
|
32
|
+
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
33
|
+
existing = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
|
|
34
|
+
}
|
|
35
|
+
const updated = {
|
|
36
|
+
...existing,
|
|
37
|
+
permissions: {
|
|
38
|
+
...(existing.permissions || {}),
|
|
39
|
+
allow: [
|
|
40
|
+
...(existing.permissions?.allow || []),
|
|
41
|
+
"Bash(*)",
|
|
42
|
+
"Read(*)",
|
|
43
|
+
"Write(*)",
|
|
44
|
+
"Edit(*)",
|
|
45
|
+
].filter((v, i, arr) => arr.indexOf(v) === i),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
fs.mkdirSync(path.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
49
|
+
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(updated, null, 2) + "\n");
|
|
50
|
+
return true;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return e.message;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// apx identity — show
|
|
57
|
+
// apx identity set — interactive or --name/--owner/--context/--language flags
|
|
58
|
+
// apx identity wizard — full onboarding (called on first run)
|
|
59
|
+
export async function cmdIdentity(args) {
|
|
60
|
+
const sub = args._[0];
|
|
61
|
+
|
|
62
|
+
if (!sub || sub === "show") {
|
|
63
|
+
const id = readIdentity();
|
|
64
|
+
if (!id) {
|
|
65
|
+
console.log("No identity configured. Run: apx identity wizard");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.log("");
|
|
69
|
+
console.log(` Agent name : ${id.agent_name}`);
|
|
70
|
+
console.log(` Personality : ${id.personality || "(not set)"}`);
|
|
71
|
+
console.log(` Owner : ${id.owner_name}`);
|
|
72
|
+
console.log(` Context : ${id.owner_context || "(not set)"}`);
|
|
73
|
+
console.log(` Language : ${id.language || "(auto-detect)"}`);
|
|
74
|
+
console.log(` Last wakeup : ${id.last_wakeup || "(never)"}`);
|
|
75
|
+
console.log(` File : ~/.apx/identity.json`);
|
|
76
|
+
console.log("");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (sub === "set") {
|
|
81
|
+
const fields = {};
|
|
82
|
+
if (args.flags.name && args.flags.name !== true) fields.agent_name = args.flags.name;
|
|
83
|
+
if (args.flags.owner && args.flags.owner !== true) fields.owner_name = args.flags.owner;
|
|
84
|
+
if (args.flags.context && args.flags.context !== true) fields.owner_context = args.flags.context;
|
|
85
|
+
if (args.flags.personality && args.flags.personality !== true) fields.personality = args.flags.personality;
|
|
86
|
+
if (args.flags.language && args.flags.language !== true) fields.language = args.flags.language;
|
|
87
|
+
|
|
88
|
+
if (Object.keys(fields).length === 0) {
|
|
89
|
+
console.log("Usage: apx identity set --name <name> --owner <name> --context <text> --personality <text> --language <lang>");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const id = writeIdentity(fields);
|
|
94
|
+
console.log("Identity updated:");
|
|
95
|
+
for (const [k, v] of Object.entries(fields)) console.log(` ${k}: ${v}`);
|
|
96
|
+
if (id.last_wakeup) {
|
|
97
|
+
writeIdentity({ last_wakeup: null });
|
|
98
|
+
console.log(" (wakeup cooldown reset — next daemon start will send a new message)");
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (sub === "wizard") {
|
|
104
|
+
await runWizard();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error(`unknown identity subcommand: ${sub}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runWizard() {
|
|
112
|
+
const existing = readIdentity() || {};
|
|
113
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
114
|
+
|
|
115
|
+
console.log("\n APX Identity Setup\n");
|
|
116
|
+
console.log(" This defines who the agent is and who it works for.");
|
|
117
|
+
console.log(" Used for wake-up messages and agent self-description.\n");
|
|
118
|
+
|
|
119
|
+
const agent_name = await ask(rl, " Agent name", existing.agent_name || "APX");
|
|
120
|
+
const personality = await ask(rl, " Personality (comma-separated traits)", existing.personality || "direct, curious, helpful");
|
|
121
|
+
const owner_name = await ask(rl, " Your name", existing.owner_name || "");
|
|
122
|
+
const owner_context = await ask(rl, " What are you building / working on", existing.owner_context || "");
|
|
123
|
+
const language = await ask(rl, " Language for agent messages (e.g. Spanish, English)", existing.language || "Spanish (Español)");
|
|
124
|
+
|
|
125
|
+
console.log("\n Claude Code permissions");
|
|
126
|
+
console.log(" APX can configure Claude Code to allow terminal commands without prompts.");
|
|
127
|
+
console.log(" This adds Bash(*), Read(*), Write(*), Edit(*) to ~/.claude/settings.json\n");
|
|
128
|
+
|
|
129
|
+
const setupPerms = await askYN(rl, " Set up Claude Code terminal permissions now?", false);
|
|
130
|
+
|
|
131
|
+
rl.close();
|
|
132
|
+
|
|
133
|
+
const id = writeIdentity({ agent_name, personality, owner_name, owner_context, language, last_wakeup: null });
|
|
134
|
+
console.log(`\n Identity saved to ~/.apx/identity.json`);
|
|
135
|
+
|
|
136
|
+
if (setupPerms) {
|
|
137
|
+
const result = setupClaudePermissions();
|
|
138
|
+
if (result === true) {
|
|
139
|
+
console.log(` Claude Code permissions updated at ~/.claude/settings.json`);
|
|
140
|
+
} else {
|
|
141
|
+
console.log(` Could not update Claude Code settings: ${result}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(` ${id.agent_name} is ready. Restart the daemon to send a wake-up message.\n`);
|
|
146
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { initApf } from "../../core/scaffold.js";
|
|
3
|
+
|
|
4
|
+
export function cmdInit(args) {
|
|
5
|
+
const dir = args._[0] || ".";
|
|
6
|
+
const name = args.flags.name === true ? undefined : args.flags.name;
|
|
7
|
+
const result = initApf(dir, { name });
|
|
8
|
+
|
|
9
|
+
console.log(`Initialized APC project at ${result.root}`);
|
|
10
|
+
console.log(` ${path.relative(process.cwd(), result.agentsMd)}`);
|
|
11
|
+
console.log(` ${path.relative(process.cwd(), result.projectJson)}`);
|
|
12
|
+
|
|
13
|
+
if (result.pendingMigration?.length > 0) {
|
|
14
|
+
console.log(`\napx: existing context files detected:`);
|
|
15
|
+
for (const { file, label } of result.pendingMigration) {
|
|
16
|
+
console.log(` ${file.padEnd(44)} ${label}`);
|
|
17
|
+
}
|
|
18
|
+
console.log(`\n .apc/migrate.md written.`);
|
|
19
|
+
console.log(` Open this project in your AI assistant — it will offer to migrate.`);
|
|
20
|
+
} else {
|
|
21
|
+
console.log(`\nNext: apx agent add <slug> --role <role> --model <model>`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { http } from "../http.js";
|
|
2
|
+
import { resolveProjectId } from "./project.js";
|
|
3
|
+
|
|
4
|
+
export async function cmdMcpList(args = {}) {
|
|
5
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
6
|
+
const mcps = await http.get(`/projects/${pid}/mcps`);
|
|
7
|
+
if (mcps.length === 0) {
|
|
8
|
+
console.log("(no MCPs registered for this project)");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
console.log("NAME".padEnd(24) + " EN " + "SOURCE".padEnd(8) + " TRANSPORT COMMAND/URL");
|
|
12
|
+
for (const m of mcps) {
|
|
13
|
+
const target = m.transport === "http"
|
|
14
|
+
? m.url
|
|
15
|
+
: (m.command || "") + (m.args?.length ? " " + m.args.join(" ") : "");
|
|
16
|
+
console.log(
|
|
17
|
+
m.name.padEnd(24) + " " +
|
|
18
|
+
(m.enabled ? "✓" : "✗").padEnd(2) + " " +
|
|
19
|
+
(m.source || "apc").padEnd(8) + " " +
|
|
20
|
+
(m.transport || "stdio").padEnd(10) + " " +
|
|
21
|
+
target
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function cmdMcpAdd(args) {
|
|
27
|
+
const name = args._[0];
|
|
28
|
+
if (!name) throw new Error("apx mcp add: missing <name>");
|
|
29
|
+
const command = args.flags.command;
|
|
30
|
+
if (!command || command === true) throw new Error("apx mcp add: --command required");
|
|
31
|
+
|
|
32
|
+
// Args after `--` go to the MCP. `args._` already excludes the `--` separator
|
|
33
|
+
// when our parser strips it. We treat anything in args._ after [0] as args.
|
|
34
|
+
const mcpArgs = args._.slice(1);
|
|
35
|
+
|
|
36
|
+
const env = {};
|
|
37
|
+
if (args.flags.env) {
|
|
38
|
+
const entries = Array.isArray(args.flags.env) ? args.flags.env : [args.flags.env];
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
const [k, ...rest] = String(e).split("=");
|
|
41
|
+
env[k] = rest.join("=");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
46
|
+
const result = await http.post(`/projects/${pid}/mcps`, {
|
|
47
|
+
name,
|
|
48
|
+
command,
|
|
49
|
+
args: mcpArgs,
|
|
50
|
+
env,
|
|
51
|
+
enabled: true,
|
|
52
|
+
});
|
|
53
|
+
console.log(`Added MCP "${result.name}"`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function cmdMcpRemove(args) {
|
|
57
|
+
const name = args._[0];
|
|
58
|
+
if (!name) throw new Error("apx mcp remove: missing <name>");
|
|
59
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
60
|
+
await http.delete(`/projects/${pid}/mcps/${name}`);
|
|
61
|
+
console.log(`Removed MCP "${name}"`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function cmdMcpEnable(args) {
|
|
65
|
+
await toggleEnabled(args, true);
|
|
66
|
+
}
|
|
67
|
+
export async function cmdMcpDisable(args) {
|
|
68
|
+
await toggleEnabled(args, false);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function toggleEnabled(args, enabled) {
|
|
72
|
+
const name = args._[0];
|
|
73
|
+
if (!name) throw new Error(`apx mcp ${enabled ? "enable" : "disable"}: missing <name>`);
|
|
74
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
75
|
+
const all = await http.get(`/projects/${pid}/mcps`);
|
|
76
|
+
const m = all.find((x) => x.name === name);
|
|
77
|
+
if (!m) throw new Error(`MCP "${name}" not registered`);
|
|
78
|
+
await http.post(`/projects/${pid}/mcps`, {
|
|
79
|
+
name: m.name,
|
|
80
|
+
command: m.command,
|
|
81
|
+
args: m.args,
|
|
82
|
+
env: m.env,
|
|
83
|
+
enabled,
|
|
84
|
+
});
|
|
85
|
+
console.log(`${enabled ? "Enabled" : "Disabled"} MCP "${name}"`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function cmdMcpRun(args) {
|
|
89
|
+
const name = args._[0];
|
|
90
|
+
const tool = args._[1];
|
|
91
|
+
if (!name || !tool) throw new Error("apx mcp run: usage: apx mcp run <name> <tool> [<json-args>]");
|
|
92
|
+
let params = {};
|
|
93
|
+
if (args._[2]) {
|
|
94
|
+
try {
|
|
95
|
+
params = JSON.parse(args._[2]);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
throw new Error(`invalid JSON args: ${e.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
101
|
+
const result = await http.post(`/projects/${pid}/mcps/${name}/call`, { tool, params });
|
|
102
|
+
process.stdout.write(JSON.stringify(result.result, null, 2) + "\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function cmdMcpTools(args) {
|
|
106
|
+
const name = args._[0];
|
|
107
|
+
if (!name) throw new Error("apx mcp tools: missing <name>");
|
|
108
|
+
// Daemon doesn't have a dedicated tools/list endpoint yet; we'd extend it in v0.2.
|
|
109
|
+
// For now, print a hint:
|
|
110
|
+
console.log(`(apx mcp tools — list of tools/list will arrive in v0.2)`);
|
|
111
|
+
console.log(`To call a tool: apx mcp run ${name} <tool> '<json>'`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function cmdMcpCheck(args = {}) {
|
|
115
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
116
|
+
const data = await http.get(`/projects/${pid}/mcps/check`);
|
|
117
|
+
|
|
118
|
+
console.log("Source files:");
|
|
119
|
+
for (const s of data.sources) {
|
|
120
|
+
console.log(` ${s.present ? "✓" : "·"} ${s.id.padEnd(8)} ${s.file}`);
|
|
121
|
+
}
|
|
122
|
+
console.log("");
|
|
123
|
+
|
|
124
|
+
if (data.entries.length === 0) {
|
|
125
|
+
console.log("(no MCPs in any source)");
|
|
126
|
+
} else {
|
|
127
|
+
console.log("Active entries (after merge):");
|
|
128
|
+
console.log(" " + "NAME".padEnd(24) + " SOURCE TRANSPORT EN");
|
|
129
|
+
for (const m of data.entries) {
|
|
130
|
+
console.log(
|
|
131
|
+
" " + m.name.padEnd(24) + " " +
|
|
132
|
+
(m.source || "apc").padEnd(8) + " " +
|
|
133
|
+
(m.transport || "stdio").padEnd(10) + " " +
|
|
134
|
+
(m.enabled ? "✓" : "✗")
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (data.conflicts && data.conflicts.length) {
|
|
140
|
+
console.log("\n⚠️ Conflicts (APC rule: first source wins):");
|
|
141
|
+
for (const c of data.conflicts) {
|
|
142
|
+
console.log(` ${c.name}: kept "${c.winner}", ignored "${c.loser}"`);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
console.log("\n✓ no conflicts");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { findApfRoot } from "../../core/parser.js";
|
|
4
|
+
import { ensureAgentDir } from "../../core/scaffold.js";
|
|
5
|
+
import { http } from "../http.js";
|
|
6
|
+
|
|
7
|
+
function requireRoot() {
|
|
8
|
+
const root = findApfRoot();
|
|
9
|
+
if (!root) throw new Error("not inside an APC project (run `apx init` first)");
|
|
10
|
+
return root;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function nudgeDaemon(root) {
|
|
14
|
+
try {
|
|
15
|
+
if (!(await http.ping())) return;
|
|
16
|
+
const projects = await http.get("/projects", { autoStart: false });
|
|
17
|
+
const me = projects.find((p) => p.path === root);
|
|
18
|
+
if (me) await http.post(`/projects/${me.id}/rebuild`, undefined, { autoStart: false });
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function cmdMemory(args) {
|
|
23
|
+
const slug = args._[0];
|
|
24
|
+
if (!slug) throw new Error("apx memory: missing <agent-slug>");
|
|
25
|
+
const root = requireRoot();
|
|
26
|
+
const memPath = path.join(root, ".apc", "agents", slug, "memory.md");
|
|
27
|
+
|
|
28
|
+
if (args.flags.replace) {
|
|
29
|
+
const newBody = readStdinSync();
|
|
30
|
+
ensureAgentDir(root, slug);
|
|
31
|
+
fs.writeFileSync(memPath, newBody);
|
|
32
|
+
await nudgeDaemon(root);
|
|
33
|
+
console.log(`replaced memory for ${slug} (${Buffer.byteLength(newBody)} bytes)`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (args.flags.append && args.flags.append !== true) {
|
|
38
|
+
const note = String(args.flags.append);
|
|
39
|
+
ensureAgentDir(root, slug);
|
|
40
|
+
let body = fs.existsSync(memPath) ? fs.readFileSync(memPath, "utf8") : "";
|
|
41
|
+
if (!/##\s+Recent context/i.test(body)) {
|
|
42
|
+
body += body.endsWith("\n") ? "\n## Recent context\n" : "\n\n## Recent context\n";
|
|
43
|
+
}
|
|
44
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
45
|
+
body = body.replace(/(##\s+Recent context\s*\n)/i, `$1- ${today}: ${note}\n`);
|
|
46
|
+
fs.writeFileSync(memPath, body);
|
|
47
|
+
await nudgeDaemon(root);
|
|
48
|
+
console.log(`appended to ${slug} memory: ${note}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(memPath)) {
|
|
53
|
+
throw new Error(`no memory for "${slug}" — agent dir not yet created`);
|
|
54
|
+
}
|
|
55
|
+
process.stdout.write(fs.readFileSync(memPath, "utf8"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readStdinSync() {
|
|
59
|
+
const chunks = [];
|
|
60
|
+
const buf = Buffer.alloc(65536);
|
|
61
|
+
try {
|
|
62
|
+
while (true) {
|
|
63
|
+
const bytes = fs.readSync(0, buf, 0, buf.length);
|
|
64
|
+
if (!bytes) break;
|
|
65
|
+
chunks.push(buf.slice(0, bytes).toString("utf8"));
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
return chunks.join("");
|
|
69
|
+
}
|