opencode-agora 0.3.0 → 0.4.1

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 (280) hide show
  1. package/README.md +86 -255
  2. package/dist/atomic-write.d.ts +10 -0
  3. package/dist/atomic-write.d.ts.map +1 -0
  4. package/dist/atomic-write.js +23 -0
  5. package/dist/atomic-write.js.map +1 -0
  6. package/dist/auth/refresh.d.ts +17 -0
  7. package/dist/auth/refresh.d.ts.map +1 -0
  8. package/dist/auth/refresh.js +50 -0
  9. package/dist/auth/refresh.js.map +1 -0
  10. package/dist/cli/app.d.ts +13 -18
  11. package/dist/cli/app.d.ts.map +1 -1
  12. package/dist/cli/app.js +184 -1187
  13. package/dist/cli/app.js.map +1 -1
  14. package/dist/cli/chat-renderer.d.ts +31 -0
  15. package/dist/cli/chat-renderer.d.ts.map +1 -0
  16. package/dist/cli/chat-renderer.js +275 -0
  17. package/dist/cli/chat-renderer.js.map +1 -0
  18. package/dist/cli/commands/browse.d.ts +4 -0
  19. package/dist/cli/commands/browse.d.ts.map +1 -0
  20. package/dist/cli/commands/browse.js +80 -0
  21. package/dist/cli/commands/browse.js.map +1 -0
  22. package/dist/cli/commands/chat.d.ts +4 -0
  23. package/dist/cli/commands/chat.d.ts.map +1 -0
  24. package/dist/cli/commands/chat.js +125 -0
  25. package/dist/cli/commands/chat.js.map +1 -0
  26. package/dist/cli/commands/community.d.ts +12 -0
  27. package/dist/cli/commands/community.d.ts.map +1 -0
  28. package/dist/cli/commands/community.js +453 -0
  29. package/dist/cli/commands/community.js.map +1 -0
  30. package/dist/cli/commands/export.d.ts +3 -0
  31. package/dist/cli/commands/export.d.ts.map +1 -0
  32. package/dist/cli/commands/export.js +108 -0
  33. package/dist/cli/commands/export.js.map +1 -0
  34. package/dist/cli/commands/init.d.ts +4 -0
  35. package/dist/cli/commands/init.d.ts.map +1 -0
  36. package/dist/cli/commands/init.js +299 -0
  37. package/dist/cli/commands/init.js.map +1 -0
  38. package/dist/cli/commands/learn.d.ts +4 -0
  39. package/dist/cli/commands/learn.d.ts.map +1 -0
  40. package/dist/cli/commands/learn.js +62 -0
  41. package/dist/cli/commands/learn.js.map +1 -0
  42. package/dist/cli/commands/marketplace.d.ts +9 -0
  43. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  44. package/dist/cli/commands/marketplace.js +321 -0
  45. package/dist/cli/commands/marketplace.js.map +1 -0
  46. package/dist/cli/commands/notify.d.ts +3 -0
  47. package/dist/cli/commands/notify.d.ts.map +1 -0
  48. package/dist/cli/commands/notify.js +59 -0
  49. package/dist/cli/commands/notify.js.map +1 -0
  50. package/dist/cli/commands/operations.d.ts +16 -0
  51. package/dist/cli/commands/operations.d.ts.map +1 -0
  52. package/dist/cli/commands/operations.js +1006 -0
  53. package/dist/cli/commands/operations.js.map +1 -0
  54. package/dist/cli/commands/ping.d.ts +3 -0
  55. package/dist/cli/commands/ping.d.ts.map +1 -0
  56. package/dist/cli/commands/ping.js +56 -0
  57. package/dist/cli/commands/ping.js.map +1 -0
  58. package/dist/cli/commands/today.d.ts +3 -0
  59. package/dist/cli/commands/today.d.ts.map +1 -0
  60. package/dist/cli/commands/today.js +142 -0
  61. package/dist/cli/commands/today.js.map +1 -0
  62. package/dist/cli/commands/types.d.ts +5 -0
  63. package/dist/cli/commands/types.d.ts.map +1 -0
  64. package/dist/cli/commands/types.js +2 -0
  65. package/dist/cli/commands/types.js.map +1 -0
  66. package/dist/cli/commands/watch.d.ts +3 -0
  67. package/dist/cli/commands/watch.d.ts.map +1 -0
  68. package/dist/cli/commands/watch.js +41 -0
  69. package/dist/cli/commands/watch.js.map +1 -0
  70. package/dist/cli/commands/welcome.d.ts +3 -0
  71. package/dist/cli/commands/welcome.d.ts.map +1 -0
  72. package/dist/cli/commands/welcome.js +97 -0
  73. package/dist/cli/commands/welcome.js.map +1 -0
  74. package/dist/cli/commands-meta.d.ts +21 -0
  75. package/dist/cli/commands-meta.d.ts.map +1 -0
  76. package/dist/cli/commands-meta.js +828 -0
  77. package/dist/cli/commands-meta.js.map +1 -0
  78. package/dist/cli/completions-gen.d.ts +2 -0
  79. package/dist/cli/completions-gen.d.ts.map +1 -0
  80. package/dist/cli/completions-gen.js +195 -0
  81. package/dist/cli/completions-gen.js.map +1 -0
  82. package/dist/cli/completions.d.ts +18 -0
  83. package/dist/cli/completions.d.ts.map +1 -0
  84. package/dist/cli/completions.js +227 -0
  85. package/dist/cli/completions.js.map +1 -0
  86. package/dist/cli/flags.d.ts +19 -0
  87. package/dist/cli/flags.d.ts.map +1 -0
  88. package/dist/cli/flags.js +91 -0
  89. package/dist/cli/flags.js.map +1 -0
  90. package/dist/cli/format.d.ts +19 -0
  91. package/dist/cli/format.d.ts.map +1 -0
  92. package/dist/cli/format.js +249 -0
  93. package/dist/cli/format.js.map +1 -0
  94. package/dist/cli/helpers.d.ts +95 -0
  95. package/dist/cli/helpers.d.ts.map +1 -0
  96. package/dist/cli/helpers.js +301 -0
  97. package/dist/cli/helpers.js.map +1 -0
  98. package/dist/cli/mcp-server.d.ts +4 -0
  99. package/dist/cli/mcp-server.d.ts.map +1 -0
  100. package/dist/cli/mcp-server.js +277 -0
  101. package/dist/cli/mcp-server.js.map +1 -0
  102. package/dist/cli/menu.d.ts +7 -0
  103. package/dist/cli/menu.d.ts.map +1 -0
  104. package/dist/cli/menu.js +172 -0
  105. package/dist/cli/menu.js.map +1 -0
  106. package/dist/cli/pages/community.d.ts +9 -0
  107. package/dist/cli/pages/community.d.ts.map +1 -0
  108. package/dist/cli/pages/community.js +1094 -0
  109. package/dist/cli/pages/community.js.map +1 -0
  110. package/dist/cli/pages/helpers.d.ts +37 -0
  111. package/dist/cli/pages/helpers.d.ts.map +1 -0
  112. package/dist/cli/pages/helpers.js +98 -0
  113. package/dist/cli/pages/helpers.js.map +1 -0
  114. package/dist/cli/pages/home.d.ts +4 -0
  115. package/dist/cli/pages/home.d.ts.map +1 -0
  116. package/dist/cli/pages/home.js +231 -0
  117. package/dist/cli/pages/home.js.map +1 -0
  118. package/dist/cli/pages/marketplace.d.ts +5 -0
  119. package/dist/cli/pages/marketplace.d.ts.map +1 -0
  120. package/dist/cli/pages/marketplace.js +583 -0
  121. package/dist/cli/pages/marketplace.js.map +1 -0
  122. package/dist/cli/pages/news.d.ts +31 -0
  123. package/dist/cli/pages/news.d.ts.map +1 -0
  124. package/dist/cli/pages/news.js +688 -0
  125. package/dist/cli/pages/news.js.map +1 -0
  126. package/dist/cli/pages/settings.d.ts +3 -0
  127. package/dist/cli/pages/settings.d.ts.map +1 -0
  128. package/dist/cli/pages/settings.js +296 -0
  129. package/dist/cli/pages/settings.js.map +1 -0
  130. package/dist/cli/pages/types.d.ts +67 -0
  131. package/dist/cli/pages/types.d.ts.map +1 -0
  132. package/dist/cli/pages/types.js +2 -0
  133. package/dist/cli/pages/types.js.map +1 -0
  134. package/dist/cli/prompter.d.ts +135 -0
  135. package/dist/cli/prompter.d.ts.map +1 -0
  136. package/dist/cli/prompter.js +710 -0
  137. package/dist/cli/prompter.js.map +1 -0
  138. package/dist/cli/shell.d.ts +23 -0
  139. package/dist/cli/shell.d.ts.map +1 -0
  140. package/dist/cli/shell.js +1106 -0
  141. package/dist/cli/shell.js.map +1 -0
  142. package/dist/cli/tui.d.ts +7 -0
  143. package/dist/cli/tui.d.ts.map +1 -0
  144. package/dist/cli/tui.js +419 -0
  145. package/dist/cli/tui.js.map +1 -0
  146. package/dist/cli.js +1 -1
  147. package/dist/cli.js.map +1 -1
  148. package/dist/commands.d.ts +14 -0
  149. package/dist/commands.d.ts.map +1 -0
  150. package/dist/commands.js +28 -0
  151. package/dist/commands.js.map +1 -0
  152. package/dist/community/client.d.ts +84 -0
  153. package/dist/community/client.d.ts.map +1 -0
  154. package/dist/community/client.js +340 -0
  155. package/dist/community/client.js.map +1 -0
  156. package/dist/community/search.d.ts +25 -0
  157. package/dist/community/search.d.ts.map +1 -0
  158. package/dist/community/search.js +62 -0
  159. package/dist/community/search.js.map +1 -0
  160. package/dist/community/types.d.ts +71 -0
  161. package/dist/community/types.d.ts.map +1 -0
  162. package/dist/community/types.js +11 -0
  163. package/dist/community/types.js.map +1 -0
  164. package/dist/config.d.ts +1 -7
  165. package/dist/config.d.ts.map +1 -1
  166. package/dist/config.js +0 -32
  167. package/dist/config.js.map +1 -1
  168. package/dist/data.d.ts +1 -1
  169. package/dist/data.d.ts.map +1 -1
  170. package/dist/data.js +778 -40
  171. package/dist/data.js.map +1 -1
  172. package/dist/format.d.ts +5 -39
  173. package/dist/format.d.ts.map +1 -1
  174. package/dist/format.js +5 -120
  175. package/dist/format.js.map +1 -1
  176. package/dist/history.d.ts +13 -0
  177. package/dist/history.d.ts.map +1 -0
  178. package/dist/history.js +37 -0
  179. package/dist/history.js.map +1 -0
  180. package/dist/hubs/cache.d.ts +6 -0
  181. package/dist/hubs/cache.d.ts.map +1 -0
  182. package/dist/hubs/cache.js +46 -0
  183. package/dist/hubs/cache.js.map +1 -0
  184. package/dist/hubs/enrichment.d.ts +43 -0
  185. package/dist/hubs/enrichment.d.ts.map +1 -0
  186. package/dist/hubs/enrichment.js +239 -0
  187. package/dist/hubs/enrichment.js.map +1 -0
  188. package/dist/hubs/github.d.ts +12 -0
  189. package/dist/hubs/github.d.ts.map +1 -0
  190. package/dist/hubs/github.js +54 -0
  191. package/dist/hubs/github.js.map +1 -0
  192. package/dist/hubs/huggingface.d.ts +27 -0
  193. package/dist/hubs/huggingface.d.ts.map +1 -0
  194. package/dist/hubs/huggingface.js +88 -0
  195. package/dist/hubs/huggingface.js.map +1 -0
  196. package/dist/hubs/quality.d.ts +26 -0
  197. package/dist/hubs/quality.d.ts.map +1 -0
  198. package/dist/hubs/quality.js +57 -0
  199. package/dist/hubs/quality.js.map +1 -0
  200. package/dist/hubs/types.d.ts +30 -0
  201. package/dist/hubs/types.d.ts.map +1 -0
  202. package/dist/hubs/types.js +2 -0
  203. package/dist/hubs/types.js.map +1 -0
  204. package/dist/index.d.ts.map +1 -1
  205. package/dist/index.js +188 -224
  206. package/dist/index.js.map +1 -1
  207. package/dist/init.d.ts.map +1 -1
  208. package/dist/init.js +6 -11
  209. package/dist/init.js.map +1 -1
  210. package/dist/live.d.ts +14 -0
  211. package/dist/live.d.ts.map +1 -1
  212. package/dist/live.js +35 -3
  213. package/dist/live.js.map +1 -1
  214. package/dist/marketplace.d.ts +25 -3
  215. package/dist/marketplace.d.ts.map +1 -1
  216. package/dist/marketplace.js +279 -22
  217. package/dist/marketplace.js.map +1 -1
  218. package/dist/news/cache.d.ts +13 -0
  219. package/dist/news/cache.d.ts.map +1 -0
  220. package/dist/news/cache.js +66 -0
  221. package/dist/news/cache.js.map +1 -0
  222. package/dist/news/score.d.ts +4 -0
  223. package/dist/news/score.d.ts.map +1 -0
  224. package/dist/news/score.js +43 -0
  225. package/dist/news/score.js.map +1 -0
  226. package/dist/news/sources/arxiv.d.ts +9 -0
  227. package/dist/news/sources/arxiv.d.ts.map +1 -0
  228. package/dist/news/sources/arxiv.js +107 -0
  229. package/dist/news/sources/arxiv.js.map +1 -0
  230. package/dist/news/sources/github-trending.d.ts +9 -0
  231. package/dist/news/sources/github-trending.d.ts.map +1 -0
  232. package/dist/news/sources/github-trending.js +97 -0
  233. package/dist/news/sources/github-trending.js.map +1 -0
  234. package/dist/news/sources/hn.d.ts +9 -0
  235. package/dist/news/sources/hn.d.ts.map +1 -0
  236. package/dist/news/sources/hn.js +57 -0
  237. package/dist/news/sources/hn.js.map +1 -0
  238. package/dist/news/sources/reddit.d.ts +9 -0
  239. package/dist/news/sources/reddit.d.ts.map +1 -0
  240. package/dist/news/sources/reddit.js +69 -0
  241. package/dist/news/sources/reddit.js.map +1 -0
  242. package/dist/news/sources/rss.d.ts +13 -0
  243. package/dist/news/sources/rss.d.ts.map +1 -0
  244. package/dist/news/sources/rss.js +14 -0
  245. package/dist/news/sources/rss.js.map +1 -0
  246. package/dist/news/types.d.ts +42 -0
  247. package/dist/news/types.d.ts.map +1 -0
  248. package/dist/news/types.js +56 -0
  249. package/dist/news/types.js.map +1 -0
  250. package/dist/preferences.d.ts +14 -0
  251. package/dist/preferences.d.ts.map +1 -0
  252. package/dist/preferences.js +31 -0
  253. package/dist/preferences.js.map +1 -0
  254. package/dist/settings.d.ts +26 -0
  255. package/dist/settings.d.ts.map +1 -0
  256. package/dist/settings.js +251 -0
  257. package/dist/settings.js.map +1 -0
  258. package/dist/state.d.ts +9 -2
  259. package/dist/state.d.ts.map +1 -1
  260. package/dist/state.js +41 -19
  261. package/dist/state.js.map +1 -1
  262. package/dist/transcript.d.ts +28 -0
  263. package/dist/transcript.d.ts.map +1 -0
  264. package/dist/transcript.js +79 -0
  265. package/dist/transcript.js.map +1 -0
  266. package/dist/types.d.ts +19 -1
  267. package/dist/types.d.ts.map +1 -1
  268. package/dist/ui.d.ts +157 -0
  269. package/dist/ui.d.ts.map +1 -0
  270. package/dist/ui.js +296 -0
  271. package/dist/ui.js.map +1 -0
  272. package/package.json +11 -9
  273. package/dist/api.d.ts +0 -72
  274. package/dist/api.d.ts.map +0 -1
  275. package/dist/api.js +0 -109
  276. package/dist/api.js.map +0 -1
  277. package/dist/logger.d.ts +0 -20
  278. package/dist/logger.d.ts.map +0 -1
  279. package/dist/logger.js +0 -59
  280. package/dist/logger.js.map +0 -1
