@agentprojectcontext/apx 1.15.6 → 1.17.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.
Files changed (222) hide show
  1. package/package.json +46 -5
  2. package/src/cli/commands/log.js +113 -0
  3. package/src/cli/commands/overlay.js +253 -0
  4. package/src/cli/commands/sys.js +88 -16
  5. package/src/cli/index.js +23 -1
  6. package/src/cli/terminal-chat/renderer.js +71 -56
  7. package/src/cli-ts/commands/agent.ts +173 -0
  8. package/src/cli-ts/commands/chat.ts +119 -0
  9. package/src/cli-ts/commands/daemon.ts +112 -0
  10. package/src/cli-ts/commands/exec.ts +109 -0
  11. package/src/cli-ts/commands/mcp.ts +235 -0
  12. package/src/cli-ts/commands/session.ts +224 -0
  13. package/src/cli-ts/commands/status.ts +61 -0
  14. package/src/cli-ts/http.ts +36 -0
  15. package/src/cli-ts/index.ts +73 -0
  16. package/src/cli-ts/ui.ts +107 -0
  17. package/src/core/logging.js +81 -0
  18. package/src/daemon/api.js +58 -0
  19. package/src/daemon/engines/anthropic.js +60 -1
  20. package/src/daemon/engines/index.js +2 -1
  21. package/src/daemon/engines/ollama.js +70 -3
  22. package/src/daemon/index.js +58 -0
  23. package/src/daemon/overlay-ws.js +40 -0
  24. package/src/daemon/plugins/index.js +2 -1
  25. package/src/daemon/plugins/overlay.js +177 -0
  26. package/src/daemon/plugins/telegram.js +15 -3
  27. package/src/daemon/super-agent-langchain.js +296 -0
  28. package/src/daemon/super-agent.js +115 -19
  29. package/src/daemon/transcription.js +262 -59
  30. package/src/daemon/whisper-server.py +57 -6
  31. package/src/overlay/index.html +44 -0
  32. package/src/overlay/main.js +480 -0
  33. package/src/overlay/package.json +3 -0
  34. package/src/overlay/preload.js +34 -0
  35. package/src/overlay/renderer.js +371 -0
  36. package/src/overlay/style.css +250 -0
  37. package/src/tui/_shims/cli-error.ts +6 -0
  38. package/src/tui/_shims/cli-logo.ts +18 -0
  39. package/src/tui/_shims/cli-ui.ts +1 -0
  40. package/src/tui/_shims/config-console-state.ts +7 -0
  41. package/src/tui/_shims/core-any.ts +30 -0
  42. package/src/tui/_shims/core-binary.ts +13 -0
  43. package/src/tui/_shims/core-flag.ts +3 -0
  44. package/src/tui/_shims/core-log.ts +14 -0
  45. package/src/tui/_shims/lsp-language.ts +1 -0
  46. package/src/tui/_shims/opencode-any.ts +135 -0
  47. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  48. package/src/tui/_shims/plugin-tui.ts +13 -0
  49. package/src/tui/_shims/provider-provider.ts +10 -0
  50. package/src/tui/_shims/session-retry.ts +1 -0
  51. package/src/tui/_shims/session-schema.ts +15 -0
  52. package/src/tui/_shims/session-session.ts +3 -0
  53. package/src/tui/_shims/snapshot.ts +4 -0
  54. package/src/tui/_shims/tool-any.ts +18 -0
  55. package/src/tui/_shims/util-error.ts +7 -0
  56. package/src/tui/_shims/util-filesystem.ts +79 -0
  57. package/src/tui/_shims/util-format.ts +7 -0
  58. package/src/tui/_shims/util-iife.ts +3 -0
  59. package/src/tui/_shims/util-locale.ts +10 -0
  60. package/src/tui/_shims/util-process.ts +38 -0
  61. package/src/tui/app.tsx +783 -0
  62. package/src/tui/asset/charge.wav +0 -0
  63. package/src/tui/asset/pulse-a.wav +0 -0
  64. package/src/tui/asset/pulse-b.wav +0 -0
  65. package/src/tui/asset/pulse-c.wav +0 -0
  66. package/src/tui/attach.ts +100 -0
  67. package/src/tui/component/bg-pulse-render.ts +436 -0
  68. package/src/tui/component/bg-pulse.tsx +99 -0
  69. package/src/tui/component/border.tsx +21 -0
  70. package/src/tui/component/dialog-agent.tsx +31 -0
  71. package/src/tui/component/dialog-console-org.tsx +103 -0
  72. package/src/tui/component/dialog-mcp.tsx +85 -0
  73. package/src/tui/component/dialog-model.tsx +175 -0
  74. package/src/tui/component/dialog-provider.tsx +456 -0
  75. package/src/tui/component/dialog-retry-action.tsx +160 -0
  76. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  77. package/src/tui/component/dialog-session-list.tsx +323 -0
  78. package/src/tui/component/dialog-session-rename.tsx +31 -0
  79. package/src/tui/component/dialog-skill.tsx +36 -0
  80. package/src/tui/component/dialog-stash.tsx +87 -0
  81. package/src/tui/component/dialog-status.tsx +168 -0
  82. package/src/tui/component/dialog-tag.tsx +44 -0
  83. package/src/tui/component/dialog-theme-list.tsx +50 -0
  84. package/src/tui/component/dialog-variant.tsx +39 -0
  85. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  86. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  87. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  88. package/src/tui/component/error-component.tsx +92 -0
  89. package/src/tui/component/logo.tsx +896 -0
  90. package/src/tui/component/plugin-route-missing.tsx +14 -0
  91. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  92. package/src/tui/component/prompt/cwd.ts +0 -0
  93. package/src/tui/component/prompt/frecency.tsx +90 -0
  94. package/src/tui/component/prompt/history.tsx +108 -0
  95. package/src/tui/component/prompt/index.tsx +1809 -0
  96. package/src/tui/component/prompt/part.ts +16 -0
  97. package/src/tui/component/prompt/stash.tsx +101 -0
  98. package/src/tui/component/prompt/traits.ts +35 -0
  99. package/src/tui/component/spinner.tsx +24 -0
  100. package/src/tui/component/startup-loading.tsx +63 -0
  101. package/src/tui/component/todo-item.tsx +32 -0
  102. package/src/tui/component/use-connected.tsx +9 -0
  103. package/src/tui/component/workspace-label.tsx +19 -0
  104. package/src/tui/config/cwd.ts +5 -0
  105. package/src/tui/config/keybind.ts +432 -0
  106. package/src/tui/config/tui-migrate.ts +154 -0
  107. package/src/tui/config/tui-schema.ts +34 -0
  108. package/src/tui/config/tui.ts +46 -0
  109. package/src/tui/context/aggregate-failures.ts +34 -0
  110. package/src/tui/context/args.tsx +15 -0
  111. package/src/tui/context/command-palette.tsx +163 -0
  112. package/src/tui/context/directory.ts +15 -0
  113. package/src/tui/context/editor-zed.ts +283 -0
  114. package/src/tui/context/editor.ts +468 -0
  115. package/src/tui/context/event-apx.ts +22 -0
  116. package/src/tui/context/event.ts +6 -0
  117. package/src/tui/context/exit.tsx +60 -0
  118. package/src/tui/context/helper.tsx +25 -0
  119. package/src/tui/context/kv.tsx +81 -0
  120. package/src/tui/context/local.tsx +608 -0
  121. package/src/tui/context/path-format.tsx +39 -0
  122. package/src/tui/context/project-apx.tsx +48 -0
  123. package/src/tui/context/project.tsx +7 -0
  124. package/src/tui/context/prompt.tsx +18 -0
  125. package/src/tui/context/route.tsx +52 -0
  126. package/src/tui/context/sdk-apx.tsx +185 -0
  127. package/src/tui/context/sdk.tsx +6 -0
  128. package/src/tui/context/sync-apx.tsx +178 -0
  129. package/src/tui/context/sync-v2.tsx +16 -0
  130. package/src/tui/context/sync.tsx +118 -0
  131. package/src/tui/context/theme/aura.json +69 -0
  132. package/src/tui/context/theme/ayu.json +80 -0
  133. package/src/tui/context/theme/carbonfox.json +248 -0
  134. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  135. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  136. package/src/tui/context/theme/catppuccin.json +112 -0
  137. package/src/tui/context/theme/cobalt2.json +225 -0
  138. package/src/tui/context/theme/cursor.json +249 -0
  139. package/src/tui/context/theme/dracula.json +219 -0
  140. package/src/tui/context/theme/everforest.json +241 -0
  141. package/src/tui/context/theme/flexoki.json +237 -0
  142. package/src/tui/context/theme/github.json +233 -0
  143. package/src/tui/context/theme/gruvbox.json +242 -0
  144. package/src/tui/context/theme/kanagawa.json +77 -0
  145. package/src/tui/context/theme/lucent-orng.json +234 -0
  146. package/src/tui/context/theme/material.json +235 -0
  147. package/src/tui/context/theme/matrix.json +77 -0
  148. package/src/tui/context/theme/mercury.json +252 -0
  149. package/src/tui/context/theme/monokai.json +221 -0
  150. package/src/tui/context/theme/nightowl.json +221 -0
  151. package/src/tui/context/theme/nord.json +223 -0
  152. package/src/tui/context/theme/one-dark.json +84 -0
  153. package/src/tui/context/theme/opencode.json +245 -0
  154. package/src/tui/context/theme/orng.json +249 -0
  155. package/src/tui/context/theme/osaka-jade.json +93 -0
  156. package/src/tui/context/theme/palenight.json +222 -0
  157. package/src/tui/context/theme/rosepine.json +234 -0
  158. package/src/tui/context/theme/solarized.json +223 -0
  159. package/src/tui/context/theme/synthwave84.json +226 -0
  160. package/src/tui/context/theme/tokyonight.json +243 -0
  161. package/src/tui/context/theme/vercel.json +245 -0
  162. package/src/tui/context/theme/vesper.json +218 -0
  163. package/src/tui/context/theme/zenburn.json +223 -0
  164. package/src/tui/context/theme.tsx +1247 -0
  165. package/src/tui/context/tui-config.tsx +9 -0
  166. package/src/tui/event.ts +16 -0
  167. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  168. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  169. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  170. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  171. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  172. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  173. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  174. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  175. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  176. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  177. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  178. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  179. package/src/tui/keymap.tsx +166 -0
  180. package/src/tui/layer.ts +6 -0
  181. package/src/tui/plugin/api.tsx +381 -0
  182. package/src/tui/plugin/command-shim.ts +109 -0
  183. package/src/tui/plugin/internal.ts +33 -0
  184. package/src/tui/plugin/runtime.ts +1069 -0
  185. package/src/tui/plugin/slots.tsx +60 -0
  186. package/src/tui/routes/home.tsx +96 -0
  187. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  188. package/src/tui/routes/session/dialog-message.tsx +108 -0
  189. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  190. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  191. package/src/tui/routes/session/footer.tsx +91 -0
  192. package/src/tui/routes/session/index.tsx +188 -0
  193. package/src/tui/routes/session/permission.tsx +722 -0
  194. package/src/tui/routes/session/question.tsx +490 -0
  195. package/src/tui/routes/session/sidebar.tsx +102 -0
  196. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  197. package/src/tui/run.ts +84 -0
  198. package/src/tui/thread.ts +261 -0
  199. package/src/tui/tsconfig.json +40 -0
  200. package/src/tui/ui/dialog-alert.tsx +66 -0
  201. package/src/tui/ui/dialog-confirm.tsx +108 -0
  202. package/src/tui/ui/dialog-export-options.tsx +217 -0
  203. package/src/tui/ui/dialog-help.tsx +40 -0
  204. package/src/tui/ui/dialog-prompt.tsx +101 -0
  205. package/src/tui/ui/dialog-select.tsx +553 -0
  206. package/src/tui/ui/dialog.tsx +211 -0
  207. package/src/tui/ui/link.tsx +34 -0
  208. package/src/tui/ui/spinner.ts +368 -0
  209. package/src/tui/ui/toast.tsx +111 -0
  210. package/src/tui/util/clipboard.ts +217 -0
  211. package/src/tui/util/editor.ts +37 -0
  212. package/src/tui/util/model.ts +23 -0
  213. package/src/tui/util/provider-origin.ts +7 -0
  214. package/src/tui/util/revert-diff.ts +18 -0
  215. package/src/tui/util/scroll.ts +25 -0
  216. package/src/tui/util/selection.ts +65 -0
  217. package/src/tui/util/signal.ts +41 -0
  218. package/src/tui/util/sound.ts +156 -0
  219. package/src/tui/util/transcript.ts +112 -0
  220. package/src/tui/validate-session.ts +29 -0
  221. package/src/tui/win32.ts +130 -0
  222. package/src/tui/worker.ts +104 -0
