@corners/cli 0.0.5 → 0.0.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.
package/dist/cli.js CHANGED
@@ -1,12 +1,38 @@
1
- import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
1
+ import { readFile, realpath } from "node:fs/promises";
2
2
  import { hostname } from "node:os";
3
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
3
+ import { basename, join, relative, resolve, sep } from "node:path";
4
4
  import { parseArgs } from "node:util";
5
5
  import { CornersApiClient as DefaultCornersApiClient, } from "./client.js";
6
6
  import { ConfigStore, LOCAL_ROOT_CONFIG_RELATIVE_PATH, } from "./config.js";
7
7
  import { buildExpectedManagedGuidanceBlock, buildGuidanceFileContent, buildGuidanceRevision, extractManagedGuidanceBlock, GUIDANCE_SYNC_COMMAND, GUIDANCE_TARGETS, normalizeGuidanceTargets, } from "./guidance.js";
8
8
  import { createPromptApi } from "./prompts.js";
9
- import { CLIError, getPackageVersion, normalizeApiUrl, openUrlInBrowser, printJson, printLine, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamStatus, toGraphQLWorkstreamUpdateType, toIsoString, } from "./support.js";
9
+ import { BUNDLED_SKILLS, buildInstalledSkillReferences, buildSkillPlannedEdits, getBundledSkillPath, parseSkillFrontmatter, resolveSkillsState, syncSkillsIfNeeded, withUpdatedSkillsConfig, } from "./skills.js";
10
+ import { buildPlannedEdit, CLIError, getPackageVersion, normalizeApiUrl, openUrlInBrowser, printJson, printLine, readOptionalUtf8, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamStatus, toGraphQLWorkstreamUpdateType, toIsoString, writePlannedEdits, } from "./support.js";
11
+ const WORKSTREAM_LIST_QUERY = `
12
+ query CliWorkstreamList($first: Int!, $filter: WorkstreamsFilter) {
13
+ workstreams(first: $first, filter: $filter) {
14
+ edges {
15
+ node {
16
+ id
17
+ accountId
18
+ cornerId
19
+ name
20
+ isOpen
21
+ summary
22
+ category
23
+ status
24
+ statusDetails
25
+ updatedAt
26
+ topic {
27
+ id
28
+ name
29
+ }
30
+ }
31
+ }
32
+ totalCount
33
+ }
34
+ }
35
+ `;
10
36
  const WORKSTREAM_LOOKUP_QUERY = `
11
37
  query CliWorkstreamLookup($id: ID!) {
12
38
  workstream(id: $id) {
@@ -128,6 +154,24 @@ const WORKSTREAM_PULL_QUERY = `
128
154
  updatedAt
129
155
  }
130
156
  }
157
+ ... on WorkstreamTodoListAttachment {
158
+ todoList {
159
+ id
160
+ title
161
+ openItemCount
162
+ totalItemCount
163
+ items {
164
+ id
165
+ title
166
+ completed
167
+ itemIndex
168
+ assignee {
169
+ id
170
+ handle
171
+ }
172
+ }
173
+ }
174
+ }
131
175
  }
132
176
  }
133
177
  }
@@ -308,6 +352,71 @@ const UPDATE_WORKSTREAM_MUTATION = `
308
352
  }
309
353
  }
310
354
  `;
