@agentprojectcontext/apx 1.15.5 → 1.16.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 +40 -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.js +102 -19
  28. package/src/daemon/transcription.js +262 -59
  29. package/src/daemon/wakeup.js +14 -19
  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.5",
3
+ "version": "1.16.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,6 +26,9 @@
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"
@@ -31,21 +36,50 @@
31
36
  "packageManager": "pnpm@10.25.0",
32
37
  "dependencies": {
33
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
+ "@opentui/core": "^0.2.8",
40
+ "@opentui/keymap": "^0.2.8",
41
+ "@opentui/solid": "^0.2.8",
42
+ "@solid-primitives/event-bus": "^1.1.3",
43
+ "@solid-primitives/keyboard": "^1.3.5",
44
+ "@solid-primitives/scheduled": "^1.5.3",
45
+ "ansi-regex": "^6.2.2",
34
46
  "chalk": "^5.6.2",
35
47
  "cli-cursor": "^5.0.0",
48
+ "cli-sound": "^1.1.3",
49
+ "clipboardy": "^5.3.1",
36
50
  "cron-parser": "^5.5.0",
51
+ "effect": "^3.21.2",
37
52
  "express": "^4.21.0",
38
- "node-fetch": "^3.3.2"
53
+ "fuzzysort": "^3.1.0",
54
+ "jsonc-parser": "^3.3.1",
55
+ "node-fetch": "^3.3.2",
56
+ "open": "^11.0.0",
57
+ "opentui-spinner": "^0.0.6",
58
+ "react": "^19.2.6",
59
+ "remeda": "^2.34.1",
60
+ "semver": "^7.8.0",
61
+ "solid-js": "^1.9.12",
62
+ "strip-ansi": "^7.2.0",
63
+ "yargs": "^18.0.0"
39
64
  },
40
65
  "optionalDependencies": {
41
66
  "fast-glob": "^3.3.2",
42
- "puppeteer": "^22.0.0"
67
+ "puppeteer": "^22.0.0",
68
+ "ws": "^8.18.0"
43
69
  },
44
70
  "devDependencies": {
71
+ "@babel/core": "^7.29.0",
45
72
  "@semantic-release/changelog": "^6.0.3",
46
73
  "@semantic-release/git": "^10.0.1",
74
+ "@types/node": "^25.7.0",
75
+ "@types/yargs": "^17.0.35",
76
+ "babel-preset-solid": "^1.9.12",
47
77
  "better-sqlite3": "^11.3.0",
48
- "conventional-changelog-conventionalcommits": "^9.3.1"
78
+ "conventional-changelog-conventionalcommits": "^9.3.1",
79
+ "electron": "^33.4.11",
80
+ "esbuild": "^0.28.0",
81
+ "esbuild-plugin-solid": "^0.6.0",
82
+ "typescript": "^6.0.3"
49
83
  },
50
84
  "keywords": [
51
85
  "apc",
@@ -59,6 +93,7 @@
59
93
  "pnpm": {
60
94
  "onlyBuiltDependencies": [
61
95
  "better-sqlite3",
96
+ "electron",
62
97
  "puppeteer"
63
98
  ]
64
99
  },
@@ -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;
package/src/cli/index.js CHANGED
@@ -54,6 +54,7 @@ import {
54
54
  cmdTelegramSetup,
55
55
  } from "./commands/telegram.js";
56
56
  import { cmdMessagesTail, cmdMessagesSearch, cmdMessagesChat } from "./commands/messages.js";
57
+ import { cmdLog } from "./commands/log.js";
57
58
  import { cmdSearch } from "./commands/search.js";
58
59
  import { cmdExec } from "./commands/exec.js";
59
60
  import {
@@ -71,6 +72,7 @@ import {
71
72
  cmdPermission,
72
73
  } from "./commands/config.js";
73
74
  import { cmdPluginsList, cmdPluginStatus } from "./commands/plugins.js";
75
+ import { cmdOverlayStart, cmdOverlayStop, cmdOverlayStatus } from "./commands/overlay.js";
74
76
  import { cmdSkillsAdd, cmdSkillsList, cmdSkillsStatus } from "./commands/skills.js";
75
77
  import { cmdIdentity } from "./commands/identity.js";
76
78
  import { cmdCommandList, cmdCommandShow } from "./commands/command.js";
@@ -1136,7 +1138,8 @@ function buildHelp(version) {
1136
1138
  hCmd("apx daemon start", 36, ""),
1137
1139
  hCmd("apx daemon stop", 36, ""),
1138
1140
  hCmd("apx daemon status", 36, ""),
1139
- hCmd("apx daemon logs", 36, "--tail N"),
1141
+ hCmd("apx daemon logs", 36, "--tail N legacy daemon stdout log"),
1142
+ hCmd("apx log", 36, "unified log (all modules) -f follow --tail N --errors only"),
1140
1143
 
1141
1144
  hSec("Telegram"),
1142
1145
  hCmd("apx telegram send \"text\"", 36, "--chat <id>"),
@@ -1453,6 +1456,15 @@ async function dispatch(cmd, rest) {
1453
1456
  break;
1454
1457
  }
1455
1458
 
1459
+ case "log":
1460
+ case "logs": {
1461
+ // `apx log` is the unified daemon log (everything: telegram, whisper,
1462
+ // super-agent, tools, overlay). For just the legacy stdout sink,
1463
+ // use `apx daemon logs`. `apx log -f` follows; `--errors` filters.
1464
+ await cmdLog(parseArgs(rest));
1465
+ break;
1466
+ }
1467
+
1456
1468
  case "exec":
1457
1469
  await cmdExec(parseArgs(rest));
1458
1470
  break;
@@ -1590,6 +1602,16 @@ async function dispatch(cmd, rest) {
1590
1602
  await cmdUpdate(parseArgs(rest), VERSION);
1591
1603
  return; // skip checkForUpdate after an update
1592
1604
 
1605
+ case "overlay": {
1606
+ const [sub, ...oRest] = rest;
1607
+ const oArgs = parseArgs(oRest);
1608
+ if (!sub || sub === "start") { await cmdOverlayStart(oArgs); return; }
1609
+ if (sub === "stop") { await cmdOverlayStop(oArgs); return; }
1610
+ if (sub === "status") { await cmdOverlayStatus(oArgs);return; }
1611
+ die(`unknown overlay sub-command: ${sub}\nUsage: apx overlay <start|stop|status>`);
1612
+ return;
1613
+ }
1614
+
1593
1615
  default:
1594
1616
  die(`unknown command: ${cmd}\nRun \`apx --help\` for usage.`);
1595
1617
  }