opencode-agora 0.2.2 → 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 (175) 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 +1202 -227
  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-files.d.ts.map +1 -1
  81. package/dist/config-files.js +11 -8
  82. package/dist/config-files.js.map +1 -1
  83. package/dist/config.d.ts +8 -8
  84. package/dist/config.d.ts.map +1 -1
  85. package/dist/config.js +16 -27
  86. package/dist/config.js.map +1 -1
  87. package/dist/data.d.ts +1 -1
  88. package/dist/data.d.ts.map +1 -1
  89. package/dist/data.js +1013 -545
  90. package/dist/data.js.map +1 -1
  91. package/dist/format.d.ts +5 -39
  92. package/dist/format.d.ts.map +1 -1
  93. package/dist/format.js +5 -118
  94. package/dist/format.js.map +1 -1
  95. package/dist/history.d.ts +13 -0
  96. package/dist/history.d.ts.map +1 -0
  97. package/dist/history.js +37 -0
  98. package/dist/history.js.map +1 -0
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +151 -236
  101. package/dist/index.js.map +1 -1
  102. package/dist/init.d.ts +4 -1
  103. package/dist/init.d.ts.map +1 -1
  104. package/dist/init.js +103 -51
  105. package/dist/init.js.map +1 -1
  106. package/dist/live.d.ts +4 -0
  107. package/dist/live.d.ts.map +1 -1
  108. package/dist/live.js +87 -19
  109. package/dist/live.js.map +1 -1
  110. package/dist/marketplace.d.ts +9 -0
  111. package/dist/marketplace.d.ts.map +1 -1
  112. package/dist/marketplace.js +128 -33
  113. package/dist/marketplace.js.map +1 -1
  114. package/dist/news/cache.d.ts +13 -0
  115. package/dist/news/cache.d.ts.map +1 -0
  116. package/dist/news/cache.js +65 -0
  117. package/dist/news/cache.js.map +1 -0
  118. package/dist/news/score.d.ts +4 -0
  119. package/dist/news/score.d.ts.map +1 -0
  120. package/dist/news/score.js +43 -0
  121. package/dist/news/score.js.map +1 -0
  122. package/dist/news/sources/arxiv.d.ts +9 -0
  123. package/dist/news/sources/arxiv.d.ts.map +1 -0
  124. package/dist/news/sources/arxiv.js +103 -0
  125. package/dist/news/sources/arxiv.js.map +1 -0
  126. package/dist/news/sources/github-trending.d.ts +9 -0
  127. package/dist/news/sources/github-trending.d.ts.map +1 -0
  128. package/dist/news/sources/github-trending.js +93 -0
  129. package/dist/news/sources/github-trending.js.map +1 -0
  130. package/dist/news/sources/hn.d.ts +9 -0
  131. package/dist/news/sources/hn.d.ts.map +1 -0
  132. package/dist/news/sources/hn.js +53 -0
  133. package/dist/news/sources/hn.js.map +1 -0
  134. package/dist/news/sources/reddit.d.ts +9 -0
  135. package/dist/news/sources/reddit.d.ts.map +1 -0
  136. package/dist/news/sources/reddit.js +68 -0
  137. package/dist/news/sources/reddit.js.map +1 -0
  138. package/dist/news/sources/rss.d.ts +14 -0
  139. package/dist/news/sources/rss.d.ts.map +1 -0
  140. package/dist/news/sources/rss.js +102 -0
  141. package/dist/news/sources/rss.js.map +1 -0
  142. package/dist/news/types.d.ts +39 -0
  143. package/dist/news/types.d.ts.map +1 -0
  144. package/dist/news/types.js +47 -0
  145. package/dist/news/types.js.map +1 -0
  146. package/dist/preferences.d.ts +14 -0
  147. package/dist/preferences.d.ts.map +1 -0
  148. package/dist/preferences.js +31 -0
  149. package/dist/preferences.js.map +1 -0
  150. package/dist/settings.d.ts +26 -0
  151. package/dist/settings.d.ts.map +1 -0
  152. package/dist/settings.js +257 -0
  153. package/dist/settings.js.map +1 -0
  154. package/dist/state.d.ts.map +1 -1
  155. package/dist/state.js +7 -6
  156. package/dist/state.js.map +1 -1
  157. package/dist/transcript.d.ts +28 -0
  158. package/dist/transcript.d.ts.map +1 -0
  159. package/dist/transcript.js +79 -0
  160. package/dist/transcript.js.map +1 -0
  161. package/dist/types.d.ts +6 -1
  162. package/dist/types.d.ts.map +1 -1
  163. package/dist/ui.d.ts +157 -0
  164. package/dist/ui.d.ts.map +1 -0
  165. package/dist/ui.js +296 -0
  166. package/dist/ui.js.map +1 -0
  167. package/package.json +21 -10
  168. package/dist/api.d.ts +0 -69
  169. package/dist/api.d.ts.map +0 -1
  170. package/dist/api.js +0 -109
  171. package/dist/api.js.map +0 -1
  172. package/dist/logger.d.ts +0 -20
  173. package/dist/logger.d.ts.map +0 -1
  174. package/dist/logger.js +0 -59
  175. package/dist/logger.js.map +0 -1
package/dist/cli/app.js CHANGED
@@ -1,24 +1,89 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import pkg from '../../package.json';
4
+ import { spawn } from 'node:child_process';
5
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';
6
10
  import { detectOpenCodeConfigPath, doctorOpenCodeConfig, loadOpenCodeConfig, writeOpenCodeConfig } from '../config-files.js';
7
- 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';
8
20
  import { scanProject, generateInitPlan, applyInitPlan, runCommands } from '../init.js';
21
+ import { installAgoraCommand } from '../commands.js';
9
22
  import { sampleWorkflows, dataRefreshedAt } from '../data.js';
10
23
  import { createDiscussionSource, discussionsSource, findMarketplaceSource, createReviewSource, findTutorialSource, listReviewsSource, profileSource, publishPackageSource, publishWorkflowSource, searchMarketplaceSource, trendingMarketplaceSource, tutorialsSource } from '../live.js';
11
24
  import { clearAuthState, detectAgoraDataDir, getAuthState, getAgoraStatePath, loadAgoraState, removeItemFromState, resolveSavedItems, saveItemToState, setAuthState, writeAgoraState } from '../state.js';