355
+ const WORKSTREAM_TASK_LIST_QUERY = `
356
+ query CliWorkstreamTasks($id: ID!, $attachmentsFirst: Int!) {
357
+ workstream(id: $id) {
358
+ id
359
+ name
360
+ attachments(first: $attachmentsFirst, kind: TODOLIST) {
361
+ edges {
362
+ node {
363
+ __typename
364
+ ... on WorkstreamTodoListAttachment {
365
+ todoList {
366
+ id
367
+ title
368
+ openItemCount
369
+ totalItemCount
370
+ items {
371
+ id
372
+ title
373
+ description
374
+ completed
375
+ completedAt
376
+ itemIndex
377
+ assignee {
378
+ id
379
+ handle
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }
388
+ }
389
+ `;
390
+ const CREATE_TODO_LIST_MUTATION = `
391
+ mutation CliCreateTodoList($input: CreateTodoListInput!) {
392
+ createTodoList(input: $input) {
393
+ id
394
+ title
395
+ openItemCount
396
+ totalItemCount
397
+ }
398
+ }
399
+ `;
400
+ const ADD_TODO_ITEM_MUTATION = `
401
+ mutation CliAddTodoItem($input: AddTodoItemInput!) {
402
+ addTodoItem(input: $input) {
403
+ id
404
+ title
405
+ completed
406
+ itemIndex
407
+ }
408
+ }
409
+ `;
410
+ const TOGGLE_TODO_ITEM_MUTATION = `
411
+ mutation CliToggleTodoItem($id: ID!, $completed: Boolean!) {
412
+ toggleTodoItem(id: $id, completed: $completed) {
413
+ id
414
+ title
415
+ completed
416
+ completedAt
417
+ }
418
+ }
419
+ `;
311
420
  const REVOKE_MCP_SESSION_MUTATION = `
312
421
  mutation CliRevokeMcpSession($id: ID!) {
313
422
  revokeMCPSession(id: $id)
@@ -379,6 +488,7 @@ function printMainHelp(runtime) {
379
488
  "Commands:",
380
489
  " auth login|logout|status",
381
490
  " guidance status|sync",
491
+ " skills list|status",
382
492
  " init",
383
493
  " corner use",
384
494
  " whoami",
@@ -403,6 +513,17 @@ function printInitHelp(runtime) {
403
513
  " corners init [--profile <name>] [--api-url <url>] [--json]",
404
514
  ].join("\n"));
405
515
  }
516
+ function printSkillsHelp(runtime) {
517
+ printLine(runtime.stdout, [
518
+ "corners skills",
519
+ "",
520
+ "Usage:",
521
+ " corners skills list [--json] List bundled skills",
522
+ " corners skills status [--json] Show installed skill state",
523
+ "",
524
+ "Skills are auto-synced on every CLI run once enabled via `corners init`.",
525
+ ].join("\n"));
526
+ }
406
527
  function printGuidanceHelp(runtime) {
407
528
  printLine(runtime.stdout, [
408
529
  "corners guidance",
@@ -443,18 +564,24 @@ function printWorkstreamHelp(runtime) {
443
564
  "corners workstream",
444
565
  "",
445
566
  "Usage:",
446
- " corners workstream list [--json]",
567
+ " corners workstream list [--all] [--json]",
447
568
  " corners workstream use <workstreamId> [--json]",
448
569
  " corners workstream current [--json]",
449
570
  " corners workstream create <name> [--summary <text>] [--corner <cornerNameOrId>] [--json]",
450
- " corners workstream pull <workstreamId> [--json]",
451
- " corners workstream update <workstreamId> [--status <status>] [--status-details <text> | --clear-status-details] [--json]",
452
- " corners workstream push <workstreamId> [--type <type>] [--message <text>] [--summary <text>] [--file <path>] [--title <title>] [--json]",
453
- " corners workstream question list <workstreamId> [--json]",
454
- " corners workstream question ask <workstreamId> [--question <text>] [--rationale <text>] [--suggested-answer <text>]... [--json]",
571
+ " corners workstream pull [-w <id>] [--json]",
572
+ " corners workstream update [-w <id>] [--status <status>] [--status-details <text> | --clear-status-details] [--json]",
573
+ " corners workstream push [-w <id>] [--type <type>] [--message <text>] [--summary <text>] [--file <path>] [--title <title>] [--json]",
574
+ " corners workstream question list [-w <id>] [--json]",
575
+ " corners workstream question ask [-w <id>] [--question <text>] [--rationale <text>] [--suggested-answer <text>]... [--json]",
455
576
  " corners workstream question answer <questionId> [--text <text>] [--json]",
456
- " corners workstream attach <workstreamId> --kind <kind> --entity-id <id> [--json]",
577
+ " corners workstream attach [-w <id>] --kind <kind> --entity-id <id> [--json]",
457
578
  " corners workstream reply-thread <threadId> [--text <text>] [--json]",
579
+ " corners workstream task list [-w <id>] [--json]",
580
+ " corners workstream task add [-w <id>] <title> [--list <todoListId>] [--json]",
581
+ " corners workstream task complete <taskItemId> [--undo] [--json]",
582
+ "",
583
+ "The -w/--workstream flag is optional when an active workstream is set.",
584
+ "Set the active workstream with: corners workstream use <workstreamId>",
458
585
  "",
459
586
  "Lifecycle statuses:",
460
587
  " scoping | in_progress | blocked | done",
@@ -672,17 +799,6 @@ async function withPrompts(runtime, run) {
672
799
  await prompts.close();
673
800
  }
674
801
  }
675
- async function readOptionalUtf8(path) {
676
- try {
677
- return await readFile(path, "utf8");
678
- }
679
- catch (error) {
680
- if (error.code === "ENOENT") {
681
- return null;
682
- }
683
- throw error;
684
- }
685
- }
686
802
  function ensureTrailingNewline(value) {
687
803
  return value.endsWith("\n") ? value : `${value}\n`;
688
804
  }
@@ -818,6 +934,7 @@ async function resolveRootContext(runtime, common, root, options) {
818
934
  printLine(runtime.stderr, warning);
819
935
  }
820
936
  }
937
+ await syncSkillsIfNeeded(root.rootDir, root.config, (dir, cfg) => runtime.config.writeLocalConfig(dir, cfg));
821
938
  return {
822
939
  root,
823
940
  guidance,
@@ -904,6 +1021,16 @@ async function resolveExplicitWorkstream(client, common, workstreamId) {
904
1021
  }
905
1022
  return data.workstream;
906
1023
  }
1024
+ async function resolveWorkstreamId(runtime, common, flagValue) {
1025
+ if (flagValue) {
1026
+ return flagValue;
1027
+ }
1028
+ const binding = await runtime.config.getBinding(runtime.cwd);
1029
+ if (binding) {
1030
+ return binding.workstreamId;
1031
+ }
1032
+ throw new CLIError("No --workstream flag and no active workstream. Use `corners workstream use <id>` first.", { json: common.json });
1033
+ }
907
1034
  function connectWorkstream(config, workstreamId) {
908
1035
  if (config.connectedWorkstreams.some((entry) => entry.id === workstreamId)) {
909
1036
  return config;
@@ -939,17 +1066,6 @@ function upsertCornerOverride(config, relativePath, corner) {
939
1066
  cornerOverrides: overrides,
940
1067
  };
941
1068
  }
942
- async function buildPlannedEdit(path, nextContent) {
943
- const existing = await readOptionalUtf8(path);
944
- if (existing === nextContent) {
945
- return null;
946
- }
947
- return {
948
- path,
949
- action: existing === null ? "create" : "update",
950
- content: nextContent,
951
- };
952
- }
953
1069
  function withUpdatedGuidanceConfig(config, managedTargets, syncedAt) {
954
1070
  return {
955
1071
  ...config,
@@ -977,12 +1093,6 @@ function ensureGitignoreEntry(existing) {
977
1093
  }
978
1094
  return `${trimmed}\n${LOCAL_ROOT_CONFIG_RELATIVE_PATH}\n`;
979
1095
  }
980
- async function writePlannedEdits(edits) {
981
- for (const edit of edits) {
982
- await mkdir(dirname(edit.path), { recursive: true });
983
- await writeFile(edit.path, edit.content, "utf8");
984
- }
985
- }
986
1096
  function formatWorkstreamLine(workstream) {
987
1097
  return [
988
1098
  workstream.id,
@@ -1304,6 +1414,7 @@ async function handleInit(args, runtime, inherited) {
1304
1414
  selectedTargets.push(target);
1305
1415
  }
1306
1416
  }
1417
+ const installSkills = await prompts.confirm("Install Corners skills? (sync, push, ask, plan)", { defaultValue: true });
1307
1418
  const selectedCornerId = await prompts.select("Choose the default corner for this local root.", joinedCorners.map((corner) => ({
1308
1419
  label: `${corner.name} (${corner.id})`,
1309
1420
  value: corner.id,
@@ -1313,7 +1424,7 @@ async function handleInit(args, runtime, inherited) {
1313
1424
  const defaultCorner = joinedCorners.find((corner) => corner.id === selectedCornerId) ??
1314
1425
  joinedCorners[0];
1315
1426
  const syncedAt = toIsoString();
1316
- const nextLocalConfig = withUpdatedGuidanceConfig({
1427
+ let nextLocalConfig = withUpdatedGuidanceConfig({
1317
1428
  version: 1,
1318
1429
  profile: profileName,
1319
1430
  workspace: profile.workspace,
@@ -1325,6 +1436,10 @@ async function handleInit(args, runtime, inherited) {
1325
1436
  cornerOverrides: existingConfig?.cornerOverrides ?? [],
1326
1437
  connectedWorkstreams: existingConfig?.connectedWorkstreams ?? [],
1327
1438
  }, selectedTargets.map((target) => target.key), syncedAt);
1439
+ if (installSkills) {
1440
+ const skillRefs = await buildInstalledSkillReferences(syncedAt);
1441
+ nextLocalConfig = withUpdatedSkillsConfig(nextLocalConfig, skillRefs, syncedAt);
1442
+ }
1328
1443
  const plannedEdits = [];
1329
1444
  const gitignorePath = join(rootDir, ".gitignore");
1330
1445
  const gitignoreContent = ensureGitignoreEntry(await readOptionalUtf8(gitignorePath));
@@ -1345,6 +1460,10 @@ async function handleInit(args, runtime, inherited) {
1345
1460
  plannedEdits.push(edit);
1346
1461
  }
1347
1462
  }
1463
+ if (installSkills) {
1464
+ const skillEdits = await buildSkillPlannedEdits(rootDir);
1465
+ plannedEdits.push(...skillEdits);
1466
+ }
1348
1467
  if (plannedEdits.length > 0 && !common.json) {
1349
1468
  printLine(runtime.stderr, [
1350
1469
  "Planned changes:",
@@ -1390,6 +1509,100 @@ async function handleInit(args, runtime, inherited) {
1390
1509
  return 0;
1391
1510
  });
1392
1511
  }
1512
+ async function handleSkills(args, runtime, inherited) {
1513
+ const subcommand = args[0];
1514
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
1515
+ printSkillsHelp(runtime);
1516
+ return 0;
1517
+ }
1518
+ switch (subcommand) {
1519
+ case "list": {
1520
+ const parsed = parseArgs({
1521
+ args: args.slice(1),
1522
+ allowPositionals: false,
1523
+ options: {
1524
+ json: { type: "boolean" },
1525
+ help: { type: "boolean", short: "h" },
1526
+ },
1527
+ });
1528
+ const common = mergeCommonOptions(inherited, {
1529
+ json: parsed.values.json,
1530
+ });
1531
+ if (parsed.values.help) {
1532
+ printSkillsHelp(runtime);
1533
+ return 0;
1534
+ }
1535
+ const skills = await Promise.all(BUNDLED_SKILLS.map(async (skill) => {
1536
+ const content = await readFile(getBundledSkillPath(skill), "utf8");
1537
+ const fm = parseSkillFrontmatter(content);
1538
+ return {
1539
+ name: skill.name,
1540
+ version: fm?.version ?? "unknown",
1541
+ description: fm?.description ?? "",
1542
+ };
1543
+ }));
1544
+ if (common.json) {
1545
+ printJson(runtime.stdout, { ok: true, skills });
1546
+ }
1547
+ else {
1548
+ printLine(runtime.stdout, [
1549
+ "Available skills:",
1550
+ ...skills.map((s) => ` ${s.name.padEnd(16)} ${s.version.padEnd(8)} ${s.description}`),
1551
+ ].join("\n"));
1552
+ }
1553
+ return 0;
1554
+ }
1555
+ case "status": {
1556
+ const parsed = parseArgs({
1557
+ args: args.slice(1),
1558
+ allowPositionals: false,
1559
+ options: {
1560
+ json: { type: "boolean" },
1561
+ help: { type: "boolean", short: "h" },
1562
+ },
1563
+ });
1564
+ const common = mergeCommonOptions(inherited, {
1565
+ json: parsed.values.json,
1566
+ });
1567
+ if (parsed.values.help) {
1568
+ printSkillsHelp(runtime);
1569
+ return 0;
1570
+ }
1571
+ const rootContext = await requireLocalRoot(runtime, common, {
1572
+ emitWarning: false,
1573
+ });
1574
+ const state = await resolveSkillsState(rootContext.root.rootDir, rootContext.root.config);
1575
+ const payload = {
1576
+ ok: true,
1577
+ root: rootContext.root.rootDir,
1578
+ skills: state,
1579
+ };
1580
+ if (common.json) {
1581
+ printJson(runtime.stdout, payload);
1582
+ }
1583
+ else {
1584
+ printLine(runtime.stdout, [
1585
+ `Root: ${payload.root}`,
1586
+ `Last synced: ${state.lastSyncedAt ?? "-"}`,
1587
+ "",
1588
+ "Skills:",
1589
+ ...state.skills.map((s) => {
1590
+ const stateLabel = s.state.padEnd(8);
1591
+ const version = s.state === "stale"
1592
+ ? `(installed: ${s.installedVersion}, available: ${s.bundledVersion})`
1593
+ : s.state === "missing"
1594
+ ? ""
1595
+ : `(${s.bundledVersion})`;
1596
+ return ` ${s.name.padEnd(16)} ${stateLabel} ${version}`;
1597
+ }),
1598
+ ].join("\n"));
1599
+ }
1600
+ return 0;
1601
+ }
1602
+ default:
1603
+ throw new CLIError(`Unknown skills command: ${subcommand}`);
1604
+ }
1605
+ }
1393
1606
  async function handleGuidance(args, runtime, inherited) {
1394
1607
  const subcommand = args[0];
1395
1608
  if (!subcommand || subcommand === "help" || subcommand === "--help") {
@@ -1587,6 +1800,7 @@ async function handleWorkstream(args, runtime, inherited) {
1587
1800
  help: { type: "boolean", short: "h" },
1588
1801
  profile: { type: "string" },
1589
1802
  "api-url": { type: "string" },
1803
+ all: { type: "boolean" },
1590
1804
  },
1591
1805
  });
1592
1806
  const common = mergeCommonOptions(inherited, {
@@ -1598,44 +1812,41 @@ async function handleWorkstream(args, runtime, inherited) {
1598
1812
  printWorkstreamHelp(runtime);
1599
1813
  return 0;
1600
1814
  }
1601
- const rootContext = await requireLocalRoot(runtime, common);
1602
- const { root, guidance, client } = await requirePinnedRootProfile(runtime, common, rootContext);
1603
- const hydrated = await Promise.all(root.config.connectedWorkstreams.map(async (entry) => {
1604
- try {
1605
- return {
1606
- entry,
1607
- workstream: await resolveExplicitWorkstream(client, common, entry.id),
1608
- };
1609
- }
1610
- catch {
1611
- return {
1612
- entry,
1613
- workstream: null,
1614
- };
1615
- }
1616
- }));
1617
- const workstreams = hydrated
1618
- .filter((entry) => entry.workstream !== null)
1619
- .map((entry) => entry.workstream);
1620
- const missingWorkstreamIds = hydrated
1621
- .filter((entry) => entry.workstream === null)
1622
- .map((entry) => entry.entry.id);
1815
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1816
+ const filter = {};
1817
+ if (!parsed.values.all) {
1818
+ filter.isOpen = true;
1819
+ }
1820
+ const data = await client.graphql(WORKSTREAM_LIST_QUERY, { first: 100, filter });
1821
+ const allWorkstreams = data.workstreams.edges.map((edge) => edge.node);
1822
+ const totalCount = data.workstreams.totalCount;
1823
+ // Try to find local root for connected workstream annotation
1824
+ const root = await runtime.config.findLocalRoot(runtime.cwd);
1825
+ const connectedIds = new Set(root?.config.connectedWorkstreams.map((entry) => entry.id) ?? []);
1623
1826
  if (common.json) {
1624
1827
  printJson(runtime.stdout, withGuidanceJsonMetadata({
1625
- root: root.rootDir,
1626
- workstreams,
1627
- missingWorkstreamIds,
1828
+ workstreams: allWorkstreams.map((ws) => ({
1829
+ ...ws,
1830
+ connected: connectedIds.has(ws.id),
1831
+ })),
1832
+ totalCount,
1628
1833
  }, guidance));
1629
1834
  }
1630
1835
  else {
1631
- if (missingWorkstreamIds.length > 0) {
1632
- printLine(runtime.stderr, missingWorkstreamIds
1633
- .map((workstreamId) => `Warning: connected workstream ${workstreamId} is missing or inaccessible.`)
1836
+ if (allWorkstreams.length === 0) {
1837
+ printLine(runtime.stdout, "No workstreams found.");
1838
+ }
1839
+ else {
1840
+ printLine(runtime.stdout, allWorkstreams
1841
+ .map((ws) => {
1842
+ const prefix = connectedIds.has(ws.id) ? "* " : " ";
1843
+ return `${prefix}${formatWorkstreamLine(ws)}`;
1844
+ })
1634
1845
  .join("\n"));
1846
+ if (totalCount > 100) {
1847
+ printLine(runtime.stderr, `Showing 100 of ${totalCount} workstreams.`);
1848
+ }
1635
1849
  }
1636
- printLine(runtime.stdout, workstreams.length > 0
1637
- ? workstreams.map(formatWorkstreamLine).join("\n")
1638
- : "No workstreams are connected to this local environment.");
1639
1850
  }
1640
1851
  return 0;
1641
1852
  }
@@ -1672,15 +1883,23 @@ async function handleWorkstream(args, runtime, inherited) {
1672
1883
  if (nextConfig !== root.config) {
1673
1884
  await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
1674
1885
  }
1886
+ await runtime.config.setBinding(runtime.cwd, {
1887
+ profile: root.config.profile,
1888
+ workspace: root.config.workspace ?? "",
1889
+ workstreamId: workstream.id,
1890
+ cornerId: workstream.cornerId,
1891
+ boundAt: toIsoString(),
1892
+ });
1675
1893
  if (common.json) {
1676
1894
  printJson(runtime.stdout, withGuidanceJsonMetadata({
1677
1895
  ok: true,
1678
1896
  root: root.rootDir,
1679
1897
  workstream,
1898
+ bound: true,
1680
1899
  }, guidance));
1681
1900
  }
1682
1901
  else {
1683
- printLine(runtime.stdout, `Connected ${workstream.id} (${workstream.name}) to ${root.rootDir}.`);
1902
+ printLine(runtime.stdout, `Connected ${workstream.id} (${workstream.name}) to ${root.rootDir} and set as active workstream.`);
1684
1903
  }
1685
1904
  return 0;
1686
1905
  }
@@ -1703,12 +1922,14 @@ async function handleWorkstream(args, runtime, inherited) {
1703
1922
  const { root, guidance } = await requireLocalRoot(runtime, common);
1704
1923
  const cwdPath = await realpath(runtime.cwd);
1705
1924
  const resolvedCorner = resolveCornerForCwd(root, cwdPath);
1925
+ const binding = await runtime.config.getBinding(runtime.cwd);
1706
1926
  const payload = {
1707
1927
  cwd: runtime.cwd,
1708
1928
  root: root.rootDir,
1709
1929
  profile: root.config.profile,
1710
1930
  workspace: root.config.workspace,
1711
1931
  resolvedCorner,
1932
+ activeWorkstreamId: binding?.workstreamId ?? null,
1712
1933
  connectedWorkstreamIds: root.config.connectedWorkstreams.map((entry) => entry.id),
1713
1934
  };
1714
1935
  if (common.json) {
@@ -1721,6 +1942,7 @@ async function handleWorkstream(args, runtime, inherited) {
1721
1942
  `Profile: ${payload.profile}`,
1722
1943
  `Workspace: ${payload.workspace ?? "-"}`,
1723
1944
  `Resolved corner: ${payload.resolvedCorner.corner.name} (${payload.resolvedCorner.corner.id})`,
1945
+ `Active workstream: ${payload.activeWorkstreamId ?? "none"}`,
1724
1946
  `Connected workstreams: ${payload.connectedWorkstreamIds.length}`,
1725
1947
  ].join("\n"));
1726
1948
  }
@@ -1785,12 +2007,13 @@ async function handleWorkstream(args, runtime, inherited) {
1785
2007
  case "pull": {
1786
2008
  const parsed = parseArgs({
1787
2009
  args: args.slice(1),
1788
- allowPositionals: true,
2010
+ allowPositionals: false,
1789
2011
  options: {
1790
2012
  json: { type: "boolean" },
1791
2013
  help: { type: "boolean", short: "h" },
1792
2014
  profile: { type: "string" },
1793
2015
  "api-url": { type: "string" },
2016
+ workstream: { type: "string", short: "w" },
1794
2017
  },
1795
2018
  });
1796
2019
  const common = mergeCommonOptions(inherited, {
@@ -1802,12 +2025,7 @@ async function handleWorkstream(args, runtime, inherited) {
1802
2025
  printWorkstreamHelp(runtime);
1803
2026
  return 0;
1804
2027
  }
1805
- const workstreamId = parsed.positionals[0];
1806
- if (!workstreamId) {
1807
- throw new CLIError("Usage: corners workstream pull <workstreamId>", {
1808
- json: common.json,
1809
- });
1810
- }
2028
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
1811
2029
  const { guidance, client } = await requireCommandProfile(runtime, common);
1812
2030
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1813
2031
  const data = await client.graphql(WORKSTREAM_PULL_QUERY, {
@@ -1820,7 +2038,7 @@ async function handleWorkstream(args, runtime, inherited) {
1820
2038
  }
1821
2039
  else {
1822
2040
  const snapshot = data.workstream;
1823
- printLine(runtime.stdout, [
2041
+ const lines = [
1824
2042
  `${snapshot.name} (${snapshot.id})`,
1825
2043
  snapshot.summary ? `Summary: ${snapshot.summary}` : "Summary: -",
1826
2044
  `State: ${snapshot.isOpen === false ? "archived" : "open"}`,
@@ -1830,19 +2048,41 @@ async function handleWorkstream(args, runtime, inherited) {
1830
2048
  `Answered questions: ${snapshot.answeredQuestions?.length ?? 0}`,
1831
2049
  `Attachments: ${snapshot.attachments?.totalCount ?? 0}`,
1832
2050
  `Feed items returned: ${snapshot.feed?.totalCount ?? 0}`,
1833
- ].join("\n"));
2051
+ ];
2052
+ // Append tasks from TodoList attachments
2053
+ const todoLists = (snapshot.attachments?.edges ?? [])
2054
+ .filter((e) => e.node.__typename === "WorkstreamTodoListAttachment" &&
2055
+ e.node.todoList)
2056
+ .map((e) => e.node.todoList);
2057
+ if (todoLists.length > 0) {
2058
+ lines.push("");
2059
+ lines.push("Tasks:");
2060
+ for (const list of todoLists) {
2061
+ const completed = list.totalItemCount - list.openItemCount;
2062
+ lines.push(` ${list.title} (${completed}/${list.totalItemCount} complete)`);
2063
+ for (const item of list.items) {
2064
+ const check = item.completed ? "[x]" : "[ ]";
2065
+ const assignee = item.assignee
2066
+ ? ` (@${item.assignee.handle})`
2067
+ : "";
2068
+ lines.push(` ${check} ${item.title}${assignee}`);
2069
+ }
2070
+ }
2071
+ }
2072
+ printLine(runtime.stdout, lines.join("\n"));
1834
2073
  }
1835
2074
  return 0;
1836
2075
  }
1837
2076
  case "update": {
1838
2077
  const parsed = parseArgs({
1839
2078
  args: args.slice(1),
1840
- allowPositionals: true,
2079
+ allowPositionals: false,
1841
2080
  options: {
1842
2081
  json: { type: "boolean" },
1843
2082
  help: { type: "boolean", short: "h" },
1844
2083
  profile: { type: "string" },
1845
2084
  "api-url": { type: "string" },
2085
+ workstream: { type: "string", short: "w" },
1846
2086
  status: { type: "string" },
1847
2087
  "status-details": { type: "string" },
1848
2088
  "clear-status-details": { type: "boolean" },
@@ -1857,10 +2097,6 @@ async function handleWorkstream(args, runtime, inherited) {
1857
2097
  printWorkstreamHelp(runtime);
1858
2098
  return 0;
1859
2099
  }
1860
- const workstreamId = parsed.positionals[0];
1861
- if (!workstreamId) {
1862
- throw new CLIError("Usage: corners workstream update <workstreamId> [--status <status>] [--status-details <text> | --clear-status-details]", { json: common.json });
1863
- }
1864
2100
  const rawStatusDetails = parsed.values["status-details"];
1865
2101
  const clearStatusDetails = parsed.values["clear-status-details"] === true;
1866
2102
  if (rawStatusDetails !== undefined && clearStatusDetails) {
@@ -1871,6 +2107,7 @@ async function handleWorkstream(args, runtime, inherited) {
1871
2107
  !clearStatusDetails) {
1872
2108
  throw new CLIError("Provide --status, --status-details, or --clear-status-details.", { json: common.json });
1873
2109
  }
2110
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
1874
2111
  const { guidance, client } = await requireCommandProfile(runtime, common);
1875
2112
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1876
2113
  const result = await client.graphql(UPDATE_WORKSTREAM_MUTATION, {
@@ -1912,6 +2149,7 @@ async function handleWorkstream(args, runtime, inherited) {
1912
2149
  help: { type: "boolean", short: "h" },
1913
2150
  profile: { type: "string" },
1914
2151
  "api-url": { type: "string" },
2152
+ workstream: { type: "string", short: "w" },
1915
2153
  type: { type: "string" },
1916
2154
  message: { type: "string" },
1917
2155
  summary: { type: "string" },
@@ -1928,15 +2166,10 @@ async function handleWorkstream(args, runtime, inherited) {
1928
2166
  printWorkstreamHelp(runtime);
1929
2167
  return 0;
1930
2168
  }
1931
- const workstreamId = parsed.positionals[0];
1932
- if (!workstreamId) {
1933
- throw new CLIError("Usage: corners workstream push <workstreamId>", {
1934
- json: common.json,
1935
- });
1936
- }
2169
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
1937
2170
  const message = parsed.values.message ??
1938
- (parsed.positionals.length > 1
1939
- ? parsed.positionals.slice(1).join(" ")
2171
+ (parsed.positionals.length > 0
2172
+ ? parsed.positionals.join(" ")
1940
2173
  : await readTextFromStdin(runtime.stdin));
1941
2174
  if (!message) {
1942
2175
  throw new CLIError("Workstream updates need a message. Use --message or pipe text on stdin.", { json: common.json });
@@ -1989,12 +2222,13 @@ async function handleWorkstream(args, runtime, inherited) {
1989
2222
  case "list": {
1990
2223
  const parsed = parseArgs({
1991
2224
  args: args.slice(2),
1992
- allowPositionals: true,
2225
+ allowPositionals: false,
1993
2226
  options: {
1994
2227
  json: { type: "boolean" },
1995
2228
  help: { type: "boolean", short: "h" },
1996
2229
  profile: { type: "string" },
1997
2230
  "api-url": { type: "string" },
2231
+ workstream: { type: "string", short: "w" },
1998
2232
  },
1999
2233
  });
2000
2234
  const common = mergeCommonOptions(inherited, {
@@ -2006,10 +2240,7 @@ async function handleWorkstream(args, runtime, inherited) {
2006
2240
  printWorkstreamHelp(runtime);
2007
2241
  return 0;
2008
2242
  }
2009
- const workstreamId = parsed.positionals[0];
2010
- if (!workstreamId) {
2011
- throw new CLIError("Usage: corners workstream question list <workstreamId>", { json: common.json });
2012
- }
2243
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
2013
2244
  const { guidance, client } = await requireCommandProfile(runtime, common);
2014
2245
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
2015
2246
  const data = await client.graphql(WORKSTREAM_QUESTION_LIST_QUERY, {
@@ -2040,6 +2271,7 @@ async function handleWorkstream(args, runtime, inherited) {
2040
2271
  help: { type: "boolean", short: "h" },
2041
2272
  profile: { type: "string" },
2042
2273
  "api-url": { type: "string" },
2274
+ workstream: { type: "string", short: "w" },
2043
2275
  question: { type: "string" },
2044
2276
  rationale: { type: "string" },
2045
2277
  "suggested-answer": { type: "string", multiple: true },
@@ -2054,13 +2286,10 @@ async function handleWorkstream(args, runtime, inherited) {
2054
2286
  printWorkstreamHelp(runtime);
2055
2287
  return 0;
2056
2288
  }
2057
- const workstreamId = parsed.positionals[0];
2058
- if (!workstreamId) {
2059
- throw new CLIError("Usage: corners workstream question ask <workstreamId> [--question <text>]", { json: common.json });
2060
- }
2289
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
2061
2290
  const question = parsed.values.question ??
2062
- (parsed.positionals.length > 1
2063
- ? parsed.positionals.slice(1).join(" ")
2291
+ (parsed.positionals.length > 0
2292
+ ? parsed.positionals.join(" ")
2064
2293
  : await readTextFromStdin(runtime.stdin));
2065
2294
  if (!question) {
2066
2295
  throw new CLIError("Question text is required. Use --question or pipe text on stdin.", { json: common.json });
@@ -2137,12 +2366,13 @@ async function handleWorkstream(args, runtime, inherited) {
2137
2366
  case "attach": {
2138
2367
  const parsed = parseArgs({
2139
2368
  args: args.slice(1),
2140
- allowPositionals: true,
2369
+ allowPositionals: false,
2141
2370
  options: {
2142
2371
  json: { type: "boolean" },
2143
2372
  help: { type: "boolean", short: "h" },
2144
2373
  profile: { type: "string" },
2145
2374
  "api-url": { type: "string" },
2375
+ workstream: { type: "string", short: "w" },
2146
2376
  kind: { type: "string" },
2147
2377
  "entity-id": { type: "string" },
2148
2378
  },
@@ -2156,12 +2386,12 @@ async function handleWorkstream(args, runtime, inherited) {
2156
2386
  printWorkstreamHelp(runtime);
2157
2387
  return 0;
2158
2388
  }
2159
- const workstreamId = parsed.positionals[0];
2160
2389
  const kind = parsed.values.kind;
2161
2390
  const entityId = parsed.values["entity-id"];
2162
- if (!workstreamId || !kind || !entityId) {
2163
- throw new CLIError("Usage: corners workstream attach <workstreamId> --kind <kind> --entity-id <id>", { json: common.json });
2391
+ if (!kind || !entityId) {
2392
+ throw new CLIError("Usage: corners workstream attach [--workstream <id>] --kind <kind> --entity-id <id>", { json: common.json });
2164
2393
  }
2394
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
2165
2395
  const { guidance, client } = await requireCommandProfile(runtime, common);
2166
2396
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
2167
2397
  const result = await client.graphql(ADD_WORKSTREAM_ATTACHMENT_MUTATION, {
@@ -2221,6 +2451,212 @@ async function handleWorkstream(args, runtime, inherited) {
2221
2451
  }
2222
2452
  return 0;
2223
2453
  }
2454
+ case "task": {
2455
+ const nested = args[1];
2456
+ if (!nested || nested === "help" || nested === "--help") {
2457
+ printWorkstreamHelp(runtime);
2458
+ return 0;
2459
+ }
2460
+ switch (nested) {
2461
+ case "list": {
2462
+ const parsed = parseArgs({
2463
+ args: args.slice(2),
2464
+ allowPositionals: false,
2465
+ options: {
2466
+ json: { type: "boolean" },
2467
+ help: { type: "boolean", short: "h" },
2468
+ profile: { type: "string" },
2469
+ "api-url": { type: "string" },
2470
+ workstream: { type: "string", short: "w" },
2471
+ },
2472
+ });
2473
+ const common = mergeCommonOptions(inherited, {
2474
+ json: parsed.values.json,
2475
+ profile: parsed.values.profile,
2476
+ apiUrl: parsed.values["api-url"],
2477
+ });
2478
+ if (parsed.values.help) {
2479
+ printWorkstreamHelp(runtime);
2480
+ return 0;
2481
+ }
2482
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
2483
+ const { guidance, client } = await requireCommandProfile(runtime, common);
2484
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
2485
+ const data = await client.graphql(WORKSTREAM_TASK_LIST_QUERY, {
2486
+ id: workstream.id,
2487
+ attachmentsFirst: 50,
2488
+ });
2489
+ if (!data.workstream) {
2490
+ throw new CLIError("Workstream not found", { json: common.json });
2491
+ }
2492
+ const todoLists = data.workstream.attachments.edges
2493
+ .filter((e) => e.node.todoList)
2494
+ .map((e) => e.node.todoList);
2495
+ if (common.json) {
2496
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
2497
+ workstreamId: workstream.id,
2498
+ workstreamName: workstream.name,
2499
+ todoLists,
2500
+ }, guidance));
2501
+ }
2502
+ else {
2503
+ if (todoLists.length === 0) {
2504
+ printLine(runtime.stdout, "No task lists attached to this workstream.");
2505
+ }
2506
+ else {
2507
+ const lines = [
2508
+ `${workstream.name} (${workstream.id}) - Tasks`,
2509
+ "",
2510
+ ];
2511
+ for (const list of todoLists) {
2512
+ const completed = list.totalItemCount - list.openItemCount;
2513
+ lines.push(`${list.title} (${completed}/${list.totalItemCount} complete)`);
2514
+ for (const item of list.items) {
2515
+ const check = item.completed ? "[x]" : "[ ]";
2516
+ const assignee = item.assignee
2517
+ ? ` (@${item.assignee.handle})`
2518
+ : "";
2519
+ lines.push(` ${check} ${item.title}${assignee}`);
2520
+ }
2521
+ }
2522
+ printLine(runtime.stdout, lines.join("\n"));
2523
+ }
2524
+ }
2525
+ return 0;
2526
+ }
2527
+ case "add": {
2528
+ const parsed = parseArgs({
2529
+ args: args.slice(2),
2530
+ allowPositionals: true,
2531
+ options: {
2532
+ json: { type: "boolean" },
2533
+ help: { type: "boolean", short: "h" },
2534
+ profile: { type: "string" },
2535
+ "api-url": { type: "string" },
2536
+ workstream: { type: "string", short: "w" },
2537
+ title: { type: "string" },
2538
+ list: { type: "string" },
2539
+ description: { type: "string" },
2540
+ },
2541
+ });
2542
+ const common = mergeCommonOptions(inherited, {
2543
+ json: parsed.values.json,
2544
+ profile: parsed.values.profile,
2545
+ apiUrl: parsed.values["api-url"],
2546
+ });
2547
+ if (parsed.values.help) {
2548
+ printWorkstreamHelp(runtime);
2549
+ return 0;
2550
+ }
2551
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
2552
+ const taskTitle = parsed.values.title ??
2553
+ (parsed.positionals.length > 0
2554
+ ? parsed.positionals.join(" ")
2555
+ : await readTextFromStdin(runtime.stdin));
2556
+ if (!taskTitle) {
2557
+ throw new CLIError("Task title is required. Use --title or provide as positional argument.", { json: common.json });
2558
+ }
2559
+ const { guidance, client } = await requireCommandProfile(runtime, common);
2560
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
2561
+ let todoListId = parsed.values.list;
2562
+ if (!todoListId) {
2563
+ // Query existing TODOLIST attachments
2564
+ const data = await client.graphql(WORKSTREAM_TASK_LIST_QUERY, {
2565
+ id: workstream.id,
2566
+ attachmentsFirst: 50,
2567
+ });
2568
+ const lists = (data.workstream?.attachments.edges ?? [])
2569
+ .filter((e) => e.node.todoList)
2570
+ .map((e) => e.node.todoList);
2571
+ if (lists.length === 1) {
2572
+ todoListId = lists[0].id;
2573
+ }
2574
+ else if (lists.length === 0) {
2575
+ // Auto-create a todo list with workstreamId for server-side attachment
2576
+ const createResult = await client.graphql(CREATE_TODO_LIST_MUTATION, {
2577
+ input: {
2578
+ title: `${workstream.name} Tasks`,
2579
+ workstreamId: workstream.id,
2580
+ },
2581
+ });
2582
+ todoListId = createResult.createTodoList.id;
2583
+ }
2584
+ else {
2585
+ const listSummary = lists
2586
+ .map((l) => ` ${l.id} (${l.title})`)
2587
+ .join("\n");
2588
+ throw new CLIError(`Multiple task lists found. Use --list <todoListId> to specify:\n${listSummary}`, { json: common.json });
2589
+ }
2590
+ }
2591
+ const result = await client.graphql(ADD_TODO_ITEM_MUTATION, {
2592
+ input: {
2593
+ todoListId,
2594
+ title: taskTitle,
2595
+ ...(parsed.values.description
2596
+ ? { description: parsed.values.description }
2597
+ : {}),
2598
+ },
2599
+ });
2600
+ if (common.json) {
2601
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
2602
+ ok: true,
2603
+ workstreamId: workstream.id,
2604
+ todoListId,
2605
+ item: result.addTodoItem,
2606
+ }, guidance));
2607
+ }
2608
+ else {
2609
+ printLine(runtime.stdout, `Added task "${result.addTodoItem.title}" to ${workstream.id}.`);
2610
+ }
2611
+ return 0;
2612
+ }
2613
+ case "complete": {
2614
+ const parsed = parseArgs({
2615
+ args: args.slice(2),
2616
+ allowPositionals: true,
2617
+ options: {
2618
+ json: { type: "boolean" },
2619
+ help: { type: "boolean", short: "h" },
2620
+ profile: { type: "string" },
2621
+ "api-url": { type: "string" },
2622
+ undo: { type: "boolean" },
2623
+ },
2624
+ });
2625
+ const common = mergeCommonOptions(inherited, {
2626
+ json: parsed.values.json,
2627
+ profile: parsed.values.profile,
2628
+ apiUrl: parsed.values["api-url"],
2629
+ });
2630
+ if (parsed.values.help) {
2631
+ printWorkstreamHelp(runtime);
2632
+ return 0;
2633
+ }
2634
+ const taskItemId = parsed.positionals[0];
2635
+ if (!taskItemId) {
2636
+ throw new CLIError("Usage: corners workstream task complete <taskItemId> [--undo]", { json: common.json });
2637
+ }
2638
+ const completed = parsed.values.undo !== true;
2639
+ const { guidance, client } = await requireCommandProfile(runtime, common);
2640
+ const result = await client.graphql(TOGGLE_TODO_ITEM_MUTATION, {
2641
+ id: taskItemId,
2642
+ completed,
2643
+ });
2644
+ if (common.json) {
2645
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
2646
+ ok: true,
2647
+ item: result.toggleTodoItem,
2648
+ }, guidance));
2649
+ }
2650
+ else {
2651
+ const verb = completed ? "Completed" : "Reopened";
2652
+ printLine(runtime.stdout, `${verb} task "${result.toggleTodoItem.title}" (${result.toggleTodoItem.id}).`);
2653
+ }
2654
+ return 0;
2655
+ }
2656
+ default:
2657
+ throw new CLIError(`Unknown workstream task command: ${nested}`);
2658
+ }
2659
+ }
2224
2660
  default:
2225
2661
  throw new CLIError(`Unknown workstream command: ${subcommand}`);
2226
2662
  }
@@ -2249,6 +2685,9 @@ export async function runCli(argv, runtime = createRuntime()) {
2249
2685
  else if (parsed.rest[1] === "guidance") {
2250
2686
  printGuidanceHelp(runtime);
2251
2687
  }
2688
+ else if (parsed.rest[1] === "skills") {
2689
+ printSkillsHelp(runtime);
2690
+ }
2252
2691
  else if (parsed.rest[1] === "init") {
2253
2692
  printInitHelp(runtime);
2254
2693
  }
@@ -2272,6 +2711,8 @@ export async function runCli(argv, runtime = createRuntime()) {
2272
2711
  return handleInit(parsed.rest.slice(1), runtime, parsed.common);
2273
2712
  case "guidance":
2274
2713
  return handleGuidance(parsed.rest.slice(1), runtime, parsed.common);
2714
+ case "skills":
2715
+ return handleSkills(parsed.rest.slice(1), runtime, parsed.common);
2275
2716
  case "auth":
2276
2717
  return handleAuth(parsed.rest.slice(1), runtime, parsed.common);
2277
2718
  case "corner":