opencode-agora 0.3.0 → 0.4.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 (168) hide show
  1. package/README.md +217 -52
  2. package/dist/cli/app.d.ts +3 -0
  3. package/dist/cli/app.d.ts.map +1 -1
  4. package/dist/cli/app.js +1053 -183
  5. package/dist/cli/app.js.map +1 -1
  6. package/dist/cli/chat-renderer.d.ts +31 -0
  7. package/dist/cli/chat-renderer.d.ts.map +1 -0
  8. package/dist/cli/chat-renderer.js +275 -0
  9. package/dist/cli/chat-renderer.js.map +1 -0
  10. package/dist/cli/commands-meta.d.ts +21 -0
  11. package/dist/cli/commands-meta.d.ts.map +1 -0
  12. package/dist/cli/commands-meta.js +600 -0
  13. package/dist/cli/commands-meta.js.map +1 -0
  14. package/dist/cli/completions.d.ts +18 -0
  15. package/dist/cli/completions.d.ts.map +1 -0
  16. package/dist/cli/completions.js +190 -0
  17. package/dist/cli/completions.js.map +1 -0
  18. package/dist/cli/mcp-server.d.ts +4 -0
  19. package/dist/cli/mcp-server.d.ts.map +1 -0
  20. package/dist/cli/mcp-server.js +277 -0
  21. package/dist/cli/mcp-server.js.map +1 -0
  22. package/dist/cli/menu.d.ts +7 -0
  23. package/dist/cli/menu.d.ts.map +1 -0
  24. package/dist/cli/menu.js +164 -0
  25. package/dist/cli/menu.js.map +1 -0
  26. package/dist/cli/pages/community.d.ts +3 -0
  27. package/dist/cli/pages/community.d.ts.map +1 -0
  28. package/dist/cli/pages/community.js +276 -0
  29. package/dist/cli/pages/community.js.map +1 -0
  30. package/dist/cli/pages/helpers.d.ts +32 -0
  31. package/dist/cli/pages/helpers.d.ts.map +1 -0
  32. package/dist/cli/pages/helpers.js +67 -0
  33. package/dist/cli/pages/helpers.js.map +1 -0
  34. package/dist/cli/pages/home.d.ts +3 -0
  35. package/dist/cli/pages/home.d.ts.map +1 -0
  36. package/dist/cli/pages/home.js +148 -0
  37. package/dist/cli/pages/home.js.map +1 -0
  38. package/dist/cli/pages/marketplace.d.ts +3 -0
  39. package/dist/cli/pages/marketplace.d.ts.map +1 -0
  40. package/dist/cli/pages/marketplace.js +179 -0
  41. package/dist/cli/pages/marketplace.js.map +1 -0
  42. package/dist/cli/pages/news.d.ts +3 -0
  43. package/dist/cli/pages/news.d.ts.map +1 -0
  44. package/dist/cli/pages/news.js +561 -0
  45. package/dist/cli/pages/news.js.map +1 -0
  46. package/dist/cli/pages/settings.d.ts +3 -0
  47. package/dist/cli/pages/settings.d.ts.map +1 -0
  48. package/dist/cli/pages/settings.js +166 -0
  49. package/dist/cli/pages/settings.js.map +1 -0
  50. package/dist/cli/pages/types.d.ts +67 -0
  51. package/dist/cli/pages/types.d.ts.map +1 -0
  52. package/dist/cli/pages/types.js +2 -0
  53. package/dist/cli/pages/types.js.map +1 -0
  54. package/dist/cli/prompter.d.ts +135 -0
  55. package/dist/cli/prompter.d.ts.map +1 -0
  56. package/dist/cli/prompter.js +675 -0
  57. package/dist/cli/prompter.js.map +1 -0
  58. package/dist/cli/shell.d.ts +23 -0
  59. package/dist/cli/shell.d.ts.map +1 -0
  60. package/dist/cli/shell.js +819 -0
  61. package/dist/cli/shell.js.map +1 -0
  62. package/dist/cli/tui.d.ts +7 -0
  63. package/dist/cli/tui.d.ts.map +1 -0
  64. package/dist/cli/tui.js +373 -0
  65. package/dist/cli/tui.js.map +1 -0
  66. package/dist/cli.js +1 -1
  67. package/dist/cli.js.map +1 -1
  68. package/dist/commands.d.ts +14 -0
  69. package/dist/commands.d.ts.map +1 -0
  70. package/dist/commands.js +28 -0
  71. package/dist/commands.js.map +1 -0
  72. package/dist/community/client.d.ts +47 -0
  73. package/dist/community/client.d.ts.map +1 -0
  74. package/dist/community/client.js +245 -0
  75. package/dist/community/client.js.map +1 -0
  76. package/dist/community/types.d.ts +50 -0
  77. package/dist/community/types.d.ts.map +1 -0
  78. package/dist/community/types.js +11 -0
  79. package/dist/community/types.js.map +1 -0
  80. package/dist/config.d.ts +1 -3
  81. package/dist/config.d.ts.map +1 -1
  82. package/dist/config.js +0 -17
  83. package/dist/config.js.map +1 -1
  84. package/dist/data.d.ts +1 -1
  85. package/dist/data.d.ts.map +1 -1
  86. package/dist/data.js +667 -3
  87. package/dist/data.js.map +1 -1
  88. package/dist/format.d.ts +5 -39
  89. package/dist/format.d.ts.map +1 -1
  90. package/dist/format.js +5 -120
  91. package/dist/format.js.map +1 -1
  92. package/dist/history.d.ts +13 -0
  93. package/dist/history.d.ts.map +1 -0
  94. package/dist/history.js +37 -0
  95. package/dist/history.js.map +1 -0
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +114 -234
  98. package/dist/index.js.map +1 -1
  99. package/dist/init.d.ts.map +1 -1
  100. package/dist/init.js +4 -9
  101. package/dist/init.js.map +1 -1
  102. package/dist/live.d.ts +4 -0
  103. package/dist/live.d.ts.map +1 -1
  104. package/dist/live.js +11 -3
  105. package/dist/live.js.map +1 -1
  106. package/dist/marketplace.d.ts +9 -0
  107. package/dist/marketplace.d.ts.map +1 -1
  108. package/dist/marketplace.js +105 -15
  109. package/dist/marketplace.js.map +1 -1
  110. package/dist/news/cache.d.ts +13 -0
  111. package/dist/news/cache.d.ts.map +1 -0
  112. package/dist/news/cache.js +65 -0
  113. package/dist/news/cache.js.map +1 -0
  114. package/dist/news/score.d.ts +4 -0
  115. package/dist/news/score.d.ts.map +1 -0
  116. package/dist/news/score.js +43 -0
  117. package/dist/news/score.js.map +1 -0
  118. package/dist/news/sources/arxiv.d.ts +9 -0
  119. package/dist/news/sources/arxiv.d.ts.map +1 -0
  120. package/dist/news/sources/arxiv.js +103 -0
  121. package/dist/news/sources/arxiv.js.map +1 -0
  122. package/dist/news/sources/github-trending.d.ts +9 -0
  123. package/dist/news/sources/github-trending.d.ts.map +1 -0
  124. package/dist/news/sources/github-trending.js +93 -0
  125. package/dist/news/sources/github-trending.js.map +1 -0
  126. package/dist/news/sources/hn.d.ts +9 -0
  127. package/dist/news/sources/hn.d.ts.map +1 -0
  128. package/dist/news/sources/hn.js +53 -0
  129. package/dist/news/sources/hn.js.map +1 -0
  130. package/dist/news/sources/reddit.d.ts +9 -0
  131. package/dist/news/sources/reddit.d.ts.map +1 -0
  132. package/dist/news/sources/reddit.js +68 -0
  133. package/dist/news/sources/reddit.js.map +1 -0
  134. package/dist/news/sources/rss.d.ts +14 -0
  135. package/dist/news/sources/rss.d.ts.map +1 -0
  136. package/dist/news/sources/rss.js +102 -0
  137. package/dist/news/sources/rss.js.map +1 -0
  138. package/dist/news/types.d.ts +39 -0
  139. package/dist/news/types.d.ts.map +1 -0
  140. package/dist/news/types.js +47 -0
  141. package/dist/news/types.js.map +1 -0
  142. package/dist/preferences.d.ts +14 -0
  143. package/dist/preferences.d.ts.map +1 -0
  144. package/dist/preferences.js +31 -0
  145. package/dist/preferences.js.map +1 -0
  146. package/dist/settings.d.ts +26 -0
  147. package/dist/settings.d.ts.map +1 -0
  148. package/dist/settings.js +257 -0
  149. package/dist/settings.js.map +1 -0
  150. package/dist/transcript.d.ts +28 -0
  151. package/dist/transcript.d.ts.map +1 -0
  152. package/dist/transcript.js +79 -0
  153. package/dist/transcript.js.map +1 -0
  154. package/dist/types.d.ts +6 -1
  155. package/dist/types.d.ts.map +1 -1
  156. package/dist/ui.d.ts +157 -0
  157. package/dist/ui.d.ts.map +1 -0
  158. package/dist/ui.js +296 -0
  159. package/dist/ui.js.map +1 -0
  160. package/package.json +11 -9
  161. package/dist/api.d.ts +0 -72
  162. package/dist/api.d.ts.map +0 -1
  163. package/dist/api.js +0 -109
  164. package/dist/api.js.map +0 -1
  165. package/dist/logger.d.ts +0 -20
  166. package/dist/logger.d.ts.map +0 -1
  167. package/dist/logger.js +0 -59
  168. package/dist/logger.js.map +0 -1
package/dist/cli/app.js CHANGED
@@ -1,33 +1,89 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
+ import { spawn } from 'node:child_process';
4
5
  import { formatConfigJson } from '../config.js';
6
+ import { formatNumber } from '../format.js';
7
+ import { COMMANDS, renderManual } from './commands-meta.js';
8
+ import { runInteractiveMenu } from './menu.js';
9
+ import { runTui } from './tui.js';
5
10
  import { detectOpenCodeConfigPath, doctorOpenCodeConfig, loadOpenCodeConfig, writeOpenCodeConfig } from '../config-files.js';
6
- import { createInstallPlan, getInstallKind, getMarketplaceItems, getTrendingTags } from '../marketplace.js';
11
+ import { createInstallPlan, getInstallKind, getMarketplaceItems, getTrendingTags, similarItems } from '../marketplace.js';
12
+ import { communityBoardsSource, communityThreadsSource, communityThreadSource, createThreadSource, createReplySource, voteSource, flagSource } from '../community/client.js';
13
+ import { normalizeNewsSource, DEFAULT_NEWS_CONFIG, hostFromUrl } from '../news/types.js';
14
+ import { rankItems } from '../news/score.js';
15
+ import { readCache, writeCache, isStale } from '../news/cache.js';
16
+ import { hnSource } from '../news/sources/hn.js';
17
+ import { redditSource } from '../news/sources/reddit.js';
18
+ import { githubTrendingSource } from '../news/sources/github-trending.js';
19
+ import { arxivSource } from '../news/sources/arxiv.js';
7
20
  import { scanProject, generateInitPlan, applyInitPlan, runCommands } from '../init.js';
21
+ import { installAgoraCommand } from '../commands.js';
8
22
  import { sampleWorkflows, dataRefreshedAt } from '../data.js';
9
23
  import { createDiscussionSource, discussionsSource, findMarketplaceSource, createReviewSource, findTutorialSource, listReviewsSource, profileSource, publishPackageSource, publishWorkflowSource, searchMarketplaceSource, trendingMarketplaceSource, tutorialsSource } from '../live.js';
10
24
  import { clearAuthState, detectAgoraDataDir, getAuthState, getAgoraStatePath, loadAgoraState, removeItemFromState, resolveSavedItems, saveItemToState, setAuthState, writeAgoraState } from '../state.js';
25
+ import { createStyler, renderBanner, renderBox, renderMeander, shouldUseColor, supportsTrueColor } from '../ui.js';
26
+ import { loadPreferences, writePreferences, prefsPath } from '../preferences.js';
27
+ import { appendHistory, loadHistory, clearHistory } from '../history.js';
11
28
  const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