12
- const VERSION = pkg.version;
13
- const booleanFlags = new Set(['api', 'help', 'json', 'live', 'offline', 'version', 'verbose', 'write']);
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';
28
+ const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
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);
34
+ const booleanFlags = new Set([
35
+ 'api',
36
+ 'continue',
37
+ 'dryRun',
38
+ 'help',
39
+ 'json',
40
+ 'live',
41
+ 'mcp',
42
+ 'offline',
43
+ 'table',
44
+ 'version',
45
+ 'verbose',
46
+ 'write'
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
+ }
14
63
  export async function runCli(argv, io) {
15
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));
16
68
  if (parsed.flags.version) {
17
69
  writeLine(io.stdout, VERSION);
18
70
  return 0;
19
71
  }
20
- if (!parsed.command || parsed.flags.help) {
21
- 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)));
22
87
  return 0;
23
88
  }
24
89
  try {
@@ -56,16 +121,65 @@ export async function runCli(argv, io) {
56
121
  case 'profile':
57
122
  return await commandProfile(parsed, io);
58
123
  case 'auth':
59
- 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);
60
131
  case 'config':
61
132
  return await commandConfig(parsed, io);
133
+ case 'mcp':
134
+ return await commandMcp(parsed, io);
135
+ case 'chat':
136
+ return await commandChat(parsed, io);
62
137
  case 'init':
63
138
  return await commandInit(parsed, io);
64
139
  case 'use':
65
140
  return await commandUse(parsed, io);
66
- case 'help':
67
- 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
+ }
68
181
  return 0;
182
+ }
69
183
  default:
70
184
  writeLine(io.stderr, `Unknown command: ${parsed.command}`);
71
185
  writeLine(io.stderr, 'Run agora help for usage.');
@@ -92,7 +206,9 @@ export function parseArgs(argv) {
92
206
  if (inlineValue !== undefined) {
93
207
  flags[key] = inlineValue;
94
208
  }
95
- else if (!booleanFlags.has(key) && argv[index + 1] && !argv[index + 1].startsWith('-')) {
209
+ else if (!booleanFlags.has(key) &&
210
+ argv[index + 1] &&
211
+ (!argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) {
96
212
  flags[key] = argv[index + 1];
97
213
  index += 1;
98
214
  }
@@ -103,7 +219,9 @@ export function parseArgs(argv) {
103
219
  }
104
220
  if (arg.startsWith('-') && arg.length > 1) {
105
221
  const key = shortFlag(arg);
106
- if (!booleanFlags.has(key) && argv[index + 1] && !argv[index + 1].startsWith('-')) {
222
+ if (!booleanFlags.has(key) &&
223
+ argv[index + 1] &&
224
+ (!argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) {
107
225
  flags[key] = argv[index + 1];
108
226
  index += 1;
109
227
  }
@@ -123,30 +241,61 @@ export function parseArgs(argv) {
123
241
  async function commandSearch(parsed, io) {
124
242
  const query = parsed.args.join(' ');
125
243
  const category = stringFlag(parsed, 'category', 'c') || 'all';
126
- const limit = numberFlag(parsed, 'limit', 'n') || 10;
127
- const result = await searchMarketplaceSource({ ...sourceOptions(parsed, io), query, category, limit });
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);
250
+ const result = await searchMarketplaceSource({
251
+ ...sourceOptions(parsed, io),
252
+ query,
253
+ category,
254
+ limit,
255
+ sortBy: sortBy,
256
+ sortOrder,
257
+ page,
258
+ perPage
259
+ });
128
260
  const results = result.data;
129
261
  warnFallback(result, io);
130
262
  if (parsed.flags.json) {
131
- 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 }));
132
264
  return 0;
133
265
  }
134
266
  if (results.length === 0) {
135
267
  writeLine(io.stdout, `No results found for "${query}".`);
136
268
  return 0;
137
269
  }
138
- const sourceLabel = result.source === 'offline'
139
- ? `source: offline, refreshed ${dataRefreshedAt}`
140
- : `source: ${result.source}`;
141
- writeLine(io.stdout, `Agora search: ${query || 'all'} (${results.length} shown, ${sourceLabel})`);
142
- 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
+ });
143
288
  return 0;
144
289
  }
145
290
  async function commandBrowse(parsed, io) {
146
291
  const id = parsed.args[0];
147
292
  if (!id)
148
293
  return usageError(io, 'browse requires an item id');
149
- const result = await findMarketplaceSource({ ...sourceOptions(parsed, io), id, type: stringFlag(parsed, 'type', 't') });
294
+ const result = await findMarketplaceSource({
295
+ ...sourceOptions(parsed, io),
296
+ id,
297
+ type: stringFlag(parsed, 'type', 't')
298
+ });
150
299
  const item = result.data;
151
300
  warnFallback(result, io);
152
301
  if (!item)
@@ -156,11 +305,22 @@ async function commandBrowse(parsed, io) {
156
305
  return 0;
157
306
  }
158
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
+ }
159
318
  return 0;
160
319
  }