@@ -0,0 +1,1106 @@
1
+ import { execSync, spawn } from 'node:child_process';
2
+ import { appendFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
3
+ import { homedir, tmpdir } from 'node:os';
4
+ import { join, resolve, sep } from 'node:path';
5
+ import { COMMANDS } from './commands-meta.js';
6
+ import { runInteractiveMenu } from './menu.js';
7
+ import { runTui } from './tui.js';
8
+ import { AGORA_VERSION } from './app.js';
9
+ import { FREE_MODELS } from './commands/chat.js';
10
+ import { detectAgoraDataDir, loadAgoraState, resolveSavedItems } from '../state.js';
11
+ import { appendTranscript, loadSessionMeta, readTranscript, recentBashContext, writeSessionMeta } from '../transcript.js';
12
+ import { gradientText, renderBanner, supportsTrueColor } from '../ui.js';
13
+ import { createChatRenderer } from './chat-renderer.js';
14
+ import { readLine } from './prompter.js';
15
+ import { completeShellLine, ghostFromHistory } from './completions.js';
16
+ import { getMarketplaceItems } from '../marketplace.js';
17
+ const SHELL_BUILTINS = new Set(['cd', 'export', 'alias', 'source', 'unset', 'umask', 'exec']);
18
+ const MAX_BASH_BUFFER = 16 * 1024;
19
+ const SHELL_HISTORY_FILE = 'shell-history.jsonl';
20
+ const SHELL_HISTORY_MAX = 2000;
21
+ /** Map slash aliases (with leading `/`) to TUI page ids. */
22
+ const TUI_SLASH_ALIASES = {
23
+ '/tui': 'default',
24
+ '/home': 'home',
25
+ '/market': 'marketplace',
26
+ '/marketplace': 'marketplace',
27
+ '/comm': 'community',
28
+ '/community': 'community',
29
+ '/news': 'news',
30
+ '/settings': 'settings'
31
+ };
32
+ const LETTER_SHORTCUTS = {
33
+ '/a': { kind: 'meta', sub: 'again' },
34
+ '/b': { kind: 'bash', cmd: 'agora browse' },
35
+ '/c': { kind: 'tui', page: 'community' },
36
+ '/d': { kind: 'bash', cmd: 'agora config doctor' },
37
+ '/e': { kind: 'meta', sub: 'env' },
38
+ '/f': { kind: 'meta', sub: 'fg' },
39
+ '/g': { kind: 'bash', cmd: 'agora search' },
40
+ '/h': { kind: 'tui', page: 'home' },
41
+ '/i': { kind: 'bash', cmd: 'agora init' },
42
+ '/j': { kind: 'meta', sub: 'jobs' },
43
+ '/k': { kind: 'bash', cmd: 'agora search' },
44
+ '/l': { kind: 'meta', sub: 'last' },
45
+ '/m': { kind: 'tui', page: 'marketplace' },
46
+ '/n': { kind: 'tui', page: 'news' },
47
+ '/o': { kind: 'bash', cmd: 'agora browse' },
48
+ '/p': { kind: 'bash', cmd: 'agora preferences' },
49
+ '/q': { kind: 'meta', sub: 'quit' },
50
+ '/r': { kind: 'bash', cmd: 'agora reviews' },
51
+ '/s': { kind: 'tui', page: 'settings' },
52
+ '/t': { kind: 'meta', sub: 'terminal' },
53
+ '/u': { kind: 'bash', cmd: 'agora use' },
54
+ '/v': { kind: 'meta', sub: 'verbose' },
55
+ '/w': { kind: 'bash', cmd: 'agora watch' },
56
+ '/x': { kind: 'bash', cmd: 'agora export' },
57
+ '/y': { kind: 'bash', cmd: 'agora history' },
58
+ '/z': { kind: 'bash', cmd: 'agora config doctor --fix' }
59
+ };
60
+ const QUESTION_STARTERS = new Set([
61
+ 'what',
62
+ 'why',
63
+ 'how',
64
+ 'which',
65
+ 'when',
66
+ 'where',
67
+ 'who',
68
+ 'whose',
69
+ 'should',
70
+ 'shall',
71
+ 'can',
72
+ 'could',
73
+ 'would',
74
+ 'will',
75
+ 'do',
76
+ 'does',
77
+ 'did',
78
+ 'is',
79
+ 'are',
80
+ 'am',
81
+ 'was',
82
+ 'were',
83
+ 'tell',
84
+ 'explain',
85
+ 'describe',
86
+ 'help',
87
+ 'hi',
88
+ 'hello',
89
+ 'hey',
90
+ 'thanks'
91
+ ]);
92
+ const ENV_VAR_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
93
+ const BACKTICK_RE = /^`[^`]+`/;
94
+ export function looksLikeQuestion(line) {
95
+ const trimmed = line.trim();
96
+ if (!trimmed)
97
+ return false;
98
+ if (trimmed.endsWith('?'))
99
+ return true;
100
+ // Env-prefixed commands (FOO=bar cmd) are always bash, never questions
101
+ if (ENV_VAR_RE.test(trimmed))
102
+ return false;
103
+ // Backtick-prefixed commands are always bash
104
+ if (BACKTICK_RE.test(trimmed))
105
+ return false;
106
+ const words = trimmed.split(/\s+/);
107
+ const firstWord = words[0].toLowerCase();
108
+ if (QUESTION_STARTERS.has(firstWord))
109
+ return true;
110
+ if (trimmed[0] === trimmed[0].toUpperCase() &&
111
+ trimmed[0] !== trimmed[0].toLowerCase() &&
112
+ words.length >= 3) {
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+ export function classifyInput(line, isExecutable) {
118
+ const trimmed = line.trim();
119
+ if (!trimmed)
120
+ return { kind: 'noop' };
121
+ if (trimmed === '/abc')
122
+ return { kind: 'meta', sub: 'abc' };
123
+ if (trimmed === '/help')
124
+ return { kind: 'meta', sub: 'help' };
125
+ if (trimmed === '/quit')
126
+ return { kind: 'meta', sub: 'quit' };
127
+ if (trimmed === '/exit')
128
+ return { kind: 'meta', sub: 'exit' };
129
+ if (trimmed === '/clear')
130
+ return { kind: 'meta', sub: 'clear' };
131
+ if (trimmed === '/transcript')
132
+ return { kind: 'meta', sub: 'transcript' };
133
+ if (trimmed === '/menu')
134
+ return { kind: 'meta', sub: 'menu' };
135
+ if (trimmed === '/terminal')
136
+ return { kind: 'meta', sub: 'terminal' };
137
+ if (trimmed === '/verbose')
138
+ return { kind: 'meta', sub: 'verbose' };
139
+ if (trimmed === '/quiet')
140
+ return { kind: 'meta', sub: 'quiet' };
141
+ if (trimmed === '/medium')
142
+ return { kind: 'meta', sub: 'medium' };
143
+ if (trimmed === '/last')
144
+ return { kind: 'meta', sub: 'last' };
145
+ if (trimmed === '/again')
146
+ return { kind: 'meta', sub: 'again' };
147
+ if (trimmed === '/jobs')
148
+ return { kind: 'meta', sub: 'jobs' };
149
+ if (trimmed === '/fg' || trimmed.startsWith('/fg '))
150
+ return { kind: 'meta', sub: 'fg', args: trimmed.slice(4).trim() };
151
+ if (trimmed === '/bg' || trimmed.startsWith('/bg '))
152
+ return { kind: 'meta', sub: 'bg', args: trimmed.slice(4).trim() };
153
+ if (trimmed === '/env' || trimmed.startsWith('/env '))
154
+ return { kind: 'meta', sub: 'env', args: trimmed.slice(5).trim() };
155
+ if (trimmed.startsWith('/? '))
156
+ return { kind: 'meta', sub: 'dry-run', args: trimmed.slice(3).trim() };
157
+ if (trimmed.startsWith('!'))
158
+ return { kind: 'bash', cmd: trimmed.slice(1).trim() };
159
+ if (trimmed.startsWith('?'))
160
+ return { kind: 'chat', msg: trimmed.slice(1).trim() };
161
+ // TUI shortcuts: `/tui` opens Home, `/home` `/market` `/comm` `/news`
162
+ // `/settings` open the TUI on that page. Recognised only as exact bare
163
+ // commands so `/tui foo` falls through to the generic CLI forwarding below.
164
+ if (TUI_SLASH_ALIASES[trimmed] !== undefined) {
165
+ const target = TUI_SLASH_ALIASES[trimmed];
166
+ return target === 'default' ? { kind: 'tui' } : { kind: 'tui', page: target };
167
+ }
168
+ // Single-letter shortcuts (exact match only, no args).
169
+ const letterDisp = LETTER_SHORTCUTS[trimmed];
170
+ if (letterDisp) {
171
+ if (letterDisp.kind === 'meta') {
172
+ return { kind: 'meta', sub: letterDisp.sub };
173
+ }
174
+ if (letterDisp.kind === 'tui') {
175
+ return { kind: 'tui', page: letterDisp.page };
176
+ }
177
+ return letterDisp;
178
+ }
179
+ // Slash-prefixed inputs that weren't an exact meta, TUI, or letter match are
180
+ // forwarded to the `agora` CLI: `/agora help`, `/agora search foo`,
181
+ // `/help tutorials`, `/foo` all become `agora <args>`. Never let
182
+ // `/anything` fall through to bash — PATH-joining an absolute name like
183
+ // `/agora` historically matched the real binary on disk and bash then
184
+ // tried to exec `/agora` literally.
185
+ if (trimmed.startsWith('/')) {
186
+ let rest = trimmed.slice(1).trim();
187
+ if (rest === 'agora' || rest.startsWith('agora ')) {
188
+ rest = rest === 'agora' ? '' : rest.slice('agora '.length).trim();
189
+ }
190
+ return { kind: 'bash', cmd: rest ? `agora ${rest}` : 'agora help' };
191
+ }
192
+ if (looksLikeQuestion(trimmed))
193
+ return { kind: 'chat', msg: trimmed };
194
+ const firstToken = trimmed.split(/\s+/)[0];
195
+ if (SHELL_BUILTINS.has(firstToken) || isExecutable(firstToken)) {
196
+ return { kind: 'bash', cmd: trimmed };
197
+ }
198
+ return { kind: 'chat', msg: trimmed };
199
+ }
200
+ // ── Executable check ────────────────────────────────────────────────────────
201
+ function makeExecutableChecker(pathEnv) {
202
+ const cache = new Map();
203
+ const dirs = (pathEnv ?? '').split(':').filter(Boolean);
204
+ return function isExecutable(name) {
205
+ if (cache.has(name))
206
+ return cache.get(name);
207
+ // PATH lookup is for bare command names only. An absolute or relative path
208
+ // (`/agora`, `./run`, `bin/agora`) is the caller's literal request — Node's
209
+ // `path.join('/usr/bin', '/agora')` discards the leading slash and would
210
+ // falsely match the real `agora` on PATH otherwise.
211
+ if (name.includes('/')) {
212
+ cache.set(name, false);
213
+ return false;
214
+ }
215
+ for (const dir of dirs) {
216
+ const full = join(dir, name);
217
+ try {
218
+ if (existsSync(full)) {
219
+ const st = statSync(full);
220
+ if (st.isFile() && (st.mode & 0o111) !== 0) {
221
+ cache.set(name, true);
222
+ return true;
223
+ }
224
+ }
225
+ }
226
+ catch {
227
+ // skip
228
+ }
229
+ }
230
+ cache.set(name, false);
231
+ return false;
232
+ };
233
+ }
234
+ // ── Helpers ─────────────────────────────────────────────────────────────────
235
+ function expandHome(p) {
236
+ if (p === '~')
237
+ return homedir();
238
+ if (p.startsWith('~/'))
239
+ return join(homedir(), p.slice(2));
240
+ return p;
241
+ }
242
+ function tailBuffer(buf, maxBytes) {
243
+ if (buf.length <= maxBytes)
244
+ return buf;
245
+ return buf.slice(buf.length - maxBytes);
246
+ }
247
+ function formatTranscriptEntry(entry) {
248
+ const time = entry.ts.slice(0, 19);
249
+ const prefix = `[${time}] [${entry.kind}]`;
250
+ const input = entry.input ? ` $ ${entry.input}` : '';
251
+ const output = entry.output ? `\n ${entry.output.split('\n').slice(0, 5).join('\n ')}` : '';
252
+ return `${prefix}${input}${output}`;
253
+ }
254
+ /** Shorten a path for display: replace HOME with ~, truncate if > 30 chars. */
255
+ function shortCwd(p) {
256
+ const home = homedir();
257
+ const withTilde = p.startsWith(home) ? '~' + p.slice(home.length) : p;
258
+ if (withTilde.length <= 30)
259
+ return withTilde;
260
+ const parts = withTilde.split(sep).filter(Boolean);
261
+ if (parts.length <= 2)
262
+ return withTilde;
263
+ return '…' + sep + parts.slice(-2).join(sep);
264
+ }
265
+ /** Check whether opencode is reachable on PATH. */
266
+ function checkOpencodeAvailable() {
267
+ try {
268
+ execSync('which opencode', { stdio: 'pipe', timeout: 2000 });
269
+ return true;
270
+ }
271
+ catch {
272
+ return false;
273
+ }
274
+ }
275
+ /** Extract the first ```bash / ```sh / ```shell block from markdown text. */
276
+ function extractFirstBashBlock(text) {
277
+ const match = text.match(/```(?:bash|sh|shell)\n([\s\S]*?)```/);
278
+ return match ? match[1].trim() : null;
279
+ }
280
+ /** Read a single keypress in raw mode without the full prompter. */
281
+ async function readOneKey() {
282
+ return new Promise((resolve) => {
283
+ const stdin = process.stdin;
284
+ const wasRaw = stdin.isRaw ?? false;
285
+ if (stdin.setRawMode)
286
+ stdin.setRawMode(true);
287
+ stdin.resume();
288
+ function onData(buf) {
289
+ stdin.removeListener('data', onData);
290
+ if (stdin.setRawMode)
291
+ stdin.setRawMode(wasRaw);
292
+ resolve(buf.toString()[0] ?? '');
293
+ }
294
+ stdin.on('data', onData);
295
+ });
296
+ }
297
+ /** Copy text to clipboard using pbcopy (macOS) or xclip (Linux). */
298
+ function copyToClipboard(text) {
299
+ try {
300
+ const cmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip -selection clipboard';
301
+ execSync(cmd, { input: text, stdio: ['pipe', 'ignore', 'ignore'], timeout: 3000 });
302
+ }
303
+ catch {
304
+ // fall back silently
305
+ }
306
+ }
307
+ // ── Shell history persistence ────────────────────────────────────────────────
308
+ function getShellHistoryPath(dataDir) {
309
+ return join(dataDir, SHELL_HISTORY_FILE);
310
+ }
311
+ function loadShellHistory(dataDir) {
312
+ const path = getShellHistoryPath(dataDir);
313
+ if (!existsSync(path))
314
+ return [];
315
+ try {
316
+ const raw = readFileSync(path, 'utf8');
317
+ const entries = [];
318
+ for (const line of raw.split('\n').filter(Boolean).reverse()) {
319
+ try {
320
+ const parsed = JSON.parse(line);
321
+ if (parsed.line)
322
+ entries.push(parsed.line);
323
+ }
324
+ catch {
325
+ continue;
326
+ }
327
+ if (entries.length >= SHELL_HISTORY_MAX)
328
+ break;
329
+ }
330
+ return entries;
331
+ }
332
+ catch {
333
+ return [];
334
+ }
335
+ }
336
+ function appendShellHistory(dataDir, line) {
337
+ const path = getShellHistoryPath(dataDir);
338
+ try {
339
+ appendFileSync(path, JSON.stringify({ line, ts: new Date().toISOString() }) + '\n', 'utf8');
340
+ }
341
+ catch {
342
+ // best-effort
343
+ }
344
+ }
345
+ // ── Main shell loop ─────────────────────────────────────────────────────────
346
+ export async function runShell(io, style) {
347
+ const env = io.env ?? {};
348
+ const trueColor = supportsTrueColor(env);
349
+ function printHome() {
350
+ const banner = renderBanner({ color: true, trueColor });
351
+ const motto = "Developers' CLI marketplace and community hub - type a command, bash or chat:";
352
+ const mottoLine = gradientText(motto, { trueColor });
353
+ const model = FREE_MODELS[0];
354
+ const infoLine = style.dim(`v${AGORA_VERSION} · ${model} · /abc · /help · /menu · /search · /quit`);
355
+ const slashLine = style.orange('/home · /marketplace · /community · /news · /settings');
356
+ process.stdout.write(`\n${banner}\n\n${mottoLine}\n\n${infoLine}\n${slashLine}\n\n`);
357
+ }
358
+ printHome();
359
+ const opencodeAvailable = checkOpencodeAvailable();
360
+ if (!opencodeAvailable) {
361
+ process.stdout.write(style.dim('Note: opencode not found on PATH. Chat will be unavailable until installed.') +
362
+ '\n\n');
363
+ }
364
+ const dataDir = detectAgoraDataDir({ cwd: io.cwd, env: io.env });
365
+ const cwd0 = io.cwd ?? process.cwd();
366
+ let currentCwd = cwd0;
367
+ let meta = loadSessionMeta(dataDir, cwd0);
368
+ if (meta) {
369
+ process.stdout.write(style.dim(`Resumed ${meta.turnCount} turns from ${meta.lastUsedAt.slice(0, 19)} · session ${(meta.sessionId ?? '').slice(0, 8)}…`) + '\n\n');
370
+ }
371
+ else {
372
+ meta = {
373
+ sessionId: null,
374
+ cwd: cwd0,
375
+ createdAt: new Date().toISOString(),
376
+ lastUsedAt: new Date().toISOString(),
377
+ turnCount: 0
378
+ };
379
+ writeSessionMeta(dataDir, cwd0, meta);
380
+ }
381
+ const isExecutable = makeExecutableChecker(env.PATH);
382
+ let firstTurn = meta.turnCount === 0;
383
+ const exitCode = 0;
384
+ let verbosity = 'medium';
385
+ let childActive = false;
386
+ let totalCost = 0;
387
+ const trackedEnv = new Map();
388
+ let jobCounter = 0;
389
+ const jobs = new Map();
390
+ // Lazy caches for completion ids
391
+ let cachedMarketplaceIds = null;
392
+ let cachedSavedIds = null;
393
+ function getMarketplaceIds() {
394
+ if (!cachedMarketplaceIds) {
395
+ cachedMarketplaceIds = getMarketplaceItems().map((item) => item.id);
396
+ }
397
+ return cachedMarketplaceIds;
398
+ }
399
+ function getSavedIds() {
400
+ if (!cachedSavedIds) {
401
+ const state = loadAgoraState(dataDir);
402
+ cachedSavedIds = resolveSavedItems(state).map((e) => e.saved.id);
403
+ }
404
+ return cachedSavedIds;
405
+ }
406
+ const agoraSlashCommands = COMMANDS.map((c) => '/' + c.name);
407
+ const slashCommands = [
408
+ '/tui',
409
+ '/home',
410
+ '/marketplace',
411
+ '/market',
412
+ '/community',
413
+ '/comm',
414
+ '/news',
415
+ '/settings',
416
+ '/abc',
417
+ '/help',
418
+ '/menu',
419
+ '/transcript',
420
+ '/verbose',
421
+ '/medium',
422
+ '/quiet',
423
+ '/clear',
424
+ '/quit',
425
+ '/exit',
426
+ '/last',
427
+ '/again',
428
+ '/env',
429
+ '/jobs',
430
+ '/fg',
431
+ '/bg',
432
+ ...agoraSlashCommands
433
+ ];
434
+ // Deduplicate: /news, /home etc may appear in both lists
435
+ const seen = new Set();
436
+ const deduped = slashCommands.filter((c) => {
437
+ if (seen.has(c))
438
+ return false;
439
+ seen.add(c);
440
+ return true;
441
+ });
442
+ const finalSlashCommands = deduped;
443
+ const completionContext = {
444
+ slashCommands: finalSlashCommands,
445
+ marketplaceIds: getMarketplaceIds,
446
+ savedIds: getSavedIds,
447
+ listDir: (p) => {
448
+ try {
449
+ return readdirSync(p);
450
+ }
451
+ catch {
452
+ return [];
453
+ }
454
+ },
455
+ cwd: currentCwd
456
+ };
457
+ // Shell history — persisted to disk and loaded on startup
458
+ const history = loadShellHistory(dataDir);
459
+ const tips = [
460
+ 'type /abc for a quick letter-shortcut reference (/a /b /c ...)',
461
+ 'type /help to see all slash commands',
462
+ 'type /menu to browse the command catalog',
463
+ 'type ?<msg> to force AI chat',
464
+ 'type !<cmd> to force bash',
465
+ 'type /clear to reset and see the home banner',
466
+ 'type /transcript to see your last 20 commands',
467
+ 'type /last to re-run the last bash command',
468
+ 'type /again to re-send the last chat message',
469
+ 'type /? <agora_cmd> to dry-run an agora command',
470
+ 'type ?how do I use MCP? to ask about the marketplace',
471
+ 'type /verbose for detailed AI responses',
472
+ 'type /quiet for minimal AI responses',
473
+ 'type /env to view tracked env vars, /env FOO=val to set one',
474
+ 'press Tab to auto-complete commands and paths',
475
+ 'press Ctrl-R to reverse-search your history',
476
+ 'press Ctrl-L to clear the screen',
477
+ 'press Esc to dismiss ghost suggestions',
478
+ 'run `agora search --table` for a table view',
479
+ 'run `agora search --sort stars` to sort by stars',
480
+ 'run `agora search --sort name --order asc` for alphabetical',
481
+ 'run `agora completions bash | source /dev/stdin` for bash completions',
482
+ 'run `agora completions zsh > /usr/local/share/zsh/site-functions/_agora` for zsh completions',
483
+ 'run `agora completions fish > ~/.config/fish/completions/agora.fish` for fish completions',
484
+ 'type /home to open the TUI Home page',
485
+ 'type /marketplace to open the TUI Marketplace page',
486
+ 'type /settings to open the TUI Settings page',
487
+ 'run `agora save <id>` to bookmark a package',
488
+ 'run `agora saved` to see your saved items',
489
+ 'run `agora auth login --api-url <url>` to connect the community',
490
+ 'run `agora config doctor` to check your OpenCode config',
491
+ 'type VAR=val command to set env vars in bash',
492
+ 'pipe output with | or redirect with > as normal in bash',
493
+ 'append & to run a command in the background',
494
+ 'type /jobs to see background jobs, /fg to bring one forward',
495
+ 'type /bg to resume a stopped job in the background',
496
+ 'run `agora shell` anywhere to re-enter this interactive mode'
497
+ ];
498
+ // Amber chevron when opencode unavailable, accent otherwise
499
+ const accentChevron = opencodeAvailable ? style.accent('›') : style.orange('›');
500
+ // Static portion of the prompt (no chevron); suffix added dynamically
501
+ function buildPromptBase() {
502
+ return style.accent('agora') + ' ' + style.dim(shortCwd(currentCwd)) + ' ';
503
+ }
504
+ function buildPromptSuffix(line) {
505
+ const d = classifyInput(line, isExecutable);
506
+ const hint = d.kind === 'bash' ? style.accent('$') : d.kind === 'chat' ? style.accent('?') : '';
507
+ return hint + accentChevron + ' ';
508
+ }
509
+ function buildContextLine() {
510
+ const model = FREE_MODELS[0];
511
+ const turnCount = meta?.turnCount ?? 0;
512
+ const tip = tips[turnCount % tips.length];
513
+ const parts = [`model: ${model}`, `${turnCount} turns`];
514
+ if (totalCost > 0)
515
+ parts.push(`$${totalCost.toFixed(6)}`);
516
+ parts.push(tip);
517
+ return style.dim(parts.join(' · '));
518
+ }
519
+ const sigintHandler = () => {
520
+ // SIGINT while idle: prompter handles Ctrl-C bytes directly via raw mode
521
+ if (childActive)
522
+ return;
523
+ };
524
+ process.on('SIGINT', sigintHandler);
525
+ try {
526
+ for (;;) {
527
+ // Update completionContext.cwd on each iteration in case cd changed it
528
+ completionContext.cwd = currentCwd;
529
+ const result = await readLine({
530
+ prompt: buildPromptBase(),
531
+ promptSuffix: buildPromptSuffix,
532
+ history,
533
+ completer: (line, cursor) => completeShellLine(line, cursor, completionContext),
534
+ ghostSuggester: (line, hist) => ghostFromHistory(line, hist),
535
+ footer: () => buildContextLine()
536
+ });
537
+ if (result.kind === 'eof') {
538
+ process.stdout.write('\n');
539
+ break;
540
+ }
541
+ if (result.kind === 'abort') {
542
+ // Ctrl-C in prompter: clear input and continue
543
+ process.stdout.write('\n');
544
+ continue;
545
+ }
546
+ const line = result.value;
547
+ if (!line.trim())
548
+ continue;
549
+ // Add to history and persist
550
+ if (history[history.length - 1] !== line) {
551
+ history.push(line);
552
+ appendShellHistory(dataDir, line);
553
+ }
554
+ const dispatch = classifyInput(line, isExecutable);
555
+ if (dispatch.kind === 'noop')
556
+ continue;
557
+ if (dispatch.kind === 'meta') {
558
+ if (dispatch.sub === 'quit' || dispatch.sub === 'exit')
559
+ break;
560
+ if (dispatch.sub === 'clear') {
561
+ process.stdout.write('\x1b[2J\x1b[H');
562
+ printHome();
563
+ continue;
564
+ }
565
+ if (dispatch.sub === 'abc') {
566
+ printLetterHelp(style);
567
+ continue;
568
+ }
569
+ if (dispatch.sub === 'help') {
570
+ printHelp(style);
571
+ continue;
572
+ }
573
+ if (dispatch.sub === 'transcript') {
574
+ const entries = readTranscript(dataDir, cwd0, { tail: 20 });
575
+ if (entries.length === 0) {
576
+ process.stdout.write(style.dim('No transcript entries yet.') + '\n');
577
+ }
578
+ else {
579
+ for (const e of entries) {
580
+ process.stdout.write(formatTranscriptEntry(e) + '\n');
581
+ }
582
+ }
583
+ continue;
584
+ }
585
+ if (dispatch.sub === 'menu') {
586
+ await runInteractiveMenu(io, style);
587
+ continue;
588
+ }
589
+ if (dispatch.sub === 'terminal') {
590
+ process.stdout.write(style.dim('Entering subshell. Type exit or Ctrl-D to return.\n'));
591
+ const child = spawn(process.env.SHELL || 'bash', [], {
592
+ stdio: 'inherit',
593
+ cwd: currentCwd,
594
+ env: env
595
+ });
596
+ await new Promise((res) => child.on('exit', () => res()));
597
+ process.stdout.write('\n');
598
+ continue;
599
+ }
600
+ if (dispatch.sub === 'verbose' || dispatch.sub === 'quiet' || dispatch.sub === 'medium') {
601
+ verbosity = dispatch.sub;
602
+ process.stdout.write(style.dim(`Verbosity: ${verbosity}`) + '\n');
603
+ continue;
604
+ }
605
+ if (dispatch.sub === 'last') {
606
+ const entries = readTranscript(dataDir, cwd0);
607
+ const lastBash = [...entries].reverse().find((e) => e.kind === 'bash' && e.input);
608
+ if (!lastBash || !lastBash.input) {
609
+ process.stdout.write(style.dim('No previous bash command in this session.') + '\n');
610
+ continue;
611
+ }
612
+ // Re-dispatch as bash
613
+ history.push(lastBash.input);
614
+ const bashDispatch = { kind: 'bash', cmd: lastBash.input };
615
+ await runBash(bashDispatch.cmd);
616
+ continue;
617
+ }
618
+ if (dispatch.sub === 'again') {
619
+ const entries = readTranscript(dataDir, cwd0);
620
+ const lastChat = [...entries].reverse().find((e) => e.kind === 'chat-user' && e.input);
621
+ if (!lastChat || !lastChat.input) {
622
+ process.stdout.write(style.dim('No previous chat message in this session.') + '\n');
623
+ continue;
624
+ }
625
+ await runChat(lastChat.input);
626
+ continue;
627
+ }
628
+ if (dispatch.sub === 'dry-run' && dispatch.args) {
629
+ const args = dispatch.args.split(/\s+/).filter(Boolean);
630
+ process.stdout.write(style.dim(`╤ dry-run · agora ${args.join(' ')}`) + '\n');
631
+ try {
632
+ const out = execSync(`agora ${args.join(' ')}`, { timeout: 15000, encoding: 'utf8' });
633
+ process.stdout.write(out + '\n');
634
+ }
635
+ catch (e) {
636
+ process.stdout.write((e.stdout ?? '') + (e.stderr ?? '') + '\n');
637
+ }
638
+ continue;
639
+ }
640
+ if (dispatch.sub === 'jobs') {
641
+ if (jobs.size === 0) {
642
+ process.stdout.write(style.dim('No background jobs. Append & to run a command in the background.') + '\n');
643
+ }
644
+ else {
645
+ process.stdout.write(style.accent('Background jobs') + '\n');
646
+ for (const [id, job] of jobs) {
647
+ const status = job.status === 'running' ? style.dim('running') : style.dim('stopped');
648
+ process.stdout.write(` [${id}] ${status} ${job.cmd}\n`);
649
+ }
650
+ }
651
+ continue;
652
+ }
653
+ if (dispatch.sub === 'fg') {
654
+ const arg = dispatch.args || '';
655
+ const targetId = arg ? parseInt(arg, 10) : Math.max(...jobs.keys());
656
+ const job = jobs.get(targetId);
657
+ if (!job) {
658
+ process.stdout.write(style.dim(`Job ${targetId} not found.`) + '\n');
659
+ continue;
660
+ }
661
+ try {
662
+ process.kill(job.pid, 0);
663
+ process.stdout.write(style.dim(`Foreground: ${job.cmd}`) + '\n');
664
+ await new Promise((resolve) => {
665
+ const check = setInterval(() => {
666
+ try {
667
+ process.kill(job.pid, 0);
668
+ }
669
+ catch {
670
+ clearInterval(check);
671
+ jobs.delete(targetId);
672
+ resolve();
673
+ }
674
+ }, 200);
675
+ });
676
+ }
677
+ catch {
678
+ jobs.delete(targetId);
679
+ process.stdout.write(style.dim(`Job ${targetId} has finished.`) + '\n');
680
+ }
681
+ continue;
682
+ }
683
+ if (dispatch.sub === 'bg') {
684
+ const arg = dispatch.args || '';
685
+ const targetId = arg ? parseInt(arg, 10) : Math.max(...jobs.keys());
686
+ const job = jobs.get(targetId);
687
+ if (!job) {
688
+ process.stdout.write(style.dim(`Job ${targetId} not found.`) + '\n');
689
+ continue;
690
+ }
691
+ try {
692
+ process.kill(job.pid, 0);
693
+ job.status = 'running';
694
+ process.stdout.write(style.dim(`[${targetId}] ${job.cmd} (background)`) + '\n');
695
+ }
696
+ catch {
697
+ jobs.delete(targetId);
698
+ process.stdout.write(style.dim(`Job ${targetId} has finished.`) + '\n');
699
+ }
700
+ continue;
701
+ }
702
+ if (dispatch.sub === 'env') {
703
+ const arg = dispatch.args || '';
704
+ if (!arg) {
705
+ if (trackedEnv.size === 0) {
706
+ process.stdout.write(style.dim('No tracked environment variables. Use /env VAR=value to set one.') + '\n');
707
+ }
708
+ else {
709
+ process.stdout.write(style.accent('Tracked environment') + '\n');
710
+ for (const [k, v] of [...trackedEnv.entries()].sort()) {
711
+ process.stdout.write(` ${k}=${v}\n`);
712
+ }
713
+ }
714
+ }
715
+ else if (arg.includes('=')) {
716
+ const eqIdx = arg.indexOf('=');
717
+ const key = arg.slice(0, eqIdx).trim();
718
+ const val = arg.slice(eqIdx + 1).trim();
719
+ trackedEnv.set(key, val);
720
+ process.stdout.write(style.dim(`${key}=${val} (tracked)`) + '\n');
721
+ }
722
+ else {
723
+ const val = trackedEnv.get(arg);
724
+ if (val === undefined) {
725
+ process.stdout.write(style.dim(`${arg} is not tracked.`) + '\n');
726
+ }
727
+ else {
728
+ process.stdout.write(`${arg}=${val}\n`);
729
+ }
730
+ }
731
+ continue;
732
+ }
733
+ continue;
734
+ }
735
+ if (dispatch.kind === 'tui') {
736
+ await runTui(io, { initial: dispatch.page ?? 'home' });
737
+ continue;
738
+ }
739
+ if (dispatch.kind === 'bash') {
740
+ // Track env vars from export statements and VAR=val prefix
741
+ const bashLine = dispatch.cmd;
742
+ const exportMatch = bashLine.match(/^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.+)$/);
743
+ if (exportMatch) {
744
+ trackedEnv.set(exportMatch[1], exportMatch[2].trim());
745
+ }
746
+ else {
747
+ const prefixMatch = bashLine.match(/^([A-Za-z_][A-Za-z0-9_]*)=(\S+)\s+/);
748
+ if (prefixMatch) {
749
+ trackedEnv.set(prefixMatch[1], prefixMatch[2].trim());
750
+ }
751
+ }
752
+ // Background job handling
753
+ const bgMatch = bashLine.match(/^(.*?)\s*&$/);
754
+ if (bgMatch) {
755
+ const actualCmd = bgMatch[1].trim();
756
+ if (actualCmd) {
757
+ jobCounter++;
758
+ const jobId = jobCounter;
759
+ const child = spawn(actualCmd, {
760
+ shell: true,
761
+ cwd: currentCwd,
762
+ env: env,
763
+ stdio: ['ignore', 'pipe', 'pipe']
764
+ });
765
+ jobs.set(jobId, { pid: child.pid ?? 0, cmd: actualCmd, status: 'running' });
766
+ process.stdout.write(style.dim(`[${jobId}] ${actualCmd} (background, pid ${child.pid ?? '?'})`) + '\n');
767
+ child.stdout?.on('data', (chunk) => {
768
+ const text = chunk.toString();
769
+ const lines = text.split('\n').filter(Boolean);
770
+ for (const l of lines)
771
+ process.stdout.write(style.dim(`[${jobId}] ${l.trimRight()} |> `));
772
+ });
773
+ child.on('close', (code) => {
774
+ jobs.delete(jobId);
775
+ process.stdout.write(style.dim(`[${jobId}] finished (exit ${code ?? 0})`) + '\n');
776
+ });
777
+ continue;
778
+ }
779
+ }
780
+ await runBash(dispatch.cmd);
781
+ continue;
782
+ }
783
+ if (dispatch.kind === 'chat') {
784
+ if (!opencodeAvailable) {
785
+ process.stdout.write(style.dim('opencode is not available on PATH. Install it to use chat.') + '\n');
786
+ continue;
787
+ }
788
+ await runChat(dispatch.msg);
789
+ }
790
+ }
791
+ }
792
+ finally {
793
+ process.removeListener('SIGINT', sigintHandler);
794
+ }
795
+ return exitCode;
796
+ // ── Bash runner ─────────────────────────────────────────────────────────
797
+ async function runBash(cmd) {
798
+ const cdMatch = cmd.match(/^cd(?:\s+(.+))?$/);
799
+ if (cdMatch) {
800
+ const target = cdMatch[1]?.trim() ?? homedir();
801
+ const resolved = resolve(currentCwd, expandHome(target));
802
+ // Verify the target exists and is a directory before updating cwd.
803
+ // Without this, subsequent spawn() calls inherit a missing cwd and
804
+ // fail with `Error: spawn /bin/sh ENOENT`.
805
+ if (!existsSync(resolved)) {
806
+ process.stdout.write(`cd: no such file or directory: ${target}\n`);
807
+ appendTranscript(dataDir, cwd0, {
808
+ ts: new Date().toISOString(),
809
+ kind: 'bash',
810
+ input: cmd,
811
+ output: `cd: no such file or directory: ${target}`,
812
+ exitCode: 1
813
+ });
814
+ return;
815
+ }
816
+ if (!statSync(resolved).isDirectory()) {
817
+ process.stdout.write(`cd: not a directory: ${target}\n`);
818
+ appendTranscript(dataDir, cwd0, {
819
+ ts: new Date().toISOString(),
820
+ kind: 'bash',
821
+ input: cmd,
822
+ output: `cd: not a directory: ${target}`,
823
+ exitCode: 1
824
+ });
825
+ return;
826
+ }
827
+ currentCwd = resolved;
828
+ completionContext.cwd = resolved;
829
+ process.stdout.write(style.dim(`→ ${resolved}`) + '\n');
830
+ appendTranscript(dataDir, cwd0, {
831
+ ts: new Date().toISOString(),
832
+ kind: 'bash',
833
+ input: cmd,
834
+ output: `→ ${resolved}`
835
+ });
836
+ if (firstTurn) {
837
+ firstTurn = false;
838
+ process.stdout.write(style.dim('Tip: type `/help` to see all agora commands.') + '\n');
839
+ }
840
+ return;
841
+ }
842
+ let buffer = '';
843
+ let done = false;
844
+ let childExitCode = 0;
845
+ let childRef = null;
846
+ const abortChild = () => {
847
+ if (childRef)
848
+ childRef.kill('SIGINT');
849
+ };
850
+ process.on('SIGINT', abortChild);
851
+ childActive = true;
852
+ await new Promise((res) => {
853
+ const child = spawn(cmd, {
854
+ shell: true,
855
+ cwd: currentCwd,
856
+ env: env,
857
+ stdio: ['inherit', 'pipe', 'pipe']
858
+ });
859
+ childRef = child;
860
+ child.stdout?.on('data', (chunk) => {
861
+ const text = chunk.toString();
862
+ process.stdout.write(text);
863
+ buffer = tailBuffer(buffer + text, MAX_BASH_BUFFER);
864
+ });
865
+ child.stderr?.on('data', (chunk) => {
866
+ const text = chunk.toString();
867
+ process.stderr.write(text);
868
+ buffer = tailBuffer(buffer + text, MAX_BASH_BUFFER);
869
+ });
870
+ child.on('close', (code) => {
871
+ childExitCode = code ?? 0;
872
+ done = true;
873
+ res();
874
+ });
875
+ child.on('error', (err) => {
876
+ process.stderr.write(`Error: ${err.message}\n`);
877
+ done = true;
878
+ res();
879
+ });
880
+ });
881
+ process.removeListener('SIGINT', abortChild);
882
+ childActive = false;
883
+ if (!done)
884
+ childExitCode = 1;
885
+ // Show non-zero exit code
886
+ if (childExitCode !== 0) {
887
+ process.stdout.write(style.dim(`· exit ${childExitCode}`) + '\n');
888
+ }
889
+ appendTranscript(dataDir, cwd0, {
890
+ ts: new Date().toISOString(),
891
+ kind: 'bash',
892
+ input: cmd,
893
+ output: buffer,
894
+ exitCode: childExitCode
895
+ });
896
+ if (firstTurn) {
897
+ firstTurn = false;
898
+ process.stdout.write(style.dim('Tip: type `/help` to see all agora commands.') + '\n');
899
+ }
900
+ }
901
+ // ── Chat runner ──────────────────────────────────────────────────────────
902
+ async function runChat(userMsg) {
903
+ const bashCtx = recentBashContext(dataDir, cwd0, { commands: 3, lines: 20 });
904
+ const systemLine = 'You are running inside the Agora shell, a marketplace TUI for OpenCode. ' +
905
+ 'Prefer using the agora_* MCP tools when the user asks marketplace questions. ' +
906
+ 'Be concise; output flows directly to a terminal.';
907
+ let fullPrompt = `<system>\n${systemLine}`;
908
+ if (bashCtx)
909
+ fullPrompt += `\n${bashCtx}`;
910
+ fullPrompt += `\n<user>\n${userMsg}`;
911
+ const modelArg = FREE_MODELS[0].includes('/') ? FREE_MODELS[0] : `opencode/${FREE_MODELS[0]}`;
912
+ const args = ['run', '--format', 'json', '--model', modelArg];
913
+ if (meta.sessionId)
914
+ args.push('--session', meta.sessionId);
915
+ args.push(fullPrompt);
916
+ const renderer = createChatRenderer({
917
+ verbosity,
918
+ style,
919
+ trueColor,
920
+ out: process.stdout
921
+ });
922
+ let chatChildRef = null;
923
+ const abortChat = () => {
924
+ if (chatChildRef)
925
+ chatChildRef.kill('SIGINT');
926
+ };
927
+ process.on('SIGINT', abortChat);
928
+ childActive = true;
929
+ // Accumulate last 4 KB of stderr for failure diagnosis
930
+ let errBuffer = '';
931
+ let chatExitCode = 0;
932
+ let spawnError = null;
933
+ await new Promise((res) => {
934
+ const child = spawn('opencode', args, {
935
+ env: env,
936
+ stdio: ['ignore', 'pipe', 'pipe'],
937
+ shell: false
938
+ });
939
+ chatChildRef = child;
940
+ child.stdout?.on('data', (chunk) => {
941
+ const text = chunk.toString();
942
+ for (const rawLine of text.split('\n').filter(Boolean)) {
943
+ renderer.handleLine(rawLine);
944
+ }
945
+ });
946
+ child.stderr?.on('data', (chunk) => {
947
+ errBuffer = tailBuffer(errBuffer + chunk.toString(), 4096);
948
+ });
949
+ child.on('close', (code) => {
950
+ chatExitCode = code ?? 0;
951
+ res();
952
+ });
953
+ child.on('error', (err) => {
954
+ spawnError = err;
955
+ res();
956
+ });
957
+ });
958
+ process.removeListener('SIGINT', abortChat);
959
+ childActive = false;
960
+ renderer.finalize();
961
+ totalCost += renderer.getTotalCost();
962
+ // Detect and report chat failures
963
+ const chatFailed = spawnError !== null || (chatExitCode !== 0 && !renderer.hasReceivedText());
964
+ if (chatFailed) {
965
+ let reason;
966
+ if (spawnError) {
967
+ reason = 'opencode binary not found';
968
+ }
969
+ else if (errBuffer.includes('Model not found')) {
970
+ reason = '/model to pick another model (or check OPENCODE_MODEL)';
971
+ }
972
+ else {
973
+ reason = 'chat failed; see /transcript for details';
974
+ }
975
+ process.stdout.write('\x1b[31m▍\x1b[0m' + style.dim(` failed · ${reason}`) + '\n');
976
+ }
977
+ const renderedSessionId = renderer.getSessionId();
978
+ if (!meta.sessionId && renderedSessionId) {
979
+ meta.sessionId = renderedSessionId;
980
+ writeSessionMeta(dataDir, cwd0, meta);
981
+ }
982
+ const assistantBuffer = renderer.getAssistantText();
983
+ appendTranscript(dataDir, cwd0, {
984
+ ts: new Date().toISOString(),
985
+ kind: 'chat-user',
986
+ input: userMsg
987
+ });
988
+ appendTranscript(dataDir, cwd0, {
989
+ ts: new Date().toISOString(),
990
+ kind: 'chat-assistant',
991
+ output: assistantBuffer
992
+ });
993
+ meta.turnCount += 1;
994
+ meta.lastUsedAt = new Date().toISOString();
995
+ writeSessionMeta(dataDir, cwd0, meta);
996
+ if (firstTurn) {
997
+ firstTurn = false;
998
+ process.stdout.write(style.dim('Tip: type `/help` to see all agora commands.') + '\n');
999
+ }
1000
+ // Code-block hotkeys
1001
+ await handleCodeBlock(assistantBuffer);
1002
+ }
1003
+ // ── Code-block hotkeys ───────────────────────────────────────────────────
1004
+ async function handleCodeBlock(text) {
1005
+ const block = extractFirstBashBlock(text);
1006
+ if (!block)
1007
+ return;
1008
+ process.stdout.write(style.dim('Code block: ') + style.accent('(r)un · (c)opy · (e)dit · (s)kip') + ' ');
1009
+ const key = await readOneKey();
1010
+ process.stdout.write('\n');
1011
+ if (key === 'r') {
1012
+ await runBash(block);
1013
+ appendTranscript(dataDir, cwd0, {
1014
+ ts: new Date().toISOString(),
1015
+ kind: 'bash',
1016
+ input: block,
1017
+ output: ''
1018
+ });
1019
+ }
1020
+ else if (key === 'c') {
1021
+ copyToClipboard(block);
1022
+ process.stdout.write(style.dim('Copied to clipboard.') + '\n');
1023
+ }
1024
+ else if (key === 'e') {
1025
+ const tmpDir = mkdtempSync(join(tmpdir(), 'agora-'));
1026
+ const tmpFile = join(tmpDir, 'block.sh');
1027
+ writeFileSync(tmpFile, block, 'utf8');
1028
+ const editor = process.env.EDITOR ?? 'vi';
1029
+ await new Promise((res) => {
1030
+ const child = spawn(editor, [tmpFile], { stdio: 'inherit', shell: false });
1031
+ child.on('close', () => res());
1032
+ child.on('error', () => res());
1033
+ });
1034
+ // After editor, offer run/skip
1035
+ process.stdout.write(style.dim('(r)un · (s)kip '));
1036
+ const key2 = await readOneKey();
1037
+ process.stdout.write('\n');
1038
+ if (key2 === 'r') {
1039
+ const { readFileSync } = await import('node:fs');
1040
+ const edited = readFileSync(tmpFile, 'utf8').trim();
1041
+ await runBash(edited);
1042
+ }
1043
+ }
1044
+ // s or other: skip silently
1045
+ }
1046
+ }
1047
+ function printLetterHelp(style) {
1048
+ const lines = [
1049
+ style.accent('Agora Shell — letter shortcuts'),
1050
+ '',
1051
+ ' /a again /b browse /c community /d doctor /e env',
1052
+ ' /f fg /g search /h home /i init /j jobs',
1053
+ ' /k search /l last /m marketplace /n news /o browse',
1054
+ ' /p preferences /q quit /r reviews /s settings /t terminal',
1055
+ ' /u use /v verbose /w watch /x export /y history',
1056
+ ' /z doctor --fix',
1057
+ '',
1058
+ 'Shortcuts are exact matches only (no arguments).',
1059
+ 'Type /help for full command reference, /abc to show this again.'
1060
+ ];
1061
+ process.stdout.write(lines.join('\n') + '\n\n');
1062
+ }
1063
+ function printHelp(style) {
1064
+ const lines = [
1065
+ style.accent('Agora Shell — help'),
1066
+ '',
1067
+ style.dim('Dispatch rules:'),
1068
+ ' <command> first word on PATH → run as bash',
1069
+ ' <anything> else → send to AI chat',
1070
+ ' !<cmd> force bash (e.g. !ls -la)',
1071
+ ' ?<msg> force chat (e.g. ?what is MCP)',
1072
+ ' /tui open the full-screen TUI on Home',
1073
+ ' /home /market /comm /news /settings open the TUI on that page',
1074
+ ' /menu open the command-browser menu',
1075
+ ' /transcript print last 20 transcript entries',
1076
+ ' /clear clear screen',
1077
+ ' /help this help',
1078
+ ' /quit exit shell',
1079
+ ' /last re-run most recent bash command',
1080
+ ' /again re-send most recent chat message',
1081
+ ' /? <cmd> dry-run an agora command (e.g. /? install mcp-github)',
1082
+ ' /abc show letter-shortcut reference (/a /b /c ...)',
1083
+ ' /jobs list background jobs',
1084
+ ' /fg [N] bring job N (or last) to foreground',
1085
+ ' /bg [N] resume job N (or last) in background',
1086
+ '',
1087
+ style.dim('Verbosity:'),
1088
+ ' /verbose /medium /quiet',
1089
+ '',
1090
+ style.dim('Free AI models:'),
1091
+ ...FREE_MODELS.map((m) => ` ${m}`),
1092
+ '',
1093
+ style.dim('Agora commands (also available as /slash shortcuts):')
1094
+ ];
1095
+ const groups = ['Marketplace', 'Setup', 'Library', 'Learn', 'Community'];
1096
+ for (const g of groups) {
1097
+ const cmds = COMMANDS.filter((c) => c.group === g);
1098
+ lines.push(` ${style.dim(g)}`);
1099
+ for (const c of cmds) {
1100
+ lines.push(` ${style.accent(c.name.padEnd(14))} ${c.summary}`);
1101
+ }
1102
+ }
1103
+ lines.push('', style.dim(`agora v${AGORA_VERSION} · run \`agora help <command>\` or \`/<command> --help\` for details`));
1104
+ process.stdout.write(lines.join('\n') + '\n\n');
1105
+ }
1106
+ //# sourceMappingURL=shell.js.map