@taskhunt/cli 0.1.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 (44) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/CLAUDE.md +13 -0
  3. package/dist/api.d.ts +16 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +48 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/commands/agents.d.ts +3 -0
  8. package/dist/commands/agents.d.ts.map +1 -0
  9. package/dist/commands/agents.js +92 -0
  10. package/dist/commands/agents.js.map +1 -0
  11. package/dist/commands/auth.d.ts +3 -0
  12. package/dist/commands/auth.d.ts.map +1 -0
  13. package/dist/commands/auth.js +62 -0
  14. package/dist/commands/auth.js.map +1 -0
  15. package/dist/commands/tasks.d.ts +3 -0
  16. package/dist/commands/tasks.d.ts.map +1 -0
  17. package/dist/commands/tasks.js +285 -0
  18. package/dist/commands/tasks.js.map +1 -0
  19. package/dist/config.d.ts +13 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +41 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/format.d.ts +8 -0
  24. package/dist/format.d.ts.map +1 -0
  25. package/dist/format.js +30 -0
  26. package/dist/format.js.map +1 -0
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +18 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/spinner.d.ts +2 -0
  32. package/dist/spinner.d.ts.map +1 -0
  33. package/dist/spinner.js +22 -0
  34. package/dist/spinner.js.map +1 -0
  35. package/package.json +28 -0
  36. package/src/api.ts +61 -0
  37. package/src/commands/agents.ts +129 -0
  38. package/src/commands/auth.ts +100 -0
  39. package/src/commands/tasks.ts +397 -0
  40. package/src/config.ts +52 -0
  41. package/src/format.ts +35 -0
  42. package/src/index.ts +22 -0
  43. package/src/spinner.ts +20 -0
  44. package/tsconfig.json +12 -0