161
320
  async function commandTrending(parsed, io) {
162
321
  const category = stringFlag(parsed, 'category', 'c') || parsed.args[0] || 'all';
163
322
  const limit = numberFlag(parsed, 'limit', 'n') || 5;
323
+ const table = Boolean(parsed.flags.table);
164
324
  const result = await trendingMarketplaceSource({ ...sourceOptions(parsed, io), category, limit });
165
325
  const items = result.data;
166
326
  warnFallback(result, io);
@@ -168,32 +328,373 @@ async function commandTrending(parsed, io) {
168
328
  writeJson(io.stdout, sourcePayload(result, { category, count: items.length, tags: getTrendingTags(), items }));
169
329
  return 0;
170
330
  }
171
- writeLine(io.stdout, `Trending in Agora (${category}, source: ${result.source})`);
172
- writeLine(io.stdout, formatItemList(items));
173
- 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(', ')}`);
174
341
  return 0;
175
342
  }
176
343
  async function commandWorkflows(parsed, io) {
177
344
  const query = parsed.args.join(' ');
178
345
  const limit = numberFlag(parsed, 'limit', 'n') || 10;
179
- const result = await searchMarketplaceSource({ ...sourceOptions(parsed, io), query, category: 'workflow', limit });
180
- const workflows = result.data.filter((item) => item.kind === 'workflow');
346
+ const result = await searchMarketplaceSource({
347
+ ...sourceOptions(parsed, io),
348
+ query,
349
+ category: 'workflow',
350
+ limit
351
+ });
352
+ const workflows = result.data;
181
353
  warnFallback(result, io);
182
354
  if (parsed.flags.json) {
183
355
  writeJson(io.stdout, sourcePayload(result, { query, count: workflows.length, workflows }));
184
356
  return 0;
185
357
  }
186
- 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, '');
187
360
  writeLine(io.stdout, formatItemList(workflows));
188
361
  return 0;
189
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
+ }
190
686
  async function commandTutorials(parsed, io) {
191
687
  const query = parsed.args.join(' ');
192
688
  const level = tutorialLevelFlag(parsed);
193
689
  if (!level.ok)
194
690
  return usageError(io, level.error);
195
691
  const limit = numberFlag(parsed, 'limit', 'n') || 20;
196
- const result = await tutorialsSource({ ...sourceOptions(parsed, io), query, level: level.value, limit });
692
+ const result = await tutorialsSource({
693
+ ...sourceOptions(parsed, io),
694
+ query,
695
+ level: level.value,
696
+ limit
697
+ });
197
698
  const tutorials = result.data;
198
699
  warnFallback(result, io);
199
700
  if (parsed.flags.json) {
@@ -204,14 +705,22 @@ async function commandTutorials(parsed, io) {
204
705
  writeLine(io.stdout, query ? `No tutorials match "${query}".` : 'No tutorials found.');
205
706
  return 0;
206
707
  }
207
- 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, '');
208
710
  writeLine(io.stdout, formatTutorialList(tutorials));
209
711
  return 0;
210
712
  }
211
713
  async function commandTutorial(parsed, io) {
212
714
  const id = parsed.args[0];
213
- if (!id)
214
- 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
+ }
215
724
  const step = tutorialStepNumber(parsed);
216
725
  if (!step.ok)
217
726
  return usageError(io, step.error);
@@ -244,14 +753,17 @@ async function commandDiscussions(parsed, io) {
244
753
  writeLine(io.stdout, 'No discussions found.');
245
754
  return 0;
246
755
  }
247
- writeLine(io.stdout, `Agora discussions (${discussions.length}, source: ${result.source})`);
248
- writeLine(io.stdout, discussions.map((discussion, index) => {
756
+ writeLine(io.stdout, header('agora discussions', [`${discussions.length} results`, sourceLabel(result)]));
757
+ writeLine(io.stdout, '');
758
+ writeLine(io.stdout, discussions
759
+ .map((discussion, index) => {
249
760
  return [
250
- `${index + 1}. ${discussion.title} [${discussion.category}]`,
761
+ `${index + 1}. ${style.accent(discussion.title)} ${style.dim('[' + discussion.category + ']')}`,
251
762
  ` ${truncate(discussion.content, 88)}`,
252
- ` replies ${discussion.replies} | stars ${discussion.stars} | by ${discussion.author}`
763
+ ` ${style.dim('replies ' + discussion.replies + ' · stars ' + discussion.stars + ' · by ' + discussion.author)}`
253
764
  ].join('\n');
254
- }).join('\n\n'));
765
+ })
766
+ .join('\n\n'));
255
767
  return 0;
256
768
  }
257
769
  async function commandDiscuss(parsed, io) {
@@ -259,7 +771,7 @@ async function commandDiscuss(parsed, io) {
259
771
  if (!source.ok)
260
772
  return usageError(io, source.error);
261
773
  const title = requiredStringFlag(parsed, 'title');
262
- const content = contentInput(parsed);
774
+ const content = contentInput(parsed, io);
263
775
  if (!title || !content) {
264
776
  return usageError(io, 'discuss requires --title and --content or --content-file');
265
777
  }
@@ -275,15 +787,19 @@ async function commandDiscuss(parsed, io) {
275
787
  writeJson(io.stdout, sourcePayload(result, { discussion: result.data }));
276
788
  return 0;
277
789
  }
278
- writeLine(io.stdout, `Created discussion ${result.data.id}`);
279
- 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)})`);
280
792
  return 0;
281
793
  }
