@agentprojectcontext/apx 1.15.6 → 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 (221) 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/whisper-server.py +57 -6
  30. package/src/overlay/index.html +44 -0
  31. package/src/overlay/main.js +480 -0
  32. package/src/overlay/package.json +3 -0
  33. package/src/overlay/preload.js +34 -0
  34. package/src/overlay/renderer.js +371 -0
  35. package/src/overlay/style.css +250 -0
  36. package/src/tui/_shims/cli-error.ts +6 -0
  37. package/src/tui/_shims/cli-logo.ts +18 -0
  38. package/src/tui/_shims/cli-ui.ts +1 -0
  39. package/src/tui/_shims/config-console-state.ts +7 -0
  40. package/src/tui/_shims/core-any.ts +30 -0
  41. package/src/tui/_shims/core-binary.ts +13 -0
  42. package/src/tui/_shims/core-flag.ts +3 -0
  43. package/src/tui/_shims/core-log.ts +14 -0
  44. package/src/tui/_shims/lsp-language.ts +1 -0
  45. package/src/tui/_shims/opencode-any.ts +135 -0
  46. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  47. package/src/tui/_shims/plugin-tui.ts +13 -0
  48. package/src/tui/_shims/provider-provider.ts +10 -0
  49. package/src/tui/_shims/session-retry.ts +1 -0
  50. package/src/tui/_shims/session-schema.ts +15 -0
  51. package/src/tui/_shims/session-session.ts +3 -0
  52. package/src/tui/_shims/snapshot.ts +4 -0
  53. package/src/tui/_shims/tool-any.ts +18 -0
  54. package/src/tui/_shims/util-error.ts +7 -0
  55. package/src/tui/_shims/util-filesystem.ts +79 -0
  56. package/src/tui/_shims/util-format.ts +7 -0
  57. package/src/tui/_shims/util-iife.ts +3 -0
  58. package/src/tui/_shims/util-locale.ts +10 -0
  59. package/src/tui/_shims/util-process.ts +38 -0
  60. package/src/tui/app.tsx +783 -0
  61. package/src/tui/asset/charge.wav +0 -0
  62. package/src/tui/asset/pulse-a.wav +0 -0
  63. package/src/tui/asset/pulse-b.wav +0 -0
  64. package/src/tui/asset/pulse-c.wav +0 -0
  65. package/src/tui/attach.ts +100 -0
  66. package/src/tui/component/bg-pulse-render.ts +436 -0
  67. package/src/tui/component/bg-pulse.tsx +99 -0
  68. package/src/tui/component/border.tsx +21 -0
  69. package/src/tui/component/dialog-agent.tsx +31 -0
  70. package/src/tui/component/dialog-console-org.tsx +103 -0
  71. package/src/tui/component/dialog-mcp.tsx +85 -0
  72. package/src/tui/component/dialog-model.tsx +175 -0
  73. package/src/tui/component/dialog-provider.tsx +456 -0
  74. package/src/tui/component/dialog-retry-action.tsx +160 -0
  75. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  76. package/src/tui/component/dialog-session-list.tsx +323 -0
  77. package/src/tui/component/dialog-session-rename.tsx +31 -0
  78. package/src/tui/component/dialog-skill.tsx +36 -0
  79. package/src/tui/component/dialog-stash.tsx +87 -0
  80. package/src/tui/component/dialog-status.tsx +168 -0
  81. package/src/tui/component/dialog-tag.tsx +44 -0
  82. package/src/tui/component/dialog-theme-list.tsx +50 -0
  83. package/src/tui/component/dialog-variant.tsx +39 -0
  84. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  85. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  86. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  87. package/src/tui/component/error-component.tsx +92 -0
  88. package/src/tui/component/logo.tsx +896 -0
  89. package/src/tui/component/plugin-route-missing.tsx +14 -0
  90. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  91. package/src/tui/component/prompt/cwd.ts +0 -0
  92. package/src/tui/component/prompt/frecency.tsx +90 -0
  93. package/src/tui/component/prompt/history.tsx +108 -0
  94. package/src/tui/component/prompt/index.tsx +1809 -0
  95. package/src/tui/component/prompt/part.ts +16 -0
  96. package/src/tui/component/prompt/stash.tsx +101 -0
  97. package/src/tui/component/prompt/traits.ts +35 -0
  98. package/src/tui/component/spinner.tsx +24 -0
  99. package/src/tui/component/startup-loading.tsx +63 -0
  100. package/src/tui/component/todo-item.tsx +32 -0
  101. package/src/tui/component/use-connected.tsx +9 -0
  102. package/src/tui/component/workspace-label.tsx +19 -0
  103. package/src/tui/config/cwd.ts +5 -0
  104. package/src/tui/config/keybind.ts +432 -0
  105. package/src/tui/config/tui-migrate.ts +154 -0
  106. package/src/tui/config/tui-schema.ts +34 -0
  107. package/src/tui/config/tui.ts +46 -0
  108. package/src/tui/context/aggregate-failures.ts +34 -0
  109. package/src/tui/context/args.tsx +15 -0
  110. package/src/tui/context/command-palette.tsx +163 -0
  111. package/src/tui/context/directory.ts +15 -0
  112. package/src/tui/context/editor-zed.ts +283 -0
  113. package/src/tui/context/editor.ts +468 -0
  114. package/src/tui/context/event-apx.ts +22 -0
  115. package/src/tui/context/event.ts +6 -0
  116. package/src/tui/context/exit.tsx +60 -0
  117. package/src/tui/context/helper.tsx +25 -0
  118. package/src/tui/context/kv.tsx +81 -0
  119. package/src/tui/context/local.tsx +608 -0
  120. package/src/tui/context/path-format.tsx +39 -0
  121. package/src/tui/context/project-apx.tsx +48 -0
  122. package/src/tui/context/project.tsx +7 -0
  123. package/src/tui/context/prompt.tsx +18 -0
  124. package/src/tui/context/route.tsx +52 -0
  125. package/src/tui/context/sdk-apx.tsx +185 -0
  126. package/src/tui/context/sdk.tsx +6 -0
  127. package/src/tui/context/sync-apx.tsx +178 -0
  128. package/src/tui/context/sync-v2.tsx +16 -0
  129. package/src/tui/context/sync.tsx +118 -0
  130. package/src/tui/context/theme/aura.json +69 -0
  131. package/src/tui/context/theme/ayu.json +80 -0
  132. package/src/tui/context/theme/carbonfox.json +248 -0
  133. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  134. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  135. package/src/tui/context/theme/catppuccin.json +112 -0
  136. package/src/tui/context/theme/cobalt2.json +225 -0
  137. package/src/tui/context/theme/cursor.json +249 -0
  138. package/src/tui/context/theme/dracula.json +219 -0
  139. package/src/tui/context/theme/everforest.json +241 -0
  140. package/src/tui/context/theme/flexoki.json +237 -0
  141. package/src/tui/context/theme/github.json +233 -0
  142. package/src/tui/context/theme/gruvbox.json +242 -0
  143. package/src/tui/context/theme/kanagawa.json +77 -0
  144. package/src/tui/context/theme/lucent-orng.json +234 -0
  145. package/src/tui/context/theme/material.json +235 -0
  146. package/src/tui/context/theme/matrix.json +77 -0
  147. package/src/tui/context/theme/mercury.json +252 -0
  148. package/src/tui/context/theme/monokai.json +221 -0
  149. package/src/tui/context/theme/nightowl.json +221 -0
  150. package/src/tui/context/theme/nord.json +223 -0
  151. package/src/tui/context/theme/one-dark.json +84 -0
  152. package/src/tui/context/theme/opencode.json +245 -0
  153. package/src/tui/context/theme/orng.json +249 -0
  154. package/src/tui/context/theme/osaka-jade.json +93 -0
  155. package/src/tui/context/theme/palenight.json +222 -0
  156. package/src/tui/context/theme/rosepine.json +234 -0
  157. package/src/tui/context/theme/solarized.json +223 -0
  158. package/src/tui/context/theme/synthwave84.json +226 -0
  159. package/src/tui/context/theme/tokyonight.json +243 -0
  160. package/src/tui/context/theme/vercel.json +245 -0
  161. package/src/tui/context/theme/vesper.json +218 -0
  162. package/src/tui/context/theme/zenburn.json +223 -0
  163. package/src/tui/context/theme.tsx +1247 -0
  164. package/src/tui/context/tui-config.tsx +9 -0
  165. package/src/tui/event.ts +16 -0
  166. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  167. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  168. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  169. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  170. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  171. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  172. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  173. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  174. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  175. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  176. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  177. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  178. package/src/tui/keymap.tsx +166 -0
  179. package/src/tui/layer.ts +6 -0
  180. package/src/tui/plugin/api.tsx +381 -0
  181. package/src/tui/plugin/command-shim.ts +109 -0
  182. package/src/tui/plugin/internal.ts +33 -0
  183. package/src/tui/plugin/runtime.ts +1069 -0
  184. package/src/tui/plugin/slots.tsx +60 -0
  185. package/src/tui/routes/home.tsx +96 -0
  186. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  187. package/src/tui/routes/session/dialog-message.tsx +108 -0
  188. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  189. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  190. package/src/tui/routes/session/footer.tsx +91 -0
  191. package/src/tui/routes/session/index.tsx +188 -0
  192. package/src/tui/routes/session/permission.tsx +722 -0
  193. package/src/tui/routes/session/question.tsx +490 -0
  194. package/src/tui/routes/session/sidebar.tsx +102 -0
  195. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  196. package/src/tui/run.ts +84 -0
  197. package/src/tui/thread.ts +261 -0
  198. package/src/tui/tsconfig.json +40 -0
  199. package/src/tui/ui/dialog-alert.tsx +66 -0
  200. package/src/tui/ui/dialog-confirm.tsx +108 -0
  201. package/src/tui/ui/dialog-export-options.tsx +217 -0
  202. package/src/tui/ui/dialog-help.tsx +40 -0
  203. package/src/tui/ui/dialog-prompt.tsx +101 -0
  204. package/src/tui/ui/dialog-select.tsx +553 -0
  205. package/src/tui/ui/dialog.tsx +211 -0
  206. package/src/tui/ui/link.tsx +34 -0
  207. package/src/tui/ui/spinner.ts +368 -0
  208. package/src/tui/ui/toast.tsx +111 -0
  209. package/src/tui/util/clipboard.ts +217 -0
  210. package/src/tui/util/editor.ts +37 -0
  211. package/src/tui/util/model.ts +23 -0
  212. package/src/tui/util/provider-origin.ts +7 -0
  213. package/src/tui/util/revert-diff.ts +18 -0
  214. package/src/tui/util/scroll.ts +25 -0
  215. package/src/tui/util/selection.ts +65 -0
  216. package/src/tui/util/signal.ts +41 -0
  217. package/src/tui/util/sound.ts +156 -0
  218. package/src/tui/util/transcript.ts +112 -0
  219. package/src/tui/validate-session.ts +29 -0
  220. package/src/tui/win32.ts +130 -0
  221. package/src/tui/worker.ts +104 -0
