@corners/cli 0.0.8 → 0.0.10

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.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,gBAAgB,EAKtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EASZ,MAAM,aAAa,CAAC;AAWrB,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA0f/D,UAAU,OAAO;IACf,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC;IACtC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE;QAC7B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC7B,KAAK,gBAAgB,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,MAAM,CAAC;CACnC;AA60GD,wBAAsB,MAAM,CAC1B,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,OAAyB,GACjC,OAAO,CAAC,MAAM,CAAC,CAiEjB"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,gBAAgB,EAKtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EASZ,MAAM,aAAa,CAAC;AAUrB,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA0f/D,UAAU,OAAO;IACf,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC;IACtC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE;QAC7B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC7B,KAAK,gBAAgB,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,MAAM,CAAC;CACnC;AAkvHD,wBAAsB,MAAM,CAC1B,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,OAAyB,GACjC,OAAO,CAAC,MAAM,CAAC,CAqEjB"}
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
- import { readFile, realpath, writeFile } from "node:fs/promises";
1
+ import { execSync } from "node:child_process";
2
+ import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
2
3
  import { hostname } from "node:os";
3
4
  import { basename, join, relative, resolve, sep } from "node:path";
4
5
  import { parseArgs } from "node:util";
5
6
  import { CornersApiClient as DefaultCornersApiClient, } from "./client.js";
6
7
  import { ConfigStore, LOCAL_ROOT_CONFIG_RELATIVE_PATH, } from "./config.js";
7
- import { buildExpectedManagedGuidanceBlock, buildGuidanceFileContent, buildGuidanceRevision, extractManagedGuidanceBlock, GUIDANCE_SYNC_COMMAND, GUIDANCE_TARGETS, normalizeGuidanceTargets, } from "./guidance.js";
8
+ import { buildExpectedManagedGuidanceBlock, buildGuidanceFileContent, buildGuidanceRevision, extractManagedGuidanceBlock, GUIDANCE_TARGETS, normalizeGuidanceTargets, } from "./guidance.js";
8
9
  import { createPromptApi } from "./prompts.js";
9
10
  import { BUNDLED_SKILLS, buildInstalledSkillReferences, buildSkillPlannedEdits, getBundledSkillPath, parseSkillFrontmatter, resolveSkillsState, syncSkillsIfNeeded, withUpdatedSkillsConfig, } from "./skills.js";
10
11
  import { buildPlannedEdit, CLIError, getPackageVersion, normalizeApiUrl, openUrlInBrowser, printJson, printLine, readOptionalUtf8, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamStatus, toGraphQLWorkstreamUpdateType, toIsoString, writePlannedEdits, } from "./support.js";