282
794
  async function commandInstall(parsed, io) {
283
795
  const id = parsed.args[0];
284
796
  if (!id)
285
797
  return usageError(io, 'install requires an item id');
286
- const source = await findMarketplaceSource({ ...sourceOptions(parsed, io), id, type: stringFlag(parsed, 'type', 't') });
798
+ const source = await findMarketplaceSource({
799
+ ...sourceOptions(parsed, io),
800
+ id,
801
+ type: stringFlag(parsed, 'type', 't')
802
+ });
287
803
  const item = source.data;
288
804
  warnFallback(source, io);
289
805
  if (!item)
@@ -315,8 +831,8 @@ async function commandInstall(parsed, io) {
315
831
  }
316
832
  if (parsed.flags.write) {
317
833
  writeOpenCodeConfig(configPath, plan.config);
318
- writeLine(io.stdout, `Installed ${item.name}`);
319
- writeLine(io.stdout, `Updated ${configPath}`);
834
+ writeLine(io.stdout, `Installed ${style.accent(item.name)}`);
835
+ writeLine(io.stdout, `${style.dim('Config')} ${configPath}`);
320
836
  if (plan.commands.length) {
321
837
  writeLine(io.stdout, 'Installing packages...');
322
838
  for (const cmd of plan.commands) {
@@ -333,6 +849,16 @@ async function commandInstall(parsed, io) {
333
849
  }
334
850
  writeLine(io.stdout, `Install preview: ${item.name}`);
335
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
+ }
336
862
  if (plan.commands.length) {
337
863
  writeLine(io.stdout, '\nCommands:');
338
864
  writeLine(io.stdout, plan.commands.join('\n'));
@@ -342,36 +868,271 @@ async function commandInstall(parsed, io) {
342
868
  writeLine(io.stdout, '\nRun with --write to update the config file and install packages.');
343
869
  return 0;
344
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
+ }
345
1040
  async function commandInit(parsed, io) {
346
1041
  const cwd = io.cwd || process.cwd();
347
1042
  const scan = scanProject(cwd);
1043
+ const plan = generateInitPlan(scan);
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
+ }
348
1056
  if (parsed.flags.json) {
349
- writeJson(io.stdout, { projectType: scan.type, frameworks: scan.frameworks });
1057
+ if (parsed.flags.dryRun) {
1058
+ writeJson(io.stdout, {
1059
+ projectType: scan.type,
1060
+ frameworks: scan.frameworks,
1061
+ config: plan.config,
1062
+ servers: plan.servers,
1063
+ commands: plan.commands,
1064
+ slashCommand: join(cwd, '.opencode', 'command', 'agora.md'),
1065
+ dryRun: true
1066
+ });
1067
+ return 0;
1068
+ }
1069
+ applyInitPlan(plan, configPath);
1070
+ const commandPath = installAgoraCommand(cwd);
1071
+ const installResults = plan.commands.length ? runCommands(plan.commands) : [];
1072
+ const installed = installResults.filter((r) => r.ok).length;
1073
+ const failed = installResults.filter((r) => !r.ok).length;
1074
+ writeJson(io.stdout, {
1075
+ projectType: scan.type,
1076
+ frameworks: scan.frameworks,
1077
+ config: plan.config,
1078
+ servers: plan.servers,
1079
+ commands: plan.commands,
1080
+ slashCommand: commandPath,
1081
+ installResults,
1082
+ installed,
1083
+ failed
1084
+ });
350
1085
  return 0;
351
1086
  }
352
1087
  writeLine(io.stdout, `Scanning ${cwd}...`);
353
- writeLine(io.stdout, ` Project type: ${scan.type}`);
1088
+ writeLine(io.stdout, ` ${style.dim('Project type')} ${scan.type}`);
354
1089
  if (scan.frameworks.length)
355
- writeLine(io.stdout, ` Frameworks: ${scan.frameworks.join(', ')}`);
1090
+ writeLine(io.stdout, ` ${style.dim('Frameworks')} ${scan.frameworks.join(', ')}`);
356
1091
  if (scan.hasDocker)
357
- writeLine(io.stdout, ' Docker: detected');
1092
+ writeLine(io.stdout, ` ${style.dim('Docker')} detected`);
358
1093
  if (scan.hasTests)
359
- writeLine(io.stdout, ' Tests: detected');
1094
+ writeLine(io.stdout, ` ${style.dim('Tests')} detected`);
360
1095
  if (scan.hasDatabase)
361
- writeLine(io.stdout, ' Database: detected');
362
- const plan = generateInitPlan(scan);
363
- const configPath = detectOpenCodeConfigPath({ cwd, env: io.env });
1096
+ writeLine(io.stdout, ` ${style.dim('Database')} detected`);
364
1097
  if (!parsed.flags.dryRun) {
365
1098
  applyInitPlan(plan, configPath);
366
1099
  writeLine(io.stdout, `\nWrote config to ${configPath}`);
1100
+ const commandPath = installAgoraCommand(cwd);
1101
+ writeLine(io.stdout, `Installed /agora slash command at ${commandPath}`);
367
1102
  if (plan.commands.length) {
368
1103
  writeLine(io.stdout, '\nInstalling MCP server packages...');
369
- runCommands(plan.commands);
370
- writeLine(io.stdout, ` Done (${plan.commands.length} packages)`);
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
+ }
1126
+ const installed = installResults.filter((r) => r.ok).length;
1127
+ const failed = installResults.filter((r) => !r.ok).length;
1128
+ writeLine(io.stdout, ` Installed ${installed} of ${plan.commands.length} packages${failed ? ` (${failed} failed)` : ''}`);
371
1129
  }
372
1130
  writeLine(io.stdout, '\n✓ Agora initialized! Restart OpenCode to pick up the changes.');
373
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.');
374
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.');
375
1136
  if (plan.workflows.length)
376
1137
  writeLine(io.stdout, ` ${plan.workflows.length} workflows available via \`agora use\`.`);
377
1138
  for (const note of plan.notes)
@@ -381,6 +1142,7 @@ async function commandInit(parsed, io) {
381
1142
  writeLine(io.stdout, '\n--- Dry run ---');
382
1143
  writeLine(io.stdout, `Target config: ${configPath}`);
383
1144
  writeLine(io.stdout, formatConfigJson(plan.config));
1145
+ writeLine(io.stdout, `\nSlash command: ${join(cwd, '.opencode', 'command', 'agora.md')}`);
384
1146
  writeLine(io.stdout, '\nPackages to install:');
385
1147
  for (const cmd of plan.commands)
386
1148
  writeLine(io.stdout, ` ${cmd}`);
@@ -390,8 +1152,14 @@ async function commandInit(parsed, io) {
390
1152
  }
391
1153
  async function commandUse(parsed, io) {
392
1154
  const id = parsed.args[0];
393
- if (!id)
394
- 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
+ }
395
1163
  const workflow = sampleWorkflows.find((w) => w.id === id || w.name.toLowerCase() === id.toLowerCase());
396
1164
  if (!workflow)
397
1165
  return usageError(io, `Workflow not found: ${id}. Run \`agora workflows\` to see available workflows.`);
@@ -412,11 +1180,13 @@ ${workflow.prompt}
412
1180
  writeFileSync(skillPath, skillContent, 'utf8');
413
1181
  const configPath = detectOpenCodeConfigPath({ cwd, env: io.env });
414
1182
  const loaded = loadOpenCodeConfig(configPath);
415
- const plugins = new Set(loaded.config.plugins || []);
1183
+ if (loaded.error)
1184
+ return usageError(io, `${loaded.path}: ${loaded.error}`);
1185
+ const plugins = new Set(loaded.config.plugin || []);
416
1186
  plugins.add(skillId);
417
1187
  const updatedConfig = {
418
1188
  ...loaded.config,
419
- plugins: Array.from(plugins),
1189
+ plugin: Array.from(plugins)
420
1190
  };
421
1191
  writeOpenCodeConfig(configPath, updatedConfig);
422
1192
  if (parsed.flags.json) {
@@ -444,51 +1214,133 @@ function commandConfig(parsed, io) {
444
1214
  writeJson(io.stdout, report);
445
1215
  return report.valid ? 0 : 1;
446
1216
  }
447
- writeLine(io.stdout, `Config path: ${report.path}`);
448
- writeLine(io.stdout, `Exists: ${report.exists ? 'yes' : 'no'}`);
449
- 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'}`);
450
1220
  if (report.error)
451
- writeLine(io.stdout, `Error: ${report.error}`);
452
- writeLine(io.stdout, `MCP servers: ${report.mcpServers}`);
453
- writeLine(io.stdout, `Plugins: ${report.plugins}`);
454
- 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'}`);
455
1225
  return report.valid ? 0 : 1;
456
1226
  }
457
- function commandAuth(parsed, io) {
1227
+ async function commandAuth(parsed, io) {
458
1228
  const subcommand = parsed.args[0] || 'status';
459
1229
  const dataDir = detectDataDir(parsed, io);
460
1230
  const state = loadAgoraState(dataDir);
461
1231
  const existingAuth = getAuthState(state);
462
1232
  if (subcommand === 'login') {
463
- const token = authTokenInput(parsed, io);
464
- if (!token) {
465
- 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;
466
1248
  }
1249
+ // ── Device-code flow ─────────────────────────────────────────────────
467
1250
  const apiUrl = stringFlag(parsed, 'apiUrl') || envString(io, 'AGORA_API_URL') || existingAuth?.apiUrl;
468
- const nextState = setAuthState(state, { token, apiUrl });
469
- const auth = getAuthState(nextState);
470
- writeAgoraState(dataDir, nextState);
471
- if (parsed.flags.json) {
472
- writeJson(io.stdout, authStatusPayload(dataDir, auth));
473
- 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'}`);
474
1330
  }
475
- writeLine(io.stdout, 'Stored Agora API token');
476
- writeLine(io.stdout, `API URL: ${auth?.apiUrl || 'not stored'}`);
477
- writeLine(io.stdout, `State: ${getAgoraStatePath(dataDir)}`);
478
- return 0;
479
1331
  }
480
1332
  if (subcommand === 'status') {
481
1333
  if (parsed.flags.json) {
482
1334
  writeJson(io.stdout, authStatusPayload(dataDir, existingAuth));
483
1335
  return 0;
484
1336
  }
485
- writeLine(io.stdout, `Authenticated: ${existingAuth ? 'yes' : 'no'}`);
1337
+ writeLine(io.stdout, `${style.dim('Authenticated')} ${existingAuth ? 'yes' : 'no'}`);
486
1338
  if (existingAuth) {
487
- writeLine(io.stdout, `Token: ${maskToken(existingAuth.token)}`);
488
- writeLine(io.stdout, `API URL: ${existingAuth.apiUrl || 'not stored'}`);
489
- 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)}`);
490
1342
  }
491
- writeLine(io.stdout, `State: ${getAgoraStatePath(dataDir)}`);
1343
+ writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
492
1344
  return 0;
493
1345
  }
494
1346
  if (subcommand === 'logout') {
@@ -514,7 +1366,11 @@ async function commandSave(parsed, io) {
514
1366
  const id = parsed.args[0];
515
1367
  if (!id)
516
1368
  return usageError(io, 'save requires an item id');
517
- const source = await findMarketplaceSource({ ...sourceOptions(parsed, io), id, type: stringFlag(parsed, 'type', 't') });
1369
+ const source = await findMarketplaceSource({
1370
+ ...sourceOptions(parsed, io),
1371
+ id,
1372
+ type: stringFlag(parsed, 'type', 't')
1373
+ });
518
1374
  const item = source.data;
519
1375
  warnFallback(source, io);
520
1376
  if (!item)
@@ -535,8 +1391,8 @@ async function commandSave(parsed, io) {
535
1391
  });
536
1392
  return 0;
537
1393
  }
538
- writeLine(io.stdout, result.added ? `Saved ${item.id}` : `${item.id} is already saved`);
539
- 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)}`);
540
1396
  return 0;
541
1397
  }
542
1398
  function commandSaved(parsed, io) {
@@ -558,7 +1414,7 @@ function commandSaved(parsed, io) {
558
1414
  writeLine(io.stdout, 'Run agora save <id> to save a package or workflow.');
559
1415
  return 0;
560
1416
  }
561
- writeLine(io.stdout, `Saved Agora items (${saved.length})`);
1417
+ writeLine(io.stdout, header('agora saved', [`${saved.length} items`]));
562
1418
  writeLine(io.stdout, formatSavedList(saved));
563
1419
  return 0;
564
1420
  }
@@ -585,7 +1441,7 @@ function commandRemove(parsed, io) {
585
1441
  if (!result.removed) {
586
1442
  return usageError(io, `Saved item not found: ${id}`);
587
1443
  }
588
- writeLine(io.stdout, `Removed ${targetId}`);
1444
+ writeLine(io.stdout, `Removed ${style.accent(targetId)}`);
589
1445
  return 0;
590
1446
  }
591
1447
  async function commandPublish(parsed, io) {
@@ -621,12 +1477,12 @@ async function commandPublish(parsed, io) {
621
1477
  writeJson(io.stdout, sourcePayload(result, { item: result.data }));
622
1478
  return 0;
623
1479
  }
624
- writeLine(io.stdout, `Published package ${result.data.id}`);
625
- 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)})`);
626
1482
  return 0;
627
1483
  }
628
- const prompt = promptInput(parsed);
629
- if (!prompt) {
1484
+ const prompt = promptInput(parsed, io);
1485
+ if (prompt === undefined) {
630
1486
  return usageError(io, 'publish workflow requires --prompt or --prompt-file');
631
1487
  }
632
1488
  const result = await publishWorkflowSource(source.options, {
@@ -641,8 +1497,8 @@ async function commandPublish(parsed, io) {
641
1497
  writeJson(io.stdout, sourcePayload(result, { item: result.data }));
642
1498
  return 0;
643
1499
  }
644
- writeLine(io.stdout, `Published workflow ${result.data.id}`);
645
- 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)})`);
646
1502
  return 0;
647
1503
  }
648
1504
  async function commandReview(parsed, io) {
@@ -667,8 +1523,8 @@ async function commandReview(parsed, io) {
667
1523
  writeJson(io.stdout, sourcePayload(result, { review: result.data }));
668
1524
  return 0;
669
1525
  }
670
- writeLine(io.stdout, `Reviewed ${result.data.itemId}`);
671
- 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)}`);
672
1528
  return 0;
673
1529
  }
674
1530
  async function commandReviews(parsed, io) {
@@ -685,7 +1541,8 @@ async function commandReviews(parsed, io) {
685
1541
  writeLine(io.stdout, itemId ? `No reviews found for ${itemId}.` : 'No reviews found.');
686
1542
  return 0;
687
1543
  }
688
- 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, '');
689
1546
  writeLine(io.stdout, formatReviewList(result.data));
690
1547
  return 0;
691
1548
  }
@@ -706,177 +1563,268 @@ async function commandProfile(parsed, io) {
706
1563
  writeLine(io.stdout, formatProfileDetail(result.data));
707
1564
  return 0;
708
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
+ }
709
1631
  function formatItemList(items) {
710
- return items.map((item, index) => {
711
- const installs = item.kind === 'package' ? ` | installs ${formatCount(item.installs)}` : '';
1632
+ const idWidth = Math.max(...items.map((item) => item.id.length));
1633
+ return items
1634
+ .map((item) => {
1635
+ const metrics = item.kind === 'package'
1636
+ ? `${formatNumber(item.installs)} installs · ${formatNumber(item.stars)} ★`
1637
+ : `${formatNumber(item.stars)} ★`;
712
1638
  return [
713
- `${index + 1}. ${item.id} [${item.category}]`,
714
- ` ${item.name}`,
715
- ` ${truncate(item.description, 88)}`,
716
- ` 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}`)
717
1643
  ].join('\n');
718
- }).join('\n\n');
1644
+ })
1645
+ .join('\n\n');
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');
719
1659
  }
720
1660
  function formatItemDetail(item) {
721
1661
  const lines = [
722
- `${item.name}`,
723
- `id: ${item.id}`,
724
- `type: ${item.kind}`,
725
- `category: ${item.category}`,
726
- `author: ${item.author}`,
727
- `stars: ${formatCount(item.stars)}`,
728
- `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)}`,
729
1669
  '',
730
1670
  item.description,
731
1671
  '',
732
- `tags: ${item.tags.join(', ')}`
1672
+ `${style.dim('tags')} ${item.tags.join(', ')}`
733
1673
  ];
734
1674
  if (item.kind === 'package') {
735
- lines.splice(5, 0, `version: ${item.version}`);
736
- 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)}`);
737
1677
  if (item.repository)