@@ -9,6 +9,7 @@ export const C = {
9
9
  altOn: "\x1b[?1049h",
10
10
  altOff: "\x1b[?1049l",
11
11
  showCursor: "\x1b[?25h",
12
+ hideCursor: "\x1b[?25l",
12
13
  setBgBlack: "\x1b]11;#000000\x07",
13
14
  resetBg: "\x1b]111\x07",
14
15
  bg: "\x1b[48;2;0;0;0m",
@@ -27,6 +28,27 @@ export const C = {
27
28
  noItalic: "\x1b[23m",
28
29
  };
29
30
 
31
+ // ---------------------------------------------------------------------------
32
+ // Output buffer — all rendering writes here; flushed once per frame.
33
+ // This eliminates flash: the terminal sees clear+draw as a single atomic op.
34
+ // ---------------------------------------------------------------------------
35
+ let _buf = "";
36
+
37
+ function _w(s) {
38
+ _buf += s;
39
+ }
40
+
41
+ function _moveTo(row, col) {
42
+ _buf += `\x1b[${Math.max(1, row + 1)};${Math.max(1, col + 1)}H`;
43
+ }
44
+
45
+ function _writeAt(row, col, text, width, bg = C.bg) {
46
+ _moveTo(row, col);
47
+ _buf += bg + padAnsi(text, width) + C.bg;
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+
30
52
  export function titlecase(value) {
31
53
  const clean = String(value || "").trim();
32
54
  if (!clean) return "";
@@ -66,25 +88,11 @@ function terminalSize() {
66
88
  };
67
89
  }
68
90
 
91
+ // Legacy direct-write moveTo used for final cursor positioning after flush
69
92
  function moveTo(row, col) {
70
93
  readline.cursorTo(process.stdout, Math.max(0, col), Math.max(0, row));
71
94
  }
72
95
 
73
- function writeAt(row, col, text, width, bg = C.bg) {
74
- moveTo(row, col);
75
- process.stdout.write(bg + padAnsi(text, width) + C.bg);
76
- }
77
-
78
- function clearFull() {
79
- const { width, height } = terminalSize();
80
- process.stdout.write(C.bg + "\x1b[2J\x1b[3J\x1b[H");
81
- for (let row = 0; row < height; row++) {
82
- process.stdout.write(C.bg + " ".repeat(width));
83
- if (row < height - 1) process.stdout.write("\n");
84
- }
85
- process.stdout.write("\x1b[H" + C.bg);
86
- }
87
-
88
96
  function centerLeft(width, contentWidth) {
89
97
  return Math.max(0, Math.floor((width - contentWidth) / 2));
90
98
  }
@@ -122,7 +130,7 @@ function renderLogo(termWidth, top) {
122
130
  const line = lines[i];
123
131
  const left = centerLeft(termWidth, visible(line));
124
132
  const color = i < 2 ? C.dim : i < 4 ? C.muted : C.text;
125
- writeAt(top + i, left, C.bold + color + line + C.normal, visible(line));
133
+ _writeAt(top + i, left, C.bold + color + line + C.normal, visible(line));
126
134
  }
127
135
  }
128
136
 
@@ -164,11 +172,11 @@ function renderPromptBlock(state, chatWidth) {
164
172
  : C.muted + C.italic + fit(placeholder, contentWidth) + C.noItalic;
165
173
  const metaLine = renderModeMeta(currentModeIdx, activeAgent, activeModel, contentWidth);
166
174
 
167
- writeAt(top, left, C.primary + "┃" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
168
- writeAt(top + 1, left, C.primary + "┃" + C.panel + " " + padAnsi(promptLine, contentWidth), boxWidth, C.bg);
169
- writeAt(top + 2, left, C.primary + "┃" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
170
- writeAt(top + 3, left, C.primary + "┃" + C.panel + " " + padAnsi(metaLine, contentWidth), boxWidth, C.bg);
171
- writeAt(top + 4, left, C.primary + "╹" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
175
+ _writeAt(top, left, C.primary + "┃" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
176
+ _writeAt(top + 1, left, C.primary + "┃" + C.panel + " " + padAnsi(promptLine, contentWidth), boxWidth, C.bg);
177
+ _writeAt(top + 2, left, C.primary + "┃" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
178
+ _writeAt(top + 3, left, C.primary + "┃" + C.panel + " " + padAnsi(metaLine, contentWidth), boxWidth, C.bg);
179
+ _writeAt(top + 4, left, C.primary + "╹" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
172
180
 
173
181
  const hotkeys =
174
182
  C.bold + C.text + "tab" + C.normal + C.muted + " agents " +
@@ -181,7 +189,7 @@ function renderPromptBlock(state, chatWidth) {
181
189
  : "") +
182
190
  C.bold + C.text + "enter" + C.normal + C.muted + " send";
183
191
  const hotkeyLeft = Math.max(left, left + boxWidth - visible(hotkeys));
184
- writeAt(top + 5, hotkeyLeft, hotkeys, visible(hotkeys), C.bg);
192
+ _writeAt(top + 5, hotkeyLeft, hotkeys, visible(hotkeys), C.bg);
185
193
 
186
194
  return { row: top + 1, col: left + 2 + visible(beforeCursor) };
187
195
  }
@@ -282,7 +290,6 @@ function transcriptLines(transcript, width) {
282
290
  addLine(lines, margin + C.primary + "┃" + C.panel + " " + C.text + padAnsi(chunk, inner), C.bg);
283
291
  }
284
292
  if (item.meta) {
285
- // Show QUEUED badge with distinct highlight color
286
293
  const badgeText = item.meta === "queued"
287
294
  ? C.warning + C.bold + "QUEUED" + C.normal + C.muted + " click to send/remove"
288
295
  : C.muted + item.meta;
@@ -315,7 +322,7 @@ function transcriptLines(transcript, width) {
315
322
  const isQuestion = trace.tool === "ask_questions";
316
323
  const label = isQuestion ? C.warning + C.bold + "QUESTION" : C.muted + "TOOL";
317
324
  const name = isQuestion ? "" : C.text + trace.tool;
318
-
325
+
319
326
  if (isQuestion && trace.args?.questions) {
320
327
  addLine(lines, "", C.bg);
321
328
  addLine(lines, margin + label + C.muted + " (…)" + C.bg, C.bg);
@@ -354,11 +361,11 @@ function renderChat(state, chatWidth, height, promptTop) {
354
361
  const slice = lines.slice(start, start + maxRows);
355
362
 
356
363
  for (let i = 0; i < slice.length && i < maxRows; i++) {
357
- writeAt(i, 0, slice[i].text, chatWidth - 1, slice[i].bg);
364
+ _writeAt(i, 0, slice[i].text, chatWidth - 1, slice[i].bg);
358
365
  }
359
366
 
360
367
  if (offset > 0 && maxRows > 1) {
361
- writeAt(0, 0, C.muted + `↑ ${offset} lines above bottom`, chatWidth - 1, C.bg);
368
+ _writeAt(0, 0, C.muted + `↑ ${offset} lines above bottom`, chatWidth - 1, C.bg);
362
369
  }
363
370
  }
364
371
 
@@ -369,37 +376,37 @@ function renderSidebar(state) {
369
376
  const sideWidth = Math.min(34, Math.max(28, Math.floor(width * 0.3)));
370
377
  const left = width - sideWidth;
371
378
  for (let row = 0; row < height; row++) {
372
- writeAt(row, left, "", sideWidth, C.panel);
379
+ _writeAt(row, left, "", sideWidth, C.panel);
373
380
  }
374
381
 
375
382
  const contentWidth = sideWidth - 4;
376
383
  const totalTokens = state.usage.input + state.usage.output;
377
384
 
378
- writeAt(1, left + 2, C.text + C.bold + "Sesión" + C.normal, contentWidth, C.panel);
379
- writeAt(2, left + 2, C.muted + fit(state.sessionTitle || "chat local", contentWidth), contentWidth, C.panel);
380
- writeAt(3, left + 2, C.muted + "agent " + C.text + state.activeAgent, contentWidth, C.panel);
381
- writeAt(4, left + 2, C.muted + "app " + C.text + `APX ${state.version}`, contentWidth, C.panel);
385
+ _writeAt(1, left + 2, C.text + C.bold + "Sesión" + C.normal, contentWidth, C.panel);
386
+ _writeAt(2, left + 2, C.muted + fit(state.sessionTitle || "chat local", contentWidth), contentWidth, C.panel);
387
+ _writeAt(3, left + 2, C.muted + "agent " + C.text + state.activeAgent, contentWidth, C.panel);
388
+ _writeAt(4, left + 2, C.muted + "app " + C.text + `APX ${state.version}`, contentWidth, C.panel);
382
389
 
383
- writeAt(6, left + 2, C.text + C.bold + "Modelo" + C.normal, contentWidth, C.panel);
390
+ _writeAt(6, left + 2, C.text + C.bold + "Modelo" + C.normal, contentWidth, C.panel);
384
391
  const modelLines = wrapText(state.activeModel || "(none)", contentWidth).slice(0, 2);
385
392
  modelLines.forEach((line, index) => {
386
- writeAt(7 + index, left + 2, C.muted + line, contentWidth, C.panel);
393
+ _writeAt(7 + index, left + 2, C.muted + line, contentWidth, C.panel);
387
394
  });
388
395
 
389
- writeAt(10, left + 2, C.text + C.bold + "Contexto" + C.normal, contentWidth, C.panel);
390
- writeAt(11, left + 2, C.muted + `${totalTokens.toLocaleString()} tokens total`, contentWidth, C.panel);
391
- writeAt(12, left + 2, C.muted + `${state.usage.input.toLocaleString()} in · ${state.usage.output.toLocaleString()} out`, contentWidth, C.panel);
392
- writeAt(13, left + 2, C.muted + `${state.usage.percent}% usado`, contentWidth, C.panel);
393
- writeAt(14, left + 2, C.muted + "$0.00 spent", contentWidth, C.panel);
396
+ _writeAt(10, left + 2, C.text + C.bold + "Contexto" + C.normal, contentWidth, C.panel);
397
+ _writeAt(11, left + 2, C.muted + `${totalTokens.toLocaleString()} tokens total`, contentWidth, C.panel);
398
+ _writeAt(12, left + 2, C.muted + `${state.usage.input.toLocaleString()} in · ${state.usage.output.toLocaleString()} out`, contentWidth, C.panel);
399
+ _writeAt(13, left + 2, C.muted + `${state.usage.percent}% usado`, contentWidth, C.panel);
400
+ _writeAt(14, left + 2, C.muted + "$0.00 spent", contentWidth, C.panel);
394
401
 
395
- writeAt(16, left + 2, C.text + C.bold + "LSP" + C.normal, contentWidth, C.panel);
396
- writeAt(17, left + 2, C.muted + "LSPs are disabled", contentWidth, C.panel);
402
+ _writeAt(16, left + 2, C.text + C.bold + "LSP" + C.normal, contentWidth, C.panel);
403
+ _writeAt(17, left + 2, C.muted + "LSPs are disabled", contentWidth, C.panel);
397
404
 
398
405
  const cwdLines = wrapText(process.cwd(), contentWidth).slice(-4);
399
406
  let row = Math.max(19, height - cwdLines.length - 4);
400
- writeAt(row++, left + 2, C.text + C.bold + "Directorio" + C.normal, contentWidth, C.panel);
401
- for (const line of cwdLines) writeAt(row++, left + 2, C.muted + line, contentWidth, C.panel);
402
- writeAt(height - 1, left + 2, C.success + "• " + C.text + "APX" + C.muted + ` ${state.version}`, contentWidth, C.panel);
407
+ _writeAt(row++, left + 2, C.text + C.bold + "Directorio" + C.normal, contentWidth, C.panel);
408
+ for (const line of cwdLines) _writeAt(row++, left + 2, C.muted + line, contentWidth, C.panel);
409
+ _writeAt(height - 1, left + 2, C.success + "• " + C.text + "APX" + C.muted + ` ${state.version}`, contentWidth, C.panel);
403
410
 
404
411
  return { left, width: sideWidth };
405
412
  }
@@ -415,17 +422,17 @@ function renderPaletteOverlay(state) {
415
422
  const left = centerLeft(width, boxWidth);
416
423
  const top = Math.max(1, Math.floor((height - boxHeight) / 2));
417
424
 
418
- writeAt(top, left, C.text + C.bold + " " + title + C.normal, boxWidth, C.panel);
419
- writeAt(top + 1, left, C.dim + "▀".repeat(boxWidth), boxWidth, C.panel);
425
+ _writeAt(top, left, C.text + C.bold + " " + title + C.normal, boxWidth, C.panel);
426
+ _writeAt(top + 1, left, C.dim + "▀".repeat(boxWidth), boxWidth, C.panel);
420
427
  for (let i = 0; i < state.paletteOptions.length; i++) {
421
428
  const active = i === state.paletteSelection;
422
429
  const marker = active ? "›" : " ";
423
430
  const bg = active ? C.panel2 : C.panel;
424
431
  const fg = active ? C.primary + C.bold : C.text;
425
- writeAt(top + 2 + i, left, fg + ` ${marker} ${state.paletteOptions[i]}` + C.normal, boxWidth, bg);
432
+ _writeAt(top + 2 + i, left, fg + ` ${marker} ${state.paletteOptions[i]}` + C.normal, boxWidth, bg);
426
433
  }
427
- writeAt(top + 2 + state.paletteOptions.length, left, C.dim + "▄".repeat(boxWidth), boxWidth, C.panel);
428
- writeAt(
434
+ _writeAt(top + 2 + state.paletteOptions.length, left, C.dim + "▄".repeat(boxWidth), boxWidth, C.panel);
435
+ _writeAt(
429
436
  top + 3 + state.paletteOptions.length,
430
437
  left,
431
438
  C.muted + "↑↓ select " + C.text + C.bold + "enter" + C.normal + C.muted + " choose " + C.text + C.bold + "esc" + C.normal + C.muted + " close",
@@ -444,18 +451,18 @@ function renderMsgActionsOverlay(state) {
444
451
  const left = centerLeft(width, boxWidth);
445
452
  const top = Math.max(1, Math.floor((height - boxHeight) / 2));
446
453
 
447
- writeAt(top, left, C.text + C.bold + " " + title + C.normal, boxWidth, C.panel);
448
- writeAt(top + 1, left, C.muted + " " + C.italic + preview + C.noItalic, boxWidth, C.panel);
449
- writeAt(top + 2, left, C.dim + "▀".repeat(boxWidth), boxWidth, C.panel);
454
+ _writeAt(top, left, C.text + C.bold + " " + title + C.normal, boxWidth, C.panel);
455
+ _writeAt(top + 1, left, C.muted + " " + C.italic + preview + C.noItalic, boxWidth, C.panel);
456
+ _writeAt(top + 2, left, C.dim + "▀".repeat(boxWidth), boxWidth, C.panel);
450
457
  for (let i = 0; i < opts.length; i++) {
451
458
  const active = i === state.msgActionsSelection;
452
459
  const marker = active ? "›" : " ";
453
460
  const bg = active ? C.panel2 : C.panel;
454
461
  const fg = active ? C.primary + C.bold : C.text;
455
- writeAt(top + 3 + i, left, fg + ` ${marker} ${opts[i]}` + C.normal, boxWidth, bg);
462
+ _writeAt(top + 3 + i, left, fg + ` ${marker} ${opts[i]}` + C.normal, boxWidth, bg);
456
463
  }
457
- writeAt(top + 3 + opts.length, left, C.dim + "▄".repeat(boxWidth), boxWidth, C.panel);
458
- writeAt(
464
+ _writeAt(top + 3 + opts.length, left, C.dim + "▄".repeat(boxWidth), boxWidth, C.panel);
465
+ _writeAt(
459
466
  top + 4 + opts.length,
460
467
  left,
461
468
  C.muted + "↑↓ select " + C.text + C.bold + "enter" + C.normal + C.muted + " choose " + C.text + C.bold + "esc" + C.normal + C.muted + " close",
@@ -465,7 +472,9 @@ function renderMsgActionsOverlay(state) {
465
472
  }
466
473
 
467
474
  export function renderTerminalChat(state) {
468
- clearFull();
475
+ // Reset buffer — clear screen and home cursor in ONE atomic write (no flash)
476
+ _buf = C.bg + "\x1b[2J\x1b[H";
477
+
469
478
  const { width, height } = terminalSize();
470
479
  const sidebar = state.hasStarted ? renderSidebar(state) : null;
471
480
  const chatWidth = sidebar ? sidebar.left : width;
@@ -481,5 +490,11 @@ export function renderTerminalChat(state) {
481
490
 
482
491
  if (state.inCommandPalette) renderPaletteOverlay(state);
483
492
  if (state.inMsgActions) renderMsgActionsOverlay(state);
484
- if (!state.inCommandPalette && !state.inMsgActions) moveTo(cursor.row, cursor.col);
493
+
494
+ // Position cursor in buffer before final flush
495
+ _buf += `\x1b[${Math.max(1, cursor.row + 1)};${Math.max(1, cursor.col + 1)}H`;
496
+
497
+ // Single atomic write — terminal renders entire frame at once, no flash
498
+ process.stdout.write(_buf);
499
+ _buf = "";
485
500
  }
@@ -0,0 +1,173 @@
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 agents in the current project",
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 agents = (await http.get(`/projects/${pid}/agents`)) as Array<{
32
+ slug: string; role?: string; model?: string; description?: string;
33
+ }>;
34
+ if (!agents?.length) { println(dim("No agents found.")); return; }
35
+ table(
36
+ agents.map((a) => ({
37
+ Slug: a.slug,
38
+ Role: a.role || "-",
39
+ Model: a.model || "-",
40
+ Description: a.description ? a.description.slice(0, 50) : "-",
41
+ })),
42
+ ["Slug", "Role", "Model", "Description"],
43
+ );
44
+ } catch (err: unknown) {
45
+ error(err instanceof Error ? err.message : String(err));
46
+ process.exit(1);
47
+ }
48
+ },
49
+ };
50
+
51
+ // ---------- get / show ----------
52
+
53
+ const getCmd: CommandModule = {
54
+ command: "get <slug>",
55
+ aliases: ["show"],
56
+ describe: "Show agent details and memory",
57
+ builder: (yargs: Argv) =>
58
+ yargs.positional("slug", { type: "string", demandOption: true, describe: "Agent slug" }),
59
+ handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
60
+ try {
61
+ const project = args.project as string | undefined;
62
+ const slug = args.slug as string;
63
+ const http = await getHttp();
64
+ const pid = await resolveProjectId(project);
65
+ const agent = (await http.get(`/projects/${pid}/agents/${slug}`)) as Record<string, unknown>;
66
+ process.stdout.write(JSON.stringify(agent, null, 2) + "\n");
67
+ } catch (err: unknown) {
68
+ error(err instanceof Error ? err.message : String(err));
69
+ process.exit(1);
70
+ }
71
+ },
72
+ };
73
+
74
+ // ---------- add / create ----------
75
+
76
+ const addCmd: CommandModule = {
77
+ command: "add <slug>",
78
+ aliases: ["create"],
79
+ describe: "Create a new agent",
80
+ builder: (yargs: Argv) =>
81
+ yargs
82
+ .positional("slug", { type: "string", demandOption: true, describe: "Agent slug (identifier)" })
83
+ .option("role", { type: "string", describe: "Agent role (system prompt)" })
84
+ .option("model", { type: "string", describe: "LLM model (e.g. claude-sonnet-4-6)" })
85
+ .option("description", { alias: "d", type: "string", describe: "Short description" })
86
+ .option("skills", { type: "string", describe: "Comma-separated skill list" })
87
+ .option("language", { type: "string", describe: "Language code (e.g. en, es)" })
88
+ .option("tools", { type: "string", describe: "Comma-separated allowed tools" }),
89
+ handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
90
+ try {
91
+ const project = args.project as string | undefined;
92
+ const slug = args.slug as string;
93
+ const role = args.role as string | undefined;
94
+ const model = args.model as string | undefined;
95
+ const description = args.description as string | undefined;
96
+ const skills = args.skills as string | undefined;
97
+ const language = args.language as string | undefined;
98
+ const tools = args.tools as string | undefined;
99
+ const http = await getHttp();
100
+ const pid = await resolveProjectId(project);
101
+ const agent = await http.post(`/projects/${pid}/agents`, {
102
+ slug,
103
+ role,
104
+ model,
105
+ description,
106
+ skills: skills?.split(",").map((s) => s.trim()).filter(Boolean),
107
+ language,
108
+ tools: tools?.split(",").map((t) => t.trim()).filter(Boolean),
109
+ });
110
+ success(`Agent created: ${highlight(slug)}`);
111
+ process.stdout.write(JSON.stringify(agent, null, 2) + "\n");
112
+ } catch (err: unknown) {
113
+ error(err instanceof Error ? err.message : String(err));
114
+ process.exit(1);
115
+ }
116
+ },
117
+ };
118
+
119
+ // ---------- memory ----------
120
+
121
+ const memoryCmd: CommandModule = {
122
+ command: "memory <slug>",
123
+ describe: "Read or write agent memory",
124
+ builder: (yargs: Argv) =>
125
+ yargs
126
+ .positional("slug", { type: "string", demandOption: true, describe: "Agent slug" })
127
+ .option("append", { type: "boolean", default: false, describe: "Append stdin to memory" })
128
+ .option("replace", { type: "boolean", default: false, describe: "Replace memory with stdin" }),
129
+ handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
130
+ try {
131
+ const project = args.project as string | undefined;
132
+ const slug = args.slug as string;
133
+ const append = args.append as boolean;
134
+ const replace = args.replace as boolean;
135
+ const http = await getHttp();
136
+ const pid = await resolveProjectId(project);
137
+ if (append || replace) {
138
+ const chunks: Buffer[] = [];
139
+ for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
140
+ const body = Buffer.concat(chunks).toString();
141
+ if (append) {
142
+ const existing = (await http.get(`/projects/${pid}/agents/${slug}/memory`)) as { body: string };
143
+ await http.put(`/projects/${pid}/agents/${slug}/memory`, { body: (existing.body || "") + "\n" + body });
144
+ } else {
145
+ await http.put(`/projects/${pid}/agents/${slug}/memory`, { body });
146
+ }
147
+ success("Memory updated.");
148
+ } else {
149
+ const mem = (await http.get(`/projects/${pid}/agents/${slug}/memory`)) as { body: string };
150
+ process.stdout.write(mem.body || "");
151
+ }
152
+ } catch (err: unknown) {
153
+ error(err instanceof Error ? err.message : String(err));
154
+ process.exit(1);
155
+ }
156
+ },
157
+ };
158
+
159
+ // ---------- parent ----------
160
+
161
+ export const agentCmd: CommandModule = {
162
+ command: "agent",
163
+ aliases: ["agents"],
164
+ describe: "Manage APC agents",
165
+ builder: (yargs: Argv) =>
166
+ yargs
167
+ .command(listCmd)
168
+ .command(getCmd)
169
+ .command(addCmd)
170
+ .command(memoryCmd)
171
+ .demandCommand(1, "Specify an agent subcommand"),
172
+ handler: () => {},
173
+ };
@@ -0,0 +1,119 @@
1
+ import type { CommandModule, Argv, ArgumentsCamelCase } from "yargs";
2
+ import { getHttp, type StreamEvent } from "../http.js";
3
+ import { error, println, dim } from "../ui.js";
4
+ import { createInterface } from "node:readline";
5
+
6
+ interface GlobalArgs { project?: string }
7
+
8
+ async function resolveProjectId(project?: string): Promise<string> {
9
+ const http = await getHttp();
10
+ const projects = (await http.get("/projects")) as Array<{ id: string; name: string; path: string }>;
11
+ if (!projects?.length) throw new Error("No projects registered. Run: apx init");
12
+ if (project) {
13
+ const match = projects.find(
14
+ (p) => p.id === project || p.name === project || p.path === project || p.path?.endsWith("/" + project),
15
+ );
16
+ if (!match) throw new Error(`Project not found: ${project}`);
17
+ return match.id;
18
+ }
19
+ return projects[0]!.id;
20
+ }
21
+
22
+ export const chatCmd: CommandModule = {
23
+ command: "chat [agent]",
24
+ describe: "Start an interactive multi-turn chat with an agent",
25
+ builder: (yargs: Argv) =>
26
+ yargs
27
+ .positional("agent", {
28
+ type: "string",
29
+ default: "default",
30
+ describe: "Agent slug (defaults to super-agent)",
31
+ })
32
+ .option("model", { type: "string", describe: "Override model" })
33
+ .option("conversation", {
34
+ alias: "c",
35
+ type: "string",
36
+ describe: "Continue an existing conversation ID",
37
+ }),
38
+ handler: async (
39
+ args: ArgumentsCamelCase<Record<string, unknown>>,
40
+ ) => {
41
+ try {
42
+ const project = args.project as string | undefined;
43
+ const agent = args.agent as string;
44
+ const model = args.model as string | undefined;
45
+ const http = await getHttp();
46
+ const pid = await resolveProjectId(project);
47
+ let conversationId = args.conversation as string | undefined;
48
+
49
+ if (!process.stdin.isTTY) {
50
+ // Non-interactive: read one prompt from stdin
51
+ const chunks: Buffer[] = [];
52
+ for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
53
+ const prompt = Buffer.concat(chunks).toString().trim();
54
+ if (!prompt) throw new Error("No prompt provided via stdin.");
55
+ const result = (await http.post(`/projects/${pid}/agents/${agent}/chat`, {
56
+ prompt,
57
+ model,
58
+ conversation_id: conversationId,
59
+ })) as { conversation_id: string; text: string };
60
+ process.stdout.write(result.text + "\n");
61
+ return;
62
+ }
63
+
64
+ // Interactive REPL
65
+ println(dim(`APX Chat — agent: ${agent} (type /exit or ctrl+c to quit)`));
66
+
67
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
68
+ rl.setPrompt("\x1b[96m> \x1b[0m");
69
+ rl.prompt();
70
+
71
+ rl.on("line", async (line) => {
72
+ const text = line.trim();
73
+ if (!text) { rl.prompt(); return; }
74
+ if (text === "/exit" || text === "/quit") { rl.close(); return; }
75
+
76
+ rl.pause();
77
+ try {
78
+ // Try streaming first
79
+ let responded = false;
80
+ try {
81
+ const result = await http.streamPost(
82
+ `/projects/${pid}/super-agent/chat/stream`,
83
+ { prompt: text, model, previousMessages: [] },
84
+ (ev: StreamEvent) => {
85
+ if (ev.type === "chunk" && typeof ev.chunk === "string") {
86
+ process.stdout.write(ev.chunk);
87
+ responded = true;
88
+ }
89
+ },
90
+ ) as { text?: string };
91
+ if (!responded && result?.text) process.stdout.write(result.text);
92
+ process.stdout.write("\n");
93
+ } catch {
94
+ // Fall back to non-streaming agent chat
95
+ const result = (await http.post(`/projects/${pid}/agents/${agent}/chat`, {
96
+ prompt: text,
97
+ model,
98
+ conversation_id: conversationId,
99
+ })) as { conversation_id: string; text: string };
100
+ conversationId = result.conversation_id;
101
+ process.stdout.write(result.text + "\n");
102
+ }
103
+ } catch (err: unknown) {
104
+ error(err instanceof Error ? err.message : String(err));
105
+ }
106
+ rl.resume();
107
+ rl.prompt();
108
+ });
109
+
110
+ rl.on("close", () => {
111
+ println(dim("\nGoodbye."));
112
+ process.exit(0);
113
+ });
114
+ } catch (err: unknown) {
115
+ error(err instanceof Error ? err.message : String(err));
116
+ process.exit(1);
117
+ }
118
+ },
119
+ };
@@ -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
+ };