@corners/cli 0.0.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/dist/cli.js ADDED
@@ -0,0 +1,1284 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { hostname } from "node:os";
3
+ import { basename } from "node:path";
4
+ import { parseArgs } from "node:util";
5
+ import { CornersApiClient as DefaultCornersApiClient, } from "./client.js";
6
+ import { ConfigStore } from "./config.js";
7
+ import { CLIError, getPackageVersion, openUrlInBrowser, printJson, printLine, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamUpdateType, toIsoString, } from "./support.js";
8
+ const LIST_WORKSTREAMS_QUERY = `
9
+ query CliListWorkstreams {
10
+ workstreams(filter: { status: ACTIVE }, first: 100) {
11
+ edges {
12
+ node {
13
+ id
14
+ cornerId
15
+ name
16
+ summary
17
+ category
18
+ status
19
+ updatedAt
20
+ topic {
21
+ id
22
+ name
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ `;
29
+ const WORKSTREAM_LOOKUP_QUERY = `
30
+ query CliWorkstreamLookup($id: ID!) {
31
+ workstream(id: $id) {
32
+ id
33
+ accountId
34
+ cornerId
35
+ name
36
+ summary
37
+ category
38
+ status
39
+ updatedAt
40
+ topic {
41
+ id
42
+ name
43
+ }
44
+ }
45
+ }
46
+ `;
47
+ const WORKSTREAM_PULL_QUERY = `
48
+ query CliWorkstreamPull($id: ID!, $attachmentsFirst: Int!, $feedFirst: Int!) {
49
+ workstream(id: $id) {
50
+ id
51
+ accountId
52
+ cornerId
53
+ name
54
+ summary
55
+ category
56
+ status
57
+ createdAt
58
+ updatedAt
59
+ firstAttachmentActivityAt
60
+ lastAttachmentActivityAt
61
+ topic {
62
+ id
63
+ name
64
+ summary
65
+ }
66
+ topicPath {
67
+ topicId
68
+ name
69
+ }
70
+ openQuestions: questions(status: OPEN) {
71
+ id
72
+ workstreamId
73
+ status
74
+ question
75
+ rationale
76
+ askOf {
77
+ actorId
78
+ externalId
79
+ userId
80
+ }
81
+ evidenceRefs
82
+ suggestedAnswers
83
+ answerText
84
+ answeredAt
85
+ answeredByUserId
86
+ createdAt
87
+ updatedAt
88
+ }
89
+ answeredQuestions: questions(status: ANSWERED) {
90
+ id
91
+ workstreamId
92
+ status
93
+ question
94
+ rationale
95
+ askOf {
96
+ actorId
97
+ externalId
98
+ userId
99
+ }
100
+ evidenceRefs
101
+ suggestedAnswers
102
+ answerText
103
+ answeredAt
104
+ answeredByUserId
105
+ createdAt
106
+ updatedAt
107
+ }
108
+ attachments(first: $attachmentsFirst) {
109
+ totalCount
110
+ edges {
111
+ node {
112
+ __typename
113
+ attachmentId
114
+ entityId
115
+ kind
116
+ title
117
+ summary
118
+ metadata
119
+ createdAt
120
+ updatedAt
121
+ lastActivityAt
122
+ ... on ArtifactThread {
123
+ id
124
+ providerType
125
+ visibility
126
+ topic
127
+ lastMessageAt
128
+ messages {
129
+ id
130
+ senderRef
131
+ content
132
+ contentType
133
+ sentAt
134
+ editedAt
135
+ deletedAt
136
+ }
137
+ }
138
+ ... on WorkstreamDocumentAttachment {
139
+ document {
140
+ id
141
+ title
142
+ content
143
+ updatedAt
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ feed(first: $feedFirst) {
150
+ totalCount
151
+ edges {
152
+ node {
153
+ id
154
+ eventType
155
+ sourceEntityType
156
+ sourceEntityId
157
+ payload
158
+ createdAt
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ `;
165
+ const WORKSTREAM_QUESTION_LIST_QUERY = `
166
+ query CliWorkstreamQuestions($id: ID!) {
167
+ workstream(id: $id) {
168
+ id
169
+ name
170
+ openQuestions: questions(status: OPEN) {
171
+ id
172
+ status
173
+ question
174
+ rationale
175
+ askOf {
176
+ actorId
177
+ externalId
178
+ userId
179
+ }
180
+ suggestedAnswers
181
+ answerText
182
+ answeredAt
183
+ answeredByUserId
184
+ createdAt
185
+ updatedAt
186
+ }
187
+ answeredQuestions: questions(status: ANSWERED) {
188
+ id
189
+ status
190
+ question
191
+ rationale
192
+ askOf {
193
+ actorId
194
+ externalId
195
+ userId
196
+ }
197
+ suggestedAnswers
198
+ answerText
199
+ answeredAt
200
+ answeredByUserId
201
+ createdAt
202
+ updatedAt
203
+ }
204
+ supersededQuestions: questions(status: SUPERSEDED) {
205
+ id
206
+ status
207
+ question
208
+ rationale
209
+ askOf {
210
+ actorId
211
+ externalId
212
+ userId
213
+ }
214
+ suggestedAnswers
215
+ answerText
216
+ answeredAt
217
+ answeredByUserId
218
+ createdAt
219
+ updatedAt
220
+ }
221
+ }
222
+ }
223
+ `;
224
+ const CREATE_WORKSTREAM_QUESTION_MUTATION = `
225
+ mutation CliCreateWorkstreamQuestion($input: CreateWorkstreamQuestionInput!) {
226
+ createWorkstreamQuestion(input: $input) {
227
+ id
228
+ workstreamId
229
+ status
230
+ question
231
+ rationale
232
+ askOf {
233
+ actorId
234
+ externalId
235
+ userId
236
+ }
237
+ suggestedAnswers
238
+ createdAt
239
+ updatedAt
240
+ }
241
+ }
242
+ `;
243
+ const ANSWER_WORKSTREAM_QUESTION_MUTATION = `
244
+ mutation CliAnswerWorkstreamQuestion($questionId: ID!, $answerText: String!) {
245
+ answerWorkstreamQuestion(questionId: $questionId, answerText: $answerText) {
246
+ id
247
+ workstreamId
248
+ status
249
+ question
250
+ answerText
251
+ answeredAt
252
+ answeredByUserId
253
+ updatedAt
254
+ }
255
+ }
256
+ `;
257
+ const ADD_WORKSTREAM_ATTACHMENT_MUTATION = `
258
+ mutation CliAddWorkstreamAttachment(
259
+ $workstreamId: ID!
260
+ $kind: WorkstreamAttachmentKind!
261
+ $entityId: ID!
262
+ ) {
263
+ addWorkstreamAttachment(
264
+ workstreamId: $workstreamId
265
+ kind: $kind
266
+ entityId: $entityId
267
+ ) {
268
+ id
269
+ cornerId
270
+ updatedAt
271
+ }
272
+ }
273
+ `;
274
+ const REPLY_ARTIFACT_THREAD_MUTATION = `
275
+ mutation CliReplyArtifactThread($id: ID!, $input: ReplyArtifactThreadInput!) {
276
+ replyArtifactThread(id: $id, input: $input) {
277
+ ok
278
+ providerType
279
+ threadContainerId
280
+ channelContainerId
281
+ dispatchedExternalId
282
+ }
283
+ }
284
+ `;
285
+ const CREATE_DOCUMENT_MUTATION = `
286
+ mutation CliCreateDocument($input: CreateDocumentInput!) {
287
+ createDocument(input: $input) {
288
+ id
289
+ title
290
+ updatedAt
291
+ }
292
+ }
293
+ `;
294
+ const RECORD_WORKSTREAM_UPDATE_MUTATION = `
295
+ mutation CliRecordWorkstreamUpdate($input: RecordWorkstreamUpdateInput!) {
296
+ recordWorkstreamUpdate(input: $input) {
297
+ id
298
+ eventType
299
+ sourceEntityType
300
+ sourceEntityId
301
+ payload
302
+ createdAt
303
+ }
304
+ }
305
+ `;
306
+ const REVOKE_MCP_SESSION_MUTATION = `
307
+ mutation CliRevokeMcpSession($id: ID!) {
308
+ revokeMCPSession(id: $id)
309
+ }
310
+ `;
311
+ function createRuntime() {
312
+ return {
313
+ config: new ConfigStore(),
314
+ stdout: process.stdout,
315
+ stderr: process.stderr,
316
+ stdin: process.stdin,
317
+ cwd: process.cwd(),
318
+ openUrl: openUrlInBrowser,
319
+ createClient: (input) => new DefaultCornersApiClient(input),
320
+ getVersion: getPackageVersion,
321
+ };
322
+ }
323
+ function printMainHelp(runtime) {
324
+ printLine(runtime.stdout, [
325
+ "corners",
326
+ "",
327
+ "Usage:",
328
+ " corners [--profile <name>] [--api-url <url>] <command> [options]",
329
+ "",
330
+ "Commands:",
331
+ " auth login|logout|status",
332
+ " workstream (ws) list|use|current|pull|push|question|attach|reply-thread",
333
+ " help",
334
+ " version",
335
+ "",
336
+ "Global options:",
337
+ " --help, -h",
338
+ " --version, -v",
339
+ " --json",
340
+ " --profile <name>",
341
+ " --api-url <url>",
342
+ " --no-browser",
343
+ ].join("\n"));
344
+ }
345
+ function printAuthHelp(runtime) {
346
+ printLine(runtime.stdout, [
347
+ "corners auth",
348
+ "",
349
+ "Usage:",
350
+ " corners auth login [--profile <name>] [--api-url <url>] [--no-browser]",
351
+ " corners auth logout [--profile <name>] [--json]",
352
+ " corners auth status [--profile <name>] [--json]",
353
+ ].join("\n"));
354
+ }
355
+ function printWorkstreamHelp(runtime) {
356
+ printLine(runtime.stdout, [
357
+ "corners workstream",
358
+ "",
359
+ "Usage:",
360
+ " corners workstream list [--json]",
361
+ " corners workstream use <workstreamId> [--json]",
362
+ " corners workstream current [--json]",
363
+ " corners workstream pull [workstreamId] [--json]",
364
+ " corners workstream push [workstreamId] [--type <type>] [--message <text>] [--summary <text>] [--file <path>] [--title <title>] [--json]",
365
+ " corners workstream question list [workstreamId] [--json]",
366
+ " corners workstream question ask [workstreamId] [--question <text>] [--rationale <text>] [--suggested-answer <text>]... [--json]",
367
+ " corners workstream question answer <questionId> [--text <text>] [--json]",
368
+ " corners workstream attach [workstreamId] --kind <kind> --entity-id <id> [--json]",
369
+ " corners workstream reply-thread <threadId> [--text <text>] [--json]",
370
+ "",
371
+ "Update types:",
372
+ " status | blocker | learning | outcome",
373
+ ].join("\n"));
374
+ }
375
+ function parseLeadingCommonOptions(args) {
376
+ const common = {
377
+ json: false,
378
+ noBrowser: false,
379
+ };
380
+ let help = false;
381
+ let version = false;
382
+ let index = 0;
383
+ while (index < args.length) {
384
+ const value = args[index];
385
+ if (!value?.startsWith("-")) {
386
+ break;
387
+ }
388
+ switch (value) {
389
+ case "--json":
390
+ common.json = true;
391
+ index += 1;
392
+ break;
393
+ case "--no-browser":
394
+ common.noBrowser = true;
395
+ index += 1;
396
+ break;
397
+ case "--api-url":
398
+ if (!args[index + 1]) {
399
+ throw new CLIError("Missing value for --api-url");
400
+ }
401
+ common.apiUrl = args[index + 1];
402
+ index += 2;
403
+ break;
404
+ case "--profile":
405
+ if (!args[index + 1]) {
406
+ throw new CLIError("Missing value for --profile");
407
+ }
408
+ common.profile = args[index + 1];
409
+ index += 2;
410
+ break;
411
+ case "--help":
412
+ case "-h":
413
+ help = true;
414
+ index += 1;
415
+ break;
416
+ case "--version":
417
+ case "-v":
418
+ version = true;
419
+ index += 1;
420
+ break;
421
+ default:
422
+ return {
423
+ common,
424
+ rest: args.slice(index),
425
+ help,
426
+ version,
427
+ };
428
+ }
429
+ }
430
+ return {
431
+ common,
432
+ rest: args.slice(index),
433
+ help,
434
+ version,
435
+ };
436
+ }
437
+ function mergeCommonOptions(base, values) {
438
+ return {
439
+ json: values.json ?? base.json,
440
+ apiUrl: values.apiUrl ?? base.apiUrl,
441
+ profile: values.profile ?? base.profile,
442
+ noBrowser: values.noBrowser ?? base.noBrowser,
443
+ };
444
+ }
445
+ async function requireStoredProfile(runtime, common) {
446
+ const selected = await runtime.config.getProfile(common.profile ?? null);
447
+ if (!selected) {
448
+ throw new CLIError("Not logged in. Run `corners auth login` first.", {
449
+ json: common.json,
450
+ });
451
+ }
452
+ const profile = {
453
+ ...selected.profile,
454
+ apiUrl: common.apiUrl ?? selected.profile.apiUrl,
455
+ };
456
+ return {
457
+ profileName: selected.name,
458
+ profile,
459
+ client: runtime.createClient({
460
+ apiUrl: profile.apiUrl,
461
+ accessToken: profile.accessToken,
462
+ }),
463
+ };
464
+ }
465
+ async function pollForDeviceToken(input) {
466
+ const deadline = Date.now() + input.device.expires_in * 1000;
467
+ let intervalMs = input.device.interval * 1000;
468
+ while (Date.now() < deadline) {
469
+ const result = await input.client.exchangeDeviceCode(input.device.device_code, input.clientId);
470
+ if (result.access_token) {
471
+ return result;
472
+ }
473
+ switch (result.error) {
474
+ case "authorization_pending":
475
+ await sleep(intervalMs);
476
+ continue;
477
+ case "slow_down":
478
+ intervalMs += 5000;
479
+ await sleep(intervalMs);
480
+ continue;
481
+ case "access_denied":
482
+ throw new CLIError(result.error_description || "Authorization denied");
483
+ case "expired_token":
484
+ throw new CLIError(result.error_description || "Device code expired");
485
+ default:
486
+ throw new CLIError(result.error_description || "Failed to exchange device code");
487
+ }
488
+ }
489
+ printLine(input.stderr, "Device code expired before authorization completed.");
490
+ throw new CLIError("Device code expired");
491
+ }
492
+ async function resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, explicitWorkstreamId) {
493
+ const workstreamId = explicitWorkstreamId ||
494
+ (await resolveBoundWorkstreamId(runtime, profileName));
495
+ const data = await client.graphql(WORKSTREAM_LOOKUP_QUERY, { id: workstreamId });
496
+ if (!data.workstream) {
497
+ throw new CLIError("Workstream not found", { json: common.json });
498
+ }
499
+ return data.workstream;
500
+ }
501
+ async function resolveBoundWorkstreamId(runtime, profileName) {
502
+ const binding = await runtime.config.getBinding(runtime.cwd);
503
+ if (!binding) {
504
+ throw new CLIError("This directory is not bound to a workstream. Run `corners workstream use <workstreamId>` first.");
505
+ }
506
+ if (binding.profile && binding.profile !== profileName) {
507
+ throw new CLIError(`This directory is bound to profile ${binding.profile}. Switch profiles or rebind the directory.`);
508
+ }
509
+ return binding.workstreamId;
510
+ }
511
+ function maybeTakeWorkstreamId(positionals) {
512
+ if (positionals[0]?.startsWith("ws_")) {
513
+ return {
514
+ workstreamId: positionals[0],
515
+ rest: positionals.slice(1),
516
+ };
517
+ }
518
+ return {
519
+ rest: positionals,
520
+ };
521
+ }
522
+ async function handleAuth(args, runtime, inherited) {
523
+ const subcommand = args[0];
524
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
525
+ printAuthHelp(runtime);
526
+ return 0;
527
+ }
528
+ switch (subcommand) {
529
+ case "login": {
530
+ const parsed = parseArgs({
531
+ args: args.slice(1),
532
+ allowPositionals: false,
533
+ options: {
534
+ json: { type: "boolean" },
535
+ help: { type: "boolean", short: "h" },
536
+ profile: { type: "string" },
537
+ "api-url": { type: "string" },
538
+ "no-browser": { type: "boolean" },
539
+ },
540
+ });
541
+ const common = mergeCommonOptions(inherited, {
542
+ json: parsed.values.json,
543
+ profile: parsed.values.profile,
544
+ apiUrl: parsed.values["api-url"],
545
+ noBrowser: parsed.values["no-browser"],
546
+ });
547
+ if (parsed.values.help) {
548
+ printAuthHelp(runtime);
549
+ return 0;
550
+ }
551
+ const existing = await runtime.config.getProfile(common.profile ?? null);
552
+ const profileName = common.profile ?? existing?.name ?? "default";
553
+ const clientId = `Corners CLI (${hostname()})`;
554
+ const client = runtime.createClient({
555
+ apiUrl: common.apiUrl ?? existing?.profile.apiUrl,
556
+ });
557
+ if (!common.json) {
558
+ printLine(runtime.stderr, `Requesting device authorization from ${client.apiUrl}...`);
559
+ }
560
+ const device = await client.createDeviceAuthorization(clientId);
561
+ if (!common.json) {
562
+ printLine(runtime.stderr, `Approve this login at ${device.verification_uri} with code ${device.user_code}`);
563
+ }
564
+ if (!common.noBrowser) {
565
+ const opened = await runtime.openUrl(device.verification_uri_complete);
566
+ if (!common.json && opened) {
567
+ printLine(runtime.stderr, "Opened the verification page in your browser.");
568
+ }
569
+ }
570
+ if (!common.json) {
571
+ printLine(runtime.stderr, "Waiting for device approval...");
572
+ }
573
+ const token = await pollForDeviceToken({
574
+ client,
575
+ device,
576
+ clientId,
577
+ stderr: runtime.stderr,
578
+ });
579
+ if (!token.access_token) {
580
+ throw new CLIError("Login completed without an access token");
581
+ }
582
+ const profile = {
583
+ apiUrl: client.apiUrl,
584
+ accessToken: token.access_token,
585
+ sessionId: token.session_id ?? null,
586
+ workspace: token.workspace ?? null,
587
+ accountId: token.account_id ?? null,
588
+ userId: token.user_id ?? null,
589
+ createdAt: toIsoString(),
590
+ };
591
+ await runtime.config.saveProfile(profileName, profile, {
592
+ setActive: true,
593
+ });
594
+ if (common.json) {
595
+ printJson(runtime.stdout, {
596
+ ok: true,
597
+ profile: profileName,
598
+ apiUrl: profile.apiUrl,
599
+ sessionId: profile.sessionId,
600
+ workspace: profile.workspace,
601
+ accountId: profile.accountId,
602
+ userId: profile.userId,
603
+ });
604
+ }
605
+ else {
606
+ printLine(runtime.stdout, `Logged in as profile ${profileName}${profile.workspace ? ` (${profile.workspace})` : ""}.`);
607
+ }
608
+ return 0;
609
+ }
610
+ case "logout": {
611
+ const parsed = parseArgs({
612
+ args: args.slice(1),
613
+ allowPositionals: false,
614
+ options: {
615
+ json: { type: "boolean" },
616
+ help: { type: "boolean", short: "h" },
617
+ profile: { type: "string" },
618
+ "api-url": { type: "string" },
619
+ },
620
+ });
621
+ const common = mergeCommonOptions(inherited, {
622
+ json: parsed.values.json,
623
+ profile: parsed.values.profile,
624
+ apiUrl: parsed.values["api-url"],
625
+ });
626
+ if (parsed.values.help) {
627
+ printAuthHelp(runtime);
628
+ return 0;
629
+ }
630
+ const selected = await runtime.config.getProfile(common.profile ?? null);
631
+ if (!selected) {
632
+ if (common.json) {
633
+ printJson(runtime.stdout, {
634
+ ok: true,
635
+ revoked: false,
636
+ loggedIn: false,
637
+ });
638
+ }
639
+ else {
640
+ printLine(runtime.stdout, "No stored profile is logged in.");
641
+ }
642
+ return 0;
643
+ }
644
+ let revoked = false;
645
+ if (selected.profile.sessionId && selected.profile.accessToken) {
646
+ try {
647
+ const client = runtime.createClient({
648
+ apiUrl: common.apiUrl ?? selected.profile.apiUrl,
649
+ accessToken: selected.profile.accessToken,
650
+ });
651
+ const data = await client.graphql(REVOKE_MCP_SESSION_MUTATION, {
652
+ id: selected.profile.sessionId,
653
+ });
654
+ revoked = data.revokeMCPSession;
655
+ }
656
+ catch (error) {
657
+ if (!common.json) {
658
+ printLine(runtime.stderr, `Warning: failed to revoke the remote session (${error.message}). Clearing local credentials anyway.`);
659
+ }
660
+ }
661
+ }
662
+ await runtime.config.removeProfile(selected.name);
663
+ await runtime.config.clearBindingsForProfile(selected.name);
664
+ if (common.json) {
665
+ printJson(runtime.stdout, {
666
+ ok: true,
667
+ revoked,
668
+ profile: selected.name,
669
+ });
670
+ }
671
+ else {
672
+ printLine(runtime.stdout, `Logged out profile ${selected.name}.`);
673
+ }
674
+ return 0;
675
+ }
676
+ case "status": {
677
+ const parsed = parseArgs({
678
+ args: args.slice(1),
679
+ allowPositionals: false,
680
+ options: {
681
+ json: { type: "boolean" },
682
+ help: { type: "boolean", short: "h" },
683
+ profile: { type: "string" },
684
+ "api-url": { type: "string" },
685
+ },
686
+ });
687
+ const common = mergeCommonOptions(inherited, {
688
+ json: parsed.values.json,
689
+ profile: parsed.values.profile,
690
+ apiUrl: parsed.values["api-url"],
691
+ });
692
+ if (parsed.values.help) {
693
+ printAuthHelp(runtime);
694
+ return 0;
695
+ }
696
+ const selected = await runtime.config.getProfile(common.profile ?? null);
697
+ if (!selected) {
698
+ const status = {
699
+ loggedIn: false,
700
+ profile: common.profile ?? null,
701
+ };
702
+ if (common.json) {
703
+ printJson(runtime.stdout, status);
704
+ }
705
+ else {
706
+ printLine(runtime.stdout, "Not logged in.");
707
+ }
708
+ return 0;
709
+ }
710
+ const client = runtime.createClient({
711
+ apiUrl: common.apiUrl ?? selected.profile.apiUrl,
712
+ accessToken: selected.profile.accessToken,
713
+ });
714
+ let remote;
715
+ try {
716
+ remote = await client.validateSession();
717
+ }
718
+ catch (error) {
719
+ remote = {
720
+ valid: false,
721
+ error: error.message,
722
+ };
723
+ }
724
+ const status = {
725
+ loggedIn: true,
726
+ profile: selected.name,
727
+ apiUrl: common.apiUrl ?? selected.profile.apiUrl,
728
+ sessionId: selected.profile.sessionId,
729
+ workspace: selected.profile.workspace,
730
+ accountId: selected.profile.accountId,
731
+ userId: selected.profile.userId,
732
+ remoteValid: remote.valid,
733
+ remoteError: remote.valid ? null : (remote.error ?? null),
734
+ };
735
+ if (common.json) {
736
+ printJson(runtime.stdout, status);
737
+ }
738
+ else {
739
+ printLine(runtime.stdout, [
740
+ `Profile: ${status.profile}`,
741
+ `API URL: ${status.apiUrl}`,
742
+ `Workspace: ${status.workspace ?? "-"}`,
743
+ `Account: ${status.accountId ?? "-"}`,
744
+ `User: ${status.userId ?? "-"}`,
745
+ `Session: ${status.sessionId ?? "-"}`,
746
+ `Remote session: ${status.remoteValid ? "valid" : `invalid (${status.remoteError ?? "unknown error"})`}`,
747
+ ].join("\n"));
748
+ }
749
+ return 0;
750
+ }
751
+ default:
752
+ throw new CLIError(`Unknown auth command: ${subcommand}`);
753
+ }
754
+ }
755
+ async function handleWorkstream(args, runtime, inherited) {
756
+ const subcommand = args[0];
757
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
758
+ printWorkstreamHelp(runtime);
759
+ return 0;
760
+ }
761
+ switch (subcommand) {
762
+ case "list": {
763
+ const parsed = parseArgs({
764
+ args: args.slice(1),
765
+ allowPositionals: false,
766
+ options: {
767
+ json: { type: "boolean" },
768
+ help: { type: "boolean", short: "h" },
769
+ profile: { type: "string" },
770
+ "api-url": { type: "string" },
771
+ },
772
+ });
773
+ const common = mergeCommonOptions(inherited, {
774
+ json: parsed.values.json,
775
+ profile: parsed.values.profile,
776
+ apiUrl: parsed.values["api-url"],
777
+ });
778
+ if (parsed.values.help) {
779
+ printWorkstreamHelp(runtime);
780
+ return 0;
781
+ }
782
+ const { client } = await requireStoredProfile(runtime, common);
783
+ const data = await client.graphql(LIST_WORKSTREAMS_QUERY);
784
+ const workstreams = data.workstreams.edges.map((edge) => edge.node);
785
+ if (common.json) {
786
+ printJson(runtime.stdout, workstreams);
787
+ }
788
+ else {
789
+ printLine(runtime.stdout, workstreams
790
+ .map((workstream) => `${workstream.id} ${workstream.name} ${workstream.status.toLowerCase()}${workstream.summary ? ` ${workstream.summary}` : ""}`)
791
+ .join("\n"));
792
+ }
793
+ return 0;
794
+ }
795
+ case "use": {
796
+ const parsed = parseArgs({
797
+ args: args.slice(1),
798
+ allowPositionals: true,
799
+ options: {
800
+ json: { type: "boolean" },
801
+ help: { type: "boolean", short: "h" },
802
+ profile: { type: "string" },
803
+ "api-url": { type: "string" },
804
+ },
805
+ });
806
+ const common = mergeCommonOptions(inherited, {
807
+ json: parsed.values.json,
808
+ profile: parsed.values.profile,
809
+ apiUrl: parsed.values["api-url"],
810
+ });
811
+ if (parsed.values.help) {
812
+ printWorkstreamHelp(runtime);
813
+ return 0;
814
+ }
815
+ const workstreamId = parsed.positionals[0];
816
+ if (!workstreamId) {
817
+ throw new CLIError("Usage: corners workstream use <workstreamId>", {
818
+ json: common.json,
819
+ });
820
+ }
821
+ const { profileName, profile, client } = await requireStoredProfile(runtime, common);
822
+ const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
823
+ await runtime.config.setBinding(runtime.cwd, {
824
+ profile: profileName,
825
+ workspace: profile.workspace ?? "",
826
+ workstreamId: workstream.id,
827
+ cornerId: workstream.cornerId,
828
+ boundAt: toIsoString(),
829
+ });
830
+ if (common.json) {
831
+ printJson(runtime.stdout, {
832
+ ok: true,
833
+ cwd: runtime.cwd,
834
+ profile: profileName,
835
+ workspace: profile.workspace,
836
+ workstream,
837
+ });
838
+ }
839
+ else {
840
+ printLine(runtime.stdout, `Bound ${runtime.cwd} to ${workstream.id} (${workstream.name}).`);
841
+ }
842
+ return 0;
843
+ }
844
+ case "current": {
845
+ const parsed = parseArgs({
846
+ args: args.slice(1),
847
+ allowPositionals: false,
848
+ options: {
849
+ json: { type: "boolean" },
850
+ help: { type: "boolean", short: "h" },
851
+ },
852
+ });
853
+ const common = mergeCommonOptions(inherited, {
854
+ json: parsed.values.json,
855
+ });
856
+ if (parsed.values.help) {
857
+ printWorkstreamHelp(runtime);
858
+ return 0;
859
+ }
860
+ const binding = await runtime.config.getBinding(runtime.cwd);
861
+ if (!binding) {
862
+ if (common.json) {
863
+ printJson(runtime.stdout, { cwd: runtime.cwd, binding: null });
864
+ }
865
+ else {
866
+ printLine(runtime.stdout, "No workstream is bound to this directory.");
867
+ }
868
+ return 0;
869
+ }
870
+ if (common.json) {
871
+ printJson(runtime.stdout, {
872
+ cwd: runtime.cwd,
873
+ binding,
874
+ });
875
+ }
876
+ else {
877
+ printLine(runtime.stdout, [
878
+ `Directory: ${runtime.cwd}`,
879
+ `Profile: ${binding.profile ?? "-"}`,
880
+ `Workspace: ${binding.workspace || "-"}`,
881
+ `Workstream: ${binding.workstreamId}`,
882
+ `Corner: ${binding.cornerId}`,
883
+ ].join("\n"));
884
+ }
885
+ return 0;
886
+ }
887
+ case "pull": {
888
+ const parsed = parseArgs({
889
+ args: args.slice(1),
890
+ allowPositionals: true,
891
+ options: {
892
+ json: { type: "boolean" },
893
+ help: { type: "boolean", short: "h" },
894
+ profile: { type: "string" },
895
+ "api-url": { type: "string" },
896
+ },
897
+ });
898
+ const common = mergeCommonOptions(inherited, {
899
+ json: parsed.values.json,
900
+ profile: parsed.values.profile,
901
+ apiUrl: parsed.values["api-url"],
902
+ });
903
+ if (parsed.values.help) {
904
+ printWorkstreamHelp(runtime);
905
+ return 0;
906
+ }
907
+ const { workstreamId } = maybeTakeWorkstreamId(parsed.positionals);
908
+ const { profileName, client } = await requireStoredProfile(runtime, common);
909
+ const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
910
+ const data = await client.graphql(WORKSTREAM_PULL_QUERY, {
911
+ id: workstream.id,
912
+ attachmentsFirst: 50,
913
+ feedFirst: 20,
914
+ });
915
+ if (common.json) {
916
+ printJson(runtime.stdout, data.workstream);
917
+ }
918
+ else {
919
+ const snapshot = data.workstream;
920
+ printLine(runtime.stdout, [
921
+ `${snapshot.name} (${snapshot.id})`,
922
+ snapshot.summary ? `Summary: ${snapshot.summary}` : "Summary: -",
923
+ `Open questions: ${snapshot.openQuestions?.length ?? 0}`,
924
+ `Answered questions: ${snapshot.answeredQuestions?.length ?? 0}`,
925
+ `Attachments: ${snapshot.attachments?.totalCount ?? 0}`,
926
+ `Feed items returned: ${snapshot.feed?.totalCount ?? 0}`,
927
+ ].join("\n"));
928
+ }
929
+ return 0;
930
+ }
931
+ case "push": {
932
+ const parsed = parseArgs({
933
+ args: args.slice(1),
934
+ allowPositionals: true,
935
+ options: {
936
+ json: { type: "boolean" },
937
+ help: { type: "boolean", short: "h" },
938
+ profile: { type: "string" },
939
+ "api-url": { type: "string" },
940
+ type: { type: "string" },
941
+ message: { type: "string" },
942
+ summary: { type: "string" },
943
+ file: { type: "string" },
944
+ title: { type: "string" },
945
+ },
946
+ });
947
+ const common = mergeCommonOptions(inherited, {
948
+ json: parsed.values.json,
949
+ profile: parsed.values.profile,
950
+ apiUrl: parsed.values["api-url"],
951
+ });
952
+ if (parsed.values.help) {
953
+ printWorkstreamHelp(runtime);
954
+ return 0;
955
+ }
956
+ const { workstreamId, rest } = maybeTakeWorkstreamId(parsed.positionals);
957
+ const message = parsed.values.message ??
958
+ (rest.length > 0
959
+ ? rest.join(" ")
960
+ : await readTextFromStdin(runtime.stdin));
961
+ if (!message) {
962
+ throw new CLIError("Workstream updates need a message. Use --message or pipe text on stdin.", { json: common.json });
963
+ }
964
+ const { profileName, client } = await requireStoredProfile(runtime, common);
965
+ const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
966
+ let createdDocument = null;
967
+ if (parsed.values.file) {
968
+ const documentContent = await readFile(parsed.values.file, "utf8");
969
+ const title = parsed.values.title ?? basename(parsed.values.file);
970
+ const documentResult = await client.graphql(CREATE_DOCUMENT_MUTATION, {
971
+ input: {
972
+ title,
973
+ content: documentContent,
974
+ privacy: "ACCOUNT",
975
+ },
976
+ });
977
+ createdDocument = documentResult.createDocument;
978
+ }
979
+ const updateResult = await client.graphql(RECORD_WORKSTREAM_UPDATE_MUTATION, {
980
+ input: {
981
+ workstreamId: workstream.id,
982
+ updateType: toGraphQLWorkstreamUpdateType(parsed.values.type ?? "status"),
983
+ content: message,
984
+ summary: parsed.values.summary,
985
+ documentId: createdDocument?.id ?? null,
986
+ },
987
+ });
988
+ const payload = {
989
+ ok: true,
990
+ workstreamId: workstream.id,
991
+ update: updateResult.recordWorkstreamUpdate,
992
+ document: createdDocument,
993
+ };
994
+ if (common.json) {
995
+ printJson(runtime.stdout, payload);
996
+ }
997
+ else {
998
+ printLine(runtime.stdout, `Recorded ${String(parsed.values.type ?? "status")} update on ${workstream.id}.`);
999
+ }
1000
+ return 0;
1001
+ }
1002
+ case "question": {
1003
+ const nested = args[1];
1004
+ if (!nested || nested === "help" || nested === "--help") {
1005
+ printWorkstreamHelp(runtime);
1006
+ return 0;
1007
+ }
1008
+ switch (nested) {
1009
+ case "list": {
1010
+ const parsed = parseArgs({
1011
+ args: args.slice(2),
1012
+ allowPositionals: true,
1013
+ options: {
1014
+ json: { type: "boolean" },
1015
+ help: { type: "boolean", short: "h" },
1016
+ profile: { type: "string" },
1017
+ "api-url": { type: "string" },
1018
+ },
1019
+ });
1020
+ const common = mergeCommonOptions(inherited, {
1021
+ json: parsed.values.json,
1022
+ profile: parsed.values.profile,
1023
+ apiUrl: parsed.values["api-url"],
1024
+ });
1025
+ if (parsed.values.help) {
1026
+ printWorkstreamHelp(runtime);
1027
+ return 0;
1028
+ }
1029
+ const { workstreamId } = maybeTakeWorkstreamId(parsed.positionals);
1030
+ const { profileName, client } = await requireStoredProfile(runtime, common);
1031
+ const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
1032
+ const data = await client.graphql(WORKSTREAM_QUESTION_LIST_QUERY, {
1033
+ id: workstream.id,
1034
+ });
1035
+ if (!data.workstream) {
1036
+ throw new CLIError("Workstream not found", { json: common.json });
1037
+ }
1038
+ if (common.json) {
1039
+ printJson(runtime.stdout, data.workstream);
1040
+ }
1041
+ else {
1042
+ printLine(runtime.stdout, [
1043
+ `${data.workstream.name} (${data.workstream.id})`,
1044
+ `Open: ${data.workstream.openQuestions.length}`,
1045
+ `Answered: ${data.workstream.answeredQuestions.length}`,
1046
+ `Superseded: ${data.workstream.supersededQuestions.length}`,
1047
+ ].join("\n"));
1048
+ }
1049
+ return 0;
1050
+ }
1051
+ case "ask": {
1052
+ const parsed = parseArgs({
1053
+ args: args.slice(2),
1054
+ allowPositionals: true,
1055
+ options: {
1056
+ json: { type: "boolean" },
1057
+ help: { type: "boolean", short: "h" },
1058
+ profile: { type: "string" },
1059
+ "api-url": { type: "string" },
1060
+ question: { type: "string" },
1061
+ rationale: { type: "string" },
1062
+ "suggested-answer": { type: "string", multiple: true },
1063
+ },
1064
+ });
1065
+ const common = mergeCommonOptions(inherited, {
1066
+ json: parsed.values.json,
1067
+ profile: parsed.values.profile,
1068
+ apiUrl: parsed.values["api-url"],
1069
+ });
1070
+ if (parsed.values.help) {
1071
+ printWorkstreamHelp(runtime);
1072
+ return 0;
1073
+ }
1074
+ const { workstreamId, rest } = maybeTakeWorkstreamId(parsed.positionals);
1075
+ const question = parsed.values.question ??
1076
+ (rest.length > 0
1077
+ ? rest.join(" ")
1078
+ : await readTextFromStdin(runtime.stdin));
1079
+ if (!question) {
1080
+ throw new CLIError("Question text is required. Use --question or pipe text on stdin.", { json: common.json });
1081
+ }
1082
+ const { profileName, client } = await requireStoredProfile(runtime, common);
1083
+ const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
1084
+ const result = await client.graphql(CREATE_WORKSTREAM_QUESTION_MUTATION, {
1085
+ input: {
1086
+ workstreamId: workstream.id,
1087
+ question,
1088
+ rationale: parsed.values.rationale,
1089
+ suggestedAnswers: parsed.values["suggested-answer"] ?? [],
1090
+ },
1091
+ });
1092
+ if (common.json) {
1093
+ printJson(runtime.stdout, result.createWorkstreamQuestion);
1094
+ }
1095
+ else {
1096
+ printLine(runtime.stdout, `Created question on ${workstream.id}.`);
1097
+ }
1098
+ return 0;
1099
+ }
1100
+ case "answer": {
1101
+ const parsed = parseArgs({
1102
+ args: args.slice(2),
1103
+ allowPositionals: true,
1104
+ options: {
1105
+ json: { type: "boolean" },
1106
+ help: { type: "boolean", short: "h" },
1107
+ profile: { type: "string" },
1108
+ "api-url": { type: "string" },
1109
+ text: { type: "string" },
1110
+ },
1111
+ });
1112
+ const common = mergeCommonOptions(inherited, {
1113
+ json: parsed.values.json,
1114
+ profile: parsed.values.profile,
1115
+ apiUrl: parsed.values["api-url"],
1116
+ });
1117
+ if (parsed.values.help) {
1118
+ printWorkstreamHelp(runtime);
1119
+ return 0;
1120
+ }
1121
+ const questionId = parsed.positionals[0];
1122
+ const answer = parsed.values.text ??
1123
+ (parsed.positionals.length > 1
1124
+ ? parsed.positionals.slice(1).join(" ")
1125
+ : await readTextFromStdin(runtime.stdin));
1126
+ if (!questionId) {
1127
+ throw new CLIError("Usage: corners workstream question answer <questionId> [--text <text>]", { json: common.json });
1128
+ }
1129
+ if (!answer) {
1130
+ throw new CLIError("Answer text is required.", {
1131
+ json: common.json,
1132
+ });
1133
+ }
1134
+ const { client } = await requireStoredProfile(runtime, common);
1135
+ const result = await client.graphql(ANSWER_WORKSTREAM_QUESTION_MUTATION, {
1136
+ questionId,
1137
+ answerText: answer,
1138
+ });
1139
+ if (common.json) {
1140
+ printJson(runtime.stdout, result.answerWorkstreamQuestion);
1141
+ }
1142
+ else {
1143
+ printLine(runtime.stdout, `Answered ${questionId}.`);
1144
+ }
1145
+ return 0;
1146
+ }
1147
+ default:
1148
+ throw new CLIError(`Unknown workstream question command: ${nested}`);
1149
+ }
1150
+ }
1151
+ case "attach": {
1152
+ const parsed = parseArgs({
1153
+ args: args.slice(1),
1154
+ allowPositionals: true,
1155
+ options: {
1156
+ json: { type: "boolean" },
1157
+ help: { type: "boolean", short: "h" },
1158
+ profile: { type: "string" },
1159
+ "api-url": { type: "string" },
1160
+ kind: { type: "string" },
1161
+ "entity-id": { type: "string" },
1162
+ },
1163
+ });
1164
+ const common = mergeCommonOptions(inherited, {
1165
+ json: parsed.values.json,
1166
+ profile: parsed.values.profile,
1167
+ apiUrl: parsed.values["api-url"],
1168
+ });
1169
+ if (parsed.values.help) {
1170
+ printWorkstreamHelp(runtime);
1171
+ return 0;
1172
+ }
1173
+ const { workstreamId } = maybeTakeWorkstreamId(parsed.positionals);
1174
+ const kind = parsed.values.kind;
1175
+ const entityId = parsed.values["entity-id"];
1176
+ if (!kind || !entityId) {
1177
+ throw new CLIError("Usage: corners workstream attach [workstreamId] --kind <kind> --entity-id <id>", { json: common.json });
1178
+ }
1179
+ const { profileName, client } = await requireStoredProfile(runtime, common);
1180
+ const workstream = await resolveExplicitOrBoundWorkstream(runtime, common, client, profileName, workstreamId);
1181
+ const result = await client.graphql(ADD_WORKSTREAM_ATTACHMENT_MUTATION, {
1182
+ workstreamId: workstream.id,
1183
+ kind: toGraphQLAttachmentKind(kind),
1184
+ entityId,
1185
+ });
1186
+ if (common.json) {
1187
+ printJson(runtime.stdout, result.addWorkstreamAttachment);
1188
+ }
1189
+ else {
1190
+ printLine(runtime.stdout, `Attached ${kind}:${entityId} to ${workstream.id}.`);
1191
+ }
1192
+ return 0;
1193
+ }
1194
+ case "reply-thread": {
1195
+ const parsed = parseArgs({
1196
+ args: args.slice(1),
1197
+ allowPositionals: true,
1198
+ options: {
1199
+ json: { type: "boolean" },
1200
+ help: { type: "boolean", short: "h" },
1201
+ profile: { type: "string" },
1202
+ "api-url": { type: "string" },
1203
+ text: { type: "string" },
1204
+ },
1205
+ });
1206
+ const common = mergeCommonOptions(inherited, {
1207
+ json: parsed.values.json,
1208
+ profile: parsed.values.profile,
1209
+ apiUrl: parsed.values["api-url"],
1210
+ });
1211
+ if (parsed.values.help) {
1212
+ printWorkstreamHelp(runtime);
1213
+ return 0;
1214
+ }
1215
+ const threadId = parsed.positionals[0];
1216
+ const text = parsed.values.text ??
1217
+ (parsed.positionals.length > 1
1218
+ ? parsed.positionals.slice(1).join(" ")
1219
+ : await readTextFromStdin(runtime.stdin));
1220
+ if (!threadId || !text) {
1221
+ throw new CLIError("Usage: corners workstream reply-thread <threadId> [--text <text>]", { json: common.json });
1222
+ }
1223
+ const { client } = await requireStoredProfile(runtime, common);
1224
+ const result = await client.graphql(REPLY_ARTIFACT_THREAD_MUTATION, {
1225
+ id: threadId,
1226
+ input: {
1227
+ text,
1228
+ },
1229
+ });
1230
+ if (common.json) {
1231
+ printJson(runtime.stdout, result.replyArtifactThread);
1232
+ }
1233
+ else {
1234
+ printLine(runtime.stdout, `Replied to ${threadId}.`);
1235
+ }
1236
+ return 0;
1237
+ }
1238
+ default:
1239
+ throw new CLIError(`Unknown workstream command: ${subcommand}`);
1240
+ }
1241
+ }
1242
+ export async function runCli(argv, runtime = createRuntime()) {
1243
+ const normalizedArgv = argv[0] === "--" ? argv.slice(1) : argv;
1244
+ const parsed = parseLeadingCommonOptions(normalizedArgv);
1245
+ if (parsed.version) {
1246
+ printLine(runtime.stdout, runtime.getVersion());
1247
+ return 0;
1248
+ }
1249
+ const command = parsed.rest[0];
1250
+ if (!command) {
1251
+ printMainHelp(runtime);
1252
+ return 0;
1253
+ }
1254
+ if (parsed.help && !command) {
1255
+ printMainHelp(runtime);
1256
+ return 0;
1257
+ }
1258
+ switch (command) {
1259
+ case "help":
1260
+ if (parsed.rest[1] === "auth") {
1261
+ printAuthHelp(runtime);
1262
+ }
1263
+ else if (parsed.rest[1] === "workstream" || parsed.rest[1] === "ws") {
1264
+ printWorkstreamHelp(runtime);
1265
+ }
1266
+ else {
1267
+ printMainHelp(runtime);
1268
+ }
1269
+ return 0;
1270
+ case "version":
1271
+ printLine(runtime.stdout, runtime.getVersion());
1272
+ return 0;
1273
+ case "auth":
1274
+ return handleAuth(parsed.rest.slice(1), runtime, parsed.common);
1275
+ case "workstream":
1276
+ case "ws":
1277
+ return handleWorkstream(parsed.rest.slice(1), runtime, parsed.common);
1278
+ default:
1279
+ throw new CLIError(`Unknown command: ${command}`, {
1280
+ json: parsed.common.json,
1281
+ });
1282
+ }
1283
+ }
1284
+ //# sourceMappingURL=cli.js.map