@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.15.6",
3
+ "version": "1.17.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -10,11 +10,13 @@
10
10
  "bin": {
11
11
  "apx": "./src/cli/index.js",
12
12
  "apx-daemon": "./src/daemon/index.js",
13
- "apx-mcp": "./src/mcp/index.js"
13
+ "apx-mcp": "./src/mcp/index.js",
14
+ "apx-ng": "./dist/cli/index.js"
14
15
  },
15
16
  "files": [
16
17
  "src/",
17
18
  "skills/",
19
+ "dist/",
18
20
  "README.md"
19
21
  ],
20
22
  "engines": {
@@ -24,28 +26,66 @@
24
26
  "start": "node src/daemon/index.js",
25
27
  "smoke": "node src/daemon/smoke.js",
26
28
  "test": "node --test --test-reporter=spec tests/*.test.js",
29
+ "build": "node scripts/build-cli.js",
30
+ "build:watch": "node scripts/build-cli.js --watch",
31
+ "typecheck": "tsc --noEmit -p tsconfig.cli.json",
27
32
  "upgrade": "pnpm install && pnpm add -g .",
28
33
  "prepack": "node scripts/sync-apc-skill.js",
29
34
  "postinstall": "node src/cli/postinstall.js"
30
35
  },
31
36
  "packageManager": "pnpm@10.25.0",
32
37
  "dependencies": {
38
+ "@langchain/anthropic": "^0.3.34",
39
+ "@langchain/community": "^0.3.59",
40
+ "@langchain/core": "^0.3.80",
41
+ "@langchain/ollama": "^0.2.4",
33
42
  "@modelcontextprotocol/sdk": "^1.29.0",
43
+ "@opentui/core": "^0.2.8",
44
+ "@opentui/keymap": "^0.2.8",
45
+ "@opentui/solid": "^0.2.8",
46
+ "@solid-primitives/event-bus": "^1.1.3",
47
+ "@solid-primitives/keyboard": "^1.3.5",
48
+ "@solid-primitives/scheduled": "^1.5.3",
49
+ "ansi-regex": "^6.2.2",
34
50
  "chalk": "^5.6.2",
35
51
  "cli-cursor": "^5.0.0",
52
+ "cli-sound": "^1.1.3",
53
+ "clipboardy": "^5.3.1",
36
54
  "cron-parser": "^5.5.0",
55
+ "effect": "^3.21.2",
37
56
  "express": "^4.21.0",
38
- "node-fetch": "^3.3.2"
57
+ "fuzzysort": "^3.1.0",
58
+ "jsonc-parser": "^3.3.1",
59
+ "langchain": "^0.3.37",
60
+ "node-fetch": "^3.3.2",
61
+ "open": "^11.0.0",
62
+ "opentui-spinner": "^0.0.6",
63
+ "react": "^19.2.6",
64
+ "remeda": "^2.34.1",
65
+ "semver": "^7.8.0",
66
+ "solid-js": "^1.9.12",
67
+ "strip-ansi": "^7.2.0",
68
+ "yargs": "^18.0.0",
69
+ "zod": "^3.25.76"
39
70
  },
40
71
  "optionalDependencies": {
41
72
  "fast-glob": "^3.3.2",
42
- "puppeteer": "^22.0.0"
73
+ "puppeteer": "^22.0.0",
74
+ "ws": "^8.18.0"
43
75
  },
44
76
  "devDependencies": {
77
+ "@babel/core": "^7.29.0",
45
78
  "@semantic-release/changelog": "^6.0.3",
46
79
  "@semantic-release/git": "^10.0.1",
80
+ "@types/node": "^25.7.0",
81
+ "@types/yargs": "^17.0.35",
82
+ "babel-preset-solid": "^1.9.12",
47
83
  "better-sqlite3": "^11.3.0",
48
- "conventional-changelog-conventionalcommits": "^9.3.1"
84
+ "conventional-changelog-conventionalcommits": "^9.3.1",
85
+ "electron": "^33.4.11",
86
+ "esbuild": "^0.28.0",
87
+ "esbuild-plugin-solid": "^0.6.0",
88
+ "typescript": "^6.0.3"
49
89
  },