738
- lines.push(`repository: ${item.repository}`);
1678
+ lines.push(`${style.dim('repo')} ${item.repository}`);
739
1679
  if (item.npmPackage)
740
- lines.push(`npm: ${item.npmPackage}`);
1680
+ lines.push(`${style.dim('npm')} ${item.npmPackage}`);
741
1681
  }
742
1682
  if (item.kind === 'workflow') {
743
- lines.push(`forks: ${item.forks}`);
1683
+ lines.push(`${style.dim('forks')} ${item.forks}`);
744
1684
  if (item.model)
745
- lines.push(`model: ${item.model}`);
746
- lines.push('', 'prompt:', item.prompt);
1685
+ lines.push(`${style.dim('model')} ${item.model}`);
1686
+ lines.push('', style.dim('prompt'), item.prompt);
747
1687
  }
748
1688
  return lines.join('\n');
749
1689
  }
750
1690
  function formatSavedList(items) {
751
- return items.map((entry, index) => {
1691
+ return items
1692
+ .map((entry, index) => {
752
1693
  if (!entry.item) {
753
1694
  return [
754
- `${index + 1}. ${entry.saved.id} [missing]`,
755
- ` saved ${formatDate(entry.saved.savedAt)}`
1695
+ `${index + 1}. ${style.accent(entry.saved.id)} ${style.dim('[missing]')}`,
1696
+ ` ${style.dim('saved ' + formatDate(entry.saved.savedAt))}`
756
1697
  ].join('\n');
757
1698
  }
758
1699
  return [
759
- `${index + 1}. ${entry.item.id} [${entry.item.category}]`,
760
- ` ${entry.item.name}`,
1700
+ `${index + 1}. ${style.accent(entry.item.id)} ${style.dim('[' + entry.item.category + ']')}`,
1701
+ ` ${style.dim(entry.item.name)}`,
761
1702
  ` ${truncate(entry.item.description, 88)}`,
762
- ` saved ${formatDate(entry.saved.savedAt)}`
1703
+ ` ${style.dim('saved ' + formatDate(entry.saved.savedAt))}`
763
1704
  ].join('\n');
764
- }).join('\n\n');
1705
+ })
1706
+ .join('\n\n');
765
1707
  }
