@arvorco/relentless 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/relentless.ts CHANGED
@@ -427,6 +427,127 @@ agents
427
427
  }
428
428
  });
429
429
 
430
+ // Queue commands
431
+ const queue = program.command("queue").description("Manage queue for mid-run guidance");
432
+
433
+ queue
434
+ .command("add <message>")
435
+ .description("Add a message or command to the queue")
436
+ .requiredOption("-f, --feature <name>", "Feature name")
437
+ .option("-d, --dir <path>", "Project directory", process.cwd())
438
+ .action(async (message, options) => {
439
+ const { queueAdd, resolveFeaturePath } = await import("../src/cli/queue");
440
+
441
+ const resolved = await resolveFeaturePath(options.dir, options.feature);
442
+ if (resolved.error) {
443
+ console.error(chalk.red(resolved.error));
444
+ process.exit(1);
445
+ }
446
+
447
+ const result = await queueAdd({
448
+ message,
449
+ featurePath: resolved.path!,
450
+ });
451
+
452
+ if (result.success) {
453
+ console.log(chalk.green(`✓ ${result.message}`));
454
+ } else {
455
+ console.error(chalk.red(`Error: ${result.error}`));
456
+ process.exit(1);
457
+ }
458
+ });
459
+
460
+ queue
461
+ .command("list")
462
+ .description("List queue contents for a feature")
463
+ .requiredOption("-f, --feature <name>", "Feature name")
464
+ .option("-d, --dir <path>", "Project directory", process.cwd())
465
+ .option("-a, --all", "Show all items including processed", false)
466
+ .action(async (options) => {
467
+ const { queueList, formatQueueList, resolveFeaturePath } = await import("../src/cli/queue");
468
+
469
+ const resolved = await resolveFeaturePath(options.dir, options.feature);
470
+ if (resolved.error) {
471
+ console.error(chalk.red(resolved.error));
472
+ process.exit(1);
473
+ }
474
+
475
+ const result = await queueList({
476
+ featurePath: resolved.path!,
477
+ showAll: options.all,
478
+ });
479
+
480
+ if (result.success) {
481
+ const output = formatQueueList({
482
+ ...result,
483
+ featureName: options.feature,
484
+ });
485
+ console.log(output);
486
+ } else {
487
+ console.error(chalk.red(`Error: ${result.error}`));
488
+ process.exit(1);
489
+ }
490
+ });
491
+
492
+ queue
493
+ .command("remove <index>")
494
+ .description("Remove an item from the queue by index (1-based)")
495
+ .requiredOption("-f, --feature <name>", "Feature name")
496
+ .option("-d, --dir <path>", "Project directory", process.cwd())
497
+ .action(async (index, options) => {
498
+ const { queueRemove, resolveFeaturePath } = await import("../src/cli/queue");
499
+
500
+ const resolved = await resolveFeaturePath(options.dir, options.feature);
501
+ if (resolved.error) {
502
+ console.error(chalk.red(resolved.error));
503
+ process.exit(1);
504
+ }
505
+
506
+ const parsedIndex = parseInt(index, 10);
507
+ if (isNaN(parsedIndex)) {
508
+ console.error(chalk.red(`Invalid index: ${index}. Must be a number`));
509
+ process.exit(1);
510
+ }
511
+
512
+ const result = await queueRemove({
513
+ index: parsedIndex,
514
+ featurePath: resolved.path!,
515
+ });
516
+
517
+ if (result.success) {
518
+ console.log(chalk.green(`✓ ${result.message}`));
519
+ } else {
520
+ console.error(chalk.red(`Error: ${result.error}`));
521
+ process.exit(1);
522
+ }
523
+ });
524
+
525
+ queue
526
+ .command("clear")
527
+ .description("Clear all items from the queue")
528
+ .requiredOption("-f, --feature <name>", "Feature name")
529
+ .option("-d, --dir <path>", "Project directory", process.cwd())
530
+ .action(async (options) => {
531
+ const { queueClear, resolveFeaturePath } = await import("../src/cli/queue");
532
+
533
+ const resolved = await resolveFeaturePath(options.dir, options.feature);
534
+ if (resolved.error) {
535
+ console.error(chalk.red(resolved.error));
536
+ process.exit(1);
537
+ }
538
+
539
+ const result = await queueClear({
540
+ featurePath: resolved.path!,
541
+ });
542
+
543
+ if (result.success) {
544
+ console.log(chalk.green(`✓ ${result.message}`));
545
+ } else {
546
+ console.error(chalk.red(`Error: ${result.error}`));
547
+ process.exit(1);
548
+ }
549
+ });
550
+
430
551
  // Analyze command
