opencode-agora 0.4.0 → 0.4.2

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 (248) hide show
  1. package/README.md +89 -415
  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 +11 -19
  11. package/dist/cli/app.d.ts.map +1 -1
  12. package/dist/cli/app.js +159 -2028
  13. package/dist/cli/app.js.map +1 -1
  14. package/dist/cli/commands/browse.d.ts +4 -0
  15. package/dist/cli/commands/browse.d.ts.map +1 -0
  16. package/dist/cli/commands/browse.js +80 -0
  17. package/dist/cli/commands/browse.js.map +1 -0
  18. package/dist/cli/commands/chat.d.ts +4 -0
  19. package/dist/cli/commands/chat.d.ts.map +1 -0
  20. package/dist/cli/commands/chat.js +125 -0
  21. package/dist/cli/commands/chat.js.map +1 -0
  22. package/dist/cli/commands/community.d.ts +12 -0
  23. package/dist/cli/commands/community.d.ts.map +1 -0
  24. package/dist/cli/commands/community.js +453 -0
  25. package/dist/cli/commands/community.js.map +1 -0
  26. package/dist/cli/commands/export.d.ts +3 -0
  27. package/dist/cli/commands/export.d.ts.map +1 -0
  28. package/dist/cli/commands/export.js +108 -0
  29. package/dist/cli/commands/export.js.map +1 -0
  30. package/dist/cli/commands/init.d.ts +4 -0
  31. package/dist/cli/commands/init.d.ts.map +1 -0
  32. package/dist/cli/commands/init.js +299 -0
  33. package/dist/cli/commands/init.js.map +1 -0
  34. package/dist/cli/commands/learn.d.ts +4 -0
  35. package/dist/cli/commands/learn.d.ts.map +1 -0
  36. package/dist/cli/commands/learn.js +62 -0
  37. package/dist/cli/commands/learn.js.map +1 -0
  38. package/dist/cli/commands/marketplace.d.ts +9 -0
  39. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  40. package/dist/cli/commands/marketplace.js +321 -0
  41. package/dist/cli/commands/marketplace.js.map +1 -0
  42. package/dist/cli/commands/notify.d.ts +3 -0
  43. package/dist/cli/commands/notify.d.ts.map +1 -0
  44. package/dist/cli/commands/notify.js +59 -0
  45. package/dist/cli/commands/notify.js.map +1 -0
  46. package/dist/cli/commands/operations.d.ts +16 -0
  47. package/dist/cli/commands/operations.d.ts.map +1 -0
  48. package/dist/cli/commands/operations.js +1041 -0
  49. package/dist/cli/commands/operations.js.map +1 -0
  50. package/dist/cli/commands/outdated.d.ts +3 -0
  51. package/dist/cli/commands/outdated.d.ts.map +1 -0
  52. package/dist/cli/commands/outdated.js +48 -0
  53. package/dist/cli/commands/outdated.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/scan.d.ts +3 -0
  59. package/dist/cli/commands/scan.d.ts.map +1 -0
  60. package/dist/cli/commands/scan.js +35 -0
  61. package/dist/cli/commands/scan.js.map +1 -0
  62. package/dist/cli/commands/today.d.ts +3 -0
  63. package/dist/cli/commands/today.d.ts.map +1 -0
  64. package/dist/cli/commands/today.js +142 -0
  65. package/dist/cli/commands/today.js.map +1 -0
  66. package/dist/cli/commands/types.d.ts +5 -0
  67. package/dist/cli/commands/types.d.ts.map +1 -0
  68. package/dist/cli/commands/types.js +2 -0
  69. package/dist/cli/commands/types.js.map +1 -0
  70. package/dist/cli/commands/watch.d.ts +3 -0
  71. package/dist/cli/commands/watch.d.ts.map +1 -0
  72. package/dist/cli/commands/watch.js +41 -0
  73. package/dist/cli/commands/watch.js.map +1 -0
  74. package/dist/cli/commands/welcome.d.ts +3 -0
  75. package/dist/cli/commands/welcome.d.ts.map +1 -0
  76. package/dist/cli/commands/welcome.js +97 -0
  77. package/dist/cli/commands/welcome.js.map +1 -0
  78. package/dist/cli/commands-meta.d.ts.map +1 -1
  79. package/dist/cli/commands-meta.js +286 -29
  80. package/dist/cli/commands-meta.js.map +1 -1
  81. package/dist/cli/completions-gen.d.ts +2 -0
  82. package/dist/cli/completions-gen.d.ts.map +1 -0
  83. package/dist/cli/completions-gen.js +195 -0
  84. package/dist/cli/completions-gen.js.map +1 -0
  85. package/dist/cli/completions.d.ts.map +1 -1
  86. package/dist/cli/completions.js +42 -5
  87. package/dist/cli/completions.js.map +1 -1
  88. package/dist/cli/flags.d.ts +19 -0
  89. package/dist/cli/flags.d.ts.map +1 -0
  90. package/dist/cli/flags.js +91 -0
  91. package/dist/cli/flags.js.map +1 -0
  92. package/dist/cli/format.d.ts +19 -0
  93. package/dist/cli/format.d.ts.map +1 -0
  94. package/dist/cli/format.js +249 -0
  95. package/dist/cli/format.js.map +1 -0
  96. package/dist/cli/helpers.d.ts +95 -0
  97. package/dist/cli/helpers.d.ts.map +1 -0
  98. package/dist/cli/helpers.js +301 -0
  99. package/dist/cli/helpers.js.map +1 -0
  100. package/dist/cli/mcp-server.d.ts +7 -1
  101. package/dist/cli/mcp-server.d.ts.map +1 -1
  102. package/dist/cli/mcp-server.js +70 -2
  103. package/dist/cli/mcp-server.js.map +1 -1
  104. package/dist/cli/menu.d.ts.map +1 -1
  105. package/dist/cli/menu.js +11 -3
  106. package/dist/cli/menu.js.map +1 -1
  107. package/dist/cli/pages/community.d.ts +6 -0
  108. package/dist/cli/pages/community.d.ts.map +1 -1
  109. package/dist/cli/pages/community.js +882 -64
  110. package/dist/cli/pages/community.js.map +1 -1
  111. package/dist/cli/pages/helpers.d.ts +14 -9
  112. package/dist/cli/pages/helpers.d.ts.map +1 -1
  113. package/dist/cli/pages/helpers.js +37 -6
  114. package/dist/cli/pages/helpers.js.map +1 -1
  115. package/dist/cli/pages/home.d.ts +1 -0
  116. package/dist/cli/pages/home.d.ts.map +1 -1
  117. package/dist/cli/pages/home.js +203 -120
  118. package/dist/cli/pages/home.js.map +1 -1
  119. package/dist/cli/pages/marketplace.d.ts +2 -0
  120. package/dist/cli/pages/marketplace.d.ts.map +1 -1
  121. package/dist/cli/pages/marketplace.js +524 -62
  122. package/dist/cli/pages/marketplace.js.map +1 -1
  123. package/dist/cli/pages/news.d.ts +28 -0
  124. package/dist/cli/pages/news.d.ts.map +1 -1
  125. package/dist/cli/pages/news.js +209 -82
  126. package/dist/cli/pages/news.js.map +1 -1
  127. package/dist/cli/pages/settings.d.ts.map +1 -1
  128. package/dist/cli/pages/settings.js +163 -33
  129. package/dist/cli/pages/settings.js.map +1 -1
  130. package/dist/cli/pages/types.d.ts +1 -1
  131. package/dist/cli/pages/types.d.ts.map +1 -1
  132. package/dist/cli/prompter.d.ts.map +1 -1
  133. package/dist/cli/prompter.js +43 -8
  134. package/dist/cli/prompter.js.map +1 -1
  135. package/dist/cli/shell.d.ts +2 -2
  136. package/dist/cli/shell.d.ts.map +1 -1
  137. package/dist/cli/shell.js +321 -18
  138. package/dist/cli/shell.js.map +1 -1
  139. package/dist/cli/tui.d.ts +1 -1
  140. package/dist/cli/tui.d.ts.map +1 -1
  141. package/dist/cli/tui.js +69 -23
  142. package/dist/cli/tui.js.map +1 -1
  143. package/dist/community/client.d.ts +45 -8
  144. package/dist/community/client.d.ts.map +1 -1
  145. package/dist/community/client.js +118 -23
  146. package/dist/community/client.js.map +1 -1
  147. package/dist/community/search.d.ts +25 -0
  148. package/dist/community/search.d.ts.map +1 -0
  149. package/dist/community/search.js +62 -0
  150. package/dist/community/search.js.map +1 -0
  151. package/dist/community/types.d.ts +21 -0
  152. package/dist/community/types.d.ts.map +1 -1
  153. package/dist/community/types.js +1 -1
  154. package/dist/config.d.ts +0 -4
  155. package/dist/config.d.ts.map +1 -1
  156. package/dist/config.js +0 -15
  157. package/dist/config.js.map +1 -1
  158. package/dist/data.d.ts.map +1 -1
  159. package/dist/data.js +142 -68
  160. package/dist/data.js.map +1 -1
  161. package/dist/format.d.ts +0 -2
  162. package/dist/format.d.ts.map +1 -1
  163. package/dist/format.js +0 -2
  164. package/dist/format.js.map +1 -1
  165. package/dist/hubs/cache.d.ts +6 -0
  166. package/dist/hubs/cache.d.ts.map +1 -0
  167. package/dist/hubs/cache.js +46 -0
  168. package/dist/hubs/cache.js.map +1 -0
  169. package/dist/hubs/enrichment.d.ts +43 -0
  170. package/dist/hubs/enrichment.d.ts.map +1 -0
  171. package/dist/hubs/enrichment.js +239 -0
  172. package/dist/hubs/enrichment.js.map +1 -0
  173. package/dist/hubs/github.d.ts +12 -0
  174. package/dist/hubs/github.d.ts.map +1 -0
  175. package/dist/hubs/github.js +54 -0
  176. package/dist/hubs/github.js.map +1 -0
  177. package/dist/hubs/huggingface.d.ts +27 -0
  178. package/dist/hubs/huggingface.d.ts.map +1 -0
  179. package/dist/hubs/huggingface.js +88 -0
  180. package/dist/hubs/huggingface.js.map +1 -0
  181. package/dist/hubs/quality.d.ts +26 -0
  182. package/dist/hubs/quality.d.ts.map +1 -0
  183. package/dist/hubs/quality.js +57 -0
  184. package/dist/hubs/quality.js.map +1 -0
  185. package/dist/hubs/types.d.ts +30 -0
  186. package/dist/hubs/types.d.ts.map +1 -0
  187. package/dist/hubs/types.js +2 -0
  188. package/dist/hubs/types.js.map +1 -0
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +84 -0
  191. package/dist/index.js.map +1 -1
  192. package/dist/init.js +2 -2
  193. package/dist/init.js.map +1 -1
  194. package/dist/live.d.ts +10 -0
  195. package/dist/live.d.ts.map +1 -1
  196. package/dist/live.js +31 -1
  197. package/dist/live.js.map +1 -1
  198. package/dist/marketplace.d.ts +16 -3
  199. package/dist/marketplace.d.ts.map +1 -1
  200. package/dist/marketplace.js +174 -7
  201. package/dist/marketplace.js.map +1 -1
  202. package/dist/news/cache.d.ts.map +1 -1
  203. package/dist/news/cache.js +4 -3
  204. package/dist/news/cache.js.map +1 -1
  205. package/dist/news/score.js +1 -1
  206. package/dist/news/sources/arxiv.d.ts.map +1 -1
  207. package/dist/news/sources/arxiv.js +10 -6
  208. package/dist/news/sources/arxiv.js.map +1 -1
  209. package/dist/news/sources/github-trending.d.ts.map +1 -1
  210. package/dist/news/sources/github-trending.js +9 -5
  211. package/dist/news/sources/github-trending.js.map +1 -1
  212. package/dist/news/sources/hn.d.ts.map +1 -1
  213. package/dist/news/sources/hn.js +8 -4
  214. package/dist/news/sources/hn.js.map +1 -1
  215. package/dist/news/sources/reddit.d.ts.map +1 -1
  216. package/dist/news/sources/reddit.js +5 -4
  217. package/dist/news/sources/reddit.js.map +1 -1
  218. package/dist/news/sources/rss.d.ts +10 -11
  219. package/dist/news/sources/rss.d.ts.map +1 -1
  220. package/dist/news/sources/rss.js +11 -99
  221. package/dist/news/sources/rss.js.map +1 -1
  222. package/dist/news/types.d.ts +3 -0
  223. package/dist/news/types.d.ts.map +1 -1
  224. package/dist/news/types.js +15 -6
  225. package/dist/news/types.js.map +1 -1
  226. package/dist/outdated.d.ts +24 -0
  227. package/dist/outdated.d.ts.map +1 -0
  228. package/dist/outdated.js +53 -0
  229. package/dist/outdated.js.map +1 -0
  230. package/dist/preferences.d.ts.map +1 -1
  231. package/dist/preferences.js +4 -4
  232. package/dist/preferences.js.map +1 -1
  233. package/dist/scan.d.ts +26 -0
  234. package/dist/scan.d.ts.map +1 -0
  235. package/dist/scan.js +179 -0
  236. package/dist/scan.js.map +1 -0
  237. package/dist/settings.d.ts.map +1 -1
  238. package/dist/settings.js +14 -20
  239. package/dist/settings.js.map +1 -1
  240. package/dist/state.d.ts +9 -2
  241. package/dist/state.d.ts.map +1 -1
  242. package/dist/state.js +41 -19
  243. package/dist/state.js.map +1 -1
  244. package/dist/types.d.ts +13 -0
  245. package/dist/types.d.ts.map +1 -1
  246. package/dist/ui.d.ts +1 -1
  247. package/dist/ui.js +1 -1
  248. package/package.json +4 -2
