@agentprojectcontext/apx 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,332 @@
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 } from "node:child_process";
10
+ import { readConfig, writeConfig } from "../../core/config.js";
11
+ import { mascot } from "../../core/mascot.js";
12
+
13
+ // ── ANSI helpers ──────────────────────────────────────────────────────────────
14
+ const c = {
15
+ reset: "\x1b[0m",
16
+ bold: "\x1b[1m",
17
+ dim: "\x1b[2m",
18
+ cyan: "\x1b[36m",
19
+ green: "\x1b[32m",
20
+ yellow:"\x1b[33m",
21
+ red: "\x1b[31m",
22
+ gray: "\x1b[90m",
23
+ };
24
+ const b = (s) => `${c.bold}${s}${c.reset}`;
25
+ const cy = (s) => `${c.cyan}${s}${c.reset}`;
26
+ const gr = (s) => `${c.green}${s}${c.reset}`;
27
+ const di = (s) => `${c.dim}${s}${c.reset}`;
28
+
29
+ // ── readline helpers ─────────────────────────────────────────────────────────
30
+ let rl;
31
+ function initRl() {
32
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
33
+ }
34
+ function ask(prompt) {
35
+ return new Promise((resolve) => rl.question(prompt, (a) => resolve(a.trim())));
36
+ }
37
+ function close() { rl.close(); }
38
+
39
+ // ── Fetch helpers ─────────────────────────────────────────────────────────────
40
+ function fetchJson(url, timeout = 4000) {
41
+ return new Promise((resolve) => {
42
+ const mod = url.startsWith("https") ? https : http;
43
+ const req = mod.get(url, { timeout }, (res) => {
44
+ let body = "";
45
+ res.on("data", (c) => (body += c));
46
+ res.on("end", () => { try { resolve(JSON.parse(body)); } catch { resolve(null); } });
47
+ });
48
+ req.on("error", () => resolve(null));
49
+ req.on("timeout", () => { req.destroy(); resolve(null); });
50
+ });
51
+ }
52
+
53
+ async function fetchOllamaModels(baseUrl) {
54
+ const data = await fetchJson(`${baseUrl.replace(/\/$/, "")}/api/tags`);
55
+ if (!data?.models) return [];
56
+ return data.models.map((m) => m.name).filter(Boolean);
57
+ }
58
+
59
+ // ── Provider definitions ──────────────────────────────────────────────────────
60
+ const PROVIDERS = [
61
+ {
62
+ id: "anthropic",
63
+ label: "Anthropic (Claude)",
64
+ needsKey: true,
65
+ keyLabel: "Anthropic API key",
66
+ keyHint: "sk-ant-...",
67
+ models: ["claude-sonnet-4-5", "claude-haiku-4-5", "claude-opus-4-5"],
68
+ },
69
+ {
70
+ id: "openai",
71
+ label: "OpenAI (GPT)",
72
+ needsKey: true,
73
+ keyLabel: "OpenAI API key",
74
+ keyHint: "sk-...",
75
+ models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
76
+ },
77
+ {
78
+ id: "ollama",
79
+ label: "Ollama (local / self-hosted)",
80
+ needsKey: false,
81
+ models: [], // fetched dynamically
82
+ },
83
+ {
84
+ id: "gemini",
85
+ label: "Gemini (Google)",
86
+ needsKey: true,
87
+ keyLabel: "Gemini API key",
88
+ keyHint: "AIza...",
89
+ models: ["gemini-2.0-flash", "gemini-1.5-pro"],
90
+ },
91
+ ];
92
+
93
+ // ── Main wizard ───────────────────────────────────────────────────────────────
94
+ export async function cmdSetup() {
95
+ initRl();
96
+
97
+ mascot("wave", "Setup Wizard — configure daemon, model, and channels");
98
+ console.log(di(" Re-run `apx setup` anytime to change settings."));
99
+ console.log();
100
+
101
+ // ── Super-agent? ────────────────────────────────────────────────────────────
102
+ const wantAgent = await ask(` Enable super-agent? ${di("[Y/n]")} `);
103
+ if (/^n/i.test(wantAgent)) {
104
+ console.log(`\n ${gr("✓")} Skipping super-agent. Run ${cy("apx daemon start")} to start the daemon.\n`);
105
+ close();
106
+ return;
107
+ }
108
+
109
+ // ── Provider ────────────────────────────────────────────────────────────────
110
+ console.log();
111
+ console.log(b(" AI Provider:"));
112
+ PROVIDERS.forEach((p, i) => console.log(` ${cy(String(i + 1))}. ${p.label}`));
113
+ console.log();
114
+ let providerIdx = -1;
115
+ while (providerIdx < 0) {
116
+ const ans = await ask(` Choose [1-${PROVIDERS.length}]: `);
117
+ const n = parseInt(ans, 10);
118
+ if (n >= 1 && n <= PROVIDERS.length) providerIdx = n - 1;
119
+ else console.log(` ${c.yellow}Please enter a number between 1 and ${PROVIDERS.length}.${c.reset}`);
120
+ }
121
+ const provider = PROVIDERS[providerIdx];
122
+ let apiKey = "";
123
+ let ollamaUrl = "http://localhost:11434";
124
+
125
+ if (provider.id === "ollama") {
126
+ const urlAns = await ask(` Ollama URL ${di("[http://localhost:11434]")}: `);
127
+ ollamaUrl = urlAns || "http://localhost:11434";
128
+ } else if (provider.needsKey) {
129
+ apiKey = await ask(` ${provider.keyLabel} ${di(`(${provider.keyHint})`)}: `);
130
+ }
131
+
132
+ // ── Model ───────────────────────────────────────────────────────────────────
133
+ console.log();
134
+ let models = [...provider.models];
135
+
136
+ if (provider.id === "ollama") {
137
+ process.stdout.write(` Fetching models from Ollama... `);
138
+ const fetched = await fetchOllamaModels(ollamaUrl);
139
+ if (fetched.length) {
140
+ models = fetched;
141
+ console.log(gr(`${fetched.length} found`));
142
+ } else {
143
+ console.log(di("(couldn't reach Ollama, enter manually)"));
144
+ }
145
+ }
146
+
147
+ let chosenModel = "";
148
+ if (models.length) {
149
+ console.log(b(" Model:"));
150
+ models.forEach((m, i) => console.log(` ${cy(String(i + 1))}. ${m}`));
151
+ console.log(` ${cy(String(models.length + 1))}. ${di("Enter manually")}`);
152
+ console.log();
153
+ let modelIdx = -1;
154
+ while (modelIdx < 0) {
155
+ const ans = await ask(` Choose [1-${models.length + 1}]: `);
156
+ const n = parseInt(ans, 10);
157
+ if (n >= 1 && n <= models.length) { modelIdx = n - 1; chosenModel = models[modelIdx]; }
158
+ else if (n === models.length + 1) {
159
+ chosenModel = await ask(" Model name: ");
160
+ modelIdx = 0;
161
+ } else {
162
+ console.log(` ${c.yellow}Invalid choice.${c.reset}`);
163
+ }
164
+ }
165
+ } else {
166
+ chosenModel = await ask(" Model name (e.g. qwen2.5:14b): ");
167
+ }
168
+ chosenModel = `${provider.id}:${chosenModel}`;
169
+
170
+ // ── Channels ────────────────────────────────────────────────────────────────
171
+ console.log();
172
+ console.log(b(" Channels:"));
173
+ console.log(` ${cy("1")}. Web (local API — always on)`);
174
+ console.log(` ${cy("2")}. Telegram`);
175
+ console.log();
176
+ const chAns = await ask(` Enable Telegram? ${di("[Y/n]")} `);
177
+ const wantTelegram = !/^n/i.test(chAns);
178
+
179
+ let botToken = "";
180
+ let chatId = "";
181
+
182
+ if (wantTelegram) {
183
+ console.log();
184
+ console.log(di(" Create a bot at https://t.me/BotFather → get the token."));
185
+ console.log(di(" Then message your bot and visit:"));
186
+ console.log(di(" https://api.telegram.org/bot<TOKEN>/getUpdates to find your chat_id."));
187
+ console.log();
188
+ botToken = await ask(" Bot token: ");
189
+ chatId = await ask(" Your chat ID: ");
190
+ }
191
+
192
+ // ── Language ────────────────────────────────────────────────────────────────
193
+ console.log();
194
+ console.log(b(" Language:"));
195
+ console.log(di(" The super-agent will always respond in your language."));
196
+ console.log();
197
+ const language = await ask(" Your language (e.g. English, Español, Português): ") || "English";
198
+
199
+ // ── Summary ─────────────────────────────────────────────────────────────────
200
+ console.log();
201
+ console.log(b(" ─── Summary ───────────────────────────────────────────"));
202
+ console.log(` Provider: ${cy(provider.label)}`);
203
+ console.log(` Model: ${cy(chosenModel)}`);
204
+ if (provider.id === "ollama") console.log(` Ollama URL: ${cy(ollamaUrl)}`);
205
+ console.log(` Telegram: ${wantTelegram ? gr("enabled") : di("disabled")}`);
206
+ console.log(` Language: ${cy(language)}`);
207
+ console.log(b(" ────────────────────────────────────────────────────────"));
208
+ console.log();
209
+
210
+ const confirm = await ask(` Start the daemon with these settings? ${di("[Y/n]")} `);
211
+ if (/^n/i.test(confirm)) {
212
+ console.log("\n Cancelled. Run `apx setup` again to configure.\n");
213
+ close();
214
+ return;
215
+ }
216
+
217
+ close(); // done with prompts
218
+
219
+ // ── Write config ─────────────────────────────────────────────────────────────
220
+ const cfg = readConfig();
221
+
222
+ cfg.super_agent.enabled = true;
223
+ cfg.super_agent.model = chosenModel;
224
+ // System prompt: language instruction only, no wizard references
225
+ cfg.super_agent.system = `Always respond in the user's language: ${language}.`;
226
+
227
+ if (provider.id === "ollama") {
228
+ cfg.engines.ollama.base_url = ollamaUrl;
229
+ } else if (provider.needsKey && apiKey) {
230
+ cfg.engines[provider.id].api_key = apiKey;
231
+ }
232
+
233
+ if (wantTelegram && botToken && chatId) {
234
+ cfg.telegram.enabled = true;
235
+ cfg.telegram.bot_token = botToken;
236
+ cfg.telegram.chat_id = chatId;
237
+ }
238
+
239
+ writeConfig(cfg);
240
+ console.log(`\n ${gr("✓")} Config saved to ${di("~/.apx/config.json")}`);
241
+
242
+ // ── Start daemon ─────────────────────────────────────────────────────────────
243
+ console.log();
244
+ process.stdout.write(` Starting daemon... `);
245
+
246
+ const start = spawnSync("apx", ["daemon", "start"], { encoding: "utf8" });
247
+ if (start.status !== 0) {
248
+ console.log(c.red + "failed" + c.reset);
249
+ console.log(start.stderr || start.stdout);
250
+ process.exit(1);
251
+ }
252
+ console.log(gr("running ✓"));
253
+
254
+ // Give daemon a moment to come up
255
+ await new Promise((r) => setTimeout(r, 2000));
256
+
257
+ // ── Wake-up Telegram message ─────────────────────────────────────────────────
258
+ if (wantTelegram && botToken && chatId) {
259
+ console.log();
260
+ process.stdout.write(` Sending wake-up message... `);
261
+ try {
262
+ const resp = await sendTelegramWakeup({ botToken, chatId, language, model: chosenModel });
263
+ if (resp) console.log(gr("sent ✓"));
264
+ else console.log(di("(couldn't reach Telegram)"));
265
+ } catch {
266
+ console.log(di("(couldn't reach Telegram)"));
267
+ }
268
+ }
269
+
270
+ console.log();
271
+ console.log(gr(b(" ✅ APX is ready!")));
272
+ console.log();
273
+ console.log(` Daemon: ${cy("http://127.0.0.1:7430")}`);
274
+ if (wantTelegram) console.log(` Telegram: ${cy("active — message your bot")}`);
275
+ console.log();
276
+ console.log(di(" Tip: run `apx daemon status` anytime to check health."));
277
+ console.log();
278
+ }
279
+
280
+ // Send a fun wake-up message via Telegram using super-agent.
281
+ // The prompt is in English so the model knows to reply in the user's language.
282
+ async function sendTelegramWakeup({ botToken, chatId, language, model }) {
283
+ const prompt =
284
+ `You are APX, an AI agent assistant that just came online. ` +
285
+ `Send a short, fun, enthusiastic wake-up message to the user. ` +
286
+ `Be playful and creative — like a friendly AI that just woke up. ` +
287
+ `Keep it under 3 sentences. ` +
288
+ `IMPORTANT: respond in ${language}. ` +
289
+ `Do not mention that you were configured or set up.`;
290
+
291
+ // Ask the daemon's super-agent (give it a second attempt window)
292
+ let text;
293
+ try {
294
+ const res = await fetchJson("http://127.0.0.1:7430/super-agent/ask", 8000);
295
+ text = res?.text;
296
+ } catch {}
297
+
298
+ // Fallback: generate a simple message without daemon
299
+ if (!text) {
300
+ text = languageFallback(language);
301
+ }
302
+
303
+ // Send via Telegram bot API
304
+ return new Promise((resolve) => {
305
+ const body = JSON.stringify({ chat_id: chatId, text, parse_mode: "Markdown" });
306
+ const req = https.request({
307
+ hostname: "api.telegram.org",
308
+ path: `/bot${botToken}/sendMessage`,
309
+ method: "POST",
310
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
311
+ timeout: 6000,
312
+ }, (res) => {
313
+ let d = "";
314
+ res.on("data", (c) => (d += c));
315
+ res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve(null); } });
316
+ });
317
+ req.on("error", () => resolve(null));
318
+ req.write(body);
319
+ req.end();
320
+ });
321
+ }
322
+
323
+ // Minimal fallback messages per common language (used only if daemon can't respond)
324
+ function languageFallback(lang) {
325
+ const l = lang.toLowerCase();
326
+ if (/espa[ñn]|spanish|arg|lat/i.test(l)) return "⚡ ¡Despierto y listo para trabajar! APX online.";
327
+ if (/portugu|brasil/i.test(l)) return "⚡ Acordei e pronto para trabalhar! APX online.";
328
+ if (/franc|french/i.test(l)) return "⚡ Réveillé et prêt à travailler ! APX en ligne.";
329
+ if (/deutsch|german/i.test(l)) return "⚡ Aufgewacht und bereit! APX ist online.";
330
+ if (/ital/i.test(l)) return "⚡ Sveglio e pronto a lavorare! APX online.";
331
+ return "⚡ I'm awake and ready to go! APX is online.";
332
+ }
package/src/cli/index.js CHANGED
@@ -72,7 +72,9 @@ import { cmdSkillsAdd, cmdSkillsList, cmdSkillsStatus } from "./commands/skills.
72
72
  import { cmdIdentity } from "./commands/identity.js";