@@ -0,0 +1,397 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { readFileSync } from 'node:fs';
4
+ import { api, sseStream } from '../api.js';
5
+ import { withSpinner } from '../spinner.js';
6
+ import { printJson, printTable, printSuccess, truncate, formatDate } from '../format.js';
7
+
8
+ interface TaskSummary {
9
+ id: string;
10
+ title: string;
11
+ status: string;
12
+ category: string;
13
+ budgetValue: string;
14
+ budgetCurrency: string;
15
+ createdAt: string;
16
+ }
17
+
18
+ interface TaskDetail extends TaskSummary {
19
+ description: string;
20
+ posterId: string;
21
+ assignedTo?: string;
22
+ bidMode: string;
23
+ complexity: unknown;
24
+ spec: unknown;
25
+ updatedAt: string;
26
+ deadline?: string;
27
+ }
28
+
29
+ interface ClaimResult {
30
+ taskId: string;
31
+ assigneeId: string;
32
+ status: string;
33
+ }
34
+
35
+ interface ProposalResult {
36
+ id: string;
37
+ taskId: string;
38
+ status: string;
39
+ }
40
+
41
+ interface SubmissionResult {
42
+ id: string;
43
+ taskId: string;
44
+ status: string;
45
+ }
46
+
47
+ interface CheckpointResult {
48
+ id: string;
49
+ taskId: string;
50
+ label: string;
51
+ }
52
+
53
+ export function createTasksCommand(): Command {
54
+ const tasks = new Command('tasks').description('Task management commands');
55
+
56
+ tasks
57
+ .command('list')
58
+ .description('List tasks')
59
+ .option('--status <status>', 'Filter by status')
60
+ .option('--category <category>', 'Filter by category')
61
+ .option('--min-budget <n>', 'Minimum budget', parseFloat)
62
+ .option('--format <format>', 'Output format (json|table)', 'table')
63
+ .action(async (opts: { status?: string; category?: string; minBudget?: number; format: string }) => {
64
+ const params = new URLSearchParams();
65
+ if (opts.status) params.set('status', opts.status);
66
+ if (opts.category) params.set('category', opts.category);
67
+ if (opts.minBudget !== undefined) params.set('min_budget', String(opts.minBudget));
68
+ const qs = params.toString();
69
+
70
+ const rows = await withSpinner('Fetching tasks...', () =>
71
+ api.get<TaskSummary[]>(`/tasks${qs ? `?${qs}` : ''}`),
72
+ );
73
+
74
+ if (opts.format === 'json') {
75
+ printJson(rows);
76
+ return;
77
+ }
78
+
79
+ if (rows.length === 0) {
80
+ console.log(chalk.dim('No tasks found.'));
81
+ return;
82
+ }
83
+
84
+ printTable(
85
+ ['ID', 'Title', 'Status', 'Category', 'Budget'],
86
+ rows.map(t => [
87
+ t.id.slice(0, 8),
88
+ truncate(t.title, 40),
89
+ t.status,
90
+ t.category,
91
+ `${t.budgetValue} ${t.budgetCurrency}`,
92
+ ]),
93
+ );
94
+ console.log(chalk.dim(`Showing ${rows.length} task(s).`));
95
+ });
96
+
97
+ tasks
98
+ .command('get <id>')
99
+ .description('Get task details')
100
+ .option('--format <format>', 'Output format (json|table)', 'table')
101
+ .action(async (id: string, opts: { format: string }) => {
102
+ const task = await withSpinner('Fetching task...', () =>
103
+ api.get<TaskDetail>(`/tasks/${id}`),
104
+ );
105
+
106
+ if (opts.format === 'json') {
107
+ printJson(task);
108
+ return;
109
+ }
110
+
111
+ console.log(chalk.bold(task.title));
112
+ console.log(chalk.dim('─'.repeat(60)));
113
+ console.log(` ID: ${task.id}`);
114
+ console.log(` Status: ${task.status}`);
115
+ console.log(` Category: ${task.category}`);
116
+ console.log(` Bid Mode: ${task.bidMode}`);
117
+ console.log(` Budget: ${task.budgetValue} ${task.budgetCurrency}`);
118
+ console.log(` Poster: ${task.posterId}`);
119
+ if (task.assignedTo) console.log(` Assignee: ${task.assignedTo}`);
120
+ if (task.deadline) console.log(` Deadline: ${formatDate(task.deadline)}`);
121
+ console.log(` Created: ${formatDate(task.createdAt)}`);
122
+ console.log(` Updated: ${formatDate(task.updatedAt)}`);
123
+ console.log();
124
+ console.log(task.description);
125
+ });
126
+
127
+ tasks
128
+ .command('search <query>')
129
+ .description('Search tasks')
130
+ .option('--format <format>', 'Output format (json|table)', 'table')
131
+ .action(async (query: string, opts: { format: string }) => {
132
+ const data = await withSpinner('Searching...', () =>
133
+ api.get<TaskSummary[]>(`/tasks?q=${encodeURIComponent(query)}`),
134
+ );
135
+
136
+ if (opts.format === 'json') {
137
+ printJson(data);
138
+ return;
139
+ }
140
+
141
+ if (data.length === 0) {
142
+ console.log(chalk.dim('No tasks found.'));
143
+ return;
144
+ }
145
+
146
+ printTable(
147
+ ['ID', 'Title', 'Status', 'Category', 'Budget'],
148
+ (data as TaskSummary[]).map(t => [
149
+ t.id.slice(0, 8),
150
+ truncate(t.title, 40),
151
+ t.status,
152
+ t.category,
153
+ `${t.budgetValue} ${t.budgetCurrency}`,
154
+ ]),
155
+ );
156
+ });
157
+
158
+ tasks
159
+ .command('claim <id>')
160
+ .description('Claim an open task (instant claim)')
161
+ .action(async (id: string) => {
162
+ const result = await withSpinner('Claiming task...', () =>
163
+ api.post<ClaimResult>(`/tasks/${id}/claim`),
164
+ );
165
+ printSuccess(`Task claimed! Status: ${result.status}`);
166
+ });
167
+
168
+ tasks
169
+ .command('propose <id>')
170
+ .description('Submit a proposal for a task')
171
+ .requiredOption('--approach <text>', 'Proposed approach')
172
+ .requiredOption('--price <n>', 'Proposed price', parseFloat)
173
+ .requiredOption('--time <minutes>', 'Estimated time in minutes', parseInt)
174
+ .action(async (id: string, opts: { approach: string; price: number; time: number }) => {
175
+ const result = await withSpinner('Submitting proposal...', () =>
176
+ api.post<ProposalResult>(`/tasks/${id}/proposals`, {
177
+ approach: opts.approach,
178
+ priceValue: String(opts.price),
179
+ priceCurrency: 'USD',
180
+ estimatedTime: opts.time,
181
+ }),
182
+ );
183
+ printSuccess(`Proposal submitted! ID: ${result.id}`);
184
+ });
185
+
186
+ tasks
187
+ .command('submit <id>')
188
+ .description('Submit work for a task')
189
+ .option('--content <text>', 'Submission content')
190
+ .option('--output <file>', 'JSON file with outputs array')
191
+ .option('--summary <text>', 'Submission summary')
192
+ .action(async (id: string, opts: { content?: string; output?: string; summary?: string }) => {
193
+ let content = opts.content;
194
+ let outputs: unknown;
195
+
196
+ // Read from stdin if no --content provided
197
+ if (!content && !opts.output && !process.stdin.isTTY) {
198
+ const chunks: Buffer[] = [];
199
+ for await (const chunk of process.stdin) {
200
+ chunks.push(chunk as Buffer);
201
+ }
202
+ content = Buffer.concat(chunks).toString('utf-8').trim();
203
+ }
204
+
205
+ if (opts.output) {
206
+ outputs = JSON.parse(readFileSync(opts.output, 'utf-8'));
207
+ }
208
+
209
+ const result = await withSpinner('Submitting work...', () =>
210
+ api.post<SubmissionResult>(`/tasks/${id}/submissions`, {
211
+ content,
212
+ outputs,
213
+ summary: opts.summary,
214
+ }),
215
+ );
216
+ printSuccess(`Submission created! ID: ${result.id}, Status: ${result.status}`);
217
+ });
218
+
219
+ tasks
220
+ .command('checkpoint <id>')
221
+ .description('Create a progress checkpoint')
222
+ .requiredOption('--label <text>', 'Checkpoint label')
223
+ .action(async (id: string, opts: { label: string }) => {
224
+ const result = await withSpinner('Creating checkpoint...', () =>
225
+ api.post<CheckpointResult>(`/tasks/${id}/checkpoint`, {
226
+ label: opts.label,
227
+ }),
228
+ );
229
+ printSuccess(`Checkpoint created: ${result.label}`);
230
+ });
231
+
232
+ tasks
233
+ .command('watch <id>')
234
+ .description('Watch task events in real-time (SSE)')
235
+ .action(async (id: string) => {
236
+ const { url, headers } = sseStream(`/events/stream?task_id=${id}`);
237
+ console.log(chalk.dim(`Watching events for task ${id}... (Ctrl+C to stop)`));
238
+ console.log();
239
+
240
+ const res = await fetch(url, { headers });
241
+ if (!res.ok || !res.body) {
242
+ console.error(chalk.red(`Failed to connect: ${res.status} ${res.statusText}`));
243
+ process.exit(1);
244
+ }
245
+
246
+ const reader = res.body.getReader();
247
+ const decoder = new TextDecoder();
248
+ let buffer = '';
249
+
250
+ while (true) {
251
+ const { done, value } = await reader.read();
252
+ if (done) break;
253
+
254
+ buffer += decoder.decode(value, { stream: true });
255
+ const lines = buffer.split('\n');
256
+ buffer = lines.pop() ?? '';
257
+
258
+ for (const line of lines) {
259
+ if (line.startsWith('data: ')) {
260
+ const payload = line.slice(6);
261
+ try {
262
+ const event = JSON.parse(payload) as { type: string; timestamp: string; data: unknown };
263
+ const time = new Date(event.timestamp).toLocaleTimeString();
264
+ console.log(`${chalk.dim(time)} ${chalk.bold(event.type)}`);
265
+ if (event.data) {
266
+ console.log(chalk.dim(JSON.stringify(event.data, null, 2)));
267
+ }
268
+ } catch {
269
+ console.log(chalk.dim(payload));
270
+ }
271
+ }
272
+ }
273
+ }
274
+ });
275
+
276
+ tasks
277
+ .command('create')
278
+ .description('Create a task from a JSON spec file')
279
+ .requiredOption('--spec <file>', 'Path to JSON spec file')
280
+ .action(async (opts: { spec: string }) => {
281
+ const spec = JSON.parse(readFileSync(opts.spec, 'utf-8'));
282
+ const result = await withSpinner('Creating task...', () =>
283
+ api.post<TaskDetail>('/tasks', spec),
284
+ );
285
+ printSuccess(`Task created! ID: ${result.id}, Status: ${result.status}`);
286
+ });
287
+
288
+ tasks
289
+ .command('publish <id>')
290
+ .description('Publish a draft task')
291
+ .action(async (id: string) => {
292
+ const result = await withSpinner('Publishing task...', () =>
293
+ api.post<TaskDetail>(`/tasks/${id}/publish`),
294
+ );
295
+ printSuccess(`Task published! Status: ${result.status}`);
296
+ });
297
+
298
+ tasks
299
+ .command('proposals <id>')
300
+ .description('List proposals for a task')
301
+ .option('--format <format>', 'Output format (json|table)', 'table')
302
+ .action(async (id: string, opts: { format: string }) => {
303
+ const rows = await withSpinner('Fetching proposals...', () =>
304
+ api.get<unknown[]>(`/tasks/${id}/proposals`),
305
+ );
306
+ if (opts.format === 'json') {
307
+ printJson(rows);
308
+ return;
309
+ }
310
+ if (rows.length === 0) {
311
+ console.log(chalk.dim('No proposals yet.'));
312
+ return;
313
+ }
314
+ printTable(
315
+ ['ID', 'Proposer', 'Price', 'Status'],
316
+ rows.map((p) => {
317
+ const proposal = p as Record<string, unknown>;
318
+ return [
319
+ String(proposal['id'] ?? '').slice(0, 8),
320
+ String(proposal['proposerId'] ?? '').slice(0, 8),
321
+ `${proposal['priceValue'] ?? ''} ${proposal['priceCurrency'] ?? ''}`,
322
+ String(proposal['status'] ?? ''),
323
+ ];
324
+ }),
325
+ );
326
+ });
327
+
328
+ tasks
329
+ .command('accept-proposal <taskId> <proposalId>')
330
+ .description('Accept a proposal (poster only)')
331
+ .action(async (taskId: string, proposalId: string) => {
332
+ const result = await withSpinner('Accepting proposal...', () =>
333
+ api.post<TaskDetail>(`/tasks/${taskId}/proposals/${proposalId}/accept`),
334
+ );
335
+ printSuccess(`Proposal accepted! Task status: ${result.status}`);
336
+ });
337
+
338
+ tasks
339
+ .command('stake <id>')
340
+ .description('Confirm stake to start task (worker, when requireStake=true)')
341
+ .action(async (id: string) => {
342
+ const result = await withSpinner('Confirming stake...', () =>
343
+ api.post<TaskDetail>(`/tasks/${id}/stake`),
344
+ );
345
+ printSuccess(`Stake confirmed! Task status: ${result.status}`);
346
+ });
347
+
348
+ tasks
349
+ .command('submissions <id>')
350
+ .description('List submissions for a task')
351
+ .option('--format <format>', 'Output format (json|table)', 'table')
352
+ .action(async (id: string, opts: { format: string }) => {
353
+ const rows = await withSpinner('Fetching submissions...', () =>
354
+ api.get<unknown[]>(`/tasks/${id}/submissions`),
355
+ );
356
+ if (opts.format === 'json') {
357
+ printJson(rows);
358
+ return;
359
+ }
360
+ if (rows.length === 0) {
361
+ console.log(chalk.dim('No submissions yet.'));
362
+ return;
363
+ }
364
+ printTable(
365
+ ['ID', 'Attempt', 'Status', 'Created'],
366
+ rows.map((s) => {
367
+ const sub = s as Record<string, unknown>;
368
+ return [
369
+ String(sub['id'] ?? '').slice(0, 8),
370
+ String(sub['attempt'] ?? ''),
371
+ String(sub['status'] ?? ''),
372
+ formatDate(String(sub['createdAt'] ?? '')),
373
+ ];
374
+ }),
375
+ );
376
+ });
377
+
378
+ tasks
379
+ .command('review <submissionId>')
380
+ .description('Review a submission (poster only)')
381
+ .requiredOption('--verdict <v>', 'APPROVED | REJECTED | REVISION_REQUESTED')
382
+ .option('--comment <text>', 'Optional review comment')
383
+ .option('--score <n>', 'Overall score 0-10', parseFloat)
384
+ .action(async (submissionId: string, opts: { verdict: string; comment?: string; score?: number }) => {
385
+ interface ReviewResult { id: string; verdict: string; }
386
+ const result = await withSpinner('Submitting review...', () =>
387
+ api.post<ReviewResult>(`/submissions/${submissionId}/review`, {
388
+ verdict: opts.verdict,
389
+ comment: opts.comment,
390
+ overallScore: opts.score,
391
+ }),
392
+ );
393
+ printSuccess(`Review submitted! Verdict: ${result.verdict}`);
394
+ });
395
+
396
+ return tasks;
397
+ }
package/src/config.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ export interface CliConfig {
6
+ apiKey: string;
7
+ apiUrl: string;
8
+ participantId?: string;
9
+ agentId?: string;
10
+ }
11
+
12
+ const CONFIG_DIR = join(homedir(), '.taskhunt');
13
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
14
+
15
+ export function getApiUrl(): string {
16
+ return process.env['TASKHUNT_API_URL'] ?? loadConfig()?.apiUrl ?? 'https://api.taskhunt.ai';
17
+ }
18
+
19
+ export function getApiKey(): string | undefined {
20
+ return process.env['TASKHUNT_API_KEY'] ?? loadConfig()?.apiKey;
21
+ }
22
+
23
+ export function requireApiKey(): string {
24
+ const key = getApiKey();
25
+ if (!key) {
26
+ console.error('Not authenticated. Run `taskhunt auth login --api-key <KEY>` first.');
27
+ process.exit(1);
28
+ }
29
+ return key;
30
+ }
31
+
32
+ export function loadConfig(): CliConfig | null {
33
+ if (!existsSync(CONFIG_FILE)) return null;
34
+ try {
35
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')) as CliConfig;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ export function saveConfig(config: CliConfig): void {
42
+ if (!existsSync(CONFIG_DIR)) {
43
+ mkdirSync(CONFIG_DIR, { recursive: true });
44
+ }
45
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
46
+ }
47
+
48
+ export function deleteConfig(): void {
49
+ if (existsSync(CONFIG_FILE)) {
50
+ unlinkSync(CONFIG_FILE);
51
+ }
52
+ }
package/src/format.ts ADDED
@@ -0,0 +1,35 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+
4
+ export function printJson(data: unknown): void {
5
+ console.log(JSON.stringify(data, null, 2));
6
+ }
7
+
8
+ export function printSuccess(msg: string): void {
9
+ console.log(chalk.green('✓') + ' ' + msg);
10
+ }
11
+
12
+ export function printError(msg: string): void {
13
+ console.error(chalk.red('✗') + ' ' + msg);
14
+ }
15
+
16
+ export function printWarning(msg: string): void {
17
+ console.log(chalk.yellow('!') + ' ' + msg);
18
+ }
19
+
20
+ export function printTable(headers: string[], rows: string[][]): void {
21
+ const table = new Table({ head: headers.map(h => chalk.bold(h)) });
22
+ for (const row of rows) {
23
+ table.push(row);
24
+ }
25
+ console.log(table.toString());
26
+ }
27
+
28
+ export function truncate(str: string, len: number): string {
29
+ if (str.length <= len) return str;
30
+ return str.slice(0, len - 1) + '…';
31
+ }
32
+
33
+ export function formatDate(iso: string): string {
34
+ return new Date(iso).toLocaleString();
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { createAuthCommand } from './commands/auth.js';
5
+ import { createTasksCommand } from './commands/tasks.js';
6
+ import { createAgentsCommand } from './commands/agents.js';
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('taskhunt')
12
+ .description('TaskHunt CLI — AI Agent task marketplace')
13
+ .version('0.1.0');
14
+
15
+ program.addCommand(createAuthCommand());
16
+ program.addCommand(createTasksCommand());
17
+ program.addCommand(createAgentsCommand());
18
+
19
+ program.parseAsync().catch((err: Error) => {
20
+ console.error(err.message);
21
+ process.exit(1);
22
+ });
package/src/spinner.ts ADDED
@@ -0,0 +1,20 @@
1
+ import ora, { type Ora } from 'ora';
2
+ import { printError } from './format.js';
3
+ import { ApiError } from './api.js';
4
+
5
+ export async function withSpinner<T>(text: string, fn: () => Promise<T>): Promise<T> {
6
+ const spinner: Ora = ora(text).start();
7
+ try {
8
+ const result = await fn();
9
+ spinner.succeed();
10
+ return result;
11
+ } catch (err) {
12
+ spinner.fail();
13
+ if (err instanceof ApiError) {
14
+ printError(`${err.code}: ${err.message}`);
15
+ } else if (err instanceof Error) {
16
+ printError(err.message);
17
+ }
18
+ process.exit(1);
19
+ }
20
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "noEmit": false
9
+ },
10
+ "include": ["src"],
11
+ "exclude": ["node_modules", "dist"]
12
+ }