12
- const VERSION = pkg.version;
29
+ export const AGORA_VERSION = pkg.version;
30
+ const VERSION = AGORA_VERSION;
31
+ // Active terminal styler. Reassigned once per `runCli` invocation from the
32
+ // caller's stream + env; defaults to plain so any direct formatter use is safe.
33
+ let style = createStyler(false);
13
34
  const booleanFlags = new Set([
14
35
  'api',
36
+ 'continue',
37
+ 'dryRun',
15
38
  'help',
16
39
  'json',
17
40
  'live',
41
+ 'mcp',
18
42
  'offline',
43
+ 'table',
19
44
  'version',
20
45
  'verbose',
21
46
  'write'
22
47
  ]);
48
+ /**
49
+ * Returns true only when both stdout and stdin are real interactive TTYs AND the
50
+ * environment supports colour (i.e. not NO_COLOR or TERM=dumb). The gate keeps
51
+ * the interactive menu away from pipes, CI, and the test harness, all of which
52
+ * use non-TTY mock streams.
53
+ */
54
+ function isInteractive(io, env) {
55
+ if (env.NO_COLOR != null)
56
+ return false;
57
+ if (env.TERM === 'dumb')
58
+ return false;
59
+ const stdoutTTY = Boolean(io.stdout.isTTY);
60
+ const stdinTTY = Boolean(process.stdin.isTTY);
61
+ return stdoutTTY && stdinTTY;
62
+ }
23
63
  export async function runCli(argv, io) {
24
64
  const parsed = parseArgs(argv);
65
+ const env = io.env ?? {};
66
+ const useColor = shouldUseColor(io.stdout, env, Boolean(parsed.flags.json));
67
+ style = createStyler(useColor, supportsTrueColor(env));
25
68
  if (parsed.flags.version) {
26
69
  writeLine(io.stdout, VERSION);
27
70
  return 0;
28
71
  }
29
- if (!parsed.command || parsed.flags.help) {
30
- writeLine(io.stdout, usage());
72
+ if (parsed.flags.help) {
73
+ if (parsed.command && COMMANDS.some((c) => c.name === parsed.command)) {
74
+ writeLine(io.stdout, commandManual(parsed.command));
75
+ }
76
+ else {
77
+ writeLine(io.stdout, usage());
78
+ }
79
+ return 0;
80
+ }
81
+ if (!parsed.command) {
82
+ if (isInteractive(io, env)) {
83
+ const { runShell } = await import('./shell.js');
84
+ return runShell(io, style);
85
+ }
86
+ writeLine(io.stdout, welcome(useColor, supportsTrueColor(env)));
31
87
  return 0;
32
88
  }
33
89
  try {
@@ -65,16 +121,65 @@ export async function runCli(argv, io) {
65
121
  case 'profile':
66
122
  return await commandProfile(parsed, io);
67
123
  case 'auth':
68
- return commandAuth(parsed, io);
124
+ return await commandAuth(parsed, io);
125
+ case 'login':
126
+ return await commandAuth({ ...parsed, args: ['login', ...parsed.args], command: 'auth' }, io);
127
+ case 'logout':
128
+ return await commandAuth({ ...parsed, args: ['logout'], command: 'auth' }, io);
129
+ case 'whoami':
130
+ return await commandAuth({ ...parsed, args: ['status'], command: 'auth', flags: { ...parsed.flags, json: true } }, io);
69
131
  case 'config':
70
132
  return await commandConfig(parsed, io);
133
+ case 'mcp':
134
+ return await commandMcp(parsed, io);
135
+ case 'chat':
136
+ return await commandChat(parsed, io);
71
137
  case 'init':
72
138
  return await commandInit(parsed, io);
73
139
  case 'use':
74
140
  return await commandUse(parsed, io);
75
- case 'help':
76
- writeLine(io.stdout, usage());
141
+ case 'similar':
142
+ return await commandSimilar(parsed, io);
143
+ case 'compare':
144
+ return await commandCompare(parsed, io);
145
+ case 'news':
146
+ return await commandNews(parsed, io);
147
+ case 'community':
148
+ return await commandCommunity(parsed, io);
149
+ case 'thread':
150
+ return await commandThread(parsed, io);
151
+ case 'post':
152
+ return await commandPost(parsed, io);
153
+ case 'reply':
154
+ return await commandReply(parsed, io);
155
+ case 'vote':
156
+ return await commandVote(parsed, io);
157
+ case 'flag':
158
+ return await commandFlag(parsed, io);
159
+ case 'preferences':
160
+ return await commandPreferences(parsed, io);
161
+ case 'history':
162
+ return await commandHistory(parsed, io);
163
+ case 'menu':
164
+ return await runInteractiveMenu(io, style);
165
+ case 'tui':
166
+ return await runTui(io, { initial: 'home' });
167
+ case 'help': {
168
+ const helpTarget = parsed.args[0];
169
+ if (helpTarget) {
170
+ const meta = COMMANDS.find((c) => c.name === helpTarget);
171
+ if (!meta) {
172
+ writeLine(io.stderr, `Unknown command: ${helpTarget}`);
173
+ writeLine(io.stderr, 'Run `agora help` for a list of commands.');
174
+ return 1;
175
+ }
176
+ writeLine(io.stdout, commandManual(helpTarget));
177
+ }
178
+ else {
179
+ writeLine(io.stdout, usage());
180
+ }
77
181
  return 0;
182
+ }
78
183
  default:
79
184
  writeLine(io.stderr, `Unknown command: ${parsed.command}`);
80
185
  writeLine(io.stderr, 'Run agora help for usage.');
@@ -136,28 +241,50 @@ export function parseArgs(argv) {
136
241
  async function commandSearch(parsed, io) {
137
242
  const query = parsed.args.join(' ');
138
243
  const category = stringFlag(parsed, 'category', 'c') || 'all';
139
- const limit = numberFlag(parsed, 'limit', 'n') || 10;
244
+ const sortBy = stringFlag(parsed, 'sort', 's') || 'relevance';
245
+ const sortOrder = (stringFlag(parsed, 'order', 'o') || 'desc');
246
+ const table = Boolean(parsed.flags.table);
247
+ const page = numberFlag(parsed, 'page', 'p') || 1;
248
+ const perPage = numberFlag(parsed, 'perPage', 'pp') || 0;
249
+ const limit = perPage > 0 ? perPage : (numberFlag(parsed, 'limit', 'n') || 10);
140
250
  const result = await searchMarketplaceSource({
141
251
  ...sourceOptions(parsed, io),
142
252
  query,
143
253
  category,
144
- limit
254
+ limit,
255
+ sortBy: sortBy,
256
+ sortOrder,
257
+ page,
258
+ perPage
145
259
  });
146
260
  const results = result.data;
147
261
  warnFallback(result, io);
148
262
  if (parsed.flags.json) {
149
- writeJson(io.stdout, sourcePayload(result, { query, category, count: results.length, items: results }));
263
+ writeJson(io.stdout, sourcePayload(result, { query, category, sortBy, sortOrder, page, count: results.length, items: results }));
150
264
  return 0;
151
265
  }
152
266
  if (results.length === 0) {
153
267
  writeLine(io.stdout, `No results found for "${query}".`);
154
268
  return 0;
155
269
  }
156
- const sourceLabel = result.source === 'offline'
157
- ? `source: offline, refreshed ${dataRefreshedAt}`
158
- : `source: ${result.source}`;
159
- writeLine(io.stdout, `Agora search: ${query || 'all'} (${results.length} shown, ${sourceLabel})`);
160
- writeLine(io.stdout, formatItemList(results));
270
+ writeLine(io.stdout, header('agora search', [`"${query || 'all'}"`, `${results.length} results`, sourceLabel(result)]));
271
+ writeLine(io.stdout, '');
272
+ if (table) {
273
+ writeLine(io.stdout, formatItemTable(results));
274
+ }
275
+ else {
276
+ writeLine(io.stdout, formatItemList(results));
277
+ }
278
+ if (perPage > 0) {
279
+ writeLine(io.stdout, '');
280
+ writeLine(io.stdout, style.dim(`Page ${page} · ${perPage} per page. Use --page N to navigate.`));
281
+ }
282
+ appendHistory(detectDataDir(parsed, io), {
283
+ type: 'search',
284
+ query,
285
+ timestamp: new Date().toISOString(),
286
+ results: results.length,
287
+ });
161
288
  return 0;
162
289
  }
163
290
  async function commandBrowse(parsed, io) {
@@ -178,11 +305,22 @@ async function commandBrowse(parsed, io) {
178
305
  return 0;
179
306
  }
180
307
  writeLine(io.stdout, formatItemDetail(item));
308
+ const related = similarItems(id, { limit: 3, type: item.kind === 'workflow' ? 'workflow' : undefined });
309
+ if (related.length > 0) {
310
+ writeLine(io.stdout, '');
311
+ writeLine(io.stdout, style.dim('Related:'));
312
+ for (const rel of related) {
313
+ const tagOverlap = (item.tags ?? []).filter((t) => (rel.tags ?? []).includes(t));
314
+ const reason = tagOverlap.length > 0 ? ` (shares tags: ${tagOverlap.join(', ')})` : '';
315
+ writeLine(io.stdout, ` ${style.accent(rel.id.padEnd(28))} ${style.dim(formatNumber(rel.installs ?? 0) + ' installs')}${style.dim(reason)}`);
316
+ }
317
+ }
181
318
  return 0;
182
319
  }
183
320
  async function commandTrending(parsed, io) {
184
321
  const category = stringFlag(parsed, 'category', 'c') || parsed.args[0] || 'all';
185
322
  const limit = numberFlag(parsed, 'limit', 'n') || 5;
323
+ const table = Boolean(parsed.flags.table);
186
324
  const result = await trendingMarketplaceSource({ ...sourceOptions(parsed, io), category, limit });
187
325
  const items = result.data;
188
326
  warnFallback(result, io);
@@ -190,9 +328,16 @@ async function commandTrending(parsed, io) {
190
328
  writeJson(io.stdout, sourcePayload(result, { category, count: items.length, tags: getTrendingTags(), items }));
191
329
  return 0;
192
330
  }
193
- writeLine(io.stdout, `Trending in Agora (${category}, source: ${result.source})`);
194
- writeLine(io.stdout, formatItemList(items));
195
- writeLine(io.stdout, `Tags: ${getTrendingTags().join(', ')}`);
331
+ writeLine(io.stdout, header('agora trending', [category, sourceLabel(result)]));
332
+ writeLine(io.stdout, '');
333
+ if (table) {
334
+ writeLine(io.stdout, formatItemTable(items));
335
+ }
336
+ else {
337
+ writeLine(io.stdout, formatItemList(items));
338
+ }
339
+ writeLine(io.stdout, '');
340
+ writeLine(io.stdout, `${style.dim('tags')} ${getTrendingTags().join(', ')}`);
196
341
  return 0;
197
342
  }
198
343
  async function commandWorkflows(parsed, io) {
@@ -204,16 +349,340 @@ async function commandWorkflows(parsed, io) {
204
349
  category: 'workflow',
205
350
  limit
206
351
  });
207
- const workflows = result.data.filter((item) => item.kind === 'workflow');
352
+ const workflows = result.data;
208
353
  warnFallback(result, io);
209
354
  if (parsed.flags.json) {
210
355
  writeJson(io.stdout, sourcePayload(result, { query, count: workflows.length, workflows }));
211
356
  return 0;
212
357
  }
213
- writeLine(io.stdout, `Agora workflows (${workflows.length} shown, source: ${result.source})`);
358
+ writeLine(io.stdout, header('agora workflows', [`${workflows.length} results`, sourceLabel(result)]));
359
+ writeLine(io.stdout, '');
214
360
  writeLine(io.stdout, formatItemList(workflows));
215
361
  return 0;
216
362
  }
363
+ async function commandSimilar(parsed, io) {
364
+ const id = parsed.args[0];
365
+ if (!id)
366
+ return usageError(io, 'similar requires an item id');
367
+ const type = stringFlag(parsed, 'type', 't');
368
+ const limit = numberFlag(parsed, 'limit', 'n') || 5;
369
+ const results = similarItems(id, { limit, type });
370
+ if (parsed.flags.json) {
371
+ writeJson(io.stdout, { id, type: type || 'all', count: results.length, items: results });
372
+ return 0;
373
+ }
374
+ if (results.length === 0) {
375
+ writeLine(io.stdout, `No similar items found for "${id}".`);
376
+ return 0;
377
+ }
378
+ writeLine(io.stdout, header('agora similar', [`to ${id}`, `${results.length} results`]));
379
+ writeLine(io.stdout, '');
380
+ writeLine(io.stdout, formatItemList(results));
381
+ return 0;
382
+ }
383
+ async function commandCompare(parsed, io) {
384
+ const ids = parsed.args;
385
+ if (ids.length < 2)
386
+ return usageError(io, 'compare requires at least two item ids');
387
+ const typeMap = (type) => type === 'package' || type === 'workflow' ? type : undefined;
388
+ const type = typeMap(stringFlag(parsed, 'type', 't'));
389
+ const items = [];
390
+ for (const id of ids) {
391
+ const item = await findMarketplaceSource({ ...sourceOptions(parsed, io), id, type });
392
+ if (!item.data) {
393
+ writeLine(io.stderr, `Item not found: ${id}`);
394
+ return 1;
395
+ }
396
+ items.push(item.data);
397
+ }
398
+ if (parsed.flags.json) {
399
+ writeJson(io.stdout, { ids, count: items.length, items });
400
+ return 0;
401
+ }
402
+ const sharedTags = items.length > 1
403
+ ? items.map((i) => new Set(i.tags ?? [])).reduce((a, b) => new Set([...a].filter((t) => b.has(t))))
404
+ : new Set();
405
+ const attrs = [
406
+ { label: 'name', get: (i) => i.name },
407
+ { label: 'author', get: (i) => i.author },
408
+ { label: 'installs', get: (i) => formatNumber(i.installs ?? 0) },
409
+ { label: 'stars', get: (i) => formatNumber(i.stars ?? 0) },
410
+ { label: 'category', get: (i) => i.category },
411
+ { label: 'tags', get: (i) => (i.tags ?? []).join(', ') },
412
+ ];
413
+ if (items.some((i) => i.kind === 'package' && i.npmPackage)) {
414
+ attrs.push({
415
+ label: 'npmPackage',
416
+ get: (i) => (i.kind === 'package' && i.npmPackage) || '-',
417
+ });
418
+ }
419
+ if (items.some((i) => i.createdAt)) {
420
+ attrs.push({
421
+ label: 'created',
422
+ get: (i) => (i.createdAt || '').slice(0, 10),
423
+ });
424
+ }
425
+ const colW = Math.max(12, ...items.map((i) => Math.max(i.id.length, i.name.length, 10)));
426
+ const labelW = Math.max(...attrs.map((a) => a.label.length));
427
+ const totalW = labelW + 3 + items.length * (colW + 3) + 1;
428
+ const top = style.accent('┌') + '─'.repeat(labelW + 2) + style.accent('┬') + '─'.repeat(totalW - labelW - 6) + style.accent('┐');
429
+ const bot = style.accent('└') + '─'.repeat(labelW + 2) + style.accent('┴') + '─'.repeat(totalW - labelW - 6) + style.accent('┘');
430
+ const hdrCells = items.map((i, idx) => {
431
+ const name = idx === 0 ? style.bold(style.accent(i.id)) : style.accent(i.id);
432
+ return name.padEnd(colW);
433
+ });
434
+ const hdr = style.accent('│') + ' '.repeat(labelW + 2) + style.accent('│') + ' ' + hdrCells.join(style.accent(' │ ') + ' ') + style.accent(' │');
435
+ const rows = attrs.map((attr) => {
436
+ const cells = items.map((item) => {
437
+ const val = attr.get(item);
438
+ if (attr.label === 'tags') {
439
+ return val.split(', ').map((t) => sharedTags.has(t) ? style.accent(t) : style.dim(t)).join(', ').padEnd(colW);
440
+ }
441
+ return (attr.label === 'name' || attr.label === 'npmPackage' ? val : style.dim(val)).padEnd(colW);
442
+ });
443
+ return style.accent('│') + ' ' + style.dim(attr.label.padEnd(labelW)) + ' ' + style.accent('│') + ' ' + cells.join(style.accent(' │ ') + ' ') + style.accent(' │');
444
+ });
445
+ const sepLine = style.accent('├') + '─'.repeat(labelW + 2) + style.accent('┼') + '─'.repeat(totalW - labelW - 6) + style.accent('┤');
446
+ writeLine(io.stdout, header('agora compare', items.map((i) => i.id)));
447
+ writeLine(io.stdout, '');
448
+ writeLine(io.stdout, [top, hdr, sepLine, ...rows, bot].join('\n'));
449
+ if (sharedTags.size > 0) {
450
+ writeLine(io.stdout, '');
451
+ writeLine(io.stdout, style.dim('Shared tags highlighted in accent.'));
452
+ }
453
+ return 0;
454
+ }
455
+ async function commandNews(parsed, io) {
456
+ const query = parsed.args.join(' ');
457
+ const sourceOpt = stringFlag(parsed, 'source', 's');
458
+ const source = sourceOpt ? normalizeNewsSource(sourceOpt) : undefined;
459
+ const limit = numberFlag(parsed, 'limit', 'n') || 20;
460
+ const refresh = Boolean(parsed.flags.refresh);
461
+ const dataDir = detectDataDir(parsed, io);
462
+ let cached = readCache(dataDir);
463
+ const now = new Date();
464
+ const config = DEFAULT_NEWS_CONFIG;
465
+ const adapters = [
466
+ ['hn', hnSource],
467
+ ['reddit', redditSource],
468
+ ['github-trending', githubTrendingSource],
469
+ ['arxiv', arxivSource],
470
+ ];
471
+ const fetchWithTimeout = (fn, ms = 10000) => Promise.race([fn(), new Promise((_, r) => setTimeout(() => r(new Error('timeout')), ms))]);
472
+ const refreshSource = async (src, adapter) => {
473
+ try {
474
+ const fresh = await fetchWithTimeout(() => adapter.fetch({}));
475
+ cached = cached.filter((i) => i.source !== src);
476
+ cached.push(...fresh);
477
+ }
478
+ catch {
479
+ // keep stale
480
+ }
481
+ };
482
+ if (refresh) {
483
+ for (const [src, adapter] of adapters) {
484
+ await refreshSource(src, adapter);
485
+ }
486
+ }
487
+ else {
488
+ for (const [src, adapter] of adapters) {
489
+ const cfg = config.sources[src];
490
+ if (cfg?.enabled && isStale(cached, src, cfg.ttlMinutes, now)) {
491
+ await refreshSource(src, adapter);
492
+ }
493
+ }
494
+ }
495
+ const ranked = rankItems(cached, config, now);
496
+ writeCache(dataDir, cached);
497
+ let items = ranked;
498
+ if (query) {
499
+ const q = query.toLowerCase();
500
+ items = items.filter((i) => i.title.toLowerCase().includes(q) || i.tags.some((t) => t.includes(q)));
501
+ }
502
+ if (source) {
503
+ items = items.filter((i) => i.source === source);
504
+ }
505
+ items = items.slice(0, limit);
506
+ if (parsed.flags.json) {
507
+ writeJson(io.stdout, { count: items.length, items, source: source || 'all' });
508
+ return 0;
509
+ }
510
+ if (items.length === 0) {
511
+ writeLine(io.stdout, 'No news items found.');
512
+ return 0;
513
+ }
514
+ writeLine(io.stdout, header('agora news', [`${items.length} stories`, source ? `source: ${source}` : 'all sources']));
515
+ writeLine(io.stdout, '');
516
+ for (const item of items) {
517
+ const ageH = Math.round((now.getTime() - new Date(item.publishedAt).getTime()) / 3600000);
518
+ const age = ageH < 1 ? '<1h' : ageH < 24 ? `${ageH}h` : `${Math.round(ageH / 24)}d`;
519
+ const host = hostFromUrl(item.url);
520
+ writeLine(io.stdout, `${style.accent(item.source.padEnd(6))} ${style.dim(age.padEnd(4))} ${style.accent(formatNumber(item.engagement).padStart(7))} ${style.dim('s' + item.score.toFixed(2))} ${item.title}`);
521
+ if (host)
522
+ writeLine(io.stdout, ` ${style.dim(host)}`);
523
+ if (query)
524
+ writeLine(io.stdout, '');
525
+ }
526
+ return 0;
527
+ }
528
+ async function commandCommunity(parsed, io) {
529
+ const board = parsed.args[0];
530
+ const sort = (stringFlag(parsed, 'sort') || 'active');
531
+ const opts = sourceOptions(parsed, io);
532
+ if (board) {
533
+ const result = await communityThreadsSource(opts, board, sort);
534
+ const threads = result.data.threads;
535
+ if (parsed.flags.json) {
536
+ writeJson(io.stdout, { board, sort, count: threads.length, threads });
537
+ return 0;
538
+ }
539
+ if (threads.length === 0) {
540
+ writeLine(io.stdout, `No threads in /${board}.`);
541
+ return 0;
542
+ }
543
+ writeLine(io.stdout, header(`agora community /${board}`, [`${threads.length} threads`, `sort: ${sort}`]));
544
+ writeLine(io.stdout, '');
545
+ for (const t of threads) {
546
+ const ageH = Math.round((Date.now() - new Date(t.createdAt).getTime()) / 3600000);
547
+ writeLine(io.stdout, ` ${style.accent(t.title)}`);
548
+ writeLine(io.stdout, ` ${style.dim(t.author + ' \u00b7 ' + ageH + 'h \u00b7 ' + t.score + '\u2191 \u00b7 ' + t.replyCount + ' replies')}`);
549
+ }
550
+ return 0;
551
+ }
552
+ const boardsResult = await communityBoardsSource(opts);
553
+ const boards = boardsResult.data.boards;
554
+ if (parsed.flags.json) {
555
+ writeJson(io.stdout, { boards });
556
+ return 0;
557
+ }
558
+ writeLine(io.stdout, header('agora community', [`${boards.length} boards`]));
559
+ writeLine(io.stdout, '');
560
+ for (const b of boards) {
561
+ writeLine(io.stdout, ` ${style.accent('/' + b.id.padEnd(12))} ${style.dim(b.threadCount + ' threads, ' + b.newToday + ' new today')}`);
562
+ }
563
+ writeLine(io.stdout, '');
564
+ writeLine(io.stdout, style.dim('Run `agora community <board>` to see threads.'));
565
+ return 0;
566
+ }
567
+ async function commandThread(parsed, io) {
568
+ const id = parsed.args[0];
569
+ if (!id)
570
+ return usageError(io, 'thread requires a thread id');
571
+ const opts = sourceOptions(parsed, io);
572
+ const result = await communityThreadSource(opts, id);
573
+ const { thread, replies } = result.data;
574
+ if (!thread)
575
+ return usageError(io, `Thread not found: ${id}`);
576
+ if (parsed.flags.json) {
577
+ writeJson(io.stdout, { thread, replies });
578
+ return 0;
579
+ }
580
+ const ageH = Math.round((Date.now() - new Date(thread.createdAt).getTime()) / 3600000);
581
+ writeLine(io.stdout, style.bold(thread.title));
582
+ writeLine(io.stdout, `${style.dim(thread.author + ' \u00b7 ' + ageH + 'h \u00b7 ' + thread.score + '\u2191 \u00b7 ' + thread.replyCount + ' replies')}`);
583
+ writeLine(io.stdout, '');
584
+ writeLine(io.stdout, thread.content);
585
+ if (replies.length > 0) {
586
+ writeLine(io.stdout, '');
587
+ writeLine(io.stdout, style.dim('--- replies ---'));
588
+ for (const r of replies) {
589
+ const rAgeH = Math.round((Date.now() - new Date(r.createdAt).getTime()) / 3600000);
590
+ writeLine(io.stdout, ` ${style.dim(r.author + ' \u00b7 ' + rAgeH + 'h')} ${style.accent(r.score + '\u2191')}`);
591
+ writeLine(io.stdout, ` ${r.content}`);
592
+ }
593
+ }
594
+ return 0;
595
+ }
596
+ async function commandPost(parsed, io) {
597
+ const source = writeSourceOptions(parsed, io);
598
+ if (!source.ok)
599
+ return usageError(io, source.error);
600
+ const board = (stringFlag(parsed, 'board') || stringFlag(parsed, 'b'));
601
+ const title = requiredStringFlag(parsed, 'title');
602
+ const content = contentInput(parsed, io);
603
+ if (!board || !title || !content) {
604
+ return usageError(io, 'post requires --board, --title and --content or --content-file');
605
+ }
606
+ const result = await createThreadSource(source.options, { board, title, content });
607
+ if (parsed.flags.json) {
608
+ writeJson(io.stdout, sourcePayload(result, { thread: result.data.thread }));
609
+ return 0;
610
+ }
611
+ writeLine(io.stdout, `Posted to /${board}: ${style.accent(result.data.thread?.title || title)}`);
612
+ writeLine(io.stdout, `${sourceLabel(result)}`);
613
+ return 0;
614
+ }
615
+ async function commandReply(parsed, io) {
616
+ const parentId = parsed.args[0];
617
+ if (!parentId)
618
+ return usageError(io, 'reply requires a thread or reply id');
619
+ const source = writeSourceOptions(parsed, io);
620
+ if (!source.ok)
621
+ return usageError(io, source.error);
622
+ const content = contentInput(parsed, io);
623
+ if (!content)
624
+ return usageError(io, 'reply requires --content or --content-file');
625
+ const result = await createReplySource(source.options, parentId, {
626
+ content,
627
+ parentId: stringFlag(parsed, 'parentId'),
628
+ });
629
+ if (parsed.flags.json) {
630
+ writeJson(io.stdout, sourcePayload(result, { reply: result.data.reply }));
631
+ return 0;
632
+ }
633
+ writeLine(io.stdout, `Replied to ${style.accent(parentId)}`);
634
+ writeLine(io.stdout, `${sourceLabel(result)}`);
635
+ return 0;
636
+ }
637
+ async function commandVote(parsed, io) {
638
+ const id = parsed.args[0];
639
+ if (!id)
640
+ return usageError(io, 'vote requires a thread or reply id');
641
+ const source = writeSourceOptions(parsed, io);
642
+ if (!source.ok)
643
+ return usageError(io, source.error);
644
+ const up = parsed.flags.up === true;
645
+ const down = parsed.flags.down === true;
646
+ if (!up && !down)
647
+ return usageError(io, 'vote requires --up or --down');
648
+ const value = up ? 1 : -1;
649
+ const targetType = (stringFlag(parsed, 'type') || 'discussion');
650
+ await voteSource(source.options, id, { value, targetType });
651
+ if (parsed.flags.json) {
652
+ writeJson(io.stdout, { id, value, targetType, success: true });
653
+ return 0;
654
+ }
655
+ writeLine(io.stdout, up ? `Upvoted ${style.accent(id)}` : `Downvoted ${style.accent(id)}`);
656
+ return 0;
657
+ }
658
+ async function commandFlag(parsed, io) {
659
+ const id = parsed.args[0];
660
+ if (!id)
661
+ return usageError(io, 'flag requires an item id');
662
+ const flagMarketplace = (kind) => !kind || kind === 'package' || kind === 'workflow';
663
+ const reasonOpt = stringFlag(parsed, 'reason');
664
+ const validReasons = ['spam', 'harassment', 'undisclosed-llm', 'malicious', 'other'];
665
+ const reason = (reasonOpt && validReasons.includes(reasonOpt) ? reasonOpt : 'other');
666
+ const type = stringFlag(parsed, 'type') || 'discussion';
667
+ if (flagMarketplace(type)) {
668
+ writeLine(io.stdout, `Flagged ${style.accent(id)} for ${reason} (${type})`);
669
+ if (parsed.flags.json) {
670
+ writeJson(io.stdout, { id, reason, type, success: true });
671
+ }
672
+ return 0;
673
+ }
674
+ const source = writeSourceOptions(parsed, io);
675
+ if (!source.ok)
676
+ return usageError(io, source.error);
677
+ const targetType = (type === 'discussion' || type === 'reply' ? type : 'discussion');
678
+ await flagSource(source.options, id, { reason, targetType, notes: stringFlag(parsed, 'notes') });
679
+ if (parsed.flags.json) {
680
+ writeJson(io.stdout, { id, reason, targetType, success: true });
681
+ return 0;
682
+ }
683
+ writeLine(io.stdout, `Flagged ${style.accent(id)} for ${reason}`);
684
+ return 0;
685
+ }
217
686
  async function commandTutorials(parsed, io) {
218
687
  const query = parsed.args.join(' ');
219
688
  const level = tutorialLevelFlag(parsed);
@@ -236,14 +705,22 @@ async function commandTutorials(parsed, io) {
236
705
  writeLine(io.stdout, query ? `No tutorials match "${query}".` : 'No tutorials found.');
237
706
  return 0;
238
707
  }
239
- writeLine(io.stdout, `Agora tutorials (${tutorials.length} shown, source: ${result.source})`);
708
+ writeLine(io.stdout, header('agora tutorials', [`${tutorials.length} results`, sourceLabel(result)]));
709
+ writeLine(io.stdout, '');
240
710
  writeLine(io.stdout, formatTutorialList(tutorials));
241
711
  return 0;
242
712
  }
243
713
  async function commandTutorial(parsed, io) {
244
714
  const id = parsed.args[0];
245
- if (!id)
246
- return usageError(io, 'tutorial requires a tutorial id');
715
+ if (!id) {
716
+ const { sampleTutorials } = await import('../data.js');
717
+ writeLine(io.stdout, header('agora tutorial', [`${sampleTutorials.length} available tutorials`]));
718
+ writeLine(io.stdout, '');
719
+ writeLine(io.stdout, sampleTutorials.map((t) => ` ${style.accent(t.id.padEnd(22))} ${style.dim(t.title)} ${style.dim('[' + t.level + ']')}`).join('\n'));
720
+ writeLine(io.stdout, '');
721
+ writeLine(io.stdout, style.dim('Run `agora tutorial <id>` to start a tutorial.'));
722
+ return 0;
723
+ }
247
724
  const step = tutorialStepNumber(parsed);
248
725
  if (!step.ok)
249
726
  return usageError(io, step.error);
@@ -276,13 +753,14 @@ async function commandDiscussions(parsed, io) {
276
753
  writeLine(io.stdout, 'No discussions found.');
277
754
  return 0;
278
755
  }
279
- writeLine(io.stdout, `Agora discussions (${discussions.length}, source: ${result.source})`);
756
+ writeLine(io.stdout, header('agora discussions', [`${discussions.length} results`, sourceLabel(result)]));
757
+ writeLine(io.stdout, '');
280
758
  writeLine(io.stdout, discussions
281
759
  .map((discussion, index) => {
282
760
  return [
283
- `${index + 1}. ${discussion.title} [${discussion.category}]`,
761
+ `${index + 1}. ${style.accent(discussion.title)} ${style.dim('[' + discussion.category + ']')}`,
284
762
  ` ${truncate(discussion.content, 88)}`,
285
- ` replies ${discussion.replies} | stars ${discussion.stars} | by ${discussion.author}`
763
+ ` ${style.dim('replies ' + discussion.replies + ' · stars ' + discussion.stars + ' · by ' + discussion.author)}`
286
764
  ].join('\n');
287
765
  })
288
766
  .join('\n\n'));
@@ -309,8 +787,8 @@ async function commandDiscuss(parsed, io) {
309
787
  writeJson(io.stdout, sourcePayload(result, { discussion: result.data }));
310
788
  return 0;
311
789
  }
312
- writeLine(io.stdout, `Created discussion ${result.data.id}`);
313
- writeLine(io.stdout, `${result.data.title} (${result.source})`);
790
+ writeLine(io.stdout, `Created discussion ${style.accent(result.data.id)}`);
791
+ writeLine(io.stdout, `${result.data.title} (${sourceLabel(result)})`);
314
792
  return 0;
315
793
  }
316
794
  async function commandInstall(parsed, io) {
@@ -353,8 +831,8 @@ async function commandInstall(parsed, io) {
353
831
  }
354
832
  if (parsed.flags.write) {
355
833
  writeOpenCodeConfig(configPath, plan.config);
356
- writeLine(io.stdout, `Installed ${item.name}`);
357
- writeLine(io.stdout, `Updated ${configPath}`);
834
+ writeLine(io.stdout, `Installed ${style.accent(item.name)}`);
835
+ writeLine(io.stdout, `${style.dim('Config')} ${configPath}`);
358
836
  if (plan.commands.length) {
359
837
  writeLine(io.stdout, 'Installing packages...');
360
838
  for (const cmd of plan.commands) {
@@ -371,6 +849,16 @@ async function commandInstall(parsed, io) {
371
849
  }
372
850
  writeLine(io.stdout, `Install preview: ${item.name}`);
373
851
  writeLine(io.stdout, `Target config: ${configPath}`);
852
+ const perms = item.permissions;
853
+ if (perms) {
854
+ writeLine(io.stdout, '\nDeclared permissions:');
855
+ if (perms.fs?.length)
856
+ writeLine(io.stdout, ` ${style.dim('filesystem')} ${perms.fs.join(', ')}`);
857
+ if (perms.net?.length)
858
+ writeLine(io.stdout, ` ${style.dim('network')} ${perms.net.join(', ')}`);
859
+ if (perms.exec?.length)
860
+ writeLine(io.stdout, ` ${style.dim('exec')} ${perms.exec.join(', ')}`);
861
+ }
374
862
  if (plan.commands.length) {
375
863
  writeLine(io.stdout, '\nCommands:');
376
864
  writeLine(io.stdout, plan.commands.join('\n'));
@@ -380,11 +868,191 @@ async function commandInstall(parsed, io) {
380
868
  writeLine(io.stdout, '\nRun with --write to update the config file and install packages.');
381
869
  return 0;
382
870
  }
871
+ async function commandMcp(_parsed, io) {
872
+ const { runMcpServer } = await import('./mcp-server.js');
873
+ try {
874
+ await runMcpServer();
875
+ }
876
+ catch (error) {
877
+ writeLine(io.stderr, error instanceof Error ? error.message : String(error));
878
+ return 1;
879
+ }
880
+ return 0;
881
+ }
882
+ export const FREE_MODELS = ['deepseek-v4-flash-free', 'minimax-m2.5-free', 'nemotron-3-super-free'];
883
+ /** Regexp to extract session ID from a JSON opencode event line. */
884
+ function extractSessionId(line) {
885
+ try {
886
+ const ev = JSON.parse(line);
887
+ if (ev.sessionID && typeof ev.sessionID === 'string')
888
+ return ev.sessionID;
889
+ }
890
+ catch {
891
+ /* not JSON, skip */
892
+ }
893
+ return null;
894
+ }
895
+ /**
896
+ * Persist the most recent chat session ID to the Agora state file so
897
+ * `--continue` can pick it up.
898
+ */
899
+ function persistChatSession(dataDir, sessionId) {
900
+ try {
901
+ const state = loadAgoraState(dataDir);
902
+ const updated = {
903
+ ...state,
904
+ _meta: { ...(state._meta || {}), lastChatSession: sessionId }
905
+ };
906
+ writeAgoraState(dataDir, updated);
907
+ }
908
+ catch {
909
+ // best-effort
910
+ }
911
+ }
912
+ function loadLastChatSession(dataDir) {
913
+ try {
914
+ const state = loadAgoraState(dataDir);
915
+ return state._meta?.lastChatSession;
916
+ }
917
+ catch {
918
+ return undefined;
919
+ }
920
+ }
921
+ async function commandChat(parsed, io) {
922
+ const message = parsed.args.join(' ');
923
+ const model = stringFlag(parsed, 'model', 'm') || FREE_MODELS[0];
924
+ const continueMode = parsed.flags.continue === true;
925
+ const explicitSession = stringFlag(parsed, 'session', 's');
926
+ const rawJson = parsed.flags.json === true;
927
+ const modelArg = model.includes('/') ? model : `opencode/${model}`;
928
+ if (!message) {
929
+ // TUI mode — hand off to opencode with inherit stdio
930
+ process.stderr.write(`Agora Chat (${model}) — press Ctrl+C to exit.\n`);
931
+ const child = spawn('opencode', ['--model', modelArg], {
932
+ env: io.env,
933
+ stdio: 'inherit',
934
+ shell: false
935
+ });
936
+ return new Promise((resolve) => {
937
+ child.on('close', (code) => resolve(code ?? 0));
938
+ child.on('error', (err) => {
939
+ writeLine(io.stderr, `Failed to run opencode: ${err.message}`);
940
+ resolve(1);
941
+ });
942
+ });
943
+ }
944
+ // One-shot mode — single message via opencode run
945
+ return new Promise((resolve) => {
946
+ const args = ['run', '--format', 'json'];
947
+ args.push('--model', modelArg);
948
+ if (explicitSession) {
949
+ args.push('--session', explicitSession);
950
+ }
951
+ else if (continueMode) {
952
+ const dataDir = detectDataDir(parsed, io);
953
+ const lastSession = loadLastChatSession(dataDir);
954
+ if (lastSession) {
955
+ args.push('--session', lastSession);
956
+ }
957
+ else {
958
+ args.push('--continue');
959
+ }
960
+ }
961
+ args.push(message);
962
+ const stderrChunks = [];
963
+ let sessionId = null;
964
+ let wroteNewline = false;
965
+ const child = spawn('opencode', args, {
966
+ env: io.env,
967
+ stdio: ['ignore', 'pipe', 'pipe'],
968
+ shell: false
969
+ });
970
+ child.stdout?.on('data', (chunk) => {
971
+ const text = chunk.toString();
972
+ for (const line of text.split('\n').filter(Boolean)) {
973
+ if (!sessionId)
974
+ sessionId = extractSessionId(line);
975
+ if (rawJson) {
976
+ process.stdout.write(line + '\n');
977
+ continue;
978
+ }
979
+ try {
980
+ const ev = JSON.parse(line);
981
+ if (ev.type === 'text' && ev.part?.text) {
982
+ process.stdout.write(ev.part.text);
983
+ wroteNewline = false;
984
+ }
985
+ if (ev.type === 'step_finish') {
986
+ const tokens = ev.part?.tokens;
987
+ if (tokens && !rawJson) {
988
+ const cost = typeof tokens.cost === 'number' ? ` · $${tokens.cost.toFixed(6)}` : '';
989
+ process.stdout.write(`\n\x1b[2m[${tokens.output} tokens${cost}]\x1b[0m\n`);
990
+ wroteNewline = true;
991
+ }
992
+ }
993
+ }
994
+ catch {
995
+ /* skip malformed lines */
996
+ }
997
+ }
998
+ });
999
+ child.stderr?.on('data', (chunk) => {
1000
+ stderrChunks.push(chunk.toString());
1001
+ });
1002
+ child.on('close', (code) => {
1003
+ if (!wroteNewline)
1004
+ process.stdout.write('\n');
1005
+ const dataDir = detectDataDir(parsed, io);
1006
+ appendHistory(dataDir, {
1007
+ type: 'chat',
1008
+ query: message,
1009
+ timestamp: new Date().toISOString(),
1010
+ model,
1011
+ });
1012
+ if (sessionId) {
1013
+ persistChatSession(dataDir, sessionId);
1014
+ if (!rawJson) {
1015
+ process.stdout.write(`\x1b[2mSession: ${sessionId.slice(0, 24)}… `);
1016
+ process.stdout.write(`Continue: agora chat --session ${sessionId} "..."\x1b[0m\n`);
1017
+ }
1018
+ }
1019
+ if (code !== 0) {
1020
+ const errText = stderrChunks.join('');
1021
+ const modelError = errText.match(/Model not found:.*?Did you mean:\s*(.+?)\?/);
1022
+ if (modelError) {
1023
+ const suggestions = modelError[1];
1024
+ writeLine(io.stderr, `\nModel not available. Try: ${suggestions}`);
1025
+ writeLine(io.stderr, `Example: agora chat -m deepseek-v4-flash-free "your question"`);
1026
+ }
1027
+ else if (errText.includes('not found')) {
1028
+ writeLine(io.stderr, '\n' + errText.replace(/^.*?ERROR\s+/gm, '').trim());
1029
+ }
1030
+ }
1031
+ resolve(code ?? 0);
1032
+ });
1033
+ child.on('error', (err) => {
1034
+ writeLine(io.stderr, `Failed to run opencode: ${err.message}`);
1035
+ writeLine(io.stderr, 'Is opencode installed and in your PATH?');
1036
+ resolve(1);
1037
+ });
1038
+ });
1039
+ }
383
1040
  async function commandInit(parsed, io) {
384
1041
  const cwd = io.cwd || process.cwd();
385
1042
  const scan = scanProject(cwd);
386
1043
  const plan = generateInitPlan(scan);
387
1044
  const configPath = detectOpenCodeConfigPath({ cwd, env: io.env });
1045
+ const withMcp = parsed.flags.mcp === true;
1046
+ if (withMcp) {
1047
+ plan.config.mcp = plan.config.mcp || {};
1048
+ plan.config.mcp.agora = {
1049
+ type: 'local',
1050
+ command: ['agora', 'mcp'],
1051
+ enabled: true
1052
+ };
1053
+ plan.servers.push('agora');
1054
+ plan.notes.push('Agora MCP server registered — OpenCode can discover marketplace tools.');
1055
+ }
388
1056
  if (parsed.flags.json) {
389
1057
  if (parsed.flags.dryRun) {
390
1058
  writeJson(io.stdout, {
@@ -393,11 +1061,13 @@ async function commandInit(parsed, io) {
393
1061
  config: plan.config,
394
1062
  servers: plan.servers,
395
1063
  commands: plan.commands,
1064
+ slashCommand: join(cwd, '.opencode', 'command', 'agora.md'),
396
1065
  dryRun: true
397
1066
  });
398
1067
  return 0;
399
1068
  }
400
1069
  applyInitPlan(plan, configPath);
1070
+ const commandPath = installAgoraCommand(cwd);
401
1071
  const installResults = plan.commands.length ? runCommands(plan.commands) : [];
402
1072
  const installed = installResults.filter((r) => r.ok).length;
403
1073
  const failed = installResults.filter((r) => !r.ok).length;
@@ -407,6 +1077,7 @@ async function commandInit(parsed, io) {
407
1077
  config: plan.config,
408
1078
  servers: plan.servers,
409
1079
  commands: plan.commands,
1080
+ slashCommand: commandPath,
410
1081
  installResults,
411
1082
  installed,
412
1083
  failed
@@ -414,28 +1085,54 @@ async function commandInit(parsed, io) {
414
1085
  return 0;
415
1086
  }
416
1087
  writeLine(io.stdout, `Scanning ${cwd}...`);
417
- writeLine(io.stdout, ` Project type: ${scan.type}`);
1088
+ writeLine(io.stdout, ` ${style.dim('Project type')} ${scan.type}`);
418
1089
  if (scan.frameworks.length)
419
- writeLine(io.stdout, ` Frameworks: ${scan.frameworks.join(', ')}`);
1090
+ writeLine(io.stdout, ` ${style.dim('Frameworks')} ${scan.frameworks.join(', ')}`);
420
1091
  if (scan.hasDocker)
421
- writeLine(io.stdout, ' Docker: detected');
1092
+ writeLine(io.stdout, ` ${style.dim('Docker')} detected`);
422
1093
  if (scan.hasTests)
423
- writeLine(io.stdout, ' Tests: detected');
1094
+ writeLine(io.stdout, ` ${style.dim('Tests')} detected`);
424
1095
  if (scan.hasDatabase)
425
- writeLine(io.stdout, ' Database: detected');
1096
+ writeLine(io.stdout, ` ${style.dim('Database')} detected`);
426
1097
  if (!parsed.flags.dryRun) {
427
1098
  applyInitPlan(plan, configPath);
428
1099
  writeLine(io.stdout, `\nWrote config to ${configPath}`);
1100
+ const commandPath = installAgoraCommand(cwd);
1101
+ writeLine(io.stdout, `Installed /agora slash command at ${commandPath}`);
429
1102
  if (plan.commands.length) {
430
1103
  writeLine(io.stdout, '\nInstalling MCP server packages...');
431
- const installResults = runCommands(plan.commands);
1104
+ const isTTY = Boolean(io.stdout.isTTY);
1105
+ const n = plan.commands.length;
1106
+ const installResults = [];
1107
+ for (let i = 0; i < n; i++) {
1108
+ const [result] = runCommands([plan.commands[i]]);
1109
+ installResults.push(result);
1110
+ if (isTTY && n > 1) {
1111
+ const pct = ((i + 1) / n) * 100;
1112
+ const bar = renderMeander({
1113
+ trueColor: supportsTrueColor(io.env ?? {}),
1114
+ mode: 'progress',
1115
+ pct
1116
+ });
1117
+ const line = ` ${bar}`;
1118
+ if (i < n - 1) {
1119
+ process.stdout.write(`\r\x1b[K${line}`);
1120
+ }
1121
+ else {
1122
+ process.stdout.write(`\r\x1b[K${line}\n`);
1123
+ }
1124
+ }
1125
+ }
432
1126
  const installed = installResults.filter((r) => r.ok).length;
433
1127
  const failed = installResults.filter((r) => !r.ok).length;
434
1128
  writeLine(io.stdout, ` Installed ${installed} of ${plan.commands.length} packages${failed ? ` (${failed} failed)` : ''}`);
435
1129
  }
436
1130
  writeLine(io.stdout, '\n✓ Agora initialized! Restart OpenCode to pick up the changes.');
437
1131
  writeLine(io.stdout, ' Plugin "opencode-agora" is now registered in your config.');
1132
+ writeLine(io.stdout, ' Type `/agora` in OpenCode to use the marketplace.');
438
1133
  writeLine(io.stdout, ` ${plan.servers.length} MCP servers configured.`);
1134
+ if (withMcp)
1135
+ writeLine(io.stdout, ' Agora MCP server registered — `agora mcp` is available as an MCP tool.');
439
1136
  if (plan.workflows.length)
440
1137
  writeLine(io.stdout, ` ${plan.workflows.length} workflows available via \`agora use\`.`);
441
1138
  for (const note of plan.notes)
@@ -445,6 +1142,7 @@ async function commandInit(parsed, io) {
445
1142
  writeLine(io.stdout, '\n--- Dry run ---');
446
1143
  writeLine(io.stdout, `Target config: ${configPath}`);
447
1144
  writeLine(io.stdout, formatConfigJson(plan.config));
1145
+ writeLine(io.stdout, `\nSlash command: ${join(cwd, '.opencode', 'command', 'agora.md')}`);
448
1146
  writeLine(io.stdout, '\nPackages to install:');
449
1147
  for (const cmd of plan.commands)
450
1148
  writeLine(io.stdout, ` ${cmd}`);
@@ -454,8 +1152,14 @@ async function commandInit(parsed, io) {
454
1152
  }
455
1153
  async function commandUse(parsed, io) {
456
1154
  const id = parsed.args[0];
457
- if (!id)
458
- return usageError(io, 'use requires a workflow id');
1155
+ if (!id) {
1156
+ writeLine(io.stdout, header('agora use', [`${sampleWorkflows.length} available workflows`]));
1157
+ writeLine(io.stdout, '');
1158
+ writeLine(io.stdout, sampleWorkflows.map((wf) => ` ${style.accent(wf.id.padEnd(22))} ${style.dim(wf.name)}`).join('\n'));
1159
+ writeLine(io.stdout, '');
1160
+ writeLine(io.stdout, style.dim('Run `agora use <id>` to apply a workflow as a skill.'));
1161
+ return 0;
1162
+ }
459
1163
  const workflow = sampleWorkflows.find((w) => w.id === id || w.name.toLowerCase() === id.toLowerCase());
460
1164
  if (!workflow)
461
1165
  return usageError(io, `Workflow not found: ${id}. Run \`agora workflows\` to see available workflows.`);
@@ -510,51 +1214,133 @@ function commandConfig(parsed, io) {
510
1214
  writeJson(io.stdout, report);
511
1215
  return report.valid ? 0 : 1;
512
1216
  }
513
- writeLine(io.stdout, `Config path: ${report.path}`);
514
- writeLine(io.stdout, `Exists: ${report.exists ? 'yes' : 'no'}`);
515
- writeLine(io.stdout, `Valid: ${report.valid ? 'yes' : 'no'}`);
1217
+ writeLine(io.stdout, `${style.dim('Config path')} ${report.path}`);
1218
+ writeLine(io.stdout, `${style.dim('Exists')} ${report.exists ? 'yes' : 'no'}`);
1219
+ writeLine(io.stdout, `${style.dim('Valid')} ${report.valid ? 'yes' : 'no'}`);
516
1220
  if (report.error)
517
- writeLine(io.stdout, `Error: ${report.error}`);
518
- writeLine(io.stdout, `MCP servers: ${report.mcpServers}`);
519
- writeLine(io.stdout, `Plugins: ${report.plugins}`);
520
- writeLine(io.stdout, `Packages: ${report.packages.length ? report.packages.join(', ') : 'none'}`);
1221
+ writeLine(io.stdout, `${style.dim('Error')} ${report.error}`);
1222
+ writeLine(io.stdout, `${style.dim('MCP servers')} ${report.mcpServers}`);
1223
+ writeLine(io.stdout, `${style.dim('Plugins')} ${report.plugins}`);
1224
+ writeLine(io.stdout, `${style.dim('Packages')} ${report.packages.length ? report.packages.join(', ') : 'none'}`);
521
1225
  return report.valid ? 0 : 1;
522
1226
  }
523
- function commandAuth(parsed, io) {
1227
+ async function commandAuth(parsed, io) {
524
1228
  const subcommand = parsed.args[0] || 'status';
525
1229
  const dataDir = detectDataDir(parsed, io);
526
1230
  const state = loadAgoraState(dataDir);
527
1231
  const existingAuth = getAuthState(state);
528
1232
  if (subcommand === 'login') {
529
- const token = authTokenInput(parsed, io);
530
- if (!token) {
531
- return usageError(io, 'auth login requires --token, AGORA_TOKEN, or AGORA_API_TOKEN');
1233
+ const explicitToken = authTokenInput(parsed, io);
1234
+ if (explicitToken) {
1235
+ // Token-paste flow (existing behaviour, for CI/automation)
1236
+ const apiUrl = stringFlag(parsed, 'apiUrl') || envString(io, 'AGORA_API_URL') || existingAuth?.apiUrl;
1237
+ const nextState = setAuthState(state, { token: explicitToken, apiUrl });
1238
+ const auth = getAuthState(nextState);
1239
+ writeAgoraState(dataDir, nextState);
1240
+ if (parsed.flags.json) {
1241
+ writeJson(io.stdout, authStatusPayload(dataDir, auth));
1242
+ return 0;
1243
+ }
1244
+ writeLine(io.stdout, 'Stored Agora API token');
1245
+ writeLine(io.stdout, `${style.dim('API URL')} ${auth?.apiUrl || 'not stored'}`);
1246
+ writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
1247
+ return 0;
532
1248
  }
1249
+ // ── Device-code flow ─────────────────────────────────────────────────
533
1250
  const apiUrl = stringFlag(parsed, 'apiUrl') || envString(io, 'AGORA_API_URL') || existingAuth?.apiUrl;
534
- const nextState = setAuthState(state, { token, apiUrl });
535
- const auth = getAuthState(nextState);
536
- writeAgoraState(dataDir, nextState);
537
- if (parsed.flags.json) {
538
- writeJson(io.stdout, authStatusPayload(dataDir, auth));
539
- return 0;
1251
+ if (!apiUrl) {
1252
+ return usageError(io, 'auth login requires --api-url, AGORA_API_URL, or stored apiUrl');
1253
+ }
1254
+ const baseUrl = apiUrl.replace(/\/+$/, '');
1255
+ process.stdout.write(`\n${style.accent('Agora Login')}\n`);
1256
+ process.stdout.write(`${style.dim('Connecting to')} ${baseUrl}...\n`);
1257
+ try {
1258
+ const codeRes = await fetch(`${baseUrl}/auth/device/code`, { method: 'POST' });
1259
+ if (!codeRes.ok) {
1260
+ const err = await codeRes.json().catch(() => ({ error: 'request failed' }));
1261
+ return usageError(io, `Device code request failed: ${err.error || codeRes.status}`);
1262
+ }
1263
+ const codeData = (await codeRes.json());
1264
+ const verificationUri = codeData.verification_uri;
1265
+ const userCode = codeData.user_code;
1266
+ const deviceCode = codeData.device_code;
1267
+ const interval = (codeData.interval || 5) * 1000;
1268
+ process.stdout.write(`\n${style.accent(userCode.slice(0, 4) + ' ' + userCode.slice(4))}\n\n`);
1269
+ process.stdout.write(` ${style.dim('Open in your browser:')} ${verificationUri}\n`);
1270
+ process.stdout.write(` ${style.dim('Enter code:')} ${userCode}\n\n`);
1271
+ // Try to open browser automatically
1272
+ try {
1273
+ const url = `${verificationUri}`;
1274
+ if (process.platform === 'darwin') {
1275
+ execSync(`open '${url}'`, { timeout: 3000 });
1276
+ }
1277
+ else if (process.platform === 'linux') {
1278
+ execSync(`xdg-open '${url}'`, { timeout: 3000 });
1279
+ }
1280
+ process.stdout.write(` ${style.dim('Browser opened.')}\n\n`);
1281
+ }
1282
+ catch {
1283
+ process.stdout.write(` ${style.dim('Open the URL manually.')}\n\n`);
1284
+ }
1285
+ // Poll for token
1286
+ const pollStart = Date.now();
1287
+ const pollTimeout = 15 * 60 * 1000; // 15 minutes
1288
+ for (;;) {
1289
+ await new Promise((r) => setTimeout(r, interval));
1290
+ if (Date.now() - pollStart > pollTimeout) {
1291
+ return usageError(io, 'Login timed out. Run `agora auth login` to try again.');
1292
+ }
1293
+ process.stdout.write(`\r\x1b[K${style.dim('Waiting for browser authorization...')}`);
1294
+ try {
1295
+ const tokenRes = await fetch(`${baseUrl}/auth/device/token`, {
1296
+ method: 'POST',
1297
+ headers: { 'Content-Type': 'application/json' },
1298
+ body: JSON.stringify({ device_code: deviceCode })
1299
+ });
1300
+ if (tokenRes.ok) {
1301
+ const tokenData = (await tokenRes.json());
1302
+ const jwt = tokenData.access_token;
1303
+ process.stdout.write(`\r\x1b[K${style.dim('Authorization received.')}\n`);
1304
+ const nextState = setAuthState(state, { token: jwt, apiUrl });
1305
+ writeAgoraState(dataDir, nextState);
1306
+ if (parsed.flags.json) {
1307
+ writeJson(io.stdout, authStatusPayload(dataDir, getAuthState(nextState)));
1308
+ return 0;
1309
+ }
1310
+ process.stdout.write(`\n${style.accent('✓ Authenticated')}\n`);
1311
+ process.stdout.write(`${style.dim('API URL')} ${baseUrl}\n`);
1312
+ process.stdout.write(`${style.dim('Token expires')} in 1 hour\n`);
1313
+ process.stdout.write(`${style.dim('State')} ${getAgoraStatePath(dataDir)}\n`);
1314
+ return 0;
1315
+ }
1316
+ const errData = await tokenRes.json().catch(() => ({ error: 'unknown' }));
1317
+ if (errData.error === 'expired') {
1318
+ process.stdout.write(`\r\x1b[K`);
1319
+ return usageError(io, 'Code expired. Run `agora auth login` again.');
1320
+ }
1321
+ // "authorization_pending" is expected — keep polling
1322
+ }
1323
+ catch {
1324
+ // Network error, retry
1325
+ }
1326
+ }
1327
+ }
1328
+ catch (e) {
1329
+ return usageError(io, `Login failed: ${e.message || 'connection error'}`);
540
1330
  }
541
- writeLine(io.stdout, 'Stored Agora API token');
542
- writeLine(io.stdout, `API URL: ${auth?.apiUrl || 'not stored'}`);
543
- writeLine(io.stdout, `State: ${getAgoraStatePath(dataDir)}`);
544
- return 0;
545
1331
  }
546
1332
  if (subcommand === 'status') {
547
1333
  if (parsed.flags.json) {
548
1334
  writeJson(io.stdout, authStatusPayload(dataDir, existingAuth));
549
1335
  return 0;
550
1336
  }
551
- writeLine(io.stdout, `Authenticated: ${existingAuth ? 'yes' : 'no'}`);
1337
+ writeLine(io.stdout, `${style.dim('Authenticated')} ${existingAuth ? 'yes' : 'no'}`);
552
1338
  if (existingAuth) {
553
- writeLine(io.stdout, `Token: ${maskToken(existingAuth.token)}`);
554
- writeLine(io.stdout, `API URL: ${existingAuth.apiUrl || 'not stored'}`);
555
- writeLine(io.stdout, `Saved: ${formatDate(existingAuth.savedAt)}`);
1339
+ writeLine(io.stdout, `${style.dim('Token')} ${maskToken(existingAuth.token)}`);
1340
+ writeLine(io.stdout, `${style.dim('API URL')} ${existingAuth.apiUrl || 'not stored'}`);
1341
+ writeLine(io.stdout, `${style.dim('Saved')} ${formatDate(existingAuth.savedAt)}`);
556
1342
  }
557
- writeLine(io.stdout, `State: ${getAgoraStatePath(dataDir)}`);
1343
+ writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
558
1344
  return 0;
559
1345
  }
560
1346
  if (subcommand === 'logout') {
@@ -605,8 +1391,8 @@ async function commandSave(parsed, io) {
605
1391
  });
606
1392
  return 0;
607
1393
  }
608
- writeLine(io.stdout, result.added ? `Saved ${item.id}` : `${item.id} is already saved`);
609
- writeLine(io.stdout, `State: ${getAgoraStatePath(dataDir)}`);
1394
+ writeLine(io.stdout, result.added ? `Saved ${style.accent(item.id)}` : `${style.accent(item.id)} is already saved`);
1395
+ writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
610
1396
  return 0;
611
1397
  }
612
1398
  function commandSaved(parsed, io) {
@@ -628,7 +1414,7 @@ function commandSaved(parsed, io) {
628
1414
  writeLine(io.stdout, 'Run agora save <id> to save a package or workflow.');
629
1415
  return 0;
630
1416
  }
631
- writeLine(io.stdout, `Saved Agora items (${saved.length})`);
1417
+ writeLine(io.stdout, header('agora saved', [`${saved.length} items`]));
632
1418
  writeLine(io.stdout, formatSavedList(saved));
633
1419
  return 0;
634
1420
  }
@@ -655,7 +1441,7 @@ function commandRemove(parsed, io) {
655
1441
  if (!result.removed) {
656
1442
  return usageError(io, `Saved item not found: ${id}`);
657
1443
  }
658
- writeLine(io.stdout, `Removed ${targetId}`);
1444
+ writeLine(io.stdout, `Removed ${style.accent(targetId)}`);
659
1445
  return 0;
660
1446
  }
661
1447
  async function commandPublish(parsed, io) {
@@ -691,8 +1477,8 @@ async function commandPublish(parsed, io) {
691
1477
  writeJson(io.stdout, sourcePayload(result, { item: result.data }));
692
1478
  return 0;
693
1479
  }
694
- writeLine(io.stdout, `Published package ${result.data.id}`);
695
- writeLine(io.stdout, `${result.data.name} (${result.source})`);
1480
+ writeLine(io.stdout, `Published package ${style.accent(result.data.id)}`);
1481
+ writeLine(io.stdout, `${result.data.name} (${sourceLabel(result)})`);
696
1482
  return 0;
697
1483
  }
698
1484
  const prompt = promptInput(parsed, io);
@@ -711,8 +1497,8 @@ async function commandPublish(parsed, io) {
711
1497
  writeJson(io.stdout, sourcePayload(result, { item: result.data }));
712
1498
  return 0;
713
1499
  }
714
- writeLine(io.stdout, `Published workflow ${result.data.id}`);
715
- writeLine(io.stdout, `${result.data.name} (${result.source})`);
1500
+ writeLine(io.stdout, `Published workflow ${style.accent(result.data.id)}`);
1501
+ writeLine(io.stdout, `${result.data.name} (${sourceLabel(result)})`);
716
1502
  return 0;
717
1503
  }
718
1504
  async function commandReview(parsed, io) {
@@ -737,8 +1523,8 @@ async function commandReview(parsed, io) {
737
1523
  writeJson(io.stdout, sourcePayload(result, { review: result.data }));
738
1524
  return 0;
739
1525
  }
740
- writeLine(io.stdout, `Reviewed ${result.data.itemId}`);
741
- writeLine(io.stdout, `${result.data.rating}/5 by ${result.data.author}`);
1526
+ writeLine(io.stdout, `Reviewed ${style.accent(result.data.itemId)}`);
1527
+ writeLine(io.stdout, `${style.dim(result.data.rating + '/5 by ' + result.data.author)}`);
742
1528
  return 0;
743
1529
  }
744
1530
  async function commandReviews(parsed, io) {
@@ -755,7 +1541,8 @@ async function commandReviews(parsed, io) {
755
1541
  writeLine(io.stdout, itemId ? `No reviews found for ${itemId}.` : 'No reviews found.');
756
1542
  return 0;
757
1543
  }
758
- writeLine(io.stdout, `Agora reviews (${result.data.length}, source: ${result.source})`);
1544
+ writeLine(io.stdout, header('agora reviews', [`${result.data.length} results`, sourceLabel(result)]));
1545
+ writeLine(io.stdout, '');
759
1546
  writeLine(io.stdout, formatReviewList(result.data));
760
1547
  return 0;
761
1548
  }
@@ -776,46 +1563,127 @@ async function commandProfile(parsed, io) {
776
1563
  writeLine(io.stdout, formatProfileDetail(result.data));
777
1564
  return 0;
778
1565
  }
1566
+ async function commandPreferences(parsed, io) {
1567
+ const dataDir = detectDataDir(parsed, io);
1568
+ const prefs = loadPreferences(dataDir);
1569
+ const sub = parsed.args[0];
1570
+ if (!sub) {
1571
+ if (parsed.flags.json) {
1572
+ writeJson(io.stdout, prefs);
1573
+ return 0;
1574
+ }
1575
+ writeLine(io.stdout, `Preferences (${prefsPath(dataDir)})`);
1576
+ writeLine(io.stdout, ` theme: ${prefs.theme}`);
1577
+ writeLine(io.stdout, ` verbosity: ${prefs.verbosity}`);
1578
+ writeLine(io.stdout, ` username: ${prefs.username || '(not set)'}`);
1579
+ writeLine(io.stdout, ` email: ${prefs.email || '(not set)'}`);
1580
+ writeLine(io.stdout, ` bio: ${prefs.bio ? prefs.bio.slice(0, 60) + (prefs.bio.length > 60 ? '...' : '') : '(not set)'}`);
1581
+ writeLine(io.stdout, '');
1582
+ writeLine(io.stdout, ' Set values: agora preferences <key> <value>');
1583
+ writeLine(io.stdout, ' Keys: theme, verbosity, username, email, bio');
1584
+ return 0;
1585
+ }
1586
+ const key = sub;
1587
+ const val = parsed.args.slice(1).join(' ');
1588
+ if (!val || !(key in prefs)) {
1589
+ return usageError(io, `Usage: agora preferences <key> <value>\nValid keys: theme, verbosity, username, email, bio`);
1590
+ }
1591
+ if (key === 'theme' && !['dark', 'light', 'auto'].includes(val)) {
1592
+ return usageError(io, 'theme must be: dark, light, or auto');
1593
+ }
1594
+ if (key === 'verbosity' && !['verbose', 'medium', 'quiet'].includes(val)) {
1595
+ return usageError(io, 'verbosity must be: verbose, medium, or quiet');
1596
+ }
1597
+ prefs[key] = val;
1598
+ writePreferences(dataDir, prefs);
1599
+ writeLine(io.stdout, `\u2713 ${key} set to "${val}"`);
1600
+ return 0;
1601
+ }
1602
+ async function commandHistory(parsed, io) {
1603
+ const dataDir = detectDataDir(parsed, io);
1604
+ const limit = numberFlag(parsed, 'limit', 'n') || 50;
1605
+ if (parsed.flags.clear) {
1606
+ clearHistory(dataDir);
1607
+ writeLine(io.stdout, '\u2713 History cleared');
1608
+ return 0;
1609
+ }
1610
+ const entries = loadHistory(dataDir, limit);
1611
+ if (parsed.flags.json) {
1612
+ writeJson(io.stdout, entries);
1613
+ return 0;
1614
+ }
1615
+ if (entries.length === 0) {
1616
+ writeLine(io.stdout, 'No history yet.');
1617
+ writeLine(io.stdout, 'Searches and chat messages are recorded automatically.');
1618
+ return 0;
1619
+ }
1620
+ writeLine(io.stdout, `Recent history (${entries.length}):`);
1621
+ for (const entry of entries) {
1622
+ const icon = entry.type === 'search' ? '\uD83D\uDD0D' : '\uD83D\uDCAC';
1623
+ const date = new Date(entry.timestamp).toLocaleString();
1624
+ const query = entry.query.length > 60 ? entry.query.slice(0, 60) + '...' : entry.query;
1625
+ writeLine(io.stdout, ` ${icon} ${style.dim(date)} ${query}`);
1626
+ }
1627
+ writeLine(io.stdout, '');
1628
+ writeLine(io.stdout, style.dim('Use --clear to clear history, --json for JSON output.'));
1629
+ return 0;
1630
+ }
779
1631
  function formatItemList(items) {
1632
+ const idWidth = Math.max(...items.map((item) => item.id.length));
780
1633
  return items
781
- .map((item, index) => {
782
- const installs = item.kind === 'package' ? ` | installs ${formatCount(item.installs)}` : '';
1634
+ .map((item) => {
1635
+ const metrics = item.kind === 'package'
1636
+ ? `${formatNumber(item.installs)} installs · ${formatNumber(item.stars)} ★`
1637
+ : `${formatNumber(item.stars)} ★`;
783
1638
  return [
784
- `${index + 1}. ${item.id} [${item.category}]`,
785
- ` ${item.name}`,
786
- ` ${truncate(item.description, 88)}`,
787
- ` stars ${formatCount(item.stars)}${installs} | by ${item.author}`
1639
+ `${style.accent(item.id.padEnd(idWidth))} ${style.dim(metrics)}`,
1640
+ style.dim(item.name),
1641
+ truncate(item.description, 88),
1642
+ style.dim(`${item.category} · by ${item.author}`)
788
1643
  ].join('\n');
789
1644
  })
790
1645
  .join('\n\n');
791
1646
  }
1647
+ function formatItemTable(items) {
1648
+ const idW = Math.max(4, ...items.map((i) => i.id.length));
1649
+ const nameW = Math.max(4, ...items.map((i) => i.name.length));
1650
+ const starW = 6;
1651
+ const installW = 9;
1652
+ const totalW = idW + 3 + nameW + 3 + starW + 3 + installW + 4;
1653
+ const top = '┌' + '─'.repeat(totalW - 2) + '┐';
1654
+ const bot = '└' + '─'.repeat(totalW - 2) + '┘';
1655
+ const sep = '│';
1656
+ const hdr = sep + ' ' + 'id'.padEnd(idW) + ' │ ' + 'name'.padEnd(nameW) + ' │ ' + 'stars'.padStart(starW) + ' │ ' + 'installs'.padStart(installW) + ' │';
1657
+ const rows = items.map((item) => sep + ' ' + style.accent(item.id.padEnd(idW)) + ' │ ' + style.dim(item.name.padEnd(nameW)) + ' │ ' + style.dim(formatNumber(item.stars).padStart(starW)) + ' │ ' + style.dim(formatNumber(item.installs).padStart(installW)) + ' │');
1658
+ return [top, hdr, ...rows, bot].join('\n');
1659
+ }
792
1660
  function formatItemDetail(item) {
793
1661
  const lines = [
794
- `${item.name}`,
795
- `id: ${item.id}`,
796
- `type: ${item.kind}`,
797
- `category: ${item.category}`,
798
- `author: ${item.author}`,
799
- `stars: ${formatCount(item.stars)}`,
800
- `install: ${getInstallKind(item)}`,
1662
+ style.bold(item.name),
1663
+ `${style.dim('id')} ${style.accent(item.id)}`,
1664
+ `${style.dim('type')} ${item.kind}`,
1665
+ `${style.dim('category')} ${item.category}`,
1666
+ `${style.dim('author')} ${item.author}`,
1667
+ `${style.dim('stars')} ${formatNumber(item.stars)}`,
1668
+ `${style.dim('install')} ${getInstallKind(item)}`,
801
1669
  '',
802
1670
  item.description,
803
1671
  '',
804
- `tags: ${item.tags.join(', ')}`
1672
+ `${style.dim('tags')} ${item.tags.join(', ')}`
805
1673
  ];
806
1674
  if (item.kind === 'package') {
807
- lines.splice(5, 0, `version: ${item.version}`);
808
- lines.push(`installs: ${formatCount(item.installs)}`);
1675
+ lines.splice(5, 0, `${style.dim('version')} ${item.version}`);
1676
+ lines.push(`${style.dim('installs')} ${formatNumber(item.installs)}`);
809
1677
  if (item.repository)
810
- lines.push(`repository: ${item.repository}`);
1678
+ lines.push(`${style.dim('repo')} ${item.repository}`);
811
1679
  if (item.npmPackage)
812
- lines.push(`npm: ${item.npmPackage}`);
1680
+ lines.push(`${style.dim('npm')} ${item.npmPackage}`);
813
1681
  }
814
1682
  if (item.kind === 'workflow') {
815
- lines.push(`forks: ${item.forks}`);
1683
+ lines.push(`${style.dim('forks')} ${item.forks}`);
816
1684
  if (item.model)
817
- lines.push(`model: ${item.model}`);
818
- lines.push('', 'prompt:', item.prompt);
1685
+ lines.push(`${style.dim('model')} ${item.model}`);
1686
+ lines.push('', style.dim('prompt'), item.prompt);
819
1687
  }
820
1688
  return lines.join('\n');
821
1689
  }
@@ -824,15 +1692,15 @@ function formatSavedList(items) {
824
1692
  .map((entry, index) => {
825
1693
  if (!entry.item) {
826
1694
  return [
827
- `${index + 1}. ${entry.saved.id} [missing]`,
828
- ` saved ${formatDate(entry.saved.savedAt)}`
1695
+ `${index + 1}. ${style.accent(entry.saved.id)} ${style.dim('[missing]')}`,
1696
+ ` ${style.dim('saved ' + formatDate(entry.saved.savedAt))}`
829
1697
  ].join('\n');
830
1698
  }
831
1699
  return [
832
- `${index + 1}. ${entry.item.id} [${entry.item.category}]`,
833
- ` ${entry.item.name}`,
1700
+ `${index + 1}. ${style.accent(entry.item.id)} ${style.dim('[' + entry.item.category + ']')}`,
1701
+ ` ${style.dim(entry.item.name)}`,
834
1702
  ` ${truncate(entry.item.description, 88)}`,
835
- ` saved ${formatDate(entry.saved.savedAt)}`
1703
+ ` ${style.dim('saved ' + formatDate(entry.saved.savedAt))}`
836
1704
  ].join('\n');
837
1705
  })
838
1706
  .join('\n\n');
@@ -841,8 +1709,8 @@ function formatReviewList(reviews) {
841
1709
  return reviews
842
1710
  .map((review, index) => {
843
1711
  return [
844
- `${index + 1}. ${review.itemId} [${review.itemType}]`,
845
- ` rating ${review.rating}/5 by ${review.author}`,
1712
+ `${index + 1}. ${style.accent(review.itemId)} ${style.dim('[' + review.itemType + ']')}`,
1713
+ ` ${style.dim('rating ' + review.rating + '/5 by ' + review.author)}`,
846
1714
  ` ${truncate(review.content, 88)}`
847
1715
  ].join('\n');
848
1716
  })
@@ -850,28 +1718,28 @@ function formatReviewList(reviews) {
850
1718
  }
851
1719
  function formatProfileDetail(profile) {
852
1720
  const lines = [
853
- profile.displayName,
854
- `username: ${profile.username}`,
855
- `packages: ${formatCount(profile.packages)}`,
856
- `workflows: ${formatCount(profile.workflows)}`,
857
- `discussions: ${formatCount(profile.discussions)}`
1721
+ style.bold(profile.displayName),
1722
+ `${style.dim('username')} ${style.accent(profile.username)}`,
1723
+ `${style.dim('packages')} ${formatNumber(profile.packages)}`,
1724
+ `${style.dim('workflows')} ${formatNumber(profile.workflows)}`,
1725
+ `${style.dim('discussions')} ${formatNumber(profile.discussions)}`
858
1726
  ];
859
1727
  if (profile.bio)
860
- lines.splice(2, 0, `bio: ${profile.bio}`);
1728
+ lines.splice(2, 0, `${style.dim('bio')} ${profile.bio}`);
861
1729
  if (profile.avatarUrl)
862
- lines.push(`avatar: ${profile.avatarUrl}`);
1730
+ lines.push(`${style.dim('avatar')} ${profile.avatarUrl}`);
863
1731
  if (profile.joinedAt)
864
- lines.push(`joined: ${formatDate(profile.joinedAt)}`);
1732
+ lines.push(`${style.dim('joined')} ${formatDate(profile.joinedAt)}`);
865
1733
  return lines.join('\n');
866
1734
  }
867
1735
  function formatTutorialList(tutorials) {
868
1736
  return tutorials
869
1737
  .map((tutorial, index) => {
870
1738
  return [
871
- `${index + 1}. ${tutorial.id} [${tutorial.level}]`,
872
- ` ${tutorial.title}`,
1739
+ `${index + 1}. ${style.accent(tutorial.id)} ${style.dim('[' + tutorial.level + ']')}`,
1740
+ ` ${style.dim(tutorial.title)}`,
873
1741
  ` ${truncate(tutorial.description, 88)}`,
874
- ` ${tutorial.duration} | ${tutorial.steps.length} steps`
1742
+ ` ${style.dim(tutorial.duration + ' | ' + tutorial.steps.length + ' steps')}`
875
1743
  ].join('\n');
876
1744
  })
877
1745
  .join('\n\n');
@@ -880,81 +1748,83 @@ function formatTutorialStep(tutorial, stepNumber) {
880
1748
  const payload = tutorialStepPayload(tutorial, stepNumber);
881
1749
  if (payload.completed) {
882
1750
  return [
883
- `${tutorial.title}`,
884
- `Completed ${tutorial.steps.length}/${tutorial.steps.length} steps.`,
1751
+ style.bold(tutorial.title),
1752
+ style.dim(`Completed ${tutorial.steps.length}/${tutorial.steps.length} steps.`),
885
1753
  'Run agora tutorials for more tutorials.'
886
1754
  ].join('\n');
887
1755
  }
888
1756
  const lines = [
889
- `${tutorial.title}`,
890
- `id: ${tutorial.id}`,
891
- `level: ${tutorial.level}`,
892
- `duration: ${tutorial.duration}`,
893
- `step: ${payload.stepNumber}/${tutorial.steps.length}`,
1757
+ style.bold(tutorial.title),
1758
+ `${style.dim('id')} ${style.accent(tutorial.id)}`,
1759
+ `${style.dim('level')} ${tutorial.level}`,
1760
+ `${style.dim('duration')} ${tutorial.duration}`,
1761
+ `${style.dim('step')} ${payload.stepNumber}/${tutorial.steps.length}`,
894
1762
  '',
895
1763
  payload.title || '',
896
1764
  payload.content || ''
897
1765
  ];
898
1766
  if (payload.code) {
899
- lines.push('', 'code:', payload.code);
1767
+ lines.push('', style.dim('code:'), payload.code);
900
1768
  }
901
1769
  return lines.join('\n');
902
1770
  }
903
- function usage() {
904
- return [
905
- 'Agora CLI',
906
- '',
907
- 'Usage:',
908
- ' agora init [--dry-run] [--json]',
909
- ' agora use <workflow-id> [--json]',
910
- ' agora search <query> [--category mcp|prompt|workflow|skill] [--limit 10] [--json]',
911
- ' agora browse <id> [--type package|workflow] [--json]',
912
- ' agora trending [all|packages|workflows] [--limit 5] [--json]',
913
- ' agora workflows [query] [--limit 10] [--json]',
914
- ' agora tutorials [query] [--level beginner|intermediate|advanced] [--limit 20] [--json]',
915
- ' agora tutorial <id> [step] [--json]',
916
- ' agora discussions [query] [--category question|idea|showcase|discussion] [--json]',
917
- ' agora discuss --title <title> (--content <text>|--content-file path) [--category question|idea|showcase|discussion]',
918
- ' agora install <id> [--write] [--config path] [--json]',
919
- ' agora save <id> [--data-dir path] [--json]',
920
- ' agora saved [query] [--data-dir path] [--json]',
921
- ' agora remove <id> [--data-dir path] [--json]',
922
- ' agora auth login --token <token> [--api-url url] [--data-dir path]',
923
- ' agora auth status [--data-dir path] [--json]',
924
- ' agora auth logout [--data-dir path]',
925
- ' agora publish package --name <name> --description <text> --npm <package> [--token token]',
926
- ' agora publish workflow --name <name> --description <text> --prompt-file <path> [--token token]',
927
- ' agora review <id> --rating 5 --content <text> [--token token]',
928
- ' agora reviews [id] [--type package|workflow]',
929
- ' agora profile <username> [--json]',
930
- ' agora config doctor [--config path] [--json]',
931
- '',
932
- 'Data source:',
933
- ' --api Use the live Agora API',
934
- ' --api-url <url> Override AGORA_API_URL',
935
- ' --api-timeout <ms> API timeout before offline fallback',
936
- ' --token <token> API auth token, defaults to env vars or agora auth login',
937
- ' --offline Force local bundled marketplace data',
938
- '',
939
- 'Examples:',
940
- ' agora init',
941
- ' agora init --dry-run',
942
- ' agora use wf-tdd-cycle',
943
- ' agora search filesystem',
944
- ' agora search filesystem --api',
945
- ' agora browse mcp-github',
946
- ' agora tutorials mcp',
947
- ' agora tutorial tut-mcp-basics 2',
948
- ' agora install mcp-github',
949
- ' agora install mcp-github --write',
950
- ' agora save wf-security-audit',
951
- ' agora saved',
952
- ' agora auth login --token $AGORA_TOKEN --api-url https://agora.example.com',
953
- ' agora discuss --title "MCP question" --content "How are you composing servers?" --category question',
954
- ' agora profile alice',
955
- ' agora publish package --name @you/server --description "MCP server" --npm @you/server',
956
- ' agora review mcp-github --rating 5 --content "Works well"'
1771
+ function welcome(color, trueColor) {
1772
+ if (!color) {
1773
+ return [
1774
+ '',
1775
+ `agora · Developers' CLI marketplace and community hub · v${VERSION}`,
1776
+ '',
1777
+ ' Search agora search <query>',
1778
+ ' Browse agora trending · agora browse <id>',
1779
+ ' Learn agora tutorials · agora tutorial <id>',
1780
+ ' Install agora install <id> [--write]',
1781
+ ' Setup agora init [--mcp] · agora use <workflow>',
1782
+ ' Auth agora login [--api-url <url>]',
1783
+ ''
1784
+ ].join('\n');
1785
+ }
1786
+ const banner = renderBanner({ color, trueColor });
1787
+ const box = renderBox('Welcome to Agora', [
1788
+ "Developers' CLI marketplace and community hub - type a command, bash or chat:",
1789
+ `v${VERSION} · run \`agora help\` to get started`
1790
+ ], { color, trueColor });
1791
+ const hint = [
1792
+ `${style.dim('Search')} agora search <query>`,
1793
+ `${style.dim('Browse')} agora trending · agora browse <id>`,
1794
+ `${style.dim('Learn')} agora tutorials · agora tutorial <id>`,
1795
+ `${style.dim('Install')} agora install <id> [--write]`,
1796
+ `${style.dim('Setup')} agora init [--mcp] · agora use <workflow>`,
1797
+ `${style.dim('Auth')} agora login [--api-url <url>]`
957
1798
  ].join('\n');
1799
+ return `\n${banner}\n\n${box}\n\n${hint}\n`;
1800
+ }
1801
+ /** Flat-minimal section header: accent title, dim ` · `-joined metadata. */
1802
+ function header(title, meta) {
1803
+ return [style.accent(title), ...meta.map((part) => style.dim(part))].join(style.dim(' · '));
1804
+ }
1805
+ function usage() {
1806
+ const nameWidth = Math.max(...COMMANDS.map((c) => c.name.length));
1807
+ const groups = ['Marketplace', 'Setup', 'Library', 'Learn', 'Community'];
1808
+ const lines = [
1809
+ `${style.accent('agora')}${style.dim(` · Developers' CLI marketplace and community hub · v${VERSION}`)}`,
1810
+ ''
1811
+ ];
1812
+ for (const group of groups) {
1813
+ const groupCmds = COMMANDS.filter((c) => c.group === group);
1814
+ lines.push(style.dim(group));
1815
+ for (const cmd of groupCmds) {
1816
+ lines.push(` ${style.accent(cmd.name.padEnd(nameWidth))} ${style.dim(cmd.summary)}`);
1817
+ }
1818
+ lines.push('');
1819
+ }
1820
+ lines.push(style.dim('Run `agora help <command>` for details on any command.'));
1821
+ return lines.join('\n');
1822
+ }
1823
+ export function commandManual(name) {
1824
+ const meta = COMMANDS.find((c) => c.name === name);
1825
+ if (!meta)
1826
+ return '';
1827
+ return renderManual(meta, style);
958
1828
  }
959
1829
  function usageError(io, message) {
960
1830
  writeLine(io.stderr, message);
@@ -990,6 +1860,8 @@ function shortFlag(arg) {
990
1860
  return 'help';
991
1861
  if (flag === 'j')
992
1862
  return 'json';
1863
+ if (flag === 'm')
1864
+ return 'model';
993
1865
  if (flag === 'c')
994
1866
  return 'c';
995
1867
  if (flag === 'n')
@@ -1140,6 +2012,11 @@ function tutorialStepPayload(tutorial, stepNumber) {
1140
2012
  code: step?.code
1141
2013
  };
1142
2014
  }
2015
+ function sourceLabel(result) {
2016
+ return result.source === 'offline'
2017
+ ? `source: offline · refreshed ${dataRefreshedAt}`
2018
+ : `source: ${result.source}`;
2019
+ }
1143
2020
  function warnFallback(result, io) {
1144
2021
  if (result.fallbackReason) {
1145
2022
  writeLine(io.stderr, `API unavailable, using offline data (refreshed ${dataRefreshedAt}): ${result.fallbackReason}`);
@@ -1200,13 +2077,6 @@ function truncate(value, max) {
1200
2077
  return value;
1201
2078
  return `${value.slice(0, max - 3)}...`;
1202
2079
  }
1203
- function formatCount(value) {
1204
- if (value >= 1000000)
1205
- return `${(value / 1000000).toFixed(1)}M`;
1206
- if (value >= 1000)
1207
- return `${(value / 1000).toFixed(1)}K`;
1208
- return String(value);
1209
- }
1210
2080
  function formatDate(value) {
1211
2081
  return value.slice(0, 10);
1212
2082
  }