766
1708
  function formatReviewList(reviews) {
767
- return reviews.map((review, index) => {
1709
+ return reviews
1710
+ .map((review, index) => {
768
1711
  return [
769
- `${index + 1}. ${review.itemId} [${review.itemType}]`,
770
- ` 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)}`,
771
1714
  ` ${truncate(review.content, 88)}`
772
1715
  ].join('\n');
773
- }).join('\n\n');
1716
+ })
1717
+ .join('\n\n');
774
1718
  }
775
1719
  function formatProfileDetail(profile) {
776
1720
  const lines = [
777
- profile.displayName,
778
- `username: ${profile.username}`,
779
- `packages: ${formatCount(profile.packages)}`,
780
- `workflows: ${formatCount(profile.workflows)}`,
781
- `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)}`
782
1726
  ];
783
1727
  if (profile.bio)
784
- lines.splice(2, 0, `bio: ${profile.bio}`);
1728
+ lines.splice(2, 0, `${style.dim('bio')} ${profile.bio}`);
785
1729
  if (profile.avatarUrl)
786
- lines.push(`avatar: ${profile.avatarUrl}`);
1730
+ lines.push(`${style.dim('avatar')} ${profile.avatarUrl}`);
787
1731
  if (profile.joinedAt)
788
- lines.push(`joined: ${formatDate(profile.joinedAt)}`);
1732
+ lines.push(`${style.dim('joined')} ${formatDate(profile.joinedAt)}`);
789
1733
  return lines.join('\n');
790
1734
  }
