@supaku/agentfactory-cli 0.7.3 → 0.7.6

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.
@@ -39,4 +39,14 @@ describe('@supaku/agentfactory-cli subpath exports', () => {
39
39
  expect(mod.runLogAnalyzer).toBeDefined();
40
40
  expect(typeof mod.runLogAnalyzer).toBe('function');
41
41
  });
42
+ it('exports runLinear from ./linear', async () => {
43
+ const mod = await import('../lib/linear-runner.js');
44
+ expect(mod.runLinear).toBeDefined();
45
+ expect(typeof mod.runLinear).toBe('function');
46
+ });
47
+ it('exports parseLinearArgs from ./linear', async () => {
48
+ const mod = await import('../lib/linear-runner.js');
49
+ expect(mod.parseLinearArgs).toBeDefined();
50
+ expect(typeof mod.parseLinearArgs).toBe('function');
51
+ });
42
52
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,QAAA,MAAM,OAAO,QAAkB,CAAA;AAE/B,iBAAS,SAAS,IAAI,IAAI,CAoBzB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,QAAA,MAAM,OAAO,QAAkB,CAAA;AAE/B,iBAAS,SAAS,IAAI,IAAI,CAqBzB"}
package/dist/src/index.js CHANGED
@@ -21,6 +21,7 @@ Commands:
21
21
  cleanup Clean up orphaned git worktrees
22
22
  queue-admin Manage Redis work queue and sessions
23
23
  analyze-logs Analyze agent session logs for errors
24
+ linear Linear issue tracker operations
24
25
  help Show this help message
25
26
 
26
27
  Run 'agentfactory <command> --help' for command-specific options.
