@agentprojectcontext/apx 1.0.3 → 1.2.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/README.md +4 -4
- package/package.json +5 -2
- package/skills/apx/SKILL.md +7 -4
- package/src/cli/commands/project.js +2 -5
- package/src/cli/commands/session.js +13 -5
- package/src/cli/commands/setup.js +336 -0
- package/src/cli/commands/update.js +75 -0
- package/src/cli/index.js +30 -5
- package/src/core/apc-context-skill.md +70 -118
- package/src/core/apx-skill.md +9 -2
- package/src/core/config.js +15 -0
- package/src/core/messages-store.js +7 -7
- package/src/core/scaffold.js +24 -3
- package/src/core/session-store.js +2 -2
- package/src/core/update-check.js +99 -0
- package/src/daemon/apc-runtime-context.js +8 -8
- package/src/daemon/api.js +14 -13
- package/src/daemon/compact.js +4 -4
- package/src/daemon/conversations.js +13 -13
- package/src/daemon/db.js +45 -2
- package/src/daemon/index.js +3 -0
- package/src/daemon/smoke.js +2 -3
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ APX is a daemon + CLI that brings the APC convention to life:
|
|
|
16
16
|
- **Plugins** — Telegram bot integration out of the box
|
|
17
17
|
- **MCP support** — each agent can expose or consume MCP servers
|
|
18
18
|
|
|
19
|
-
APX is opinionated about storage: the filesystem is the source of truth.
|
|
19
|
+
APX is opinionated about storage: the filesystem is the source of truth. Project definitions and curated memory live in the repo. Runtime state such as sessions, conversations, messages, and caches lives in `~/.apx/` and is never committed.
|
|
20
20
|
|
|
21
21
|
## Quick start
|
|
22
22
|
|
|
@@ -65,13 +65,12 @@ Runtime state — local machine only, never committed:
|
|
|
65
65
|
```text
|
|
66
66
|
~/.apx/projects/<project-id>/
|
|
67
67
|
├── project.db ← regenerable SQLite cache
|
|
68
|
+
├── messages/ ← local message history
|
|
68
69
|
└── agents/
|
|
69
70
|
├── <slug>/
|
|
70
|
-
│ ├── memory.md ← durable memory, updated by the agent
|
|
71
71
|
│ ├── sessions/ ← one .md per runtime invocation
|
|
72
72
|
│ └── conversations/ ← LLM conversation threads
|
|
73
73
|
└── default/ ← fallback when no agent role is active
|
|
74
|
-
├── memory.md
|
|
75
74
|
└── sessions/
|
|
76
75
|
```
|
|
77
76
|
|
|
@@ -94,7 +93,8 @@ apx messages tail --channel runtime # only agent invocations
|
|
|
94
93
|
|
|
95
94
|
## Message channels
|
|
96
95
|
|
|
97
|
-
|
|
96
|
+
Activity belongs to APX runtime state, not `.apc/`. Message storage is local to APX, under
|
|
97
|
+
`~/.apx/`:
|
|
98
98
|
|
|
99
99
|
| Channel | What it captures |
|
|
100
100
|
|---------|-----------------|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentprojectcontext/apx",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
"node-fetch": "^3.3.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"
|
|
36
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
37
|
+
"@semantic-release/git": "^10.0.1",
|
|
38
|
+
"better-sqlite3": "^11.3.0",
|
|
39
|
+
"conventional-changelog-conventionalcommits": "^9.3.1"
|
|
37
40
|
},
|
|
38
41
|
"keywords": [
|
|
39
42
|
"apc",
|
package/skills/apx/SKILL.md
CHANGED
|
@@ -4,11 +4,13 @@ description: "APX CLI skill. Activate ONLY when the user asks about running agen
|
|
|
4
4
|
homepage: https://github.com/agentprojectcontext/apx
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# APX — Agent Project
|
|
7
|
+
# APX — Agent Project Context Runtime
|
|
8
8
|
|
|
9
9
|
This project uses **APX**. The daemon runs on `127.0.0.1:7430` and auto-starts on first `apx` call.
|
|
10
10
|
Your current session, project, and agent are already injected above this block — refer to them.
|
|
11
11
|
|
|
12
|
+
APX runtime state belongs outside `.apc/`, under `~/.apx/projects/<project-id>/`.
|
|
13
|
+
|
|
12
14
|
---
|
|
13
15
|
|
|
14
16
|
## Discover the project
|
|
@@ -32,7 +34,7 @@ apx exec <slug> "<prompt>"
|
|
|
32
34
|
The output of `apx run` / `apx exec` is the agent's full stdout.
|
|
33
35
|
If the agent printed `APC_RESULT: <value>`, that value is also captured as structured output.
|
|
34
36
|
|
|
35
|
-
## Memory — durable,
|
|
37
|
+
## Memory — durable, safe facts
|
|
36
38
|
|
|
37
39
|
```bash
|
|
38
40
|
apx memory <slug> # read agent's memory.md
|
|
@@ -40,7 +42,7 @@ apx memory <slug> --append "<fact>" # append a durable note (non-destructive
|
|
|
40
42
|
apx memory <slug> --replace < file.md # replace entire memory from stdin
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
Write to memory when you discover
|
|
45
|
+
Write to memory only when you discover safe project context the agent should know on future runs.
|
|
44
46
|
|
|
45
47
|
## Observe activity
|
|
46
48
|
|
|
@@ -74,4 +76,5 @@ Print this on the last meaningful line of your output:
|
|
|
74
76
|
APC_RESULT: <one-line summary or value>
|
|
75
77
|
```
|
|
76
78
|
The invoker (`apx run`, super-agent, Telegram bot) captures it as structured output.
|
|
77
|
-
Keep it factual and short
|
|
79
|
+
Keep it factual and short. It becomes the session result stored in APX local runtime state, not
|
|
80
|
+
inside `.apc/`.
|
|
@@ -109,11 +109,8 @@ export async function resolveProjectId(target) {
|
|
|
109
109
|
// No override: walk up from cwd
|
|
110
110
|
const root = findApfRoot();
|
|
111
111
|
if (!root) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
? `registered projects: ${registered.map((p) => `"${p.name}" (id ${p.id})`).join(", ")}`
|
|
115
|
-
: "no projects registered yet — run `apx project add <path>` first";
|
|
116
|
-
throw new Error(`not inside an APC project. Use --project <name|id|path>. ${hint}`);
|
|
112
|
+
// Fall back to the default project (id=0) — always available, no .apc/ required.
|
|
113
|
+
return 0;
|
|
117
114
|
}
|
|
118
115
|
const projects = await http.get("/projects");
|
|
119
116
|
const found = projects.find((p) => p.path === root);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { findApfRoot, readAgents } from "../../core/parser.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getOrCreateApxId } from "../../core/scaffold.js";
|
|
5
5
|
import { generateSessionId } from "../../core/session-store.js";
|
|
6
|
+
import { ensureProjectStorage } from "../../core/config.js";
|
|
6
7
|
import { http } from "../http.js";
|
|
7
8
|
import { resolveProjectId } from "./project.js";
|
|
8
9
|
|
|
@@ -14,6 +15,12 @@ function requireRoot() {
|
|
|
14
15
|
return root;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
function requireStorageRoot(root) {
|
|
19
|
+
const apxId = getOrCreateApxId(root);
|
|
20
|
+
if (!apxId) throw new Error("could not resolve APX project storage id");
|
|
21
|
+
return ensureProjectStorage(apxId);
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
18
25
|
|
|
19
26
|
function readStdinSync() {
|
|
@@ -61,7 +68,7 @@ function setFrontmatterField(text, field, value) {
|
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
function listAllSessions(root) {
|
|
64
|
-
const agentsDir = path.join(root, "
|
|
71
|
+
const agentsDir = path.join(requireStorageRoot(root), "agents");
|
|
65
72
|
if (!fs.existsSync(agentsDir)) return [];
|
|
66
73
|
const out = [];
|
|
67
74
|
for (const slug of fs.readdirSync(agentsDir)) {
|
|
@@ -124,10 +131,11 @@ export function cmdSessionNew(args) {
|
|
|
124
131
|
throw new Error(`agent "${slug}" not found in AGENTS.md`);
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
const id = generateSessionId(
|
|
134
|
+
const storageRoot = requireStorageRoot(root);
|
|
135
|
+
const id = generateSessionId(storageRoot, slug);
|
|
129
136
|
const filename = `${id}.md`;
|
|
130
|
-
const filepath = path.join(
|
|
137
|
+
const filepath = path.join(storageRoot, "agents", slug, "sessions", filename);
|
|
138
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
131
139
|
|
|
132
140
|
const taskRef = args.flags["task-ref"] === true ? "" : (args.flags["task-ref"] || "");
|
|
133
141
|
let body = "";
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// apx setup — interactive first-run wizard.
|
|
2
|
+
// Guides the user through provider, model, channels, and language.
|
|
3
|
+
// Starts the daemon and sends a fun wake-up message when done.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import https from "node:https";
|
|
7
|
+
import http from "node:http";
|
|
8
|
+
import readline from "node:readline";
|
|
9
|
+
import { spawnSync, spawn } from "node:child_process";
|
|
10
|
+
import { readConfig, writeConfig, APX_HOME } from "../../core/config.js";
|
|
11
|
+
|
|
12
|
+
// ── ANSI helpers ──────────────────────────────────────────────────────────────
|
|
13
|
+
const c = {
|
|
14
|
+
reset: "\x1b[0m",
|
|
15
|
+
bold: "\x1b[1m",
|
|
16
|
+
dim: "\x1b[2m",
|
|
17
|
+
cyan: "\x1b[36m",
|
|
18
|
+
green: "\x1b[32m",
|
|
19
|
+
yellow:"\x1b[33m",
|
|
20
|
+
red: "\x1b[31m",
|
|
21
|
+
gray: "\x1b[90m",
|
|
22
|
+
};
|
|
23
|
+
const b = (s) => `${c.bold}${s}${c.reset}`;
|
|
24
|
+
const cy = (s) => `${c.cyan}${s}${c.reset}`;
|
|
25
|
+
const gr = (s) => `${c.green}${s}${c.reset}`;
|
|
26
|
+
const di = (s) => `${c.dim}${s}${c.reset}`;
|
|
27
|
+
|
|
28
|
+
// ── readline helpers ─────────────────────────────────────────────────────────
|
|
29
|
+
let rl;
|
|
30
|
+
function initRl() {
|
|
31
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
+
}
|
|
33
|
+
function ask(prompt) {
|
|
34
|
+
return new Promise((resolve) => rl.question(prompt, (a) => resolve(a.trim())));
|
|
35
|
+
}
|
|
36
|
+
function close() { rl.close(); }
|
|
37
|
+
|
|
38
|
+
// ── Fetch helpers ─────────────────────────────────────────────────────────────
|
|
39
|
+
function fetchJson(url, timeout = 4000) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const mod = url.startsWith("https") ? https : http;
|
|
42
|
+
const req = mod.get(url, { timeout }, (res) => {
|
|
43
|
+
let body = "";
|
|
44
|
+
res.on("data", (c) => (body += c));
|
|
45
|
+
res.on("end", () => { try { resolve(JSON.parse(body)); } catch { resolve(null); } });
|
|
46
|
+
});
|
|
47
|
+
req.on("error", () => resolve(null));
|
|
48
|
+
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function fetchOllamaModels(baseUrl) {
|
|
53
|
+
const data = await fetchJson(`${baseUrl.replace(/\/$/, "")}/api/tags`);
|
|
54
|
+
if (!data?.models) return [];
|
|
55
|
+
return data.models.map((m) => m.name).filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Provider definitions ──────────────────────────────────────────────────────
|
|
59
|
+
const PROVIDERS = [
|
|
60
|
+
{
|
|
61
|
+
id: "anthropic",
|
|
62
|
+
label: "Anthropic (Claude)",
|
|
63
|
+
needsKey: true,
|
|
64
|
+
keyLabel: "Anthropic API key",
|
|
65
|
+
keyHint: "sk-ant-...",
|
|
66
|
+
models: ["claude-sonnet-4-5", "claude-haiku-4-5", "claude-opus-4-5"],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "openai",
|
|
70
|
+
label: "OpenAI (GPT)",
|
|
71
|
+
needsKey: true,
|
|
72
|
+
keyLabel: "OpenAI API key",
|
|
73
|
+
keyHint: "sk-...",
|
|
74
|
+
models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "ollama",
|
|
78
|
+
label: "Ollama (local / self-hosted)",
|
|
79
|
+
needsKey: false,
|
|
80
|
+
models: [], // fetched dynamically
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "gemini",
|
|
84
|
+
label: "Gemini (Google)",
|
|
85
|
+
needsKey: true,
|
|
86
|
+
keyLabel: "Gemini API key",
|
|
87
|
+
keyHint: "AIza...",
|
|
88
|
+
models: ["gemini-2.0-flash", "gemini-1.5-pro"],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// ── Main wizard ───────────────────────────────────────────────────────────────
|
|
93
|
+
export async function cmdSetup() {
|
|
94
|
+
initRl();
|
|
95
|
+
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(b(cy(" ╔═══════════════════════════════════╗")));
|
|
98
|
+
console.log(b(cy(" ║ APX Setup Wizard ║")));
|
|
99
|
+
console.log(b(cy(" ╚═══════════════════════════════════╝")));
|
|
100
|
+
console.log();
|
|
101
|
+
console.log(di(" This will configure the APX daemon and super-agent."));
|
|
102
|
+
console.log(di(" You can re-run `apx setup` at any time to change settings."));
|
|
103
|
+
console.log();
|
|
104
|
+
|
|
105
|
+
// ── Super-agent? ────────────────────────────────────────────────────────────
|
|
106
|
+
const wantAgent = await ask(` Enable super-agent? ${di("[Y/n]")} `);
|
|
107
|
+
if (/^n/i.test(wantAgent)) {
|
|
108
|
+
console.log(`\n ${gr("✓")} Skipping super-agent. Run ${cy("apx daemon start")} to start the daemon.\n`);
|
|
109
|
+
close();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Provider ────────────────────────────────────────────────────────────────
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(b(" AI Provider:"));
|
|
116
|
+
PROVIDERS.forEach((p, i) => console.log(` ${cy(String(i + 1))}. ${p.label}`));
|
|
117
|
+
console.log();
|
|
118
|
+
let providerIdx = -1;
|
|
119
|
+
while (providerIdx < 0) {
|
|
120
|
+
const ans = await ask(` Choose [1-${PROVIDERS.length}]: `);
|
|
121
|
+
const n = parseInt(ans, 10);
|
|
122
|
+
if (n >= 1 && n <= PROVIDERS.length) providerIdx = n - 1;
|
|
123
|
+
else console.log(` ${c.yellow}Please enter a number between 1 and ${PROVIDERS.length}.${c.reset}`);
|
|
124
|
+
}
|
|
125
|
+
const provider = PROVIDERS[providerIdx];
|
|
126
|
+
let apiKey = "";
|
|
127
|
+
let ollamaUrl = "http://localhost:11434";
|
|
128
|
+
|
|
129
|
+
if (provider.id === "ollama") {
|
|
130
|
+
const urlAns = await ask(` Ollama URL ${di("[http://localhost:11434]")}: `);
|
|
131
|
+
ollamaUrl = urlAns || "http://localhost:11434";
|
|
132
|
+
} else if (provider.needsKey) {
|
|
133
|
+
apiKey = await ask(` ${provider.keyLabel} ${di(`(${provider.keyHint})`)}: `);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Model ───────────────────────────────────────────────────────────────────
|
|
137
|
+
console.log();
|
|
138
|
+
let models = [...provider.models];
|
|
139
|
+
|
|
140
|
+
if (provider.id === "ollama") {
|
|
141
|
+
process.stdout.write(` Fetching models from Ollama... `);
|
|
142
|
+
const fetched = await fetchOllamaModels(ollamaUrl);
|
|
143
|
+
if (fetched.length) {
|
|
144
|
+
models = fetched;
|
|
145
|
+
console.log(gr(`${fetched.length} found`));
|
|
146
|
+
} else {
|
|
147
|
+
console.log(di("(couldn't reach Ollama, enter manually)"));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let chosenModel = "";
|
|
152
|
+
if (models.length) {
|
|
153
|
+
console.log(b(" Model:"));
|
|
154
|
+
models.forEach((m, i) => console.log(` ${cy(String(i + 1))}. ${m}`));
|
|
155
|
+
console.log(` ${cy(String(models.length + 1))}. ${di("Enter manually")}`);
|
|
156
|
+
console.log();
|
|
157
|
+
let modelIdx = -1;
|
|
158
|
+
while (modelIdx < 0) {
|
|
159
|
+
const ans = await ask(` Choose [1-${models.length + 1}]: `);
|
|
160
|
+
const n = parseInt(ans, 10);
|
|
161
|
+
if (n >= 1 && n <= models.length) { modelIdx = n - 1; chosenModel = models[modelIdx]; }
|
|
162
|
+
else if (n === models.length + 1) {
|
|
163
|
+
chosenModel = await ask(" Model name: ");
|
|
164
|
+
modelIdx = 0;
|
|
165
|
+
} else {
|
|
166
|
+
console.log(` ${c.yellow}Invalid choice.${c.reset}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
chosenModel = await ask(" Model name (e.g. qwen2.5:14b): ");
|
|
171
|
+
}
|
|
172
|
+
chosenModel = `${provider.id}:${chosenModel}`;
|
|
173
|
+
|
|
174
|
+
// ── Channels ────────────────────────────────────────────────────────────────
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(b(" Channels:"));
|
|
177
|
+
console.log(` ${cy("1")}. Web (local API — always on)`);
|
|
178
|
+
console.log(` ${cy("2")}. Telegram`);
|
|
179
|
+
console.log();
|
|
180
|
+
const chAns = await ask(` Enable Telegram? ${di("[Y/n]")} `);
|
|
181
|
+
const wantTelegram = !/^n/i.test(chAns);
|
|
182
|
+
|
|
183
|
+
let botToken = "";
|
|
184
|
+
let chatId = "";
|
|
185
|
+
|
|
186
|
+
if (wantTelegram) {
|
|
187
|
+
console.log();
|
|
188
|
+
console.log(di(" Create a bot at https://t.me/BotFather → get the token."));
|
|
189
|
+
console.log(di(" Then message your bot and visit:"));
|
|
190
|
+
console.log(di(" https://api.telegram.org/bot<TOKEN>/getUpdates to find your chat_id."));
|
|
191
|
+
console.log();
|
|
192
|
+
botToken = await ask(" Bot token: ");
|
|
193
|
+
chatId = await ask(" Your chat ID: ");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Language ────────────────────────────────────────────────────────────────
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(b(" Language:"));
|
|
199
|
+
console.log(di(" The super-agent will always respond in your language."));
|
|
200
|
+
console.log();
|
|
201
|
+
const language = await ask(" Your language (e.g. English, Español, Português): ") || "English";
|
|
202
|
+
|
|
203
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(b(" ─── Summary ───────────────────────────────────────────"));
|
|
206
|
+
console.log(` Provider: ${cy(provider.label)}`);
|
|
207
|
+
console.log(` Model: ${cy(chosenModel)}`);
|
|
208
|
+
if (provider.id === "ollama") console.log(` Ollama URL: ${cy(ollamaUrl)}`);
|
|
209
|
+
console.log(` Telegram: ${wantTelegram ? gr("enabled") : di("disabled")}`);
|
|
210
|
+
console.log(` Language: ${cy(language)}`);
|
|
211
|
+
console.log(b(" ────────────────────────────────────────────────────────"));
|
|
212
|
+
console.log();
|
|
213
|
+
|
|
214
|
+
const confirm = await ask(` Start the daemon with these settings? ${di("[Y/n]")} `);
|
|
215
|
+
if (/^n/i.test(confirm)) {
|
|
216
|
+
console.log("\n Cancelled. Run `apx setup` again to configure.\n");
|
|
217
|
+
close();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
close(); // done with prompts
|
|
222
|
+
|
|
223
|
+
// ── Write config ─────────────────────────────────────────────────────────────
|
|
224
|
+
const cfg = readConfig();
|
|
225
|
+
|
|
226
|
+
cfg.super_agent.enabled = true;
|
|
227
|
+
cfg.super_agent.model = chosenModel;
|
|
228
|
+
// System prompt: language instruction only, no wizard references
|
|
229
|
+
cfg.super_agent.system = `Always respond in the user's language: ${language}.`;
|
|
230
|
+
|
|
231
|
+
if (provider.id === "ollama") {
|
|
232
|
+
cfg.engines.ollama.base_url = ollamaUrl;
|
|
233
|
+
} else if (provider.needsKey && apiKey) {
|
|
234
|
+
cfg.engines[provider.id].api_key = apiKey;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (wantTelegram && botToken && chatId) {
|
|
238
|
+
cfg.telegram.enabled = true;
|
|
239
|
+
cfg.telegram.bot_token = botToken;
|
|
240
|
+
cfg.telegram.chat_id = chatId;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
writeConfig(cfg);
|
|
244
|
+
console.log(`\n ${gr("✓")} Config saved to ${di("~/.apx/config.json")}`);
|
|
245
|
+
|
|
246
|
+
// ── Start daemon ─────────────────────────────────────────────────────────────
|
|
247
|
+
console.log();
|
|
248
|
+
process.stdout.write(` Starting daemon... `);
|
|
249
|
+
|
|
250
|
+
const start = spawnSync("apx", ["daemon", "start"], { encoding: "utf8" });
|
|
251
|
+
if (start.status !== 0) {
|
|
252
|
+
console.log(c.red + "failed" + c.reset);
|
|
253
|
+
console.log(start.stderr || start.stdout);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
console.log(gr("running ✓"));
|
|
257
|
+
|
|
258
|
+
// Give daemon a moment to come up
|
|
259
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
260
|
+
|
|
261
|
+
// ── Wake-up Telegram message ─────────────────────────────────────────────────
|
|
262
|
+
if (wantTelegram && botToken && chatId) {
|
|
263
|
+
console.log();
|
|
264
|
+
process.stdout.write(` Sending wake-up message... `);
|
|
265
|
+
try {
|
|
266
|
+
const resp = await sendTelegramWakeup({ botToken, chatId, language, model: chosenModel });
|
|
267
|
+
if (resp) console.log(gr("sent ✓"));
|
|
268
|
+
else console.log(di("(couldn't reach Telegram)"));
|
|
269
|
+
} catch {
|
|
270
|
+
console.log(di("(couldn't reach Telegram)"));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log();
|
|
275
|
+
console.log(gr(b(" ✅ APX is ready!")));
|
|
276
|
+
console.log();
|
|
277
|
+
console.log(` Daemon: ${cy("http://127.0.0.1:7430")}`);
|
|
278
|
+
if (wantTelegram) console.log(` Telegram: ${cy("active — message your bot")}`);
|
|
279
|
+
console.log();
|
|
280
|
+
console.log(di(" Tip: run `apx daemon status` anytime to check health."));
|
|
281
|
+
console.log();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Send a fun wake-up message via Telegram using super-agent.
|
|
285
|
+
// The prompt is in English so the model knows to reply in the user's language.
|
|
286
|
+
async function sendTelegramWakeup({ botToken, chatId, language, model }) {
|
|
287
|
+
const prompt =
|
|
288
|
+
`You are APX, an AI agent assistant that just came online. ` +
|
|
289
|
+
`Send a short, fun, enthusiastic wake-up message to the user. ` +
|
|
290
|
+
`Be playful and creative — like a friendly AI that just woke up. ` +
|
|
291
|
+
`Keep it under 3 sentences. ` +
|
|
292
|
+
`IMPORTANT: respond in ${language}. ` +
|
|
293
|
+
`Do not mention that you were configured or set up.`;
|
|
294
|
+
|
|
295
|
+
// Ask the daemon's super-agent (give it a second attempt window)
|
|
296
|
+
let text;
|
|
297
|
+
try {
|
|
298
|
+
const res = await fetchJson("http://127.0.0.1:7430/super-agent/ask", 8000);
|
|
299
|
+
text = res?.text;
|
|
300
|
+
} catch {}
|
|
301
|
+
|
|
302
|
+
// Fallback: generate a simple message without daemon
|
|
303
|
+
if (!text) {
|
|
304
|
+
text = languageFallback(language);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Send via Telegram bot API
|
|
308
|
+
return new Promise((resolve) => {
|
|
309
|
+
const body = JSON.stringify({ chat_id: chatId, text, parse_mode: "Markdown" });
|
|
310
|
+
const req = https.request({
|
|
311
|
+
hostname: "api.telegram.org",
|
|
312
|
+
path: `/bot${botToken}/sendMessage`,
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
315
|
+
timeout: 6000,
|
|
316
|
+
}, (res) => {
|
|
317
|
+
let d = "";
|
|
318
|
+
res.on("data", (c) => (d += c));
|
|
319
|
+
res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve(null); } });
|
|
320
|
+
});
|
|
321
|
+
req.on("error", () => resolve(null));
|
|
322
|
+
req.write(body);
|
|
323
|
+
req.end();
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Minimal fallback messages per common language (used only if daemon can't respond)
|
|
328
|
+
function languageFallback(lang) {
|
|
329
|
+
const l = lang.toLowerCase();
|
|
330
|
+
if (/espa[ñn]|spanish|arg|lat/i.test(l)) return "⚡ ¡Despierto y listo para trabajar! APX online.";
|
|
331
|
+
if (/portugu|brasil/i.test(l)) return "⚡ Acordei e pronto para trabalhar! APX online.";
|
|
332
|
+
if (/franc|french/i.test(l)) return "⚡ Réveillé et prêt à travailler ! APX en ligne.";
|
|
333
|
+
if (/deutsch|german/i.test(l)) return "⚡ Aufgewacht und bereit! APX ist online.";
|
|
334
|
+
if (/ital/i.test(l)) return "⚡ Sveglio e pronto a lavorare! APX online.";
|
|
335
|
+
return "⚡ I'm awake and ready to go! APX is online.";
|
|
336
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import { getLatestVersion } from "../../core/update-check.js";
|
|
4
|
+
|
|
5
|
+
const PACKAGE_NAME = "@agentprojectcontext/apx";
|
|
6
|
+
|
|
7
|
+
export async function cmdUpdate(args) {
|
|
8
|
+
const force = args.flags.force || args.flags.yes || args.flags.y;
|
|
9
|
+
|
|
10
|
+
console.log("Checking for updates...");
|
|
11
|
+
const latest = await getLatestVersion();
|
|
12
|
+
|
|
13
|
+
if (!latest) {
|
|
14
|
+
console.error("Could not reach npm registry. Check your connection.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Read current version from the package that owns this file.
|
|
19
|
+
const { createRequire } = await import("node:module");
|
|
20
|
+
const { fileURLToPath } = await import("node:url");
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const pkg = require("../../package.json");
|
|
23
|
+
const current = pkg.version;
|
|
24
|
+
|
|
25
|
+
function isNewer(cur, lat) {
|
|
26
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
27
|
+
const [ma, mi, pa] = parse(cur);
|
|
28
|
+
const [mb, mib, pb] = parse(lat);
|
|
29
|
+
if (mb > ma) return true;
|
|
30
|
+
if (mb === ma && mib > mi) return true;
|
|
31
|
+
if (mb === ma && mib === mi && pb > pa) return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!isNewer(current, latest)) {
|
|
36
|
+
console.log(`✅ Already up to date (${current})`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`\n Current: ${current}`);
|
|
41
|
+
console.log(` Latest: ${latest}`);
|
|
42
|
+
|
|
43
|
+
if (!force) {
|
|
44
|
+
const confirmed = await confirm(`\nUpdate to ${latest}? [y/N] `);
|
|
45
|
+
if (!confirmed) {
|
|
46
|
+
console.log("Cancelled.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(`\nRunning: npm install -g ${PACKAGE_NAME}@${latest}\n`);
|
|
52
|
+
const result = spawnSync(
|
|
53
|
+
"npm",
|
|
54
|
+
["install", "-g", `${PACKAGE_NAME}@${latest}`],
|
|
55
|
+
{ stdio: "inherit" }
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (result.status !== 0) {
|
|
59
|
+
console.error(`\n❌ Update failed (exit ${result.status})`);
|
|
60
|
+
process.exit(result.status || 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`\n✅ Updated to ${latest}. Restart any running apx daemon:`);
|
|
64
|
+
console.log(` apx daemon stop && apx daemon start`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function confirm(prompt) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
70
|
+
rl.question(prompt, (answer) => {
|
|
71
|
+
rl.close();
|
|
72
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// apx — unified CLI for APC (Agent Project
|
|
2
|
+
// apx — unified CLI for APC (Agent Project Context).
|
|
3
3
|
// ESM, Node >= 18.
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
@@ -71,6 +71,9 @@ import { cmdPluginsList, cmdPluginStatus } from "./commands/plugins.js";
|
|
|
71
71
|
import { cmdSkillsAdd, cmdSkillsList, cmdSkillsStatus } from "./commands/skills.js";
|
|
72
72
|
import { cmdIdentity } from "./commands/identity.js";
|
|
73
73
|
import { cmdCommandList, cmdCommandShow } from "./commands/command.js";
|
|
74
|
+
import { cmdUpdate } from "./commands/update.js";
|
|
75
|
+
import { cmdSetup } from "./commands/setup.js";
|
|
76
|
+
import { checkForUpdate } from "../core/update-check.js";
|
|
74
77
|
import {
|
|
75
78
|
cmdRoutineList,
|
|
76
79
|
cmdRoutineGet,
|
|
@@ -87,17 +90,18 @@ const VERSION = JSON.parse(
|
|
|
87
90
|
fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8")
|
|
88
91
|
).version;
|
|
89
92
|
|
|
90
|
-
const HELP = `apx — Agent Project
|
|
93
|
+
const HELP = `apx — Agent Project Context
|
|
91
94
|
|
|
92
95
|
Usage:
|
|
93
96
|
apx <command> [<subcommand>] [args] [--flags]
|
|
94
97
|
|
|
95
98
|
Bootstrap:
|
|
96
99
|
apx init [path] [--name "<name>"] initialize an APC project
|
|
97
|
-
apx project add
|
|
100
|
+
apx project add [path] register a project with the daemon
|
|
98
101
|
apx project list
|
|
99
102
|
apx project remove <path|id>
|
|
100
|
-
apx project rebuild [<path|id>] rebuild
|
|
103
|
+
apx project rebuild [<path|id>] rebuild project index from filesystem
|
|
104
|
+
apx add project [path] alias for: apx project add
|
|
101
105
|
|
|
102
106
|
Agents:
|
|
103
107
|
apx agent add <slug> [--role R] [--model M] [--skills a,b] [--language es-AR] [--description D]
|
|
@@ -144,7 +148,7 @@ Telegram:
|
|
|
144
148
|
Messages:
|
|
145
149
|
apx messages tail [--agent <slug>] [--channel <ch>] [-n 50] [--global]
|
|
146
150
|
global channels (telegram, direct, whatsapp) → ~/.apx/messages/<ch>/
|
|
147
|
-
project channels (runtime, a2a, exec) →
|
|
151
|
+
project channels (runtime, a2a, exec) → ~/.apx/projects/<id>/messages/
|
|
148
152
|
apx messages search "<query>"
|
|
149
153
|
|
|
150
154
|
LLM engines (v0.2):
|
|
@@ -200,6 +204,8 @@ Skills (IDE integration):
|
|
|
200
204
|
apx skills status show which IDE targets are installed (project + global)
|
|
201
205
|
|
|
202
206
|
Other:
|
|
207
|
+
apx setup interactive setup wizard (alias: apx install)
|
|
208
|
+
apx update check for updates and upgrade (alias: apx upgrade)
|
|
203
209
|
apx --help
|
|
204
210
|
apx --version
|
|
205
211
|
|
|
@@ -466,6 +472,24 @@ async function dispatch(cmd, rest) {
|
|
|
466
472
|
await cmdIdentity(parseArgs(rest));
|
|
467
473
|
break;
|
|
468
474
|
|
|
475
|
+
case "add": {
|
|
476
|
+
// apx add <domain> [...args] — consistent alternative to apx <domain> add
|
|
477
|
+
const sub = rest[0];
|
|
478
|
+
if (sub === "project") await cmdProjectAdd(parseArgs(rest.slice(1)));
|
|
479
|
+
else die(`unknown 'add' subcommand: ${sub || "(none)"} — try: project`);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
case "setup":
|
|
484
|
+
case "install":
|
|
485
|
+
await cmdSetup();
|
|
486
|
+
return;
|
|
487
|
+
|
|
488
|
+
case "update":
|
|
489
|
+
case "upgrade":
|
|
490
|
+
await cmdUpdate(parseArgs(rest));
|
|
491
|
+
return; // skip checkForUpdate after an update
|
|
492
|
+
|
|
469
493
|
default:
|
|
470
494
|
die(`unknown command: ${cmd}\nRun \`apx --help\` for usage.`);
|
|
471
495
|
}
|
|
@@ -475,6 +499,7 @@ const [topCmd, ...topRest] = argv;
|
|
|
475
499
|
(async () => {
|
|
476
500
|
try {
|
|
477
501
|
await dispatch(topCmd, topRest);
|
|
502
|
+
checkForUpdate(VERSION);
|
|
478
503
|
} catch (err) {
|
|
479
504
|
die(err && err.message ? err.message : String(err));
|
|
480
505
|
}
|