@@ -492,7 +493,9 @@ function printMainHelp(runtime) {
492
493
  " init",
493
494
  " corner use",
494
495
  " whoami",
496
+ " agent start|checkpoint|intent|end",
495
497
  " workstream (ws) list|use|current|create|pull|update|push|question|attach|reply-thread",
498
+ " open [--workstream <id>] [--corner <slug>]",
496
499
  " help",
497
500
  " version",
498
501
  "",
@@ -663,6 +666,29 @@ function mergeCommonOptions(base, values) {
663
666
  async function requireStoredProfile(runtime, common) {
664
667
  const selected = await runtime.config.getProfile(common.profile ?? null);
665
668
  if (!selected) {
669
+ // Fallback: try reading the desktop app's auth token
670
+ const desktopToken = await runtime.config.getDesktopToken();
671
+ if (desktopToken) {
672
+ const apiUrl = common.apiUrl ?? desktopToken.apiUrl;
673
+ const desktopProfile = {
674
+ apiUrl,
675
+ accessToken: desktopToken.accessToken,
676
+ sessionId: null,
677
+ workspace: desktopToken.workspace,
678
+ accountId: null,
679
+ userId: null,
680
+ handle: null,
681
+ createdAt: new Date().toISOString(),
682
+ };
683
+ return {
684
+ profileName: "desktop",
685
+ profile: desktopProfile,
686
+ client: runtime.createClient({
687
+ apiUrl,
688
+ accessToken: desktopToken.accessToken,
689
+ }),
690
+ };
691
+ }
666
692
  throw new CLIError("Not logged in. Run `corners auth login` first.", {
667
693
  json: common.json,
668
694
  });
@@ -716,6 +742,34 @@ async function maybeRefreshStoredProfileIdentity(input) {
716
742
  async function resolveAuthStatus(runtime, common) {
717
743
  const selected = await runtime.config.getProfile(common.profile ?? null);
718
744
  if (!selected) {
745
+ // Fallback: check desktop app's auth token
746
+ const desktopToken = await runtime.config.getDesktopToken();
747
+ if (desktopToken) {
748
+ const apiUrl = common.apiUrl ?? desktopToken.apiUrl;
749
+ const client = runtime.createClient({
750
+ apiUrl,
751
+ accessToken: desktopToken.accessToken,
752
+ });
753
+ let remote;
754
+ try {
755
+ remote = await client.validateSession();
756
+ }
757
+ catch (error) {
758
+ remote = { valid: false, error: error.message };
759
+ }
760
+ return {
761
+ loggedIn: true,
762
+ profile: "desktop (via Corners app)",
763
+ apiUrl,
764
+ sessionId: null,
765
+ workspace: desktopToken.workspace,
766
+ handle: remote.valid ? null : null,
767
+ accountId: null,
768
+ userId: null,
769
+ remoteValid: remote.valid,
770
+ remoteError: remote.valid ? null : (remote.error ?? null),
771
+ };
772
+ }
719
773
  return {
720
774
  loggedIn: false,
721
775
  profile: common.profile ?? null,
@@ -898,21 +952,41 @@ async function resolveGuidanceState(root) {
898
952
  files,
899
953
  };
900
954
  }
901
- function buildGuidanceWarning(guidance) {
955
+ /**
956
+ * Silently syncs stale guidance files and updates config.
957
+ * Called automatically from resolveRootContext() so users never need to run `guidance sync` manually.
958
+ */
959
+ async function autoSyncGuidanceIfStale(root, guidance, writeConfig) {
902
960
  if (!guidance.stale) {
903
- return null;
904
- }
905
- const syncedSuffix = guidance.lastSyncedAt
906
- ? ` Last synced: ${guidance.lastSyncedAt}.`
907
- : "";
908
- return `Warning: managed Corners guidance is out of date in ${guidance.staleFiles.length} file(s).${syncedSuffix} Run \`${GUIDANCE_SYNC_COMMAND}\`.`;
961
+ return { root, guidance };
962
+ }
963
+ const managedTargets = root.config.guidance?.managedTargets !== undefined
964
+ ? normalizeGuidanceTargets(root.config.guidance.managedTargets)
965
+ : guidance.managedTargets;
966
+ const edits = [];
967
+ for (const target of GUIDANCE_TARGETS) {
968
+ if (!managedTargets.includes(target.key))
969
+ continue;
970
+ const targetPath = join(root.rootDir, target.relativePath);
971
+ const nextContent = buildGuidanceFileContent(target.key, await readOptionalUtf8(targetPath));
972
+ const edit = await buildPlannedEdit(targetPath, nextContent);
973
+ if (edit)
974
+ edits.push(edit);
975
+ }
976
+ if (edits.length > 0) {
977
+ await writePlannedEdits(edits);
978
+ }
979
+ const nextConfig = withUpdatedGuidanceConfig(root.config, managedTargets, toIsoString());
980
+ await writeConfig(root.rootDir, nextConfig);
981
+ const nextRoot = { ...root, config: nextConfig };
982
+ const nextGuidance = await resolveGuidanceState(nextRoot);
983
+ return { root: nextRoot, guidance: nextGuidance };
909
984
  }
910
985
  function buildGuidanceJsonMetadata(guidance) {
911
986
  return {
912
987
  stale: guidance.stale,
913
988
  lastSyncedAt: guidance.lastSyncedAt,
914
989
  staleFiles: guidance.staleFiles,
915
- syncCommand: GUIDANCE_SYNC_COMMAND,
916
990
  };
917
991
  }
918
992
  function withGuidanceJsonMetadata(payload, guidance) {
@@ -926,26 +1000,21 @@ function withGuidanceJsonMetadata(payload, guidance) {
926
1000
  },
927
1001
  };
928
1002
  }
929
- async function resolveRootContext(runtime, common, root, options) {
1003
+ async function resolveRootContext(runtime, _common, root) {
930
1004
  const guidance = await resolveGuidanceState(root);
931
- if ((options?.emitWarning ?? true) && !common.json) {
932
- const warning = buildGuidanceWarning(guidance);
933
- if (warning) {
934
- printLine(runtime.stderr, warning);
935
- }
936
- }
937
- await syncSkillsIfNeeded(root.rootDir, root.config, (dir, cfg) => runtime.config.writeLocalConfig(dir, cfg));
1005
+ const synced = await autoSyncGuidanceIfStale(root, guidance, (dir, cfg) => runtime.config.writeLocalConfig(dir, cfg));
1006
+ await syncSkillsIfNeeded(synced.root.rootDir, synced.root.config, (dir, cfg) => runtime.config.writeLocalConfig(dir, cfg));
938
1007
  return {
939
- root,
940
- guidance,
1008
+ root: synced.root,
1009
+ guidance: synced.guidance,
941
1010
  };
942
1011
  }
943
- async function requireLocalRoot(runtime, common, options) {
1012
+ async function requireLocalRoot(runtime, common) {
944
1013
  const root = await runtime.config.findLocalRoot(runtime.cwd);
945
1014
  if (!root) {
946
1015
  throw new CLIError("No local Corners CLI root found. Run `corners init` from your project root first.", { json: common.json });
947
1016
  }
948
- return resolveRootContext(runtime, common, root, options);
1017
+ return resolveRootContext(runtime, common, root);
949
1018
  }
950
1019
  async function requirePinnedRootProfile(runtime, common, rootContext) {
951
1020
  const { root, guidance } = rootContext;
@@ -1489,17 +1558,88 @@ async function handleInit(args, runtime, inherited) {
1489
1558
  }
1490
1559
  await writePlannedEdits(plannedEdits);
1491
1560
  }
1561
+ // --- Claude Code Settings Hook ---
1562
+ if (selectedTargets.some((t) => t.key === "claude")) {
1563
+ const claudeSettingsPath = join(rootDir, ".claude", "settings.json");
1564
+ try {
1565
+ await mkdir(join(rootDir, ".claude"), { recursive: true });
1566
+ let existing = {};
1567
+ try {
1568
+ const raw = await readOptionalUtf8(claudeSettingsPath);
1569
+ if (raw) {
1570
+ existing = JSON.parse(raw);
1571
+ }
1572
+ }
1573
+ catch {
1574
+ // Malformed JSON — start fresh
1575
+ existing = {};
1576
+ }
1577
+ // Merge permissions.allow
1578
+ const existingPerms = existing.permissions ?? {};
1579
+ const existingAllow = Array.isArray(existingPerms.allow)
1580
+ ? existingPerms.allow
1581
+ : [];
1582
+ const newPerms = [
1583
+ "Bash(corners workstream:*)",
1584
+ "Bash(corners agent:*)",
1585
+ ];
1586
+ const mergedAllow = [
1587
+ ...existingAllow,
1588
+ ...newPerms.filter((p) => !existingAllow.includes(p)),
1589
+ ];
1590
+ // Merge hooks.SessionStart
1591
+ const existingHooks = existing.hooks ?? {};
1592
+ const existingSessionStart = Array.isArray(existingHooks.SessionStart)
1593
+ ? existingHooks.SessionStart
1594
+ : [];
1595
+ const hookCommand = "command -v corners >/dev/null 2>&1 && corners workstream current --json || true";
1596
+ const alreadyHasHook = existingSessionStart.some((entry) => Array.isArray(entry.hooks) &&
1597
+ entry.hooks.some((h) => h.command === hookCommand));
1598
+ const mergedSessionStart = alreadyHasHook
1599
+ ? existingSessionStart
1600
+ : [
1601
+ ...existingSessionStart,
1602
+ {
1603
+ matcher: "",
1604
+ hooks: [{ type: "command", command: hookCommand }],
1605
+ },
1606
+ ];
1607
+ const merged = {
1608
+ ...existing,
1609
+ permissions: {
1610
+ ...existingPerms,
1611
+ allow: mergedAllow,
1612
+ },
1613
+ hooks: {
1614
+ ...existingHooks,
1615
+ SessionStart: mergedSessionStart,
1616
+ },
1617
+ };
1618
+ await writeFile(claudeSettingsPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
1619
+ if (!common.json) {
1620
+ printLine(runtime.stderr, " Wrote Claude Code settings with SessionStart hook");
1621
+ }
1622
+ }
1623
+ catch (error) {
1624
+ if (!common.json) {
1625
+ printLine(runtime.stderr, ` Warning: could not write .claude/settings.json: ${error.message}`);
1626
+ }
1627
+ }
1628
+ }
1492
1629
  // --- Agent Communication Setup ---
1493
1630
  const setupAgentComms = await prompts.confirm("Set up agent communication? (auto-connect workstream + CLAUDE.md instructions)", { defaultValue: true });
1494
1631
  let connectedWorkstream = null;
1495
1632
  if (setupAgentComms) {
1496
1633
  // Auto-connect or create a workstream
1497
- const workstreams = await client.graphql(`query CliInitWorkstreams($cornerId: ID!) {
1634
+ const workstreams = await client
1635
+ .graphql(`query CliInitWorkstreams($cornerId: ID!) {
1498
1636
  corner(id: $cornerId) { workstreams(first: 20) { edges { node { id name status } } } }
1499
- }`, { cornerId: defaultCorner.id }).then((r) => {
1637
+ }`, { cornerId: defaultCorner.id })
1638
+ .then((r) => {
1500
1639
  const corner = r.corner;
1501
1640
  return corner?.workstreams?.edges?.map((e) => e.node) ?? [];
1502
- }).catch(() => []);
1641
+ })
1642
+ .catch(() => []);
1503
1643
  if (workstreams.length > 0) {
1504
1644
  const wsChoice = await prompts.select("Connect a workstream for agent updates:", [
1505
1645
  ...workstreams.map((ws) => ({
@@ -1515,7 +1655,9 @@ async function handleInit(args, runtime, inherited) {
1515
1655
  connectedWorkstream = created.createWorkstream;
1516
1656
  }
1517
1657
  else if (wsChoice !== "__skip__") {
1518
- connectedWorkstream = workstreams.find((ws) => ws.id === wsChoice) ?? null;
1658
+ connectedWorkstream =
1659
+ workstreams.find((ws) => ws.id === wsChoice) ??
1660
+ null;
1519
1661
  }
1520
1662
  }
1521
1663
  else {
@@ -1536,22 +1678,8 @@ async function handleInit(args, runtime, inherited) {
1536
1678
  const updatedConfigPath = runtime.config.getLocalConfigPath(rootDir);
1537
1679
  await writeFile(updatedConfigPath, `${JSON.stringify(nextLocalConfig, null, 2)}\n`, "utf8");
1538
1680
  }
1539
- // Write CLAUDE.md agent instructions
1540
- const claudeMdPath = join(rootDir, "CLAUDE.md");
1541
- const existingClaudeMd = await readOptionalUtf8(claudeMdPath);
1542
- const agentSection = `\n## Agent Communication\n\nWhen working on tasks, report progress to the team through Corners:\n- At the start: \`corners workstream push --type session_start --intent "description" --json\`\n- At natural checkpoints: \`corners workstream push --type checkpoint --message "what you did" --files "file1,file2" --json\`\n- When done: \`corners workstream push --type session_end --summary "what was accomplished" --json\`\n`;
1543
- if (existingClaudeMd && !existingClaudeMd.includes("## Agent Communication")) {
1544
- await writeFile(claudeMdPath, existingClaudeMd + agentSection, "utf8");
1545
- if (!common.json) {
1546
- printLine(runtime.stderr, " Added agent communication instructions to CLAUDE.md");
1547
- }
1548
- }
1549
- else if (!existingClaudeMd) {
1550
- await writeFile(claudeMdPath, `# ${basename(rootDir)}\n${agentSection}`, "utf8");
1551
- if (!common.json) {
1552
- printLine(runtime.stderr, " Created CLAUDE.md with agent communication instructions");
1553
- }
1554
- }
1681
+ // Agent communication instructions are now part of the managed guidance
1682
+ // block in buildGuidanceSection(). No separate append needed.
1555
1683
  // Send test checkpoint
1556
1684
  if (connectedWorkstream) {
1557
1685
  try {
@@ -1591,7 +1719,7 @@ async function handleInit(args, runtime, inherited) {
1591
1719
  const wsMsg = connectedWorkstream
1592
1720
  ? ` Connected workstream: ${connectedWorkstream.name}.`
1593
1721
  : "";
1594
- printLine(runtime.stdout, `Initialized Corners CLI at ${rootDir} with default corner ${defaultCorner.name}.${wsMsg} Run \`${GUIDANCE_SYNC_COMMAND}\` later to refresh managed guidance.`);
1722
+ printLine(runtime.stdout, `Initialized Corners CLI at ${rootDir} with default corner ${defaultCorner.name}.${wsMsg}`);
1595
1723
  }
1596
1724
  return 0;
1597
1725
  });
@@ -1655,9 +1783,7 @@ async function handleSkills(args, runtime, inherited) {
1655
1783
  printSkillsHelp(runtime);
1656
1784
  return 0;
1657
1785
  }
1658
- const rootContext = await requireLocalRoot(runtime, common, {
1659
- emitWarning: false,
1660
- });
1786
+ const rootContext = await requireLocalRoot(runtime, common);
1661
1787
  const state = await resolveSkillsState(rootContext.root.rootDir, rootContext.root.config);
1662
1788
  const payload = {
1663
1789
  ok: true,
@@ -1713,9 +1839,7 @@ async function handleGuidance(args, runtime, inherited) {
1713
1839
  printGuidanceHelp(runtime);
1714
1840
  return 0;
1715
1841
  }
1716
- const rootContext = await requireLocalRoot(runtime, common, {
1717
- emitWarning: false,
1718
- });
1842
+ const rootContext = await requireLocalRoot(runtime, common);
1719
1843
  const payload = {
1720
1844
  ok: true,
1721
1845
  root: rootContext.root.rootDir,
@@ -1751,9 +1875,7 @@ async function handleGuidance(args, runtime, inherited) {
1751
1875
  printGuidanceHelp(runtime);
1752
1876
  return 0;
1753
1877
  }
1754
- const rootContext = await requireLocalRoot(runtime, common, {
1755
- emitWarning: false,
1756
- });
1878
+ const rootContext = await requireLocalRoot(runtime, common);
1757
1879
  const managedTargets = rootContext.root.config.guidance?.managedTargets !== undefined
1758
1880
  ? normalizeGuidanceTargets(rootContext.root.config.guidance.managedTargets)
1759
1881
  : rootContext.guidance.managedTargets;
@@ -1871,6 +1993,167 @@ async function handleCorner(args, runtime, inherited) {
1871
1993
  throw new CLIError(`Unknown corner command: ${subcommand}`);
1872
1994
  }
1873
1995
  }
1996
+ function printAgentHelp(runtime) {
1997
+ printLine(runtime.stdout, [
1998
+ "corners agent",
1999
+ "",
2000
+ "Usage:",
2001
+ " corners agent start [--intent <text>] [--json]",
2002
+ " corners agent checkpoint --message <text> [--files <paths>] [--json]",
2003
+ " corners agent intent --scope <text> [--files <paths>] [--json]",
2004
+ " corners agent end [--summary <text>] [--json]",
2005
+ "",
2006
+ "Thin wrappers over workstream push for agent session lifecycle.",
2007
+ "Auto-creates a workstream if none is active.",
2008
+ ].join("\n"));
2009
+ }
2010
+ function resolveGitBranch() {
2011
+ try {
2012
+ return (execSync("git branch --show-current", {
2013
+ encoding: "utf8",
2014
+ timeout: 3000,
2015
+ }).trim() || "unknown");
2016
+ }
2017
+ catch {
2018
+ return "unknown";
2019
+ }
2020
+ }
2021
+ async function resolveAgentWorkstreamId(runtime, common, client) {
2022
+ // Try the normal resolution first (explicit flag or binding)
2023
+ const binding = await runtime.config.getBinding(runtime.cwd);
2024
+ if (binding) {
2025
+ return binding.workstreamId;
2026
+ }
2027
+ // Auto-create a workstream named after the git branch
2028
+ const root = await runtime.config.findLocalRoot(runtime.cwd);
2029
+ if (!root) {
2030
+ throw new CLIError("No Corners root found. Run `corners init` first.", {
2031
+ json: common.json,
2032
+ });
2033
+ }
2034
+ const branch = resolveGitBranch();
2035
+ const wsName = `agent/${branch}`;
2036
+ const result = await client.graphql(`mutation CliAgentAutoCreate($input: CreateWorkstreamInput!) { createWorkstream(input: $input) { id name } }`, { input: { cornerId: root.config.defaultCorner.id, name: wsName } });
2037
+ // Bind the auto-created workstream so future commands use it
2038
+ await runtime.config.setBinding(runtime.cwd, {
2039
+ profile: root.config.profile,
2040
+ workspace: root.config.workspace ?? "",
2041
+ workstreamId: result.createWorkstream.id,
2042
+ cornerId: root.config.defaultCorner.id,
2043
+ boundAt: toIsoString(),
2044
+ });
2045
+ if (!common.json) {
2046
+ printLine(runtime.stderr, `Auto-created workstream "${wsName}" (${result.createWorkstream.id})`);
2047
+ }
2048
+ return result.createWorkstream.id;
2049
+ }
2050
+ async function handleAgent(args, runtime, inherited) {
2051
+ const subcommand = args[0];
2052
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
2053
+ printAgentHelp(runtime);
2054
+ return 0;
2055
+ }
2056
+ const parsed = parseArgs({
2057
+ args: args.slice(1),
2058
+ allowPositionals: false,
2059
+ options: {
2060
+ json: { type: "boolean" },
2061
+ help: { type: "boolean", short: "h" },
2062
+ profile: { type: "string" },
2063
+ "api-url": { type: "string" },
2064
+ workstream: { type: "string", short: "w" },
2065
+ message: { type: "string" },
2066
+ intent: { type: "string" },
2067
+ summary: { type: "string" },
2068
+ scope: { type: "string" },
2069
+ files: { type: "string" },
2070
+ },
2071
+ });
2072
+ const common = mergeCommonOptions(inherited, {
2073
+ json: parsed.values.json,
2074
+ profile: parsed.values.profile,
2075
+ apiUrl: parsed.values["api-url"],
2076
+ });
2077
+ if (parsed.values.help) {
2078
+ printAgentHelp(runtime);
2079
+ return 0;
2080
+ }
2081
+ // Resolve update type and synthesize message from subcommand-specific flags
2082
+ let updateType;
2083
+ let message;
2084
+ switch (subcommand) {
2085
+ case "start":
2086
+ updateType = "session_start";
2087
+ message = parsed.values.intent ?? "session started";
2088
+ break;
2089
+ case "checkpoint":
2090
+ updateType = "checkpoint";
2091
+ message = parsed.values.message ?? "";
2092
+ if (!message) {
2093
+ throw new CLIError("Checkpoint needs a message. Use --message.", {
2094
+ json: common.json,
2095
+ });
2096
+ }
2097
+ break;
2098
+ case "intent":
2099
+ updateType = "intent";
2100
+ message = parsed.values.scope ?? "";
2101
+ if (!message) {
2102
+ throw new CLIError("Intent needs a scope. Use --scope.", {
2103
+ json: common.json,
2104
+ });
2105
+ }
2106
+ break;
2107
+ case "end":
2108
+ updateType = "session_end";
2109
+ message = parsed.values.summary ?? "session ended";
2110
+ break;
2111
+ default:
2112
+ throw new CLIError(`Unknown agent command: ${subcommand}`, {
2113
+ json: common.json,
2114
+ });
2115
+ }
2116
+ const { guidance, client } = await requireCommandProfile(runtime, common);
2117
+ // Resolve workstream with auto-create fallback
2118
+ const workstreamId = parsed.values.workstream
2119
+ ? parsed.values.workstream
2120
+ : await resolveAgentWorkstreamId(runtime, common, client);
2121
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
2122
+ // Detect agent type from environment
2123
+ const agentType = (process.env.CLAUDE_CODE ? "claude-code" : null) ??
2124
+ (process.env.CODEX_SESSION ? "codex" : null) ??
2125
+ null;
2126
+ const filesTouched = parsed.values.files
2127
+ ? parsed.values.files.split(",").map((f) => f.trim())
2128
+ : null;
2129
+ const scope = parsed.values.scope ?? null;
2130
+ const updateResult = await client.graphql(RECORD_WORKSTREAM_UPDATE_MUTATION, {
2131
+ input: {
2132
+ workstreamId: workstream.id,
2133
+ updateType: toGraphQLWorkstreamUpdateType(updateType),
2134
+ content: message,
2135
+ summary: parsed.values.summary,
2136
+ ...(agentType ? { agentType } : {}),
2137
+ ...(filesTouched ? { filesTouched } : {}),
2138
+ ...(scope ? { scope } : {}),
2139
+ },
2140
+ });
2141
+ const payload = {
2142
+ ok: true,
2143
+ success: true,
2144
+ workstreamId: workstream.id,
2145
+ type: updateType,
2146
+ messageId: updateResult.recordWorkstreamUpdate.id,
2147
+ update: updateResult.recordWorkstreamUpdate,
2148
+ };
2149
+ if (common.json) {
2150
+ printJson(runtime.stdout, withGuidanceJsonMetadata(payload, guidance));
2151
+ }
2152
+ else {
2153
+ printLine(runtime.stdout, `Recorded agent ${subcommand} on ${workstream.id}.`);
2154
+ }
2155
+ return 0;
2156
+ }
1874
2157
  async function handleWorkstream(args, runtime, inherited) {
1875
2158
  const subcommand = args[0];
1876
2159
  if (!subcommand || subcommand === "help" || subcommand === "--help") {
@@ -2768,6 +3051,50 @@ async function handleWorkstream(args, runtime, inherited) {
2768
3051
  throw new CLIError(`Unknown workstream command: ${subcommand}`);
2769
3052
  }
2770
3053
  }
3054
+ async function handleOpen(args, runtime) {
3055
+ const parsed = parseArgs({
3056
+ args,
3057
+ allowPositionals: false,
3058
+ options: {
3059
+ workstream: { type: "string", short: "w" },
3060
+ corner: { type: "string", short: "c" },
3061
+ help: { type: "boolean", short: "h" },
3062
+ },
3063
+ });
3064
+ if (parsed.values.help) {
3065
+ printLine(runtime.stdout, [
3066
+ "corners open",
3067
+ "",
3068
+ "Open the Corners desktop app, optionally navigating to a specific workstream or corner.",
3069
+ "",
3070
+ "Usage:",
3071
+ " corners open [--workstream <id>] [--corner <slug>]",
3072
+ "",
3073
+ "Options:",
3074
+ " --workstream, -w <id> Open to a specific workstream",
3075
+ " --corner, -c <slug> Open to a specific corner",
3076
+ " --help, -h Show this help",
3077
+ "",
3078
+ "Examples:",
3079
+ " corners open",
3080
+ " corners open --workstream ws_abc123",
3081
+ " corners open --corner engineering",
3082
+ ].join("\n"));
3083
+ return 0;
3084
+ }
3085
+ let url = "corners://";
3086
+ if (parsed.values.workstream) {
3087
+ url = `corners://open?workstream=${encodeURIComponent(parsed.values.workstream)}`;
3088
+ }
3089
+ else if (parsed.values.corner) {
3090
+ url = `corners://open?corner=${encodeURIComponent(parsed.values.corner)}`;
3091
+ }
3092
+ const opened = await openUrlInBrowser(url);
3093
+ if (!opened) {
3094
+ throw new CLIError("Could not open the Corners desktop app. Is it installed?");
3095
+ }
3096
+ return 0;
3097
+ }
2771
3098
  async function handleMcp(args, runtime, inherited) {
2772
3099
  const subcommand = args[0];
2773
3100
  if (!subcommand || subcommand === "help" || subcommand === "--help") {
@@ -2855,11 +3182,15 @@ export async function runCli(argv, runtime = createRuntime()) {
2855
3182
  return handleCorner(parsed.rest.slice(1), runtime, parsed.common);
2856
3183
  case "whoami":
2857
3184
  return handleWhoAmI(parsed.rest.slice(1), runtime, parsed.common);
3185
+ case "agent":
3186
+ return handleAgent(parsed.rest.slice(1), runtime, parsed.common);
2858
3187
  case "workstream":
2859
3188
  case "ws":
2860
3189
  return handleWorkstream(parsed.rest.slice(1), runtime, parsed.common);
2861
3190
  case "mcp":
2862
3191
  return handleMcp(parsed.rest.slice(1), runtime, parsed.common);
3192
+ case "open":
3193
+ return handleOpen(parsed.rest.slice(1), runtime);
2863
3194
  default:
2864
3195
  throw new CLIError(`Unknown command: ${command}`, {
2865
3196
  json: parsed.common.json,