73
73
  import { cmdCommandList, cmdCommandShow } from "./commands/command.js";
74
74
  import { cmdUpdate } from "./commands/update.js";
75
+ import { cmdSetup } from "./commands/setup.js";
75
76
  import { checkForUpdate } from "../core/update-check.js";
77
+ import { mascot } from "../core/mascot.js";
76
78
  import {
77
79
  cmdRoutineList,
78
80
  cmdRoutineGet,
@@ -203,6 +205,7 @@ Skills (IDE integration):
203
205
  apx skills status show which IDE targets are installed (project + global)
204
206
 
205
207
  Other:
208
+ apx setup interactive setup wizard (alias: apx install)
206
209
  apx update check for updates and upgrade (alias: apx upgrade)
207
210
  apx --help
208
211
  apx --version
@@ -244,7 +247,9 @@ function parseArgs(argv) {
244
247
  }
245
248
 
246
249
  function die(msg, code = 1) {
247
- process.stderr.write(`apx: ${msg}\n`);
250
+ // Show panda mascot for user-facing errors
251
+ const isUnknown = msg.startsWith("unknown command") || msg.startsWith("unknown");
252
+ mascot(isUnknown ? "confused" : "sad", `apx: ${msg}`);
248
253
  process.exit(code);
249
254
  }
250
255
 
@@ -478,6 +483,11 @@ async function dispatch(cmd, rest) {
478
483
  break;
479
484
  }
480
485
 
486
+ case "setup":
487
+ case "install":
488
+ await cmdSetup();
489
+ return;
490
+
481
491
  case "update":
482
492
  case "upgrade":
483
493
  await cmdUpdate(parseArgs(rest));
@@ -0,0 +1,124 @@
1
+ // APX mascot — a panda that appears in different moods across the CLI.
2
+ // Usage: import { mascot } from '../core/mascot.js'; mascot('happy');
3
+
4
+ const R = "\x1b[0m";
5
+ const B = "\x1b[1m";
6
+ const W = "\x1b[97m"; // bright white
7
+ const K = "\x1b[30m"; // black
8
+ const BK = "\x1b[40m"; // bg black
9
+ const BW = "\x1b[47m"; // bg white
10
+ const CY = "\x1b[36m";
11
+ const YE = "\x1b[33m";
12
+ const GR = "\x1b[32m";
13
+ const RE = "\x1b[31m";
14
+ const DI = "\x1b[2m";
15
+ const BL = "\x1b[34m";
16
+
17
+ // Each mood: [panda lines, caption]
18
+ const MOODS = {
19
+ // ─── happy: default greeting / daemon started ────────────────────────────
20
+ happy: {
21
+ color: GR,
22
+ lines: [
23
+ ` ${BK}${W} ▄███████▄ ${R}`,
24
+ ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
25
+ ` ${BK}${W} █ ◕ ◕ █ ${R}`,
26
+ ` ${BK}${W} █ ╰ω╯ █ ${R}`,
27
+ ` ${BK}${W} ▀███████▀ ${R}`,
28
+ ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
29
+ ],
30
+ caption: `${GR}${B}ready to go!${R}`,
31
+ },
32
+
33
+ // ─── wave: first run / setup ─────────────────────────────────────────────
34
+ wave: {
35
+ color: CY,
36
+ lines: [
37
+ ` ${BK}${W} ▄███████▄ ${R} 👋`,
38
+ ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
39
+ ` ${BK}${W} █ ◕ ◕ █ ${R}`,
40
+ ` ${BK}${W} █ ╰▽╯ █ ${R}`,
41
+ ` ${BK}${W} ▀███████▀ ${R}`,
42
+ ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
43
+ ],
44
+ caption: `${CY}${B}APX — Agent Project Context${R}`,
45
+ },
46
+
47
+ // ─── confused: unknown command / not found ────────────────────────────────
48
+ confused: {
49
+ color: YE,
50
+ lines: [
51
+ ` ${BK}${W} ▄███████▄ ${R} ${YE}?${R}`,
52
+ ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
53
+ ` ${BK}${W} █ ◔ ◔ █ ${R}`,
54
+ ` ${BK}${W} █ ╰~╯ █ ${R}`,
55
+ ` ${BK}${W} ▀███████▀ ${R}`,
56
+ ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
57
+ ],
58
+ caption: `${YE}${B}hmm, I don't know that one${R}`,
59
+ },
60
+
61
+ // ─── sad: error ───────────────────────────────────────────────────────────
62
+ sad: {
63
+ color: RE,
64
+ lines: [
65
+ ` ${BK}${W} ▄███████▄ ${R}`,
66
+ ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
67
+ ` ${BK}${W} █ ╥ ╥ █ ${R}`,
68
+ ` ${BK}${W} █ ╰︵╯ █ ${R}`,
69
+ ` ${BK}${W} ▀███████▀ ${R}`,
70
+ ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
71
+ ],
72
+ caption: `${RE}${B}something went wrong${R}`,
73
+ },
74
+
75
+ // ─── excited: update available ────────────────────────────────────────────
76
+ excited: {
77
+ color: BL,
78
+ lines: [
79
+ ` ${BK}${W} ▄███████▄ ${R} ${BL}⬆${R}`,
80
+ ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
81
+ ` ${BK}${W} █ ★ ★ █ ${R}`,
82
+ ` ${BK}${W} █ ╰◡╯ █ ${R}`,
83
+ ` ${BK}${W} ▀███████▀ ${R}`,
84
+ ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
85
+ ],
86
+ caption: `${BL}${B}new version available!${R}`,
87
+ },
88
+
89
+ // ─── sleeping: daemon not running ────────────────────────────────────────
90
+ sleeping: {
91
+ color: DI,
92
+ lines: [
93
+ ` ${BK}${W} ▄███████▄ ${R} ${DI}z z z${R}`,
94
+ ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
95
+ ` ${BK}${W} █ − − █ ${R}`,
96
+ ` ${BK}${W} █ ╰_╯ █ ${R}`,
97
+ ` ${BK}${W} ▀███████▀ ${R}`,
98
+ ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
99
+ ],
100
+ caption: `${DI}${B}daemon is not running${R}`,
101
+ },
102
+ };
103
+
104
+ // Print the mascot to stderr (doesn't interfere with piped output).
105
+ // mood: 'happy' | 'wave' | 'confused' | 'sad' | 'excited' | 'sleeping'
106
+ export function mascot(mood = "happy", message = "") {
107
+ const def = MOODS[mood] || MOODS.happy;
108
+ const out = [
109
+ "",
110
+ ...def.lines,
111
+ ` ${def.caption}`,
112
+ message ? ` ${def.color}${message}${R}` : "",
113
+ "",
114
+ ].join("\n");
115
+ process.stderr.write(out + "\n");
116
+ }
117
+
118
+ // One-liner for inline use: mascot.confused("apx: unknown command: foo")
119
+ mascot.confused = (msg) => mascot("confused", msg);
120
+ mascot.sad = (msg) => mascot("sad", msg);
121
+ mascot.happy = (msg) => mascot("happy", msg);
122
+ mascot.wave = (msg) => mascot("wave", msg);
123
+ mascot.excited = (msg) => mascot("excited", msg);
124
+ mascot.sleeping = (msg) => mascot("sleeping", msg);