791
1735
  function formatTutorialList(tutorials) {
792
- return tutorials.map((tutorial, index) => {
1736
+ return tutorials
1737
+ .map((tutorial, index) => {
793
1738
  return [
794
- `${index + 1}. ${tutorial.id} [${tutorial.level}]`,
795
- ` ${tutorial.title}`,
1739
+ `${index + 1}. ${style.accent(tutorial.id)} ${style.dim('[' + tutorial.level + ']')}`,
1740
+ ` ${style.dim(tutorial.title)}`,
796
1741
  ` ${truncate(tutorial.description, 88)}`,
797
- ` ${tutorial.duration} | ${tutorial.steps.length} steps`
1742
+ ` ${style.dim(tutorial.duration + ' | ' + tutorial.steps.length + ' steps')}`
798
1743
  ].join('\n');
799
- }).join('\n\n');
1744
+ })
1745
+ .join('\n\n');
800
1746
  }
801
1747
  function formatTutorialStep(tutorial, stepNumber) {
802
1748
  const payload = tutorialStepPayload(tutorial, stepNumber);
803
1749
  if (payload.completed) {
804
1750
  return [
805
- `${tutorial.title}`,
806
- `Completed ${tutorial.steps.length}/${tutorial.steps.length} steps.`,
1751
+ style.bold(tutorial.title),
1752
+ style.dim(`Completed ${tutorial.steps.length}/${tutorial.steps.length} steps.`),
807
1753
  'Run agora tutorials for more tutorials.'
808
1754
  ].join('\n');
809
1755
  }
810
1756
  const lines = [
811
- `${tutorial.title}`,
812
- `id: ${tutorial.id}`,
813
- `level: ${tutorial.level}`,
814
- `duration: ${tutorial.duration}`,
815
- `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}`,
816
1762
  '',
817
1763
  payload.title || '',
818
1764
  payload.content || ''
819
1765
  ];
820
1766
  if (payload.code) {
821
- lines.push('', 'code:', payload.code);
1767
+ lines.push('', style.dim('code:'), payload.code);
822
1768
  }
823
1769
  return lines.join('\n');
824
1770
  }
825
- function usage() {
826
- return [
827
- 'Agora CLI',
828
- '',
829
- 'Usage:',
830
- ' agora init [--dry-run] [--json]',
831
- ' agora use <workflow-id> [--json]',
832
- ' agora search <query> [--category mcp|prompt|workflow|skill] [--limit 10] [--json]',
833
- ' agora browse <id> [--type package|workflow] [--json]',
834
- ' agora trending [all|packages|workflows] [--limit 5] [--json]',
835
- ' agora workflows [query] [--limit 10] [--json]',
836
- ' agora tutorials [query] [--level beginner|intermediate|advanced] [--limit 20] [--json]',
837
- ' agora tutorial <id> [step] [--json]',
838
- ' agora discussions [query] [--category question|idea|showcase|discussion] [--json]',
839
- ' agora discuss --title <title> (--content <text>|--content-file path) [--category question|idea|showcase|discussion]',
840
- ' agora install <id> [--write] [--config path] [--json]',
841
- ' agora save <id> [--data-dir path] [--json]',
842
- ' agora saved [query] [--data-dir path] [--json]',
843
- ' agora remove <id> [--data-dir path] [--json]',
844
- ' agora auth login --token <token> [--api-url url] [--data-dir path]',
845
- ' agora auth status [--data-dir path] [--json]',
846
- ' agora auth logout [--data-dir path]',
847
- ' agora publish package --name <name> --description <text> --npm <package> [--token token]',
848
- ' agora publish workflow --name <name> --description <text> --prompt-file <path> [--token token]',
849
- ' agora review <id> --rating 5 --content <text> [--token token]',
850
- ' agora reviews [id] [--type package|workflow]',
851
- ' agora profile <username> [--json]',
852
- ' agora config doctor [--config path] [--json]',
853
- '',
854
- 'Data source:',
855
- ' --api Use the live Agora API',
856
- ' --api-url <url> Override AGORA_API_URL',
857
- ' --api-timeout <ms> API timeout before offline fallback',
858
- ' --token <token> API auth token, defaults to env vars or agora auth login',
859
- ' --offline Force local bundled marketplace data',
860
- '',
861
- 'Examples:',
862
- ' agora init',
863
- ' agora init --dry-run',
864
- ' agora use wf-tdd-cycle',
865
- ' agora search filesystem',
866
- ' agora search filesystem --api',
867
- ' agora browse mcp-github',
868
- ' agora tutorials mcp',
869
- ' agora tutorial tut-mcp-basics 2',
870
- ' agora install mcp-github',
871
- ' agora install mcp-github --write',
872
- ' agora save wf-security-audit',
873
- ' agora saved',
874
- ' agora auth login --token $AGORA_TOKEN --api-url https://agora.example.com',
875
- ' agora discuss --title "MCP question" --content "How are you composing servers?" --category question',
876
- ' agora profile alice',
877
- ' agora publish package --name @you/server --description "MCP server" --npm @you/server',
878
- ' 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>]`
879
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);
880
1828
  }