50
90
  "keywords": [
51
91
  "apc",
@@ -59,6 +99,7 @@
59
99
  "pnpm": {
60
100
  "onlyBuiltDependencies": [
61
101
  "better-sqlite3",
102
+ "electron",
62
103
  "puppeteer"
63
104
  ]
64
105
  },
@@ -0,0 +1,113 @@
1
+ // apx log — read the unified daemon log (~/.apx/logs/apx.log)
2
+ //
3
+ // apx log tail last 100 lines
4
+ // apx log --tail N tail last N lines
5
+ // apx log -f follow (tail -f)
6
+ // apx log --follow same as -f
7
+ // apx log --errors only show [ERROR] lines (works with --tail / -f too)
8
+ //
9
+ // Every line written through core/logging.js or the daemon's log() lands
10
+ // here, regardless of which module produced it (telegram, whisper, super-agent…).
11
+
12
+ import fs from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+
16
+ const APX_LOG_PATH = path.join(os.homedir(), ".apx", "logs", "apx.log");
17
+ const ERROR_TRACE_PATH = path.join(os.homedir(), ".apx", "logs", "errors.jsonl");
18
+
19
+ const c = {
20
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
21
+ green: "\x1b[32m", red: "\x1b[31m", yellow: "\x1b[33m",
22
+ cyan: "\x1b[36m", gray: "\x1b[90m", white: "\x1b[97m",
23
+ };
24
+ const colorize = (line) =>
25
+ line
26
+ .replace(/^\[([\d-]+\s[\d:.]+)\]/, (_m, ts) => `[${c.gray}${ts}${c.reset}]`)
27
+ .replace(/\[ERROR\s*\]/, `[${c.red}ERROR${c.reset}]`)
28
+ .replace(/\[WARN\s*\]/, `[${c.yellow}WARN ${c.reset}]`)
29
+ .replace(/\[INFO\s*\]/, `[${c.cyan}INFO ${c.reset}]`)
30
+ .replace(/\[([a-z_-]+)\]/i, (_m, mod) => `[${c.bold}${mod}${c.reset}]`);
31
+
32
+ function readLastLines(file, n) {
33
+ if (!fs.existsSync(file)) return [];
34
+ const content = fs.readFileSync(file, "utf8");
35
+ return content.split("\n").filter(Boolean).slice(-n);
36
+ }
37
+
38
+ function shouldShow(line, opts) {
39
+ if (opts.errors) return /\[ERROR\s*\]/.test(line);
40
+ return true;
41
+ }
42
+
43
+ export async function cmdLog(args = {}) {
44
+ const flags = args.flags || {};
45
+ const follow = !!(flags.follow || flags.f);
46
+ const errorsOnly = !!flags.errors;
47
+ const tail = flags.tail ? parseInt(flags.tail, 10) : 100;
48
+
49
+ // --errors without an explicit log file inspects both the unified log AND
50
+ // the structured errors.jsonl, since some surfaces (api routes) only emit
51
+ // there. We surface a small banner when we hit the structured file.
52
+ if (!fs.existsSync(APX_LOG_PATH)) {
53
+ console.log(`${c.gray} (no log yet at ${APX_LOG_PATH})${c.reset}`);
54
+ if (errorsOnly && fs.existsSync(ERROR_TRACE_PATH)) {
55
+ console.log(`${c.gray} showing structured errors from ${ERROR_TRACE_PATH}:${c.reset}\n`);
56
+ const traces = readLastLines(ERROR_TRACE_PATH, tail);
57
+ for (const t of traces) {
58
+ try {
59
+ const j = JSON.parse(t);
60
+ console.log(`${c.gray}[${j.ts}]${c.reset} ${c.red}ERROR${c.reset} ${c.bold}${j.surface || "api"}${c.reset} ${j.route || ""} ${j.error?.message || ""}`);
61
+ } catch { console.log(t); }
62
+ }
63
+ }
64
+ return;
65
+ }
66
+
67
+ const lines = readLastLines(APX_LOG_PATH, tail);
68
+ const filtered = lines.filter((l) => shouldShow(l, { errors: errorsOnly }));
69
+ for (const line of filtered) {
70
+ console.log(colorize(line));
71
+ }
72
+
73
+ if (errorsOnly && fs.existsSync(ERROR_TRACE_PATH)) {
74
+ const traces = readLastLines(ERROR_TRACE_PATH, tail);
75
+ if (traces.length > 0) {
76
+ console.log(`\n${c.gray}── structured errors (${ERROR_TRACE_PATH}) ──${c.reset}`);
77
+ for (const t of traces) {
78
+ try {
79
+ const j = JSON.parse(t);
80
+ console.log(`${c.gray}[${j.ts}]${c.reset} ${c.red}ERROR${c.reset} ${c.bold}${j.surface || "api"}${c.reset} ${j.route || ""} ${j.error?.message || ""}`);
81
+ } catch { console.log(t); }
82
+ }
83
+ }
84
+ }
85
+
86
+ if (!follow) return;
87
+
88
+ // tail -f mode. fs.watch on macOS sometimes loses events on heavy writes,
89
+ // so we also poll size every 500ms as a safety net.
90
+ let currentSize = fs.statSync(APX_LOG_PATH).size;
91
+
92
+ const drain = () => {
93
+ let newSize;
94
+ try { newSize = fs.statSync(APX_LOG_PATH).size; }
95
+ catch { return; }
96
+ if (newSize === currentSize) return;
97
+ if (newSize < currentSize) { currentSize = newSize; return; } // truncated/rotated
98
+ const fd = fs.openSync(APX_LOG_PATH, "r");
99
+ const buf = Buffer.alloc(newSize - currentSize);
100
+ fs.readSync(fd, buf, 0, buf.length, currentSize);
101
+ fs.closeSync(fd);
102
+ currentSize = newSize;
103
+ const chunkLines = buf.toString("utf8").split("\n").filter(Boolean);
104
+ for (const l of chunkLines) {
105
+ if (shouldShow(l, { errors: errorsOnly })) console.log(colorize(l));
106
+ }
107
+ };
108
+
109
+ try { fs.watch(APX_LOG_PATH, () => drain()); } catch {}
110
+ const poll = setInterval(drain, 500);
111
+ // Keep process alive
112
+ return new Promise(() => { void poll; });
113
+ }
@@ -0,0 +1,253 @@
1
+ // apx overlay — launch/manage the floating voice overlay window.
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawn, execFileSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { http } from "../http.js";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const OVERLAY_MAIN = path.resolve(__dirname, "../../overlay/main.js");
13
+ const OVERLAY_PID = path.join(os.homedir(), ".apx", "overlay.pid");
14
+
15
+ // ── ANSI ─────────────────────────────────────────────────────────────────────
16
+ const c = { reset:"\x1b[0m", bold:"\x1b[1m", dim:"\x1b[2m", green:"\x1b[32m",
17
+ red:"\x1b[31m", yellow:"\x1b[33m", cyan:"\x1b[36m", gray:"\x1b[90m" };
18
+ const fmt = {
19
+ bold:(s)=>`${c.bold}${s}${c.reset}`, dim:(s)=>`${c.dim}${s}${c.reset}`,
20
+ green:(s)=>`${c.green}${s}${c.reset}`, red:(s)=>`${c.red}${s}${c.reset}`,
21
+ cyan:(s)=>`${c.cyan}${s}${c.reset}`, gray:(s)=>`${c.gray}${s}${c.reset}`,
22
+ };
23
+
24
+ // ── Helpers ───────────────────────────────────────────────────────────────────
25
+
26
+ function readPid() {
27
+ try { return parseInt(fs.readFileSync(OVERLAY_PID, "utf8").trim(), 10); } catch { return null; }
28
+ }
29
+ function writePid(pid) {
30
+ fs.mkdirSync(path.dirname(OVERLAY_PID), { recursive: true });
31
+ fs.writeFileSync(OVERLAY_PID, String(pid));
32
+ }
33
+ function clearPid() { try { fs.unlinkSync(OVERLAY_PID); } catch {} }
34
+ function pidAlive(pid) {
35
+ if (!pid) return false;
36
+ try { process.kill(pid, 0); return true; } catch { return false; }
37
+ }
38
+
39
+ function findElectron() {
40
+ // 1. Local node_modules (pnpm/npm install electron)
41
+ const candidates = [
42
+ path.resolve(__dirname, "../../../node_modules/.bin/electron"),
43
+ path.resolve(__dirname, "../../../node_modules/electron/cli.js"),
44
+ ];
45
+ for (const c of candidates) {
46
+ if (fs.existsSync(c)) return c;
47
+ }
48
+ // 2. Global electron
49
+ try {
50
+ const which = execFileSync("which", ["electron"], { stdio: ["ignore","pipe","ignore"] }).toString().trim();
51
+ if (which) return which;
52
+ } catch {}
53
+ // 3. npx electron
54
+ return null;
55
+ }
56
+
57
+ // ── Commands ──────────────────────────────────────────────────────────────────
58
+
59
+ export async function cmdOverlayStart(args = {}) {
60
+ const debug = !!(args.debug || args.d);
61
+
62
+ const pid = readPid();
63
+ if (pidAlive(pid)) {
64
+ if (debug) {
65
+ console.log(`\n ${fmt.cyan("●")} Overlay already running ${fmt.dim("pid " + pid)} — stop it first with: apx overlay stop\n`);
66
+ return;
67
+ }
68
+ console.log(`\n ${fmt.cyan("●")} Overlay already running ${fmt.dim("pid " + pid)}\n`);
69
+ return;
70
+ }
71
+ clearPid();
72
+
73
+ if (!fs.existsSync(OVERLAY_MAIN)) {
74
+ console.error(`\n ${fmt.red("✗")} Overlay app not found at ${fmt.dim(OVERLAY_MAIN)}\n`);
75
+ process.exit(1);
76
+ }
77
+
78
+ const electronBin = findElectron();
79
+ if (!electronBin) {
80
+ console.error(
81
+ `\n ${fmt.red("✗")} Electron not found.\n` +
82
+ ` Install it with: ${fmt.cyan("pnpm add -D electron")}\n` +
83
+ ` or globally: ${fmt.cyan("npm install -g electron")}\n`
84
+ );
85
+ process.exit(1);
86
+ }
87
+
88
+ // Get daemon port from running daemon or env
89
+ let daemonPort = process.env.APX_PORT || "7430";
90
+ try {
91
+ const health = await http.get("/health").catch(() => null);
92
+ if (health?.port) daemonPort = String(health.port);
93
+ } catch {}
94
+
95
+ const isScript = electronBin.endsWith(".js");
96
+ const cmd = isScript ? process.execPath : electronBin;
97
+ const argv = isScript
98
+ ? [electronBin, OVERLAY_MAIN, "--port", daemonPort]
99
+ : [OVERLAY_MAIN, "--port", daemonPort];
100
+
101
+ const logFile = path.join(os.homedir(), ".apx", "overlay.log");
102
+
103
+ if (debug) {
104
+ // ── Debug mode: start overlay normally, then tail -f the log ─────────
105
+ // Truncate log so we only see fresh output
106
+ try { fs.writeFileSync(logFile, `--- APX Overlay debug started ${new Date().toISOString()} ---\n`); } catch {}
107
+
108
+ const logFd = fs.openSync(logFile, "a");
109
+ const child = spawn(cmd, argv, {
110
+ detached: false,
111
+ stdio: ["ignore", logFd, logFd],
112
+ env: { ...process.env, ELECTRON_ENABLE_LOGGING: "1" },
113
+ });
114
+ fs.closeSync(logFd);
115
+
116
+ if (child.pid) writePid(child.pid);
117
+
118
+ // Small pause so Electron writes its first lines before we tail
119
+ await new Promise(r => setTimeout(r, 600));
120
+
121
+ console.log(
122
+ `\n ${fmt.cyan("◉")} ${fmt.bold("APX Overlay")} ${fmt.yellow("[DEBUG]")}` +
123
+ ` pid ${child.pid} port ${daemonPort}` +
124
+ `\n ${fmt.dim("Tailing:")} ${logFile}` +
125
+ `\n ${fmt.dim("Press Ctrl+C to stop tailing (overlay keeps running).")}\n`
126
+ );
127
+
128
+ // Tail the log file live — read new bytes as they arrive
129
+ const logStream = fs.createReadStream(logFile, { encoding: "utf8", start: 0 });
130
+ logStream.pipe(process.stdout);
131
+
132
+ // After initial content, watch for new data
133
+ const watcher = fs.watch(logFile, () => {});
134
+ let pos = fs.statSync(logFile).size;
135
+ const interval = setInterval(() => {
136
+ const stat = fs.statSync(logFile);
137
+ if (stat.size > pos) {
138
+ const stream = fs.createReadStream(logFile, { start: pos, end: stat.size });
139
+ stream.on("data", (chunk) => {
140
+ const lines = chunk.toString();
141
+ // Filter Chromium noise
142
+ lines.split("\n").forEach(line => {
143
+ if (!/^\[[\d:]+\]/.test(line.trim())) process.stdout.write(line + (line ? "\n" : ""));
144
+ });
145
+ });
146
+ pos = stat.size;
147
+ }
148
+ }, 300);
149
+
150
+ child.on("exit", (code) => {
151
+ clearInterval(interval);
152
+ watcher.close();
153
+ console.log(`\n ${code === 0 ? fmt.green("✓") : fmt.red("✗")} Overlay exited (code ${code})\n`);
154
+ clearPid();
155
+ process.exit(code || 0);
156
+ });
157
+
158
+ await new Promise((resolve) => {
159
+ process.on("SIGINT", () => {
160
+ clearInterval(interval);
161
+ watcher.close();
162
+ console.log(`\n ${fmt.dim("Stopped tailing. Overlay is still running.")}\n`);
163
+ resolve();
164
+ });
165
+ process.on("SIGTERM", resolve);
166
+ });
167
+ return;
168
+ }
169
+
170
+ // ── Normal (detached) mode ────────────────────────────────────────────
171
+ const logFd = fs.openSync(logFile, "a");
172
+ const child = spawn(cmd, argv, {
173
+ detached: false,
174
+ stdio: ["ignore", logFd, logFd],
175
+ env: { ...process.env, ELECTRON_ENABLE_LOGGING: "1" },
176
+ });
177
+
178
+ // Give the process 1.5s to fail fast, then detach and let it run
179
+ await new Promise((res) => {
180
+ let exited = false;
181
+ child.on("exit", (code) => {
182
+ exited = true;
183
+ if (code !== 0) {
184
+ console.error(
185
+ `\n ${fmt.red("✗")} Overlay exited with code ${code}\n` +
186
+ ` ${fmt.dim("Check log:")} ${logFile}\n` +
187
+ ` ${fmt.dim("Or run with:")} ${fmt.cyan("apx overlay start --debug")}\n`
188
+ );
189
+ }
190
+ res();
191
+ });
192
+ setTimeout(() => {
193
+ if (!exited) {
194
+ child.unref();
195
+ res();
196
+ }
197
+ }, 1500);
198
+ });
199
+
200
+ if (!child.exitCode && child.pid) writePid(child.pid);
201
+ else return;
202
+
203
+ // Read configured shortcut (if any) for display
204
+ let shortcutHint = "⌘G (macOS) / Ctrl+G (Win/Linux)";
205
+ try {
206
+ const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".apx", "config.json"), "utf8"));
207
+ if (cfg?.overlay?.shortcut) shortcutHint = cfg.overlay.shortcut;
208
+ } catch {}
209
+
210
+ console.log(
211
+ `\n ${fmt.green("●")} ${fmt.bold("APX Overlay")} started` +
212
+ ` ${fmt.dim("pid " + child.pid)}` +
213
+ ` ${fmt.dim("port " + daemonPort)}` +
214
+ `\n ${fmt.dim("Shortcut:")} ${fmt.cyan(shortcutHint)}` +
215
+ `\n ${fmt.dim("Debug:")} ${fmt.gray("apx overlay start --debug")}` +
216
+ `\n ${fmt.dim("Log:")} ${fmt.gray(logFile)}\n`
217
+ );
218
+ }
219
+
220
+ export async function cmdOverlayStop(_args = {}) {
221
+ const pid = readPid();
222
+ if (!pidAlive(pid)) {
223
+ console.log(`\n ${fmt.dim("Overlay is not running.")}\n`);
224
+ clearPid();
225
+ return;
226
+ }
227
+ try {
228
+ process.kill(pid, "SIGTERM");
229
+ clearPid();
230
+ console.log(`\n ${fmt.green("✓")} Overlay stopped ${fmt.dim("(pid " + pid + ")")}\n`);
231
+ } catch (e) {
232
+ console.error(`\n ${fmt.red("✗")} Could not stop overlay: ${e.message}\n`);
233
+ }
234
+ }
235
+
236
+ export async function cmdOverlayStatus(_args = {}) {
237
+ const pid = readPid();
238
+ const alive = pidAlive(pid);
239
+
240
+ let daemonClients = 0;
241
+ try {
242
+ const s = await http.get("/overlay/status").catch(() => null);
243
+ daemonClients = s?.connected_clients ?? 0;
244
+ } catch {}
245
+
246
+ const icon = alive ? fmt.green("●") : fmt.dim("○");
247
+ const state = alive ? fmt.green("running") : fmt.dim("stopped");
248
+ console.log(
249
+ `\n ${icon} ${fmt.bold("APX Overlay")} ${state}` +
250
+ (pid ? ` ${fmt.dim("pid " + pid)}` : "") +
251
+ `\n ${fmt.dim("daemon connections:")} ${daemonClients}\n`
252
+ );
253
+ }
@@ -10,8 +10,15 @@ import {
10
10
  renderTerminalChat,
11
11
  titlecase,
12
12
  } from "../terminal-chat/renderer.js";