@@ -47,6 +48,9 @@ switch (command) {
47
48
  case 'analyze-logs':
48
49
  import('./analyze-logs');
49
50
  break;
51
+ case 'linear':
52
+ import('./linear');
53
+ break;
50
54
  case 'help':
51
55
  case '--help':
52
56
  case '-h':
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Linear CLI Runner — process-agnostic Linear operations.
3
+ *
4
+ * All 15 command implementations extracted from packages/core/src/linear-cli.ts.
5
+ * This module does NOT call process.exit, read process.argv, or load dotenv.
6
+ */
7
+ export interface LinearRunnerConfig {
8
+ command: string;
9
+ args: Record<string, string | string[] | boolean>;
10
+ positionalArgs: string[];
11
+ apiKey?: string;
12
+ }
13
+ export interface LinearRunnerResult {
14
+ output: unknown;
15
+ }
16
+ /**
17
+ * Parse CLI arguments into a structured object.
18
+ *
19
+ * Supports:
20
+ * - `--key value` pairs
21
+ * - JSON array values: `--labels '["Bug", "Feature"]'`
22
+ * - Comma-separated values for array fields: `--labels "Bug,Feature"`
23
+ * - Boolean flags: `--dry-run` (value = "true")
24
+ *
25
+ * Returns the command (first non-flag arg), named args, and positional args.
26
+ */
27
+ export declare function parseLinearArgs(argv: string[]): {
28
+ command: string | undefined;
29
+ args: Record<string, string | string[] | boolean>;
30
+ positionalArgs: string[];
31
+ };
32
+ export declare function runLinear(config: LinearRunnerConfig): Promise<LinearRunnerResult>;
33
+ //# sourceMappingURL=linear-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/linear-runner.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAA;IACjD,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAA;CAChB;AAOD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAC/C,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAA;IACjD,cAAc,EAAE,MAAM,EAAE,CAAA;CACzB,CA2CA;AA+cD,wBAAsB,SAAS,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAyLvF"}
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Linear CLI Runner — process-agnostic Linear operations.
3
+ *
4
+ * All 15 command implementations extracted from packages/core/src/linear-cli.ts.
5
+ * This module does NOT call process.exit, read process.argv, or load dotenv.
6
+ */
7
+ import { createLinearAgentClient } from '@supaku/agentfactory-linear';
8
+ import { checkPRDeploymentStatus, formatDeploymentStatus, } from '@supaku/agentfactory';
9
+ // ── Arg parsing ────────────────────────────────────────────────────
10
+ /** Fields that should be split on commas to create arrays */
11
+ const ARRAY_FIELDS = new Set(['labels']);
12
+ /**
13
+ * Parse CLI arguments into a structured object.
14
+ *
15
+ * Supports:
16
+ * - `--key value` pairs
17
+ * - JSON array values: `--labels '["Bug", "Feature"]'`
18
+ * - Comma-separated values for array fields: `--labels "Bug,Feature"`
19
+ * - Boolean flags: `--dry-run` (value = "true")
20
+ *
21
+ * Returns the command (first non-flag arg), named args, and positional args.
22
+ */
23
+ export function parseLinearArgs(argv) {
24
+ const command = argv[0] && !argv[0].startsWith('--') ? argv[0] : undefined;
25
+ const rest = command ? argv.slice(1) : argv;
26
+ const args = {};
27
+ const positionalArgs = [];
28
+ for (let i = 0; i < rest.length; i++) {
29
+ const arg = rest[i];
30
+ if (arg.startsWith('--')) {
31
+ const key = arg.slice(2);
32
+ const value = rest[i + 1];
33
+ if (value && !value.startsWith('--')) {
34
+ // Support JSON array format: --labels '["Bug", "Feature"]'
35
+ if (value.startsWith('[') && value.endsWith(']')) {
36
+ try {
37
+ const parsed = JSON.parse(value);
38
+ if (Array.isArray(parsed)) {
39
+ args[key] = parsed;
40
+ i++;
41
+ continue;
42
+ }
43
+ }
44
+ catch {
45
+ // Not valid JSON, fall through to normal handling
46
+ }
47
+ }
48
+ // Only split on comma for known array fields
49
+ if (ARRAY_FIELDS.has(key) && value.includes(',')) {
50
+ args[key] = value.split(',').map((v) => v.trim());
51
+ }
52
+ else {
53
+ args[key] = value;
54
+ }
55
+ i++;
56
+ }
57
+ else {
58
+ args[key] = true;
59
+ }
60
+ }
61
+ else {
62
+ positionalArgs.push(arg);
63
+ }
64
+ }
65
+ return { command, args, positionalArgs };
66
+ }
67
+ // ── Command implementations ────────────────────────────────────────
68
+ async function getIssue(client, issueId) {
69
+ const issue = await client.getIssue(issueId);
70
+ const state = await issue.state;
71
+ const team = await issue.team;
72
+ const project = await issue.project;
73
+ const labels = await issue.labels();
74
+ return {
75
+ id: issue.id,
76
+ identifier: issue.identifier,
77
+ title: issue.title,
78
+ description: issue.description,
79
+ url: issue.url,
80
+ status: state?.name,
81
+ team: team?.name,
82
+ project: project?.name,
83
+ labels: labels.nodes.map((l) => l.name),
84
+ createdAt: issue.createdAt,
85
+ updatedAt: issue.updatedAt,
86
+ };
87
+ }
88
+ async function createIssue(client, options) {
89
+ const team = await client.getTeam(options.team);
90
+ const createPayload = {
91
+ teamId: team.id,
92
+ title: options.title,
93
+ };
94
+ if (options.description) {
95
+ createPayload.description = options.description;
96
+ }
97
+ if (options.parentId) {
98
+ createPayload.parentId = options.parentId;
99
+ }
100
+ if (options.project) {
101
+ const projects = await client.linearClient.projects({
102
+ filter: { name: { eq: options.project } },
103
+ });
104
+ if (projects.nodes.length > 0) {
105
+ createPayload.projectId = projects.nodes[0].id;
106
+ }
107
+ }
108
+ if (options.state) {
109
+ const statuses = await client.getTeamStatuses(team.id);
110
+ const stateId = statuses[options.state];
111
+ if (stateId) {
112
+ createPayload.stateId = stateId;
113
+ }
114
+ }
115
+ if (options.labels && options.labels.length > 0) {
116
+ const allLabels = await client.linearClient.issueLabels();
117
+ const labelIds = [];
118
+ for (const labelName of options.labels) {
119
+ const label = allLabels.nodes.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
120
+ if (label) {
121
+ labelIds.push(label.id);
122
+ }
123
+ }
124
+ if (labelIds.length > 0) {
125
+ createPayload.labelIds = labelIds;
126
+ }
127
+ }
128
+ const payload = await client.linearClient.createIssue(createPayload);
129
+ if (!payload.success) {
130
+ throw new Error('Failed to create issue');
131
+ }
132
+ const issue = await payload.issue;
133
+ if (!issue) {
134
+ throw new Error('Issue created but not returned');
135
+ }
136
+ return {
137
+ id: issue.id,
138
+ identifier: issue.identifier,
139
+ title: issue.title,
140
+ url: issue.url,
141
+ };
142
+ }
143
+ async function updateIssue(client, issueId, options) {
144
+ const issue = await client.getIssue(issueId);
145
+ const team = await issue.team;
146
+ const updateData = {};
147
+ if (options.title) {
148
+ updateData.title = options.title;
149
+ }
150
+ if (options.description) {
151
+ updateData.description = options.description;
152
+ }
153
+ if (options.state && team) {
154
+ const statuses = await client.getTeamStatuses(team.id);
155
+ const stateId = statuses[options.state];
156
+ if (stateId) {
157
+ updateData.stateId = stateId;
158
+ }
159
+ }
160
+ if (options.labels && options.labels.length > 0) {
161
+ const allLabels = await client.linearClient.issueLabels();
162
+ const labelIds = [];
163
+ for (const labelName of options.labels) {
164
+ const label = allLabels.nodes.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
165
+ if (label) {
166
+ labelIds.push(label.id);
167
+ }
168
+ }
169
+ updateData.labelIds = labelIds;
170
+ }
171
+ const updatedIssue = await client.updateIssue(issue.id, updateData);
172
+ const state = await updatedIssue.state;
173
+ return {
174
+ id: updatedIssue.id,
175
+ identifier: updatedIssue.identifier,
176
+ title: updatedIssue.title,
177
+ status: state?.name,
178
+ url: updatedIssue.url,
179
+ };
180
+ }
181
+ async function listComments(client, issueId) {
182
+ const comments = await client.getIssueComments(issueId);
183
+ return comments.map((c) => ({
184
+ id: c.id,
185
+ body: c.body,
186
+ createdAt: c.createdAt,
187
+ }));
188
+ }
189
+ async function createComment(client, issueId, body) {
190
+ const comment = await client.createComment(issueId, body);
191
+ return {
192
+ id: comment.id,
193
+ body: comment.body,
194
+ createdAt: comment.createdAt,
195
+ };
196
+ }
197
+ async function addRelation(client, issueId, relatedIssueId, relationType) {
198
+ const result = await client.createIssueRelation({
199
+ issueId,
200
+ relatedIssueId,
201
+ type: relationType,
202
+ });
203
+ return {
204
+ success: result.success,
205
+ relationId: result.relationId,
206
+ issueId,
207
+ relatedIssueId,
208
+ type: relationType,
209
+ };
210
+ }
211
+ async function listRelations(client, issueId) {
212
+ const result = await client.getIssueRelations(issueId);
213
+ return {
214
+ issueId,
215
+ relations: result.relations.map((r) => ({
216
+ id: r.id,
217
+ type: r.type,
218
+ relatedIssue: r.relatedIssueIdentifier ?? r.relatedIssueId,
219
+ createdAt: r.createdAt,
220
+ })),
221
+ inverseRelations: result.inverseRelations.map((r) => ({
222
+ id: r.id,
223
+ type: r.type,
224
+ sourceIssue: r.issueIdentifier ?? r.issueId,
225
+ createdAt: r.createdAt,
226
+ })),
227
+ };
228
+ }
229
+ async function removeRelation(client, relationId) {
230
+ const result = await client.deleteIssueRelation(relationId);
231
+ return {
232
+ success: result.success,
233
+ relationId,
234
+ };
235
+ }
236
+ async function listBacklogIssues(client, projectName) {
237
+ const projects = await client.linearClient.projects({
238
+ filter: { name: { eqIgnoreCase: projectName } },
239
+ });
240
+ if (projects.nodes.length === 0) {
241
+ throw new Error(`Project not found: ${projectName}`);
242
+ }
243
+ const project = projects.nodes[0];
244
+ const issues = await client.linearClient.issues({
245
+ filter: {
246
+ project: { id: { eq: project.id } },
247
+ state: { name: { eqIgnoreCase: 'Backlog' } },
248
+ },
249
+ });
250
+ const results = [];
251
+ for (const issue of issues.nodes) {
252
+ const state = await issue.state;
253
+ const labels = await issue.labels();
254
+ results.push({
255
+ id: issue.id,
256
+ identifier: issue.identifier,
257
+ title: issue.title,
258
+ description: issue.description,
259
+ url: issue.url,
260
+ priority: issue.priority,
261
+ status: state?.name,
262
+ labels: labels.nodes.map((l) => l.name),
263
+ });
264
+ }
265
+ results.sort((a, b) => {
266
+ const aPriority = a.priority || 5;
267
+ const bPriority = b.priority || 5;
268
+ return aPriority - bPriority;
269
+ });
270
+ return results;
271
+ }
272
+ async function getBlockingIssues(client, issueId) {
273
+ const relations = await client.getIssueRelations(issueId);
274
+ const blockingIssues = [];
275
+ for (const relation of relations.inverseRelations) {
276
+ if (relation.type === 'blocks') {
277
+ const blockingIssue = await client.getIssue(relation.issueId);
278
+ const state = await blockingIssue.state;
279
+ const statusName = state?.name ?? 'Unknown';
280
+ if (statusName !== 'Accepted') {
281
+ blockingIssues.push({
282
+ identifier: blockingIssue.identifier,
283
+ title: blockingIssue.title,
284
+ status: statusName,
285
+ });
286
+ }
287
+ }
288
+ }
289
+ return blockingIssues;
290
+ }
291
+ async function listUnblockedBacklogIssues(client, projectName) {
292
+ const projects = await client.linearClient.projects({
293
+ filter: { name: { eqIgnoreCase: projectName } },
294
+ });
295
+ if (projects.nodes.length === 0) {
296
+ throw new Error(`Project not found: ${projectName}`);
297
+ }
298
+ const project = projects.nodes[0];
299
+ const issues = await client.linearClient.issues({
300
+ filter: {
301
+ project: { id: { eq: project.id } },
302
+ state: { name: { eqIgnoreCase: 'Backlog' } },
303
+ },
304
+ });
305
+ const results = [];
306
+ for (const issue of issues.nodes) {
307
+ const blockingIssues = await getBlockingIssues(client, issue.id);
308
+ const state = await issue.state;
309
+ const labels = await issue.labels();
310
+ results.push({
311
+ id: issue.id,
312
+ identifier: issue.identifier,
313
+ title: issue.title,
314
+ description: issue.description,
315
+ url: issue.url,
316
+ priority: issue.priority,
317
+ status: state?.name,
318
+ labels: labels.nodes.map((l) => l.name),
319
+ blocked: blockingIssues.length > 0,
320
+ blockedBy: blockingIssues,
321
+ });
322
+ }
323
+ const unblockedResults = results.filter((r) => !r.blocked);
324
+ unblockedResults.sort((a, b) => {
325
+ const aPriority = a.priority || 5;
326
+ const bPriority = b.priority || 5;
327
+ return aPriority - bPriority;
328
+ });
329
+ return unblockedResults;
330
+ }
331
+ async function checkBlocked(client, issueId) {
332
+ const blockingIssues = await getBlockingIssues(client, issueId);
333
+ return {
334
+ issueId,
335
+ blocked: blockingIssues.length > 0,
336
+ blockedBy: blockingIssues,
337
+ };
338
+ }
339
+ async function listSubIssues(client, issueId) {
340
+ const graph = await client.getSubIssueGraph(issueId);
341
+ return {
342
+ parentId: graph.parentId,
343
+ parentIdentifier: graph.parentIdentifier,
344
+ subIssueCount: graph.subIssues.length,
345
+ subIssues: graph.subIssues.map((node) => ({
346
+ id: node.issue.id,
347
+ identifier: node.issue.identifier,
348
+ title: node.issue.title,
349
+ status: node.issue.status,
350
+ priority: node.issue.priority,
351
+ labels: node.issue.labels,
352
+ url: node.issue.url,
353
+ blockedBy: node.blockedBy,
354
+ blocks: node.blocks,
355
+ })),
356
+ };
357
+ }
358
+ async function listSubIssueStatuses(client, issueId) {
359
+ const statuses = await client.getSubIssueStatuses(issueId);
360
+ return {
361
+ parentIssue: issueId,
362
+ subIssueCount: statuses.length,
363
+ subIssues: statuses,
364
+ allFinishedOrLater: statuses.every((s) => ['Finished', 'Delivered', 'Accepted', 'Canceled'].includes(s.status)),
365
+ incomplete: statuses.filter((s) => !['Finished', 'Delivered', 'Accepted', 'Canceled'].includes(s.status)),
366
+ };
367
+ }
368
+ async function updateSubIssue(client, issueId, options) {
369
+ const issue = await client.getIssue(issueId);
370
+ if (options.state) {
371
+ await client.updateIssueStatus(issue.id, options.state);
372
+ }
373
+ if (options.comment) {
374
+ await client.createComment(issue.id, options.comment);
375
+ }
376
+ const updatedIssue = await client.getIssue(issueId);
377
+ const state = await updatedIssue.state;
378
+ return {
379
+ id: updatedIssue.id,
380
+ identifier: updatedIssue.identifier,
381
+ title: updatedIssue.title,
382
+ status: state?.name,
383
+ url: updatedIssue.url,
384
+ };
385
+ }
386
+ async function checkDeployment(prNumber, format = 'json') {
387
+ const result = await checkPRDeploymentStatus(prNumber);
388
+ if (!result) {
389
+ throw new Error(`Could not get deployment status for PR #${prNumber}. Make sure the PR exists and you have access to it.`);
390
+ }
391
+ if (format === 'markdown') {
392
+ return formatDeploymentStatus(result);
393
+ }
394
+ return result;
395
+ }
396
+ // ── Commands that don't require LINEAR_API_KEY ─────────────────────
397
+ const NO_API_KEY_COMMANDS = new Set(['check-deployment']);
398
+ // ── Main runner ────────────────────────────────────────────────────
399
+ export async function runLinear(config) {
400
+ const { command, args, positionalArgs, apiKey } = config;
401
+ // Lazy client — only created for commands that need it
402
+ let _client = null;
403
+ function client() {
404
+ if (!_client) {
405
+ if (!apiKey) {
406
+ throw new Error('LINEAR_API_KEY environment variable is required');
407
+ }
408
+ _client = createLinearAgentClient({ apiKey });
409
+ }
410
+ return _client;
411
+ }
412
+ // Validate API key for commands that need it
413
+ if (!NO_API_KEY_COMMANDS.has(command) && !apiKey) {
414
+ throw new Error('LINEAR_API_KEY environment variable is required');
415
+ }
416
+ // Helper: get first positional or error
417
+ function requirePositional(name) {
418
+ const val = positionalArgs[0];
419
+ if (!val || val.startsWith('--')) {
420
+ throw new Error(`Missing required argument: <${name}>`);
421
+ }
422
+ return val;
423
+ }
424
+ // Parse sub-options from the original args (for commands that re-parse after positional)
425
+ function subArgs() {
426
+ return args;
427
+ }
428
+ let output;
429
+ switch (command) {
430
+ case 'get-issue': {
431
+ const issueId = requirePositional('issue-id');
432
+ output = await getIssue(client(), issueId);
433
+ break;
434
+ }
435
+ case 'create-issue': {
436
+ if (!args.title || !args.team) {
437
+ throw new Error('Usage: af-linear create-issue --title "Title" --team "Team" [--description "..."] [--project "..."] [--labels "Label1,Label2"] [--state "Backlog"] [--parentId "..."]');
438
+ }
439
+ output = await createIssue(client(), {
440
+ title: args.title,
441
+ team: args.team,
442
+ description: args.description,
443
+ project: args.project,
444
+ labels: args.labels,
445
+ state: args.state,
446
+ parentId: args.parentId,
447
+ });
448
+ break;
449
+ }
450
+ case 'update-issue': {
451
+ const issueId = requirePositional('issue-id');
452
+ const opts = subArgs();
453
+ output = await updateIssue(client(), issueId, {
454
+ title: opts.title,
455
+ description: opts.description,
456
+ state: opts.state,
457
+ labels: opts.labels,
458
+ });
459
+ break;
460
+ }
461
+ case 'list-comments': {
462
+ const issueId = requirePositional('issue-id');
463
+ output = await listComments(client(), issueId);
464
+ break;
465
+ }
466
+ case 'create-comment': {
467
+ const issueId = requirePositional('issue-id');
468
+ if (!args.body) {
469
+ throw new Error('Usage: af-linear create-comment <issue-id> --body "Comment text"');
470
+ }
471
+ output = await createComment(client(), issueId, args.body);
472
+ break;
473
+ }
474
+ case 'list-backlog-issues': {
475
+ if (!args.project) {
476
+ throw new Error('Usage: af-linear list-backlog-issues --project "ProjectName"');
477
+ }
478
+ output = await listBacklogIssues(client(), args.project);
479
+ break;
480
+ }
481
+ case 'list-unblocked-backlog': {
482
+ if (!args.project) {
483
+ throw new Error('Usage: af-linear list-unblocked-backlog --project "ProjectName"');
484
+ }
485
+ output = await listUnblockedBacklogIssues(client(), args.project);
486
+ break;
487
+ }
488
+ case 'check-blocked': {
489
+ const issueId = requirePositional('issue-id');
490
+ output = await checkBlocked(client(), issueId);
491
+ break;
492
+ }
493
+ case 'add-relation': {
494
+ const issueId = positionalArgs[0];
495
+ const relatedIssueId = positionalArgs[1];
496
+ const relationType = args.type;
497
+ if (!issueId ||
498
+ issueId.startsWith('--') ||
499
+ !relatedIssueId ||
500
+ relatedIssueId.startsWith('--') ||
501
+ !relationType ||
502
+ !['related', 'blocks', 'duplicate'].includes(relationType)) {
503
+ throw new Error('Usage: af-linear add-relation <issue-id> <related-issue-id> --type <related|blocks|duplicate>');
504
+ }
505
+ output = await addRelation(client(), issueId, relatedIssueId, relationType);
506
+ break;
507
+ }
508
+ case 'list-relations': {
509
+ const issueId = requirePositional('issue-id');
510
+ output = await listRelations(client(), issueId);
511
+ break;
512
+ }
513
+ case 'remove-relation': {
514
+ const relationId = requirePositional('relation-id');
515
+ output = await removeRelation(client(), relationId);
516
+ break;
517
+ }
518
+ case 'list-sub-issues': {
519
+ const issueId = requirePositional('issue-id');
520
+ output = await listSubIssues(client(), issueId);
521
+ break;
522
+ }
523
+ case 'list-sub-issue-statuses': {
524
+ const issueId = requirePositional('issue-id');
525
+ output = await listSubIssueStatuses(client(), issueId);
526
+ break;
527
+ }
528
+ case 'update-sub-issue': {
529
+ const issueId = requirePositional('issue-id');
530
+ const opts = subArgs();
531
+ output = await updateSubIssue(client(), issueId, {
532
+ state: opts.state,
533
+ comment: opts.comment,
534
+ });
535
+ break;
536
+ }
537
+ case 'check-deployment': {
538
+ const prArg = requirePositional('pr-number');
539
+ const prNumber = parseInt(prArg, 10);
540
+ if (isNaN(prNumber)) {
541
+ throw new Error('PR number must be a valid integer');
542
+ }
543
+ const format = args.format || 'json';
544
+ output = await checkDeployment(prNumber, format);
545
+ break;
546
+ }
547
+ default:
548
+ throw new Error(`Unknown command: ${command}`);
549
+ }
550
+ return { output };
551
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"worker-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/worker-runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAwED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAm4Bf"}
1
+ {"version":3,"file":"worker-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/worker-runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAwED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAw5Bf"}
@@ -54,6 +54,7 @@ export async function runWorker(config, signal) {
54
54
  let shutdownInProgress = false;
55
55
  let consecutiveHeartbeatFailures = 0;
56
56
  let reregistrationInProgress = false;
57
+ let claimFailureCount = 0;
57
58
  const activeOrchestrators = new Map();
58
59
  // Logger — will be re-created after registration with worker context
59
60
  let log = createLogger({}, { showTimestamp: true });
@@ -241,6 +242,10 @@ export async function runWorker(config, signal) {
241
242
  });
242
243
  if (result.data) {
243
244
  consecutiveHeartbeatFailures = 0;
245
+ if (claimFailureCount > 0) {
246
+ log.info('Claim race summary since last heartbeat', { claimFailures: claimFailureCount });
247
+ claimFailureCount = 0;
248
+ }
244
249
  log.debug('Heartbeat acknowledged', {
245
250
  activeCount,
246
251
  pendingWorkCount: result.data.pendingWorkCount,
@@ -608,6 +613,19 @@ export async function runWorker(config, signal) {
608
613
  workerShortId = registration.workerId.substring(0, 8);
609
614
  // Update logger with worker context
610
615
  log = createLogger({ workerId, workerShortId }, { showTimestamp: true });
616
+ // Auto-inherit projects from server if not explicitly configured
617
+ if (!config.projects?.length) {
618
+ try {
619
+ const serverConfig = await apiRequest('/api/config');
620
+ if (serverConfig?.projects?.length) {
621
+ config.projects = serverConfig.projects;
622
+ log.info('Auto-inherited projects from server', { projects: config.projects });
623
+ }
624
+ }
625
+ catch {
626
+ log.debug('Could not fetch server config, using no project filter');
627
+ }
628
+ }
611
629
  // Set up heartbeat
612
630
  heartbeatTimer = setInterval(() => sendHeartbeat(), registration.heartbeatInterval);
613
631
  // Send initial heartbeat
@@ -644,7 +662,8 @@ export async function runWorker(config, signal) {
644
662
  }
645
663
  }
646
664
  else {
647
- log.warn(`Failed to claim work: ${item.issueIdentifier}`);
665
+ claimFailureCount++;
666
+ log.debug(`Failed to claim work: ${item.issueIdentifier}`);
648
667
  }
649
668
  }
650
669
  }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AgentFactory Linear CLI
4
+ *
5
+ * Thin wrapper around the linear runner.
6
+ *
7
+ * Usage:
8
+ * af-linear <command> [options]
9
+ *
10
+ * Commands:
11
+ * get-issue <id> Get issue details
12
+ * create-issue Create a new issue
13
+ * update-issue <id> Update an existing issue
14
+ * list-comments <issueId> List comments on an issue
15
+ * create-comment <issueId> Create a comment on an issue
16
+ * list-backlog-issues List backlog issues for a project
17
+ * list-unblocked-backlog List unblocked backlog issues
18
+ * check-blocked <id> Check if an issue is blocked
19
+ * add-relation <id> <id> Create relation between issues
20
+ * list-relations <id> List relations for an issue
21
+ * remove-relation <id> Remove a relation by ID
22
+ * list-sub-issues <id> List sub-issues of a parent issue
23
+ * list-sub-issue-statuses <id> List sub-issue statuses (lightweight)
24
+ * update-sub-issue <id> Update sub-issue status with comment
25
+ * check-deployment <PR> Check Vercel deployment status for a PR
26
+ *
27
+ * Array Values:
28
+ * --labels accepts comma-separated: --labels "Bug,Feature"
29
+ * For values with commas, use JSON: --labels '["Bug", "UI, UX"]'
30
+ * Text fields (--description, --title, --body) preserve commas.
31
+ *
32
+ * Environment:
33
+ * LINEAR_API_KEY Required API key for authentication
34
+ */
35
+ export {};
36
+ //# sourceMappingURL=linear.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear.d.ts","sourceRoot":"","sources":["../../src/linear.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG"}
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AgentFactory Linear CLI
4
+ *
5
+ * Thin wrapper around the linear runner.
6
+ *
7
+ * Usage:
8
+ * af-linear <command> [options]
9
+ *
10
+ * Commands:
11
+ * get-issue <id> Get issue details
12
+ * create-issue Create a new issue
13
+ * update-issue <id> Update an existing issue
14
+ * list-comments <issueId> List comments on an issue
15
+ * create-comment <issueId> Create a comment on an issue
16
+ * list-backlog-issues List backlog issues for a project
17
+ * list-unblocked-backlog List unblocked backlog issues
18
+ * check-blocked <id> Check if an issue is blocked
19
+ * add-relation <id> <id> Create relation between issues
20
+ * list-relations <id> List relations for an issue
21
+ * remove-relation <id> Remove a relation by ID
22
+ * list-sub-issues <id> List sub-issues of a parent issue
23
+ * list-sub-issue-statuses <id> List sub-issue statuses (lightweight)
24
+ * update-sub-issue <id> Update sub-issue status with comment
25
+ * check-deployment <PR> Check Vercel deployment status for a PR
26
+ *
27
+ * Array Values:
28
+ * --labels accepts comma-separated: --labels "Bug,Feature"
29
+ * For values with commas, use JSON: --labels '["Bug", "UI, UX"]'
30
+ * Text fields (--description, --title, --body) preserve commas.
31
+ *
32
+ * Environment:
33
+ * LINEAR_API_KEY Required API key for authentication
34
+ */
35
+ import path from 'path';
36
+ import { config } from 'dotenv';
37
+ // Load environment variables from .env.local
38
+ config({ path: path.resolve(process.cwd(), '.env.local') });
39
+ import { runLinear, parseLinearArgs } from './lib/linear-runner.js';
40
+ function printHelp() {
41
+ console.log(`
42
+ AgentFactory Linear CLI — Linear issue tracker operations
43
+
44
+ Usage:
45
+ af-linear <command> [options]
46
+
47
+ Commands:
48
+ get-issue <id> Get issue details
49
+ create-issue Create a new issue
50
+ update-issue <id> Update an existing issue
51
+ list-comments <issueId> List comments on an issue
52
+ create-comment <issueId> Create a comment on an issue
53
+ list-backlog-issues List backlog issues for a project
54
+ list-unblocked-backlog List unblocked backlog issues
55
+ check-blocked <id> Check if an issue is blocked
56
+ add-relation <id> <id> Create relation between issues
57
+ list-relations <id> List relations for an issue
58
+ remove-relation <id> Remove a relation by ID
59
+ list-sub-issues <id> List sub-issues of a parent issue
60
+ list-sub-issue-statuses <id> List sub-issue statuses (lightweight)
61
+ update-sub-issue <id> Update sub-issue status with comment
62
+ check-deployment <PR> Check Vercel deployment status for a PR
63
+ help Show this help message
64
+
65
+ Options:
66
+ --help, -h Show this help message
67
+
68
+ Array Values:
69
+ --labels accepts comma-separated: --labels "Bug,Feature"
70
+ For values with commas, use JSON: --labels '["Bug", "UI, UX"]'
71
+
72
+ Environment:
73
+ LINEAR_API_KEY Required API key for authentication
74
+ LINEAR_ACCESS_TOKEN Alternative to LINEAR_API_KEY
75
+
76
+ Examples:
77
+ af-linear get-issue PROJ-123
78
+ af-linear create-issue --title "Add auth" --team "Engineering" --project "Backend"
79
+ af-linear update-issue PROJ-123 --state "Finished"
80
+ af-linear list-backlog-issues --project "MyProject"
81
+ af-linear check-deployment 42
82
+ `);
83
+ }
84
+ async function main() {
85
+ const { command, args, positionalArgs } = parseLinearArgs(process.argv.slice(2));
86
+ if (!command || command === 'help' || args['help'] || args['h']) {
87
+ printHelp();
88
+ return;
89
+ }
90
+ const apiKey = process.env.LINEAR_API_KEY || process.env.LINEAR_ACCESS_TOKEN;
91
+ const result = await runLinear({
92
+ command,
93
+ args,
94
+ positionalArgs,
95
+ apiKey,
96
+ });
97
+ // check-deployment with markdown format outputs a string, not JSON
98
+ if (typeof result.output === 'string') {
99
+ console.log(result.output);
100
+ }
101
+ else {
102
+ console.log(JSON.stringify(result.output, null, 2));
103
+ }
104
+ // Exit with error code if deployment check failed
105
+ if (command === 'check-deployment' &&
106
+ typeof result.output === 'object' &&
107
+ result.output !== null &&
108
+ 'anyFailed' in result.output &&
109
+ result.output.anyFailed) {
110
+ process.exit(1);
111
+ }
112
+ }
113
+ main().catch((error) => {
114
+ console.error('Error:', error instanceof Error ? error.message : error);
115
+ process.exit(1);
116
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supaku/agentfactory-cli",
3
- "version": "0.7.3",
3
+ "version": "0.7.6",
4
4
  "type": "module",
5
5
  "description": "CLI tools for AgentFactory — local orchestrator, remote worker, queue admin",
6
6
  "author": "Supaku (https://supaku.com)",
@@ -33,7 +33,8 @@
33
33
  "af-worker-fleet": "./dist/src/worker-fleet.js",
34
34
  "af-cleanup": "./dist/src/cleanup.js",
35
35
  "af-queue-admin": "./dist/src/queue-admin.js",
36
- "af-analyze-logs": "./dist/src/analyze-logs.js"
36
+ "af-analyze-logs": "./dist/src/analyze-logs.js",
37
+ "af-linear": "./dist/src/linear.js"
37
38
  },
38
39
  "main": "./dist/src/index.js",
39
40
  "module": "./dist/src/index.js",
@@ -73,6 +74,11 @@
73
74
  "types": "./dist/src/lib/analyze-logs-runner.d.ts",
74
75
  "import": "./dist/src/lib/analyze-logs-runner.js",
75
76
  "default": "./dist/src/lib/analyze-logs-runner.js"
77
+ },
78
+ "./linear": {
79
+ "types": "./dist/src/lib/linear-runner.d.ts",
80
+ "import": "./dist/src/lib/linear-runner.js",
81
+ "default": "./dist/src/lib/linear-runner.js"
76
82
  }
77
83
  },
78
84
  "files": [
@@ -82,9 +88,9 @@
82
88
  ],
83
89
  "dependencies": {
84
90
  "dotenv": "^17.2.3",
85
- "@supaku/agentfactory": "0.7.3",
86
- "@supaku/agentfactory-server": "0.7.3",
87
- "@supaku/agentfactory-linear": "0.7.3"
91
+ "@supaku/agentfactory": "0.7.6",
92
+ "@supaku/agentfactory-linear": "0.7.6",
93
+ "@supaku/agentfactory-server": "0.7.6"
88
94
  },
89
95
  "devDependencies": {
90
96
  "@types/node": "^22.5.4",