@@ -0,0 +1,112 @@
1
+ import type { CommandModule, Argv, ArgumentsCamelCase } from "yargs";
2
+ import { getHttp } from "../http.js";
3
+ import { println, error, success, dim } from "../ui.js";
4
+ import { spawn } from "node:child_process";
5
+
6
+ // ---------- status ----------
7
+
8
+ const statusCmd: CommandModule = {
9
+ command: "status",
10
+ describe: "Show daemon status",
11
+ handler: async () => {
12
+ try {
13
+ const http = await getHttp();
14
+ const health = (await http.get("/health")) as {
15
+ status: string; version?: string; uptime_s?: number;
16
+ };
17
+ success(`Daemon running version=${health.version ?? "?"} uptime=${health.uptime_s ?? "?"}s`);
18
+ } catch {
19
+ error("Daemon is not running.");
20
+ process.exit(1);
21
+ }
22
+ },
23
+ };
24
+
25
+ // ---------- start ----------
26
+
27
+ const startCmd: CommandModule = {
28
+ command: "start",
29
+ describe: "Start the APX daemon in the background",
30
+ handler: async () => {
31
+ try {
32
+ const http = await getHttp();
33
+ await http.get("/health");
34
+ println(dim("Daemon is already running."));
35
+ } catch {
36
+ const daemon = spawn("apx-daemon", [], {
37
+ detached: true,
38
+ stdio: "ignore",
39
+ });
40
+ daemon.unref();
41
+ success("Daemon started.");
42
+ }
43
+ },
44
+ };
45
+
46
+ // ---------- stop ----------
47
+
48
+ const stopCmd: CommandModule = {
49
+ command: "stop",
50
+ describe: "Gracefully stop the APX daemon",
51
+ handler: async () => {
52
+ try {
53
+ const http = await getHttp();
54
+ await http.post("/admin/shutdown", {});
55
+ success("Daemon stopped.");
56
+ } catch (err: unknown) {
57
+ error(err instanceof Error ? err.message : String(err));
58
+ process.exit(1);
59
+ }
60
+ },
61
+ };
62
+
63
+ // ---------- logs ----------
64
+
65
+ const logsCmd: CommandModule = {
66
+ command: "logs",
67
+ describe: "Stream daemon logs",
68
+ builder: (yargs: Argv) =>
69
+ yargs.option("tail", {
70
+ alias: "n",
71
+ type: "number",
72
+ default: 50,
73
+ describe: "Number of lines to show",
74
+ }),
75
+ handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
76
+ try {
77
+ const tail = (args.tail as number) ?? 50;
78
+ const { homedir } = await import("node:os");
79
+ const { createReadStream } = await import("node:fs");
80
+ const { createInterface } = await import("node:readline");
81
+ const logPath = `${homedir()}/.apx/daemon.log`;
82
+ try {
83
+ const rl = createInterface({ input: createReadStream(logPath), crlfDelay: Infinity });
84
+ const lines: string[] = [];
85
+ for await (const line of rl) lines.push(line);
86
+ const tailLines = lines.slice(-tail);
87
+ tailLines.forEach((l) => println(l));
88
+ } catch {
89
+ error(`Log file not found: ${logPath}`);
90
+ process.exit(1);
91
+ }
92
+ } catch (err: unknown) {
93
+ error(err instanceof Error ? err.message : String(err));
94
+ process.exit(1);
95
+ }
96
+ },
97
+ };
98
+
99
+ // ---------- parent ----------
100
+
101
+ export const daemonCmd: CommandModule = {
102
+ command: "daemon",
103
+ describe: "Manage the APX background daemon",
104
+ builder: (yargs: Argv) =>
105
+ yargs
106
+ .command(statusCmd)
107
+ .command(startCmd)
108
+ .command(stopCmd)
109
+ .command(logsCmd)
110
+ .demandCommand(1, "Specify a daemon subcommand"),
111
+ handler: () => {},
112
+ };
@@ -0,0 +1,109 @@
1
+ import type { CommandModule, Argv, ArgumentsCamelCase } from "yargs";
2
+ import { getHttp, type StreamEvent } from "../http.js";
3
+ import { error } from "../ui.js";
4
+
5
+ interface GlobalArgs { project?: string }
6
+
7
+ async function resolveProjectId(project?: string): Promise<string> {
8
+ const http = await getHttp();
9
+ const projects = (await http.get("/projects")) as Array<{ id: string; name: string; path: string }>;
10
+ if (!projects?.length) throw new Error("No projects registered. Run: apx init");
11
+ if (project) {
12
+ const match = projects.find(
13
+ (p) => p.id === project || p.name === project || p.path === project || p.path?.endsWith("/" + project),
14
+ );
15
+ if (!match) throw new Error(`Project not found: ${project}`);
16
+ return match.id;
17
+ }
18
+ return projects[0]!.id;
19
+ }
20
+
21
+ export const execCmd: CommandModule = {
22
+ command: "exec <agent> [prompt..]",
23
+ aliases: ["run"],
24
+ describe: "Run a one-shot prompt through an agent (non-interactive)",
25
+ builder: (yargs: Argv) =>
26
+ yargs
27
+ .positional("agent", { type: "string", demandOption: true, describe: "Agent slug" })
28
+ .positional("prompt", { type: "string", array: true, describe: "Prompt text" })
29
+ .option("model", { type: "string", describe: "Override model" })
30
+ .option("max-tokens", { type: "number", describe: "Max output tokens" })
31
+ .option("temperature", { type: "number", describe: "Sampling temperature (0–1)" })
32
+ .option("format", {
33
+ choices: ["text", "json"] as const,
34
+ default: "text" as const,
35
+ describe: "Output format",
36
+ })
37
+ .option("stream", {
38
+ type: "boolean",
39
+ default: true,
40
+ describe: "Stream output as it arrives",
41
+ }),
42
+ handler: async (
43
+ args: ArgumentsCamelCase<Record<string, unknown>>,
44
+ ) => {
45
+ try {
46
+ const project = args.project as string | undefined;
47
+ const agent = args.agent as string;
48
+ const promptArgs = args.prompt as string[] | undefined;
49
+ const model = args.model as string | undefined;
50
+ const maxTokens = args.maxTokens as number | undefined;
51
+ const temperature = args.temperature as number | undefined;
52
+ const format = args.format as "text" | "json";
53
+ const stream = args.stream as boolean;
54
+
55
+ // Build prompt from args + stdin
56
+ let prompt = (promptArgs ?? []).join(" ");
57
+ if (!process.stdin.isTTY) {
58
+ const chunks: Buffer[] = [];
59
+ for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
60
+ const piped = Buffer.concat(chunks).toString().trim();
61
+ if (piped) prompt = prompt ? prompt + "\n\n" + piped : piped;
62
+ }
63
+ if (!prompt) throw new Error("No prompt provided. Pass text as argument or via stdin.");
64
+
65
+ const http = await getHttp();
66
+ const pid = await resolveProjectId(project);
67
+ const body = {
68
+ prompt,
69
+ model,
70
+ maxTokens,
71
+ temperature,
72
+ };
73
+
74
+ if (stream) {
75
+ // Try streaming endpoint first
76
+ try {
77
+ const result = await http.streamPost(
78
+ `/projects/${pid}/super-agent/chat/stream`,
79
+ { ...body, contextNote: `Agent: ${agent}` },
80
+ (ev: StreamEvent) => {
81
+ if (ev.type === "chunk" && typeof ev.chunk === "string") {
82
+ process.stdout.write(ev.chunk);
83
+ }
84
+ if (ev.type === "event" && ev.event === "assistant_text" && typeof (ev as Record<string, unknown>).text === "string") {
85
+ process.stdout.write((ev as Record<string, unknown>).text as string);
86
+ }
87
+ },
88
+ ) as { text?: string };
89
+ if (!result?.text) process.stdout.write("\n");
90
+ return;
91
+ } catch {
92
+ // Fall through to non-streaming
93
+ }
94
+ }
95
+
96
+ const result = (await http.post(`/projects/${pid}/agents/${agent}/exec`, body)) as {
97
+ text: string; usage?: { input_tokens: number; output_tokens: number };
98
+ };
99
+ if (format === "json") {
100
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
101
+ } else {
102
+ process.stdout.write(result.text + "\n");
103
+ }
104
+ } catch (err: unknown) {
105
+ error(err instanceof Error ? err.message : String(err));
106
+ process.exit(1);
107
+ }
108
+ },
109
+ };
@@ -0,0 +1,235 @@
1
+ import type { CommandModule, Argv, ArgumentsCamelCase } from "yargs";
2
+ import { getHttp } from "../http.js";
3
+ import { println, error, success, dim, highlight, table } from "../ui.js";
4
+
5
+ interface GlobalArgs { project?: string }
6
+
7
+ async function resolveProjectId(project?: string): Promise<string> {
8
+ const http = await getHttp();
9
+ const projects = (await http.get("/projects")) as Array<{ id: string; name: string; path: string }>;
10
+ if (!projects?.length) throw new Error("No projects registered. Run: apx init");
11
+ if (project) {
12
+ const match = projects.find(
13
+ (p) => p.id === project || p.name === project || p.path === project || p.path?.endsWith("/" + project),
14
+ );
15
+ if (!match) throw new Error(`Project not found: ${project}`);
16
+ return match.id;
17
+ }
18
+ return projects[0]!.id;
19
+ }
20
+
21
+ // ---------- list ----------
22
+
23
+ const listCmd: CommandModule = {
24
+ command: "list",
25
+ aliases: ["ls"],
26
+ describe: "List registered MCP servers",
27
+ handler: async (args: ArgumentsCamelCase<GlobalArgs>) => {
28
+ try {
29
+ const http = await getHttp();
30
+ const pid = await resolveProjectId(args.project as string | undefined);
31
+ const mcps = (await http.get(`/projects/${pid}/mcps`)) as Array<{
32
+ name: string; transport?: string; enabled?: boolean; source?: string;
33
+ }>;
34
+ if (!mcps?.length) { println(dim("No MCP servers configured.")); return; }
35
+ table(
36
+ mcps.map((m) => ({
37
+ Name: m.name,
38
+ Transport: m.transport || "-",
39
+ Enabled: m.enabled ? "✓" : "✗",
40
+ Source: m.source || "-",
41
+ })),
42
+ ["Name", "Transport", "Enabled", "Source"],
43
+ );
44
+ } catch (err: unknown) {
45
+ error(err instanceof Error ? err.message : String(err));
46
+ process.exit(1);
47
+ }
48
+ },
49
+ };
50
+
51
+ // ---------- add ----------
52
+
53
+ const addCmd: CommandModule = {
54
+ command: "add <name>",
55
+ describe: "Register an MCP server",
56
+ builder: (yargs: Argv) =>
57
+ yargs
58
+ .positional("name", { type: "string", demandOption: true, describe: "MCP server name" })
59
+ .option("command", { alias: "c", type: "string", describe: "Command to launch MCP server" })
60
+ .option("url", { type: "string", describe: "Remote MCP server URL (for HTTP transport)" })
61
+ .option("env", {
62
+ type: "array",
63
+ string: true,
64
+ describe: "Environment variables (KEY=VALUE)",
65
+ })
66
+ .option("enabled", { type: "boolean", default: true, describe: "Enable the server" }),
67
+ handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
68
+ try {
69
+ const project = args.project as string | undefined;
70
+ const name = args.name as string;
71
+ const command = args.command as string | undefined;
72
+ const url = args.url as string | undefined;
73
+ const env = args.env as string[] | undefined;
74
+ const enabled = args.enabled as boolean;
75
+ const http = await getHttp();
76
+ const pid = await resolveProjectId(project);
77
+ // Parse env KEY=VALUE pairs
78
+ const envRecord: Record<string, string> = {};
79
+ for (const e of env ?? []) {
80
+ const idx = e.indexOf("=");
81
+ if (idx > 0) envRecord[e.slice(0, idx)] = e.slice(idx + 1);
82
+ }
83
+ const body: Record<string, unknown> = {
84
+ name,
85
+ enabled,
86
+ };
87
+ if (command) body.command = command;
88
+ if (url) body.url = url;
89
+ if (Object.keys(envRecord).length) body.env = envRecord;
90
+
91
+ await http.post(`/projects/${pid}/mcps`, body);
92
+ success(`MCP server registered: ${highlight(name)}`);
93
+ } catch (err: unknown) {
94
+ error(err instanceof Error ? err.message : String(err));
95
+ process.exit(1);
96
+ }
97
+ },
98
+ };
99
+
100
+ // ---------- remove ----------
101
+
102
+ const removeCmd: CommandModule = {
103
+ command: "remove <name>",
104
+ aliases: ["rm"],
105
+ describe: "Remove an MCP server",
106
+ builder: (yargs: Argv) =>
107
+ yargs.positional("name", { type: "string", demandOption: true, describe: "MCP server name" }),
108
+ handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
109
+ try {
110
+ const project = args.project as string | undefined;
111
+ const name = args.name as string;
112
+ const http = await getHttp();
113
+ const pid = await resolveProjectId(project);
114
+ await http.delete(`/projects/${pid}/mcps/${name}`);
115
+ success(`MCP server removed: ${name}`);
116
+ } catch (err: unknown) {
117
+ error(err instanceof Error ? err.message : String(err));
118
+ process.exit(1);
119
+ }
120
+ },
121
+ };
122
+
123
+ // ---------- enable / disable ----------
124
+
125
+ const enableCmd: CommandModule = {
126
+ command: "enable <name>",
127
+ describe: "Enable an MCP server",
128
+ builder: (yargs: Argv) =>
129
+ yargs.positional("name", { type: "string", demandOption: true }),
130
+ handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
131
+ try {
132
+ const project = args.project as string | undefined;
133
+ const name = args.name as string;
134
+ const http = await getHttp();
135
+ const pid = await resolveProjectId(project);
136
+ await http.post(`/projects/${pid}/mcps`, { name, enabled: true });
137
+ success(`Enabled: ${name}`);
138
+ } catch (err: unknown) { error(String(err)); process.exit(1); }
139
+ },
140
+ };
141
+
142
+ const disableCmd: CommandModule = {
143
+ command: "disable <name>",
144
+ describe: "Disable an MCP server",
145
+ builder: (yargs: Argv) =>
146
+ yargs.positional("name", { type: "string", demandOption: true }),
147
+ handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
148
+ try {
149
+ const project = args.project as string | undefined;
150
+ const name = args.name as string;
151
+ const http = await getHttp();
152
+ const pid = await resolveProjectId(project);
153
+ await http.post(`/projects/${pid}/mcps`, { name, enabled: false });
154
+ success(`Disabled: ${name}`);
155
+ } catch (err: unknown) { error(String(err)); process.exit(1); }
156
+ },
157
+ };
158
+
159
+ // ---------- tools ----------
160
+
161
+ const toolsCmd: CommandModule = {
162
+ command: "tools <name>",
163
+ describe: "List tools exposed by an MCP server",
164
+ builder: (yargs: Argv) =>
165
+ yargs.positional("name", { type: "string", demandOption: true }),
166
+ handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
167
+ try {
168
+ const project = args.project as string | undefined;
169
+ const name = args.name as string;
170
+ const http = await getHttp();
171
+ const pid = await resolveProjectId(project);
172
+ const result = (await http.post(`/mcp/run`, {
173
+ project: pid,
174
+ name,
175
+ tool: "list_tools",
176
+ params: {},
177
+ })) as { result?: { tools?: Array<{ name: string; description?: string }> } };
178
+ const tools = result?.result?.tools ?? [];
179
+ if (!tools.length) { println(dim("No tools found.")); return; }
180
+ table(
181
+ tools.map((t) => ({ Tool: t.name, Description: t.description?.slice(0, 60) || "-" })),
182
+ ["Tool", "Description"],
183
+ );
184
+ } catch (err: unknown) {
185
+ error(err instanceof Error ? err.message : String(err));
186
+ process.exit(1);
187
+ }
188
+ },
189
+ };
190
+
191
+ // ---------- check ----------
192
+
193
+ const checkCmd: CommandModule = {
194
+ command: "check",
195
+ describe: "Validate MCP configuration",
196
+ handler: async (args: ArgumentsCamelCase<GlobalArgs>) => {
197
+ try {
198
+ const http = await getHttp();
199
+ const pid = await resolveProjectId(args.project as string | undefined);
200
+ const result = (await http.get(`/projects/${pid}/mcps/check`)) as {
201
+ conflicts?: string[];
202
+ entries?: Array<{ name: string; ok: boolean; error?: string }>;
203
+ };
204
+ if (result.conflicts?.length) {
205
+ println("\x1b[93m⚠ Conflicts:\x1b[0m");
206
+ result.conflicts.forEach((c) => println(" " + c));
207
+ }
208
+ result.entries?.forEach((e) => {
209
+ if (e.ok) success(e.name);
210
+ else error(`${e.name}: ${e.error}`);
211
+ });
212
+ } catch (err: unknown) {
213
+ error(err instanceof Error ? err.message : String(err));
214
+ process.exit(1);
215
+ }
216
+ },
217
+ };
218
+
219
+ // ---------- parent ----------
220
+
221
+ export const mcpCmd: CommandModule = {
222
+ command: "mcp",
223
+ describe: "Manage MCP (Model Context Protocol) servers",
224
+ builder: (yargs: Argv) =>
225
+ yargs
226
+ .command(listCmd)
227
+ .command(addCmd)
228
+ .command(removeCmd)
229
+ .command(enableCmd)
230
+ .command(disableCmd)
231
+ .command(toolsCmd)
232
+ .command(checkCmd)
233
+ .demandCommand(1, "Specify an mcp subcommand"),
234
+ handler: () => {},
235
+ };
@@ -0,0 +1,224 @@
1
+ import type { CommandModule, Argv, ArgumentsCamelCase } from "yargs";
2
+ import { getHttp } from "../http.js";
3
+ import { println, error, success, dim, bold, table, highlight } from "../ui.js";
4
+
5
+ interface Session {
6
+ filename?: string;
7
+ title?: string;
8
+ started_at?: string;
9
+ ended_at?: string;
10
+ agent?: string;
11
+ status?: string;
12
+ }
13
+
14
+ interface GlobalArgs {
15
+ project?: string;
16
+ }
17
+
18
+ async function resolveProjectId(project?: string): Promise<string> {
19
+ const http = await getHttp();
20
+ const projects = (await http.get("/projects")) as Array<{
21
+ id: string;
22
+ name: string;
23
+ path: string;
24
+ }>;
25
+ if (!projects?.length) throw new Error("No projects registered. Run: apx init");
26
+
27
+ if (project) {
28
+ const match = projects.find(
29
+ (p) =>
30
+ p.id === project ||
31
+ p.name === project ||
32
+ p.path === project ||
33
+ p.path?.endsWith("/" + project),
34
+ );
35
+ if (!match) throw new Error(`Project not found: ${project}`);
36
+ return match.id;
37
+ }
38
+ // Default to first project
39
+ return projects[0]!.id;
40
+ }
41
+
42
+ // ---------- list ----------
43
+
44
+ const listCmd: CommandModule<GlobalArgs, GlobalArgs & { last: number; format: string }> = {
45
+ command: "list",
46
+ aliases: ["ls"],
47
+ describe: "List sessions for the current project",
48
+ builder: (yargs: Argv<GlobalArgs>) =>
49
+ yargs
50
+ .option("last", {
51
+ alias: "n",
52
+ type: "number",
53
+ default: 20,
54
+ describe: "Number of recent sessions to show",
55
+ })
56
+ .option("format", {
57
+ choices: ["table", "json"] as const,
58
+ default: "table" as const,
59
+ describe: "Output format",
60
+ }),
61
+ handler: async (args: ArgumentsCamelCase<GlobalArgs & { last: number; format: string }>) => {
62
+ try {
63
+ const http = await getHttp();
64
+ const pid = await resolveProjectId(args.project);
65
+ const sessions = (await http.get(`/projects/${pid}/sessions`)) as Session[];
66
+ if (!sessions?.length) {
67
+ println(dim("No sessions found."));
68
+ return;
69
+ }
70
+ const slice = sessions.slice(-args.last);
71
+ if (args.format === "json") {
72
+ process.stdout.write(JSON.stringify(slice, null, 2) + "\n");
73
+ return;
74
+ }
75
+ table(
76
+ slice.map((s) => ({
77
+ Title: s.title || s.filename || "(no title)",
78
+ Agent: s.agent || "-",
79
+ Started: s.started_at ? new Date(s.started_at).toLocaleString() : "-",
80
+ Status: s.status || "open",
81
+ })),
82
+ ["Title", "Agent", "Started", "Status"],
83
+ );
84
+ } catch (err: unknown) {
85
+ error(err instanceof Error ? err.message : String(err));
86
+ process.exit(1);
87
+ }
88
+ },
89
+ };
90
+
91
+ // ---------- new ----------
92
+
93
+ const newCmd: CommandModule<GlobalArgs, GlobalArgs & { title?: string; body?: string; agent?: string }> = {
94
+ command: "new",
95
+ describe: "Create a new session",
96
+ builder: (yargs: Argv<GlobalArgs>) =>
97
+ yargs
98
+ .option("title", {
99
+ type: "string",
100
+ describe: "Session title",
101
+ })
102
+ .option("body", {
103
+ type: "string",
104
+ describe: "Initial session body / context",
105
+ })
106
+ .option("agent", {
107
+ type: "string",
108
+ describe: "Agent slug to associate the session with",
109
+ }),
110
+ handler: async (args: ArgumentsCamelCase<GlobalArgs & { title?: string; body?: string; agent?: string }>) => {
111
+ try {
112
+ const http = await getHttp();
113
+ const pid = await resolveProjectId(args.project);
114
+ const agentSlug = args.agent || "default";
115
+ const session = (await http.post(`/projects/${pid}/agents/${agentSlug}/sessions`, {
116
+ title: args.title || `Session ${new Date().toLocaleDateString()}`,
117
+ body: args.body,
118
+ })) as { filename: string; path: string };
119
+ success(`Session created: ${highlight(session.filename)}`);
120
+ if (session.path) println(dim(session.path));
121
+ } catch (err: unknown) {
122
+ error(err instanceof Error ? err.message : String(err));
123
+ process.exit(1);
124
+ }
125
+ },
126
+ };
127
+
128
+ // ---------- get / show ----------
129
+
130
+ const getCmd: CommandModule<GlobalArgs, GlobalArgs & { id: string; body: boolean }> = {
131
+ command: "get <id>",
132
+ aliases: ["show"],
133
+ describe: "Show a session by filename or ID",
134
+ builder: (yargs: Argv<GlobalArgs>) =>
135
+ yargs
136
+ .positional("id", { type: "string", demandOption: true, describe: "Session filename or ID" })
137
+ .option("body", {
138
+ type: "boolean",
139
+ default: false,
140
+ describe: "Print session body / markdown",
141
+ }),
142
+ handler: async (args: ArgumentsCamelCase<GlobalArgs & { id: string; body: boolean }>) => {
143
+ try {
144
+ const http = await getHttp();
145
+ const pid = await resolveProjectId(args.project);
146
+ const session = (await http.get(`/projects/${pid}/sessions/${args.id}`)) as Record<string, unknown>;
147
+ if (args.body) {
148
+ process.stdout.write(String(session.body_md || session.body || "") + "\n");
149
+ return;
150
+ }
151
+ process.stdout.write(JSON.stringify(session, null, 2) + "\n");
152
+ } catch (err: unknown) {
153
+ error(err instanceof Error ? err.message : String(err));
154
+ process.exit(1);
155
+ }
156
+ },
157
+ };
158
+
159
+ // ---------- delete ----------
160
+
161
+ const deleteCmd: CommandModule<GlobalArgs, GlobalArgs & { id: string }> = {
162
+ command: "delete <id>",
163
+ aliases: ["rm"],
164
+ describe: "Delete a session",
165
+ builder: (yargs: Argv<GlobalArgs>) =>
166
+ yargs.positional("id", { type: "string", demandOption: true, describe: "Session filename or ID" }),
167
+ handler: async (args: ArgumentsCamelCase<GlobalArgs & { id: string }>) => {
168
+ try {
169
+ const http = await getHttp();
170
+ const pid = await resolveProjectId(args.project);
171
+ await http.delete(`/projects/${pid}/sessions/${args.id}`);
172
+ success(`Session deleted: ${args.id}`);
173
+ } catch (err: unknown) {
174
+ error(err instanceof Error ? err.message : String(err));
175
+ process.exit(1);
176
+ }
177
+ },
178
+ };
179
+
180
+ // ---------- compact ----------
181
+
182
+ const compactCmd: CommandModule<GlobalArgs, GlobalArgs & { id?: string; model?: string }> = {
183
+ command: "compact [id]",
184
+ describe: "Summarize and compact a session's conversation history",
185
+ builder: (yargs: Argv<GlobalArgs>) =>
186
+ yargs
187
+ .positional("id", { type: "string", describe: "Session ID (defaults to latest)" })
188
+ .option("model", { type: "string", describe: "Model to use for summarization" }),
189
+ handler: async (args: ArgumentsCamelCase<GlobalArgs & { id?: string; model?: string }>) => {
190
+ try {
191
+ const http = await getHttp();
192
+ const pid = await resolveProjectId(args.project);
193
+ const path = args.id
194
+ ? `/projects/${pid}/sessions/${args.id}/compact`
195
+ : `/sessions/${pid}/compact`;
196
+ const result = (await http.post(path, { model: args.model, project: pid })) as {
197
+ compacted_turns?: number;
198
+ summary?: string;
199
+ };
200
+ success(`Compacted ${result.compacted_turns ?? "?"} turns.`);
201
+ if (result.summary) println(dim(result.summary));
202
+ } catch (err: unknown) {
203
+ error(err instanceof Error ? err.message : String(err));
204
+ process.exit(1);
205
+ }
206
+ },
207
+ };
208
+
209
+ // ---------- parent command ----------
210
+
211
+ export const sessionCmd: CommandModule<GlobalArgs, GlobalArgs> = {
212
+ command: "session",
213
+ aliases: ["sessions"],
214
+ describe: "Manage APC sessions",
215
+ builder: (yargs: Argv<GlobalArgs>) =>
216
+ yargs
217
+ .command(listCmd as CommandModule)
218
+ .command(newCmd as CommandModule)
219
+ .command(getCmd as CommandModule)
220
+ .command(deleteCmd as CommandModule)
221
+ .command(compactCmd as CommandModule)
222
+ .demandCommand(1, "Specify a session subcommand"),
223
+ handler: () => {},
224
+ };