13
+ import { existsSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { dirname, resolve } from "node:path";
16
+ import { spawnSync } from "node:child_process";
13
17
 
14
- const MAIN_PALETTE_OPTIONS = ["Switch model", "Connect provider", "Open editor", "Exit"];
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const TUI_SRC = resolve(__dirname, "../../tui/run.ts");
20
+
21
+ const MAIN_PALETTE_OPTIONS = ["Switch model", "Switch agent", "Connect provider", "Open editor", "Exit"];
15
22
 
16
23
  // Message Actions overlay options for a queued message
17
24
  const MSG_ACTION_SEND = "Send now (interrupt current)";
@@ -24,6 +31,20 @@ export async function cmdSys(args) {
24
31
  const cfg = readConfig();
25
32
  const id = readIdentity();
26
33
 
34
+ // Launch new Solid.js TUI via bun (runs TS source directly — no esbuild bundle needed)
35
+ if (existsSync(TUI_SRC)) {
36
+ const bunBin = process.env.BUN_PATH || "bun";
37
+ spawnSync(bunBin, [
38
+ "--preload", "@opentui/solid/preload",
39
+ TUI_SRC,
40
+ "--pid", pid,
41
+ "--agent", id?.agent_name || cfg.super_agent?.name || "super-agent",
42
+ "--model", cfg.super_agent?.model || "claude-3-5-sonnet",
43
+ ], { stdio: "inherit", cwd: resolve(__dirname, "../../..") });
44
+ return;
45
+ }
46
+
47
+
27
48
  const state = {
28
49
  currentModeIdx: 0,
29
50
  inputText: "",
@@ -162,7 +183,7 @@ export async function cmdSys(args) {
162
183
  }
163
184
 
164
185
  if (state.inCommandPalette) {
165
- await handlePaletteKey(key, cfg, state, renderScreen, close);
186
+ await handlePaletteKey(key, pid, cfg, state, renderScreen, close);
166
187
  return;
167
188
  }
168
189
 
@@ -341,7 +362,7 @@ export function handleScrollKey(key, state, renderScreen) {
341
362
  return false;
342
363
  }
343
364
 
344
- async function handlePaletteKey(key, cfg, state, renderScreen, close) {
365
+ async function handlePaletteKey(key, pid, cfg, state, renderScreen, close) {
345
366
  if (key.name === "up") {
346
367
  state.paletteSelection = Math.max(0, state.paletteSelection - 1);
347
368
  renderScreen();
@@ -362,19 +383,28 @@ async function handlePaletteKey(key, cfg, state, renderScreen, close) {
362
383
  const selected = state.paletteOptions[state.paletteSelection];
363
384
 
364
385
  if (state.paletteState === "main") {
365
- if (selected === "Exit") close();
386
+ if (selected === "Exit") { close(); return; }
366
387
 
367
388
  if (selected === "Switch model") {
368
389
  state.paletteState = "switch_model";
369
390
  state.paletteOptions = ["Loading models..."];
370
391
  state.paletteSelection = 0;
371
392
  renderScreen();
372
- loadModelOptions(cfg, state, renderScreen);
393
+ loadModelOptions(pid, cfg, state, renderScreen);
394
+ return;
395
+ }
396
+
397
+ if (selected === "Switch agent") {
398
+ state.paletteState = "switch_agent";
399
+ state.paletteOptions = ["Loading agents..."];
400
+ state.paletteSelection = 0;
401
+ renderScreen();
402
+ loadAgentOptions(pid, state, renderScreen);
373
403
  return;
374
404
  }
375
405
 
376
406
  state.inCommandPalette = false;
377
- state.transcript.push({ type: "status", text: `Executing command: ${selected} (not implemented yet)` });
407
+ state.transcript.push({ type: "status", text: `Command: ${selected} (not implemented yet)` });
378
408
  renderScreen();
379
409
  return;
380
410
  }
@@ -393,7 +423,20 @@ async function handlePaletteKey(key, cfg, state, renderScreen, close) {
393
423
  configModule.writeConfig(currentCfg);
394
424
 
395
425
  state.inCommandPalette = false;
396
- state.transcript.push({ type: "status", text: `Model updated globally to ${selected}` });
426
+ state.transcript.push({ type: "status", text: `Model ${selected}` });
427
+ renderScreen();
428
+ return;
429
+ }
430
+
431
+ if (
432
+ state.paletteState === "switch_agent" &&
433
+ !selected.startsWith("Loading") &&
434
+ !selected.startsWith("Failed") &&
435
+ !selected.startsWith("No ")
436
+ ) {
437
+ state.activeAgent = selected;
438
+ state.inCommandPalette = false;
439
+ state.transcript.push({ type: "status", text: `Agent → ${selected}` });
397
440
  renderScreen();
398
441
  return;
399
442
  }
@@ -401,23 +444,52 @@ async function handlePaletteKey(key, cfg, state, renderScreen, close) {
401
444
  renderScreen();
402
445
  }
403
446
 
404
- function loadModelOptions(cfg, state, renderScreen) {
405
- const baseUrl = cfg.engines?.ollama?.base_url || "http://127.0.0.1:11434";
406
- fetch(`${baseUrl}/api/tags`)
447
+ function loadModelOptions(pid, cfg, state, renderScreen) {
448
+ // Load engines from APX daemon first, then fall back to Ollama tags
449
+ const apxEnginesPromise = pid
450
+ ? http.get("/engines").then((d) => d?.engines || []).catch(() => [])
451
+ : Promise.resolve([]);
452
+
453
+ const ollamaBaseUrl = cfg.engines?.ollama?.base_url || "http://127.0.0.1:11434";
454
+ const ollamaPromise = fetch(`${ollamaBaseUrl}/api/tags`)
407
455
  .then((r) => r.json())
408
- .then((data) => {
409
- state.paletteOptions = data.models?.length
410
- ? data.models.map((m) => "ollama:" + m.name)
411
- : ["No Ollama models found"];
412
- state.paletteOptions.push("openai:gpt-4o", "anthropic:claude-3-5-sonnet-20240620");
456
+ .then((d) => (d.models || []).map((m) => "ollama:" + m.name))
457
+ .catch(() => []);
458
+
459
+ Promise.all([apxEnginesPromise, ollamaPromise])
460
+ .then(([apxEngines, ollamaModels]) => {
461
+ const all = [
462
+ ...apxEngines.filter((e) => typeof e === "string"),
463
+ ...ollamaModels,
464
+ ];
465
+ state.paletteOptions = all.length ? all : ["No models found"];
413
466
  if (state.paletteState === "switch_model") renderScreen();
414
467
  })
415
468
  .catch(() => {
416
- state.paletteOptions = ["Failed to load from Ollama", "openai:gpt-4o", "anthropic:claude-3-5-sonnet-20240620"];
469
+ state.paletteOptions = ["Failed to load models"];
417
470
  if (state.paletteState === "switch_model") renderScreen();
418
471
  });
419
472
  }
420
473
 
474
+ function loadAgentOptions(pid, state, renderScreen) {
475
+ if (!pid) {
476
+ state.paletteOptions = ["No project selected"];
477
+ renderScreen();
478
+ return;
479
+ }
480
+ http.get(`/projects/${pid}/agents`)
481
+ .then((agents) => {
482
+ state.paletteOptions = Array.isArray(agents) && agents.length
483
+ ? agents.map((a) => a.slug || a.name || String(a))
484
+ : ["No agents found"];
485
+ if (state.paletteState === "switch_agent") renderScreen();
486
+ })
487
+ .catch(() => {
488
+ state.paletteOptions = ["Failed to load agents"];
489
+ if (state.paletteState === "switch_agent") renderScreen();
490
+ });
491
+ }
492
+
421
493
  export function handleEditingKey(str, key, state, renderScreen) {
422
494
  if (key.name === "tab") {
423
495
  state.currentModeIdx = (state.currentModeIdx + 1) % MODES.length;