881
1829
  function usageError(io, message) {
882
1830
  writeLine(io.stderr, message);
@@ -898,9 +1846,9 @@ function numberFlag(parsed, longName, shortName) {
898
1846
  return Number.isFinite(parsedValue) ? parsedValue : undefined;
899
1847
  }
900
1848
  function authTokenInput(parsed, io) {
901
- return requiredStringFlag(parsed, 'token') ||
1849
+ return (requiredStringFlag(parsed, 'token') ||
902
1850
  envString(io, 'AGORA_TOKEN') ||
903
- envString(io, 'AGORA_API_TOKEN');
1851
+ envString(io, 'AGORA_API_TOKEN'));
904
1852
  }
905
1853
  function envString(io, name) {
906
1854
  const value = io.env?.[name];
@@ -912,6 +1860,8 @@ function shortFlag(arg) {
912
1860
  return 'help';
913
1861
  if (flag === 'j')
914
1862
  return 'json';
1863
+ if (flag === 'm')
1864
+ return 'model';
915
1865
  if (flag === 'c')
916
1866
  return 'c';
917
1867
  if (flag === 'n')
@@ -933,12 +1883,13 @@ function sourceOptions(parsed, io) {
933
1883
  const storedAuth = getAuthState(loadAgoraState(detectDataDir(parsed, io)));
934
1884
  const storedApiUrl = storedAuth?.apiUrl;
935
1885
  const apiUrl = explicitApiUrl || envApiUrl || storedApiUrl || '';
936
- const useApi = !parsed.flags.offline && Boolean(parsed.flags.api ||
937
- parsed.flags.live ||
938
- explicitApiUrl ||
939
- envApiUrl ||
940
- storedApiUrl ||
941
- io.env?.AGORA_USE_API === 'true');
1886
+ const useApi = !parsed.flags.offline &&
1887
+ Boolean(parsed.flags.api ||
1888
+ parsed.flags.live ||
1889
+ explicitApiUrl ||
1890
+ envApiUrl ||
1891
+ storedApiUrl ||
1892
+ io.env?.AGORA_USE_API === 'true');
942
1893
  return {
943
1894
  useApi,
944
1895
  apiUrl,
@@ -950,10 +1901,16 @@ function sourceOptions(parsed, io) {
950
1901
  function writeSourceOptions(parsed, io) {
951
1902
  const options = sourceOptions(parsed, io);
952
1903
  if (!options.apiUrl) {
953
- return { ok: false, error: 'This command requires --api-url, AGORA_API_URL, or an auth login API URL' };
1904
+ return {
1905
+ ok: false,
1906
+ error: 'This command requires --api-url, AGORA_API_URL, or an auth login API URL'
1907
+ };
954
1908
  }
955
1909
  if (!options.token) {
956
- return { ok: false, error: 'This command requires --token, AGORA_TOKEN, AGORA_API_TOKEN, or agora auth login' };
1910
+ return {
1911
+ ok: false,
1912
+ error: 'This command requires --token, AGORA_TOKEN, AGORA_API_TOKEN, or agora auth login'
1913
+ };
957
1914
  }
958
1915
  return {
959
1916
  ok: true,
@@ -966,7 +1923,10 @@ function writeSourceOptions(parsed, io) {
966
1923
  function readSourceOptions(parsed, io) {
967
1924
  const options = sourceOptions(parsed, io);
968
1925
  if (!options.apiUrl) {
969
- return { ok: false, error: 'This command requires --api-url, AGORA_API_URL, or an auth login API URL' };
1926
+ return {
1927
+ ok: false,
1928
+ error: 'This command requires --api-url, AGORA_API_URL, or an auth login API URL'
1929
+ };
970
1930
  }
971
1931
  return { ok: true, options: { ...options, useApi: true } };
972
1932
  }
@@ -974,24 +1934,35 @@ function tagsFlag(parsed) {
974
1934
  const value = stringFlag(parsed, 'tags');
975
1935
  if (!value)
976
1936
  return [];
977
- return value.split(',').map((tag) => tag.trim()).filter(Boolean);
1937
+ return value
1938
+ .split(',')
1939
+ .map((tag) => tag.trim())
1940
+ .filter(Boolean);
978
1941
  }
979
- function promptInput(parsed) {
1942
+ function promptInput(parsed, io) {
980
1943
  const prompt = stringFlag(parsed, 'prompt');
981
1944
  if (prompt)
982
1945
  return prompt;
983
1946
  const promptFile = stringFlag(parsed, 'promptFile');
984
1947
  if (!promptFile)
985
1948
  return undefined;
1949
+ if (!existsSync(promptFile)) {
1950
+ usageError(io, `prompt-file not found: ${promptFile}`);
1951
+ return undefined;
1952
+ }
986
1953
  return readFileSync(promptFile, 'utf8');
987
1954
  }
988
- function contentInput(parsed) {
1955
+ function contentInput(parsed, io) {
989
1956
  const content = requiredStringFlag(parsed, 'content');
990
1957
  if (content)
991
1958
  return content;
992
1959
  const contentFile = stringFlag(parsed, 'contentFile');
993
1960
  if (!contentFile)
994
1961
  return undefined;
1962
+ if (!existsSync(contentFile)) {
1963
+ usageError(io, `content-file not found: ${contentFile}`);
1964
+ return undefined;
1965
+ }
995
1966
  return readFileSync(contentFile, 'utf8').trim();
996
1967
  }
997
1968
  function itemTypeFlag(parsed, itemId) {
@@ -1002,10 +1973,16 @@ function itemTypeFlag(parsed, itemId) {
1002
1973
  }
1003
1974
  function discussionCategoryFlag(parsed) {
1004
1975
  const category = stringFlag(parsed, 'category', 'c') || 'discussion';
1005
- if (category === 'question' || category === 'idea' || category === 'showcase' || category === 'discussion') {
1976
+ if (category === 'question' ||
1977
+ category === 'idea' ||
1978
+ category === 'showcase' ||
1979
+ category === 'discussion') {
1006
1980
  return { ok: true, value: category };
1007
1981
  }
1008
- return { ok: false, error: 'discussion category must be question, idea, showcase, or discussion' };
1982
+ return {
1983
+ ok: false,
1984
+ error: 'discussion category must be question, idea, showcase, or discussion'
1985
+ };
1009
1986
  }
1010
1987
  function tutorialLevelFlag(parsed) {
1011
1988
  const level = stringFlag(parsed, 'level') || 'all';
@@ -1035,6 +2012,11 @@ function tutorialStepPayload(tutorial, stepNumber) {
1035
2012
  code: step?.code
1036
2013
  };
1037
2014
  }
2015
+ function sourceLabel(result) {
2016
+ return result.source === 'offline'
2017
+ ? `source: offline · refreshed ${dataRefreshedAt}`
2018
+ : `source: ${result.source}`;
2019
+ }
1038
2020
  function warnFallback(result, io) {
1039
2021
  if (result.fallbackReason) {
1040
2022
  writeLine(io.stderr, `API unavailable, using offline data (refreshed ${dataRefreshedAt}): ${result.fallbackReason}`);
@@ -1095,13 +2077,6 @@ function truncate(value, max) {
1095
2077
  return value;
1096
2078
  return `${value.slice(0, max - 3)}...`;
1097
2079
  }
1098
- function formatCount(value) {
1099
- if (value >= 1000000)
1100
- return `${(value / 1000000).toFixed(1)}M`;
1101
- if (value >= 1000)
1102
- return `${(value / 1000).toFixed(1)}K`;
1103
- return String(value);
1104
- }
1105
2080
  function formatDate(value) {
1106
2081
  return value.slice(0, 10);
1107
2082
  }