package/dist/cli/shell.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { execSync, spawn } from 'node:child_process';
2
- import { existsSync, mkdtempSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { appendFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { homedir, tmpdir } from 'node:os';
4
4
  import { join, resolve, sep } from 'node:path';
5
5
  import { COMMANDS } from './commands-meta.js';
6
6
  import { runInteractiveMenu } from './menu.js';
7
7
  import { runTui } from './tui.js';
8
- import { AGORA_VERSION, FREE_MODELS } from './app.js';
8
+ import { AGORA_VERSION } from './app.js';
9
+ import { FREE_MODELS } from './commands/chat.js';
9
10
  import { detectAgoraDataDir, loadAgoraState, resolveSavedItems } from '../state.js';
10
11
  import { appendTranscript, loadSessionMeta, readTranscript, recentBashContext, writeSessionMeta } from '../transcript.js';
11
12
  import { gradientText, renderBanner, supportsTrueColor } from '../ui.js';
@@ -15,6 +16,8 @@ import { completeShellLine, ghostFromHistory } from './completions.js';
15
16
  import { getMarketplaceItems } from '../marketplace.js';
16
17
  const SHELL_BUILTINS = new Set(['cd', 'export', 'alias', 'source', 'unset', 'umask', 'exec']);
17
18
  const MAX_BASH_BUFFER = 16 * 1024;
19
+ const SHELL_HISTORY_FILE = 'shell-history.jsonl';
20
+ const SHELL_HISTORY_MAX = 2000;
18
21
  /** Map slash aliases (with leading `/`) to TUI page ids. */
19
22
  const TUI_SLASH_ALIASES = {
20
23
  '/tui': 'default',
@@ -26,6 +29,34 @@ const TUI_SLASH_ALIASES = {
26
29
  '/news': 'news',
27
30
  '/settings': 'settings'
28
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
+ };
29
60
  const QUESTION_STARTERS = new Set([
30
61
  'what',
31
62
  'why',
@@ -58,12 +89,20 @@ const QUESTION_STARTERS = new Set([
58
89
  'hey',
59
90
  'thanks'
60
91
  ]);
92
+ const ENV_VAR_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
93
+ const BACKTICK_RE = /^`[^`]+`/;
61
94
  export function looksLikeQuestion(line) {
62
95
  const trimmed = line.trim();
63
96
  if (!trimmed)
64
97
  return false;
65
98
  if (trimmed.endsWith('?'))
66
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;
67
106
  const words = trimmed.split(/\s+/);
68
107
  const firstWord = words[0].toLowerCase();
69
108
  if (QUESTION_STARTERS.has(firstWord))
@@ -79,6 +118,8 @@ export function classifyInput(line, isExecutable) {
79
118
  const trimmed = line.trim();
80
119
  if (!trimmed)
81
120
  return { kind: 'noop' };
121
+ if (trimmed === '/abc')
122
+ return { kind: 'meta', sub: 'abc' };
82
123
  if (trimmed === '/help')
83
124
  return { kind: 'meta', sub: 'help' };
84
125
  if (trimmed === '/quit')
@@ -103,6 +144,14 @@ export function classifyInput(line, isExecutable) {
103
144
  return { kind: 'meta', sub: 'last' };
104
145
  if (trimmed === '/again')
105
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() };
106
155
  if (trimmed.startsWith('/? '))
107
156
  return { kind: 'meta', sub: 'dry-run', args: trimmed.slice(3).trim() };
108
157
  if (trimmed.startsWith('!'))
@@ -116,7 +165,18 @@ export function classifyInput(line, isExecutable) {
116
165
  const target = TUI_SLASH_ALIASES[trimmed];
117
166
  return target === 'default' ? { kind: 'tui' } : { kind: 'tui', page: target };
118
167
  }
119
- // Slash-prefixed inputs that weren't an exact meta or TUI match are
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
120
180
  // forwarded to the `agora` CLI: `/agora help`, `/agora search foo`,
121
181
  // `/help tutorials`, `/foo` all become `agora <args>`. Never let
122
182
  // `/anything` fall through to bash — PATH-joining an absolute name like
@@ -244,6 +304,44 @@ function copyToClipboard(text) {
244
304
  // fall back silently
245
305
  }
246
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
+ }
247
345
  // ── Main shell loop ─────────────────────────────────────────────────────────
248
346
  export async function runShell(io, style) {
249
347
  const env = io.env ?? {};
@@ -253,7 +351,7 @@ export async function runShell(io, style) {
253
351
  const motto = "Developers' CLI marketplace and community hub - type a command, bash or chat:";
254
352
  const mottoLine = gradientText(motto, { trueColor });
255
353
  const model = FREE_MODELS[0];
256
- const infoLine = style.dim(`v${AGORA_VERSION} · ${model} · /terminal · /help · /menu · /search · /quit`);
354
+ const infoLine = style.dim(`v${AGORA_VERSION} · ${model} · /abc · /help · /menu · /search · /quit`);
257
355
  const slashLine = style.orange('/home · /marketplace · /community · /news · /settings');
258
356
  process.stdout.write(`\n${banner}\n\n${mottoLine}\n\n${infoLine}\n${slashLine}\n\n`);
259
357
  }
@@ -286,6 +384,9 @@ export async function runShell(io, style) {
286
384
  let verbosity = 'medium';
287
385
  let childActive = false;
288
386
  let totalCost = 0;
387
+ const trackedEnv = new Map();
388
+ let jobCounter = 0;
389
+ const jobs = new Map();
289
390
  // Lazy caches for completion ids
290
391
  let cachedMarketplaceIds = null;
291
392
  let cachedSavedIds = null;
@@ -312,6 +413,7 @@ export async function runShell(io, style) {
312
413
  '/comm',
313
414
  '/news',
314
415
  '/settings',
416
+ '/abc',
315
417
  '/help',
316
418
  '/menu',
317
419
  '/transcript',
@@ -323,6 +425,10 @@ export async function runShell(io, style) {
323
425
  '/exit',
324
426
  '/last',
325
427
  '/again',
428
+ '/env',
429
+ '/jobs',
430
+ '/fg',
431
+ '/bg',
326
432
  ...agoraSlashCommands
327
433
  ];
328
434
  // Deduplicate: /news, /home etc may appear in both lists
@@ -348,9 +454,10 @@ export async function runShell(io, style) {
348
454
  },
349
455
  cwd: currentCwd
350
456
  };
351
- // In-memory history for prompter (not persisted transcript covers that)
352
- const history = [];
457
+ // Shell history persisted to disk and loaded on startup
458
+ const history = loadShellHistory(dataDir);
353
459
  const tips = [
460
+ 'type /abc for a quick letter-shortcut reference (/a /b /c ...)',
354
461
  'type /help to see all slash commands',
355
462
  'type /menu to browse the command catalog',
356
463
  'type ?<msg> to force AI chat',
@@ -358,20 +465,39 @@ export async function runShell(io, style) {
358
465
  'type /clear to reset and see the home banner',
359
466
  'type /transcript to see your last 20 commands',
360
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',
361
470
  'type ?how do I use MCP? to ask about the marketplace',
362
471
  'type /verbose for detailed AI responses',
363
472
  'type /quiet for minimal AI responses',
473
+ 'type /env to view tracked env vars, /env FOO=val to set one',
364
474
  'press Tab to auto-complete commands and paths',
365
475
  'press Ctrl-R to reverse-search your history',
366
476
  'press Ctrl-L to clear the screen',
367
477
  'press Esc to dismiss ghost suggestions',
368
478
  'run `agora search --table` for a table view',
369
479
  'run `agora search --sort stars` to sort by stars',
370
- 'run `agora search --sort name --order asc` for alphabetical'
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'
371
497
  ];
372
498
  // Amber chevron when opencode unavailable, accent otherwise
373
- const accentChevron = opencodeAvailable ? style.accent('›') : '\x1b[38;5;214m›\x1b[0m';
374
- // B.3 — static portion of the prompt (no chevron); suffix added dynamically
499
+ const accentChevron = opencodeAvailable ? style.accent('›') : style.orange('');
500
+ // Static portion of the prompt (no chevron); suffix added dynamically
375
501
  function buildPromptBase() {
376
502
  return style.accent('agora') + ' ' + style.dim(shortCwd(currentCwd)) + ' ';
377
503
  }
@@ -382,15 +508,18 @@ export async function runShell(io, style) {
382
508
  }
383
509
  function buildContextLine() {
384
510
  const model = FREE_MODELS[0];
385
- const tip = tips[(meta?.turnCount ?? 0) % tips.length];
386
- return style.dim(`model: ${model} · ${tip}`);
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(' · '));
387
518
  }
388
519
  const sigintHandler = () => {
520
+ // SIGINT while idle: prompter handles Ctrl-C bytes directly via raw mode
389
521
  if (childActive)
390
522
  return;
391
- // Ctrl-C while idle: the prompter handles abort; SIGINT from outside is rare
392
- process.stdout.write('\n');
393
- process.exit(0);
394
523
  };
395
524
  process.on('SIGINT', sigintHandler);
396
525
  try {
@@ -417,9 +546,10 @@ export async function runShell(io, style) {
417
546
  const line = result.value;
418
547
  if (!line.trim())
419
548
  continue;
420
- // Add to history
549
+ // Add to history and persist
421
550
  if (history[history.length - 1] !== line) {
422
551
  history.push(line);
552
+ appendShellHistory(dataDir, line);
423
553
  }
424
554
  const dispatch = classifyInput(line, isExecutable);
425
555
  if (dispatch.kind === 'noop')
@@ -432,6 +562,10 @@ export async function runShell(io, style) {
432
562
  printHome();
433
563
  continue;
434
564
  }
565
+ if (dispatch.sub === 'abc') {
566
+ printLetterHelp(style);
567
+ continue;
568
+ }
435
569
  if (dispatch.sub === 'help') {
436
570
  printHelp(style);
437
571
  continue;
@@ -503,6 +637,115 @@ export async function runShell(io, style) {
503
637
  }
504
638
  continue;
505
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
+ if (jobs.size === 0) {
655
+ process.stdout.write(style.dim('No background jobs. Append & to run a command in the background.') + '\n');
656
+ continue;
657
+ }
658
+ const arg = dispatch.args || '';
659
+ if (arg && Number.isNaN(parseInt(arg, 10))) {
660
+ process.stdout.write(style.dim(`Invalid job id: ${arg}`) + '\n');
661
+ continue;
662
+ }
663
+ const targetId = arg ? parseInt(arg, 10) : Math.max(...Array.from(jobs.keys()));
664
+ const job = jobs.get(targetId);
665
+ if (!job) {
666
+ process.stdout.write(style.dim(`Job ${targetId} not found.`) + '\n');
667
+ continue;
668
+ }
669
+ try {
670
+ process.kill(job.pid, 0);
671
+ process.stdout.write(style.dim(`Foreground: ${job.cmd}`) + '\n');
672
+ await new Promise((resolve) => {
673
+ const check = setInterval(() => {
674
+ try {
675
+ process.kill(job.pid, 0);
676
+ }
677
+ catch {
678
+ clearInterval(check);
679
+ jobs.delete(targetId);
680
+ resolve();
681
+ }
682
+ }, 200);
683
+ });
684
+ }
685
+ catch {
686
+ jobs.delete(targetId);
687
+ process.stdout.write(style.dim(`Job ${targetId} has finished.`) + '\n');
688
+ }
689
+ continue;
690
+ }
691
+ if (dispatch.sub === 'bg') {
692
+ if (jobs.size === 0) {
693
+ process.stdout.write(style.dim('No background jobs. Append & to run a command in the background.') + '\n');
694
+ continue;
695
+ }
696
+ const arg = dispatch.args || '';
697
+ if (arg && Number.isNaN(parseInt(arg, 10))) {
698
+ process.stdout.write(style.dim(`Invalid job id: ${arg}`) + '\n');
699
+ continue;
700
+ }
701
+ const targetId = arg ? parseInt(arg, 10) : Math.max(...Array.from(jobs.keys()));
702
+ const job = jobs.get(targetId);
703
+ if (!job) {
704
+ process.stdout.write(style.dim(`Job ${targetId} not found.`) + '\n');
705
+ continue;
706
+ }
707
+ try {
708
+ process.kill(job.pid, 0);
709
+ job.status = 'running';
710
+ process.stdout.write(style.dim(`[${targetId}] ${job.cmd} (background)`) + '\n');
711
+ }
712
+ catch {
713
+ jobs.delete(targetId);
714
+ process.stdout.write(style.dim(`Job ${targetId} has finished.`) + '\n');
715
+ }
716
+ continue;
717
+ }
718
+ if (dispatch.sub === 'env') {
719
+ const arg = dispatch.args || '';
720
+ if (!arg) {
721
+ if (trackedEnv.size === 0) {
722
+ process.stdout.write(style.dim('No tracked environment variables. Use /env VAR=value to set one.') + '\n');
723
+ }
724
+ else {
725
+ process.stdout.write(style.accent('Tracked environment') + '\n');
726
+ for (const [k, v] of [...trackedEnv.entries()].sort()) {
727
+ process.stdout.write(` ${k}=${v}\n`);
728
+ }
729
+ }
730
+ }
731
+ else if (arg.includes('=')) {
732
+ const eqIdx = arg.indexOf('=');
733
+ const key = arg.slice(0, eqIdx).trim();
734
+ const val = arg.slice(eqIdx + 1).trim();
735
+ trackedEnv.set(key, val);
736
+ process.stdout.write(style.dim(`${key}=${val} (tracked)`) + '\n');
737
+ }
738
+ else {
739
+ const val = trackedEnv.get(arg);
740
+ if (val === undefined) {
741
+ process.stdout.write(style.dim(`${arg} is not tracked.`) + '\n');
742
+ }
743
+ else {
744
+ process.stdout.write(`${arg}=${val}\n`);
745
+ }
746
+ }
747
+ continue;
748
+ }
506
749
  continue;
507
750
  }
508
751
  if (dispatch.kind === 'tui') {
@@ -510,6 +753,46 @@ export async function runShell(io, style) {
510
753
  continue;
511
754
  }
512
755
  if (dispatch.kind === 'bash') {
756
+ // Track env vars from export statements and VAR=val prefix
757
+ const bashLine = dispatch.cmd;
758
+ const exportMatch = bashLine.match(/^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.+)$/);
759
+ if (exportMatch) {
760
+ trackedEnv.set(exportMatch[1], exportMatch[2].trim());
761
+ }
762
+ else {
763
+ const prefixMatch = bashLine.match(/^([A-Za-z_][A-Za-z0-9_]*)=(\S+)\s+/);
764
+ if (prefixMatch) {
765
+ trackedEnv.set(prefixMatch[1], prefixMatch[2].trim());
766
+ }
767
+ }
768
+ // Background job handling
769
+ const bgMatch = bashLine.match(/^(.*?)\s*&$/);
770
+ if (bgMatch) {
771
+ const actualCmd = bgMatch[1].trim();
772
+ if (actualCmd) {
773
+ jobCounter++;
774
+ const jobId = jobCounter;
775
+ const child = spawn(actualCmd, {
776
+ shell: true,
777
+ cwd: currentCwd,
778
+ env: env,
779
+ stdio: ['ignore', 'pipe', 'pipe']
780
+ });
781
+ jobs.set(jobId, { pid: child.pid ?? 0, cmd: actualCmd, status: 'running' });
782
+ process.stdout.write(style.dim(`[${jobId}] ${actualCmd} (background, pid ${child.pid ?? '?'})`) + '\n');
783
+ child.stdout?.on('data', (chunk) => {
784
+ const text = chunk.toString();
785
+ const lines = text.split('\n').filter(Boolean);
786
+ for (const l of lines)
787
+ process.stdout.write(style.dim(`[${jobId}] ${l.trimRight()} |> `));
788
+ });
789
+ child.on('close', (code) => {
790
+ jobs.delete(jobId);
791
+ process.stdout.write(style.dim(`[${jobId}] finished (exit ${code ?? 0})`) + '\n');
792
+ });
793
+ continue;
794
+ }
795
+ }
513
796
  await runBash(dispatch.cmd);
514
797
  continue;
515
798
  }
@@ -615,7 +898,7 @@ export async function runShell(io, style) {
615
898
  childActive = false;
616
899
  if (!done)
617
900
  childExitCode = 1;
618
- // B.4 — show non-zero exit code
901
+ // Show non-zero exit code
619
902
  if (childExitCode !== 0) {
620
903
  process.stdout.write(style.dim(`· exit ${childExitCode}`) + '\n');
621
904
  }
@@ -659,7 +942,7 @@ export async function runShell(io, style) {
659
942
  };
660
943
  process.on('SIGINT', abortChat);
661
944
  childActive = true;
662
- // B.4 — accumulate last 4 KB of stderr for failure diagnosis
945
+ // Accumulate last 4 KB of stderr for failure diagnosis
663
946
  let errBuffer = '';
664
947
  let chatExitCode = 0;
665
948
  let spawnError = null;
@@ -692,7 +975,7 @@ export async function runShell(io, style) {
692
975
  childActive = false;
693
976
  renderer.finalize();
694
977
  totalCost += renderer.getTotalCost();
695
- // B.4 — detect and report chat failures
978
+ // Detect and report chat failures
696
979
  const chatFailed = spawnError !== null || (chatExitCode !== 0 && !renderer.hasReceivedText());
697
980
  if (chatFailed) {
698
981
  let reason;
@@ -777,6 +1060,22 @@ export async function runShell(io, style) {
777
1060
  // s or other: skip silently
778
1061
  }
779
1062
  }
1063
+ function printLetterHelp(style) {
1064
+ const lines = [
1065
+ style.accent('Agora Shell — letter shortcuts'),
1066
+ '',
1067
+ ' /a again /b browse /c community /d doctor /e env',
1068
+ ' /f fg /g search /h home /i init /j jobs',
1069
+ ' /k search /l last /m marketplace /n news /o browse',
1070
+ ' /p preferences /q quit /r reviews /s settings /t terminal',
1071
+ ' /u use /v verbose /w watch /x export /y history',
1072
+ ' /z doctor --fix',
1073
+ '',
1074
+ 'Shortcuts are exact matches only (no arguments).',
1075
+ 'Type /help for full command reference, /abc to show this again.'
1076
+ ];
1077
+ process.stdout.write(lines.join('\n') + '\n\n');
1078
+ }
780
1079
  function printHelp(style) {
781
1080
  const lines = [
782
1081
  style.accent('Agora Shell — help'),
@@ -796,6 +1095,10 @@ function printHelp(style) {
796
1095
  ' /last re-run most recent bash command',
797
1096
  ' /again re-send most recent chat message',
798
1097
  ' /? <cmd> dry-run an agora command (e.g. /? install mcp-github)',
1098
+ ' /abc show letter-shortcut reference (/a /b /c ...)',
1099
+ ' /jobs list background jobs',
1100
+ ' /fg [N] bring job N (or last) to foreground',
1101
+ ' /bg [N] resume job N (or last) in background',
799
1102
  '',
800
1103
  style.dim('Verbosity:'),
801
1104
  ' /verbose /medium /quiet',