431
552
  program
432
553
  .command("analyze")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arvorco/relentless",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Universal AI agent orchestrator - works with Claude Code, Amp, OpenCode, Codex, Droid, and Gemini",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -0,0 +1,406 @@
1
+ /**
2
+ * CLI Queue Functions
3
+ *
4
+ * Functions for queue CLI commands.
5
+ */
6
+
7
+ import { join } from "node:path";
8
+ import { existsSync } from "node:fs";
9
+ import { findRelentlessDir } from "../config";
10
+ import {
11
+ addToQueue,
12
+ loadQueue,
13
+ removeFromQueue,
14
+ clearQueue,
15
+ type QueueItem,
16
+ } from "../queue";
17
+
18
+ /** Result of a queue add operation */
19
+ export interface QueueAddResult {
20
+ success: boolean;
21
+ message?: string;
22
+ error?: string;
23
+ }
24
+
25
+ /** Options for queueAdd function */
26
+ export interface QueueAddOptions {
27
+ message: string;
28
+ featurePath: string;
29
+ }
30
+
31
+ /** Result of resolving a feature path */
32
+ export interface ResolveFeaturePathResult {
33
+ path?: string;
34
+ error?: string;
35
+ }
36
+
37
+ /**
38
+ * Resolves the feature path from a working directory and feature name.
39
+ *
40
+ * @param workingDir - The working directory (project root)
41
+ * @param featureName - The feature name
42
+ * @returns The resolved feature path or an error
43
+ */
44
+ export async function resolveFeaturePath(
45
+ workingDir: string,
46
+ featureName: string
47
+ ): Promise<ResolveFeaturePathResult> {
48
+ const relentlessDir = findRelentlessDir(workingDir);
49
+
50
+ if (!relentlessDir) {
51
+ return {
52
+ error: "Relentless not initialized. Run: relentless init",
53
+ };
54
+ }
55
+
56
+ const featurePath = join(relentlessDir, "features", featureName);
57
+
58
+ if (!existsSync(featurePath)) {
59
+ return {
60
+ error: `Feature '${featureName}' not found`,
61
+ };
62
+ }
63
+
64
+ return { path: featurePath };
65
+ }
66
+
67
+ /**
68
+ * Adds a message to the queue for a feature.
69
+ *
70
+ * @param options - The queue add options
71
+ * @returns The result of the operation
72
+ */
73
+ export async function queueAdd(options: QueueAddOptions): Promise<QueueAddResult> {
74
+ const { message, featurePath } = options;
75
+
76
+ // Validate feature path exists
77
+ if (!existsSync(featurePath)) {
78
+ return {
79
+ success: false,
80
+ error: `Feature path not found: ${featurePath}`,
81
+ };
82
+ }
83
+
84
+ try {
85
+ await addToQueue(featurePath, message);
86
+
87
+ return {
88
+ success: true,
89
+ message: `Added to queue: ${message}`,
90
+ };
91
+ } catch (error) {
92
+ return {
93
+ success: false,
94
+ error: `Failed to add to queue: ${(error as Error).message}`,
95
+ };
96
+ }
97
+ }
98
+
99
+ /** Item in queue list output */
100
+ export interface QueueListItem {
101
+ index: number;
102
+ timestamp: string;
103
+ content: string;
104
+ type: "prompt" | "command";
105
+ }
106
+
107
+ /** Result of a queue list operation */
108
+ export interface QueueListResult {
109
+ success: boolean;
110
+ isEmpty: boolean;
111
+ pendingItems: QueueListItem[];
112
+ processedItems: QueueListItem[];
113
+ featureName?: string;
114
+ error?: string;
115
+ }
116
+
117
+ /** Options for queueList function */
118
+ export interface QueueListOptions {
119
+ featurePath: string;
120
+ showAll: boolean;
121
+ }
122
+
123
+ /**
124
+ * Converts a QueueItem to a QueueListItem.
125
+ *
126
+ * @param item - The QueueItem to convert
127
+ * @param index - The 1-based index
128
+ * @returns The QueueListItem
129
+ */
130
+ function toQueueListItem(item: QueueItem, index: number): QueueListItem {
131
+ return {
132
+ index,
133
+ timestamp: item.addedAt,
134
+ content: item.content,
135
+ type: item.type,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Lists queue items for a feature.
141
+ *
142
+ * @param options - The queue list options
143
+ * @returns The result of the operation
144
+ */
145
+ export async function queueList(options: QueueListOptions): Promise<QueueListResult> {
146
+ const { featurePath, showAll } = options;
147
+
148
+ // Validate feature path exists
149
+ if (!existsSync(featurePath)) {
150
+ return {
151
+ success: false,
152
+ isEmpty: true,
153
+ pendingItems: [],
154
+ processedItems: [],
155
+ error: `Feature path not found: ${featurePath}`,
156
+ };
157
+ }
158
+
159
+ try {
160
+ const state = await loadQueue(featurePath);
161
+
162
+ const pendingItems = state.pending.map((item, index) =>
163
+ toQueueListItem(item, index + 1)
164
+ );
165
+
166
+ const processedItems = showAll
167
+ ? state.processed.map((item, index) => toQueueListItem(item, index + 1))
168
+ : [];
169
+
170
+ const isEmpty = pendingItems.length === 0 && processedItems.length === 0;
171
+
172
+ return {
173
+ success: true,
174
+ isEmpty,
175
+ pendingItems,
176
+ processedItems,
177
+ featureName: featurePath.split("/").pop(),
178
+ };
179
+ } catch (error) {
180
+ return {
181
+ success: false,
182
+ isEmpty: true,
183
+ pendingItems: [],
184
+ processedItems: [],
185
+ error: `Failed to list queue: ${(error as Error).message}`,
186
+ };
187
+ }
188
+ }
189
+
190
+ /** Options for formatting queue list output */
191
+ export interface FormatQueueListOptions {
192
+ success: boolean;
193
+ isEmpty: boolean;
194
+ pendingItems: QueueListItem[];
195
+ processedItems: QueueListItem[];
196
+ featureName: string;
197
+ }
198
+
199
+ /**
200
+ * Formats the time portion of an ISO timestamp.
201
+ *
202
+ * @param timestamp - ISO timestamp string
203
+ * @returns Time in HH:MM format
204
+ */
205
+ function formatTime(timestamp: string): string {
206
+ try {
207
+ const date = new Date(timestamp);
208
+ const hours = date.getUTCHours().toString().padStart(2, "0");
209
+ const minutes = date.getUTCMinutes().toString().padStart(2, "0");
210
+ return `${hours}:${minutes}`;
211
+ } catch {
212
+ return "--:--";
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Formats the date portion of an ISO timestamp.
218
+ *
219
+ * @param timestamp - ISO timestamp string
220
+ * @returns Date in YYYY-MM-DD format
221
+ */
222
+ function formatDate(timestamp: string): string {
223
+ try {
224
+ return timestamp.split("T")[0];
225
+ } catch {
226
+ return "----:--:--";
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Formats queue list output for display.
232
+ *
233
+ * @param options - The format options
234
+ * @returns Formatted string for display
235
+ */
236
+ export function formatQueueList(options: FormatQueueListOptions): string {
237
+ const { isEmpty, pendingItems, processedItems, featureName } = options;
238
+
239
+ const lines: string[] = [];
240
+
241
+ // Header with feature name
242
+ lines.push(`\nQueue: ${featureName}`);
243
+ lines.push("");
244
+
245
+ if (isEmpty) {
246
+ lines.push("Queue is empty");
247
+ lines.push("");
248
+ return lines.join("\n");
249
+ }
250
+
251
+ // Pending items
252
+ if (pendingItems.length > 0) {
253
+ lines.push(`Pending (${pendingItems.length} items):`);
254
+ for (const item of pendingItems) {
255
+ const time = formatTime(item.timestamp);
256
+ const date = formatDate(item.timestamp);
257
+ const typeIndicator = item.type === "command" ? " [cmd]" : "";
258
+ lines.push(` ${item.index}. [${date} ${time}] ${item.content}${typeIndicator}`);
259
+ }
260
+ lines.push("");
261
+ }
262
+
263
+ // Processed items
264
+ if (processedItems.length > 0) {
265
+ lines.push(`Processed (${processedItems.length} items):`);
266
+ for (const item of processedItems) {
267
+ const time = formatTime(item.timestamp);
268
+ const date = formatDate(item.timestamp);
269
+ lines.push(` ${item.index}. [${date} ${time}] ${item.content} ✓`);
270
+ }
271
+ lines.push("");
272
+ }
273
+
274
+ return lines.join("\n");
275
+ }
276
+
277
+ /** Result of a queue remove operation */
278
+ export interface QueueRemoveResult {
279
+ success: boolean;
280
+ message?: string;
281
+ removedContent?: string;
282
+ error?: string;
283
+ }
284
+
285
+ /** Options for queueRemove function */
286
+ export interface QueueRemoveOptions {
287
+ index: number;
288
+ featurePath: string;
289
+ }
290
+
291
+ /**
292
+ * Removes an item from the queue by 1-based index.
293
+ *
294
+ * @param options - The queue remove options
295
+ * @returns The result of the operation
296
+ */
297
+ export async function queueRemove(
298
+ options: QueueRemoveOptions
299
+ ): Promise<QueueRemoveResult> {
300
+ const { index, featurePath } = options;
301
+
302
+ // Validate feature path exists
303
+ if (!existsSync(featurePath)) {
304
+ return {
305
+ success: false,
306
+ error: `Feature path not found: ${featurePath}`,
307
+ };
308
+ }
309
+
310
+ // Validate index is positive
311
+ if (index < 1) {
312
+ return {
313
+ success: false,
314
+ error: `Invalid index: ${index}. Index must be 1 or greater`,
315
+ };
316
+ }
317
+
318
+ // Load queue to check state
319
+ const state = await loadQueue(featurePath);
320
+ const queueLength = state.pending.length;
321
+
322
+ // Handle empty queue
323
+ if (queueLength === 0) {
324
+ return {
325
+ success: false,
326
+ error: "Queue is empty",
327
+ };
328
+ }
329
+
330
+ // Validate index against queue length
331
+ if (index > queueLength) {
332
+ const itemWord = queueLength === 1 ? "item" : "items";
333
+ return {
334
+ success: false,
335
+ error: `Invalid index: ${index}. Queue has ${queueLength} ${itemWord}`,
336
+ };
337
+ }
338
+
339
+ // Remove the item
340
+ const removedItem = await removeFromQueue(featurePath, index);
341
+
342
+ if (!removedItem) {
343
+ return {
344
+ success: false,
345
+ error: `Failed to remove item at index ${index}`,
346
+ };
347
+ }
348
+
349
+ return {
350
+ success: true,
351
+ message: `Removed: ${removedItem.content}`,
352
+ removedContent: removedItem.content,
353
+ };
354
+ }
355
+
356
+ /** Result of a queue clear operation */
357
+ export interface QueueClearResult {
358
+ success: boolean;
359
+ message?: string;
360
+ clearedCount: number;
361
+ error?: string;
362
+ }
363
+
364
+ /** Options for queueClear function */
365
+ export interface QueueClearOptions {
366
+ featurePath: string;
367
+ }
368
+
369
+ /**
370
+ * Clears all items from the queue.
371
+ *
372
+ * @param options - The queue clear options
373
+ * @returns The result of the operation
374
+ */
375
+ export async function queueClear(
376
+ options: QueueClearOptions
377
+ ): Promise<QueueClearResult> {
378
+ const { featurePath } = options;
379
+
380
+ // Validate feature path exists
381
+ if (!existsSync(featurePath)) {
382
+ return {
383
+ success: false,
384
+ clearedCount: 0,
385
+ error: `Feature path not found: ${featurePath}`,
386
+ };
387
+ }
388
+
389
+ // Clear the queue
390
+ const count = await clearQueue(featurePath);
391
+
392
+ if (count === 0) {
393
+ return {
394
+ success: true,
395
+ clearedCount: 0,
396
+ message: "Queue is already empty",
397
+ };
398
+ }
399
+
400
+ const itemWord = count === 1 ? "item" : "items";
401
+ return {
402
+ success: true,
403
+ clearedCount: count,
404
+ message: `Cleared ${count} ${itemWord} from queue`,
405
+ };
406
+ }