@trycadence/cli 0.1.2 → 0.1.4

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.
Files changed (2) hide show
  1. package/dist/cadence +551 -25
  2. package/package.json +1 -1
package/dist/cadence CHANGED
@@ -1451,9 +1451,13 @@ function createCadenceClient(options = {}) {
1451
1451
  cli: {
1452
1452
  start: (input) => rpc.auth.cli.start.mutate(input),
1453
1453
  poll: (input) => rpc.auth.cli.poll.query(input),
1454
- complete: (input) => rpc.auth.cli.complete.mutate(input)
1454
+ complete: (input) => rpc.auth.cli.complete.mutate(input),
1455
+ refresh: (input) => rpc.auth.cli.refresh.mutate(input)
1455
1456
  }
1456
1457
  },
1458
+ actors: {
1459
+ ensureWorkspaceAgent: (input) => rpc.actors.ensureWorkspaceAgent.mutate(input)
1460
+ },
1457
1461
  events: {
1458
1462
  list: (input) => rpc.events.list.query(input)
1459
1463
  },
@@ -1467,7 +1471,8 @@ function createCadenceClient(options = {}) {
1467
1471
  attach: (input) => rpc.tickets.attach.mutate(input),
1468
1472
  update: (input) => rpc.tickets.update.mutate(input),
1469
1473
  changeStatus: (input) => rpc.tickets.changeStatus.mutate(input),
1470
- complete: (input) => rpc.tickets.complete.mutate(input)
1474
+ complete: (input) => rpc.tickets.complete.mutate(input),
1475
+ log: (input) => rpc.tickets.log.mutate(input)
1471
1476
  },
1472
1477
  intake: {
1473
1478
  create: (input) => rpc.intake.create.mutate(input),
@@ -1476,6 +1481,7 @@ function createCadenceClient(options = {}) {
1476
1481
  sessions: {
1477
1482
  start: (input) => rpc.sessions.start.mutate(input),
1478
1483
  end: (input) => rpc.sessions.end.mutate(input),
1484
+ files: (input) => rpc.sessions.files.mutate(input),
1479
1485
  current: (input) => rpc.sessions.current.query(input),
1480
1486
  leases: {
1481
1487
  create: (input) => rpc.sessions.leases.create.mutate(input),
@@ -1485,7 +1491,13 @@ function createCadenceClient(options = {}) {
1485
1491
  changesets: {
1486
1492
  create: (input) => rpc.changesets.create.mutate(input),
1487
1493
  get: (input) => rpc.changesets.get.query(input),
1488
- list: (input) => rpc.changesets.list.query(input)
1494
+ context: (input) => rpc.changesets.context.query(input),
1495
+ list: (input) => rpc.changesets.list.query(input),
1496
+ notes: {
1497
+ get: (input) => rpc.changesets.notes.get.query(input),
1498
+ put: (input) => rpc.changesets.notes.put.mutate(input),
1499
+ apply: (input) => rpc.changesets.notes.markApplied.mutate(input)
1500
+ }
1489
1501
  },
1490
1502
  projects: {
1491
1503
  default: () => rpc.projects.default.query(),
@@ -1497,14 +1509,23 @@ function createCadenceClient(options = {}) {
1497
1509
 
1498
1510
  // src/index.ts
1499
1511
  import { spawnSync } from "child_process";
1500
- import { mkdir, writeFile } from "fs/promises";
1501
- import { dirname, join, parse } from "path";
1512
+ import { createHash, randomUUID } from "crypto";
1513
+ import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
1514
+ import { basename, dirname, isAbsolute, join, parse } from "path";
1502
1515
  import { createInterface } from "readline/promises";
1503
1516
  var ticketPriorities = ["low", "normal", "high", "urgent"];
1504
1517
  var ticketStatuses = ["backlog", "ready", "in_progress", "blocked", "review", "done", "abandoned"];
1518
+ var sessionFileChangeKinds = ["added", "modified", "deleted", "renamed", "unknown"];
1519
+ var workLogEntryKinds = ["intent", "decision", "rationale", "action", "verification", "blocker", "correction", "note"];
1520
+ var workLogParentSelectors = ["last", "ticket-last", "session-last", "last-decision", "last-correction", "last-action"];
1521
+ var changesetPrNoteSources = ["agent", "human", "system"];
1505
1522
  var defaultLeaseTtlSeconds = 5 * 60 * 60;
1506
1523
  var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
1507
1524
  var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
1525
+ var credentialRefreshSkewMs = 60 * 1000;
1526
+ var credentialRefreshLockTimeoutMs = 10 * 1000;
1527
+ var credentialRefreshLockStaleMs = 60 * 1000;
1528
+ var credentialRefreshLockPollMs = 100;
1508
1529
 
1509
1530
  class CliError extends Error {
1510
1531
  code;
@@ -1524,14 +1545,20 @@ function readFlagValue(argv, index, flag) {
1524
1545
  return value;
1525
1546
  }
1526
1547
  var knownCommandPaths = [
1548
+ ["actors", "ensure-workspace-agent"],
1527
1549
  ["auth", "login"],
1528
1550
  ["auth", "status"],
1529
1551
  ["auth", "logout"],
1530
1552
  ["changesets", "create"],
1531
1553
  ["changesets", "list"],
1532
1554
  ["changesets", "get"],
1555
+ ["changesets", "current"],
1556
+ ["changesets", "notes", "get"],
1557
+ ["changesets", "notes", "put"],
1558
+ ["changesets", "notes", "apply"],
1533
1559
  ["sessions", "start"],
1534
1560
  ["sessions", "end"],
1561
+ ["sessions", "files"],
1535
1562
  ["sessions", "current"],
1536
1563
  ["intake", "dismiss"],
1537
1564
  ["intake"],
@@ -1539,6 +1566,7 @@ var knownCommandPaths = [
1539
1566
  ["tickets", "complete"],
1540
1567
  ["tickets", "release"],
1541
1568
  ["tickets", "claim"],
1569
+ ["tickets", "log"],
1542
1570
  ["tickets", "update"],
1543
1571
  ["tickets", "create"],
1544
1572
  ["tickets", "list"],
@@ -1603,7 +1631,10 @@ function parseCliArgs(argv) {
1603
1631
  continue;
1604
1632
  }
1605
1633
  if (arg.startsWith("--")) {
1606
- options[arg.slice(2)] = readFlagValue(argv, index, arg);
1634
+ const key = arg.slice(2);
1635
+ const value = readFlagValue(argv, index, arg);
1636
+ options[key] = key === "file" && options[key] ? `${options[key]}
1637
+ ${value}` : value;
1607
1638
  index += 1;
1608
1639
  continue;
1609
1640
  }
@@ -1632,12 +1663,40 @@ function safeJsonParse(source, filePath) {
1632
1663
  }
1633
1664
  const record = parsed;
1634
1665
  const server = typeof record.server === "string" ? record.server : undefined;
1666
+ const webBaseUrl = typeof record.webBaseUrl === "string" ? record.webBaseUrl : undefined;
1667
+ const profile = typeof record.profile === "string" ? record.profile : undefined;
1635
1668
  const projectId = typeof record.projectId === "string" ? record.projectId : typeof record.project === "string" ? record.project : undefined;
1669
+ const profiles = parseProfiles(record.profiles, filePath);
1636
1670
  return {
1637
1671
  ...server ? { server } : {},
1638
- ...projectId ? { projectId } : {}
1672
+ ...webBaseUrl ? { webBaseUrl } : {},
1673
+ ...projectId ? { projectId } : {},
1674
+ ...profile ? { profile } : {},
1675
+ ...profiles ? { profiles } : {}
1639
1676
  };
1640
1677
  }
1678
+ function parseProfiles(value, filePath) {
1679
+ if (value === undefined) {
1680
+ return;
1681
+ }
1682
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1683
+ throw new CliError("CONFIG_ERROR", `${filePath} profiles must be a JSON object.`);
1684
+ }
1685
+ const profiles = {};
1686
+ for (const [name, rawProfile] of Object.entries(value)) {
1687
+ if (!rawProfile || typeof rawProfile !== "object" || Array.isArray(rawProfile)) {
1688
+ throw new CliError("CONFIG_ERROR", `${filePath} profile '${name}' must be a JSON object.`);
1689
+ }
1690
+ const profileRecord = rawProfile;
1691
+ const server = typeof profileRecord.server === "string" ? profileRecord.server : undefined;
1692
+ const webBaseUrl = typeof profileRecord.webBaseUrl === "string" ? profileRecord.webBaseUrl : undefined;
1693
+ profiles[name] = {
1694
+ ...server ? { server } : {},
1695
+ ...webBaseUrl ? { webBaseUrl } : {}
1696
+ };
1697
+ }
1698
+ return profiles;
1699
+ }
1641
1700
  async function readOptionalConfig(filePath) {
1642
1701
  const file = Bun.file(filePath);
1643
1702
  if (!await file.exists()) {
@@ -1657,12 +1716,104 @@ async function mergeConfigFile(filePath, updates) {
1657
1716
  ...updates
1658
1717
  });
1659
1718
  }
1660
- async function findRepoConfigPath(cwd) {
1719
+ function parseAgentIdentityCache(source, filePath) {
1720
+ const parsed = JSON.parse(source);
1721
+ if (!parsed || typeof parsed !== "object") {
1722
+ throw new CliError("CONFIG_ERROR", `${filePath} must contain a JSON object.`);
1723
+ }
1724
+ const record = parsed;
1725
+ const identities = record.identities;
1726
+ if (record.version !== 1 || typeof record.workspaceRef !== "string" || typeof record.workspaceName !== "string" || !identities || typeof identities !== "object" || Array.isArray(identities)) {
1727
+ throw new CliError("CONFIG_ERROR", `${filePath} is not a valid Cadence agent identity cache.`);
1728
+ }
1729
+ const normalizedIdentities = {};
1730
+ for (const [key, value] of Object.entries(identities)) {
1731
+ if (!value || typeof value !== "object") {
1732
+ throw new CliError("CONFIG_ERROR", `${filePath} contains an invalid identity entry.`);
1733
+ }
1734
+ const identity2 = value;
1735
+ if (typeof identity2.actorId !== "string" || typeof identity2.displayName !== "string" || identity2.kind !== "agent" || typeof identity2.agentKind !== "string" || typeof identity2.updatedAt !== "string") {
1736
+ throw new CliError("CONFIG_ERROR", `${filePath} contains an invalid identity entry.`);
1737
+ }
1738
+ normalizedIdentities[key] = {
1739
+ actorId: identity2.actorId,
1740
+ displayName: identity2.displayName,
1741
+ kind: "agent",
1742
+ agentKind: identity2.agentKind,
1743
+ updatedAt: identity2.updatedAt
1744
+ };
1745
+ }
1746
+ return {
1747
+ version: 1,
1748
+ workspaceRef: record.workspaceRef,
1749
+ workspaceName: record.workspaceName,
1750
+ identities: normalizedIdentities
1751
+ };
1752
+ }
1753
+ async function readOptionalAgentIdentityCache(filePath) {
1754
+ const file = Bun.file(filePath);
1755
+ if (!await file.exists()) {
1756
+ return null;
1757
+ }
1758
+ return parseAgentIdentityCache(await file.text(), filePath);
1759
+ }
1760
+ function agentIdentityCachePath(config, options) {
1761
+ const cwd = options.cwd ?? process.cwd();
1762
+ const cadenceDirectory = config.localConfigPath ? dirname(config.localConfigPath) : config.repoConfigPath ? dirname(config.repoConfigPath) : join(cwd, ".cadence");
1763
+ return join(cadenceDirectory, "agent-identities.json");
1764
+ }
1765
+ async function writeAgentIdentityCache(filePath, cache) {
1766
+ await mkdir(dirname(filePath), { recursive: true });
1767
+ await writeFile(filePath, `${JSON.stringify(cache, null, 2)}
1768
+ `);
1769
+ }
1770
+ async function ensureWorkspaceAgentIdentity(parsed, config, client, options) {
1771
+ const projectId = requireProjectId(config);
1772
+ const agentKind = requireOption(parsed, "agent-kind");
1773
+ const filePath = agentIdentityCachePath(config, options);
1774
+ const existing = await readOptionalAgentIdentityCache(filePath);
1775
+ const explicitWorkspaceRef = parsed.options["workspace-ref"];
1776
+ const workspaceRef = explicitWorkspaceRef ?? existing?.workspaceRef ?? `local:${randomUUID()}`;
1777
+ const workspaceName = parsed.options["workspace-name"] ?? existing?.workspaceName ?? basename(options.cwd ?? process.cwd());
1778
+ const identities = explicitWorkspaceRef && existing && explicitWorkspaceRef !== existing.workspaceRef ? {} : { ...existing?.identities ?? {} };
1779
+ const ensured = await client.actors.ensureWorkspaceAgent({
1780
+ projectId,
1781
+ workspaceRef,
1782
+ workspaceName,
1783
+ agentKind,
1784
+ ...parsed.options["display-name"] ? { displayName: parsed.options["display-name"] } : {}
1785
+ });
1786
+ const updatedAt = new Date().toISOString();
1787
+ const cache = {
1788
+ version: 1,
1789
+ workspaceRef,
1790
+ workspaceName,
1791
+ identities: {
1792
+ ...identities,
1793
+ [agentKind]: {
1794
+ actorId: ensured.actorId,
1795
+ displayName: ensured.displayName,
1796
+ kind: "agent",
1797
+ agentKind: ensured.agentKind,
1798
+ updatedAt
1799
+ }
1800
+ }
1801
+ };
1802
+ await writeAgentIdentityCache(filePath, cache);
1803
+ return {
1804
+ ...cache.identities[agentKind],
1805
+ workspaceRef: cache.workspaceRef,
1806
+ workspaceName: cache.workspaceName
1807
+ };
1808
+ }
1809
+ async function findRepoCadenceDirectory(cwd) {
1661
1810
  let current = cwd;
1662
1811
  while (true) {
1663
- const candidate = join(current, ".cadence", "config.json");
1664
- if (await Bun.file(candidate).exists()) {
1665
- return candidate;
1812
+ const cadenceDirectory = join(current, ".cadence");
1813
+ const repoConfig = join(cadenceDirectory, "config.json");
1814
+ const localConfig = join(cadenceDirectory, "config.local.json");
1815
+ if (await Bun.file(repoConfig).exists() || await Bun.file(localConfig).exists()) {
1816
+ return cadenceDirectory;
1666
1817
  }
1667
1818
  const parent = dirname(current);
1668
1819
  if (parent === current) {
@@ -1678,18 +1829,34 @@ async function resolveCliConfig(flags, options = {}) {
1678
1829
  const env = options.env ?? process.env;
1679
1830
  const cwd = options.cwd ?? process.cwd();
1680
1831
  const globalConfigPath = join(getConfigHome(env), "config.json");
1681
- const repoConfigPath = await findRepoConfigPath(cwd);
1832
+ const repoCadenceDirectory = await findRepoCadenceDirectory(cwd);
1833
+ const repoConfigPath = repoCadenceDirectory ? join(repoCadenceDirectory, "config.json") : null;
1834
+ const localConfigPath = repoCadenceDirectory ? join(repoCadenceDirectory, "config.local.json") : null;
1682
1835
  const repoConfigPromise = repoConfigPath ? readOptionalConfig(repoConfigPath) : Promise.resolve({});
1683
- const [globalConfig, repoConfig] = await Promise.all([
1836
+ const localConfigPromise = localConfigPath ? readOptionalConfig(localConfigPath) : Promise.resolve({});
1837
+ const [globalConfig, repoConfig, localConfig] = await Promise.all([
1684
1838
  readOptionalConfig(globalConfigPath),
1685
- repoConfigPromise
1839
+ repoConfigPromise,
1840
+ localConfigPromise
1686
1841
  ]);
1687
- const server = flags.server ?? repoConfig.server ?? globalConfig.server ?? defaultCliApiBaseUrl;
1688
- const projectId = flags.project ?? repoConfig.projectId ?? globalConfig.projectId ?? null;
1842
+ const profiles = {
1843
+ ...globalConfig.profiles ?? {},
1844
+ ...repoConfig.profiles ?? {},
1845
+ ...localConfig.profiles ?? {}
1846
+ };
1847
+ const envProfile = env.CADENCE_PROFILE;
1848
+ const profile = envProfile ?? localConfig.profile ?? repoConfig.profile ?? globalConfig.profile ?? null;
1849
+ const profileConfig = profile ? profiles[profile] : undefined;
1850
+ const server = flags.server ?? env.CADENCE_SERVER ?? localConfig.server ?? profileConfig?.server ?? repoConfig.server ?? globalConfig.server ?? defaultCliApiBaseUrl;
1851
+ const webBaseUrl = env.CADENCE_WEB_BASE_URL ?? localConfig.webBaseUrl ?? profileConfig?.webBaseUrl ?? repoConfig.webBaseUrl ?? globalConfig.webBaseUrl ?? deriveWebBaseUrl(server);
1852
+ const projectId = flags.project ?? localConfig.projectId ?? repoConfig.projectId ?? globalConfig.projectId ?? null;
1689
1853
  return {
1690
1854
  server,
1855
+ webBaseUrl,
1856
+ profile,
1691
1857
  projectId,
1692
1858
  repoConfigPath,
1859
+ localConfigPath,
1693
1860
  globalConfigPath
1694
1861
  };
1695
1862
  }
@@ -1740,7 +1907,7 @@ function isInteractive(options) {
1740
1907
  return options.isInteractive ?? Boolean(process.stdin.isTTY && process.stderr.isTTY);
1741
1908
  }
1742
1909
  function getCliWebBaseUrl(config, parsed, options) {
1743
- return parsed.options["web-base-url"] ?? options.env?.CADENCE_WEB_BASE_URL ?? process.env.CADENCE_WEB_BASE_URL ?? deriveWebBaseUrl(config.server);
1910
+ return parsed.options["web-base-url"] ?? options.env?.CADENCE_WEB_BASE_URL ?? process.env.CADENCE_WEB_BASE_URL ?? config.webBaseUrl;
1744
1911
  }
1745
1912
  function deriveWebBaseUrl(server) {
1746
1913
  try {
@@ -1775,6 +1942,17 @@ async function sleep2(milliseconds, options) {
1775
1942
  }
1776
1943
  await new Promise((resolve) => setTimeout(resolve, milliseconds));
1777
1944
  }
1945
+ async function writeInteractiveStatus(message, options) {
1946
+ if (!isInteractive(options)) {
1947
+ return;
1948
+ }
1949
+ if (options.writeStatus) {
1950
+ await options.writeStatus(message);
1951
+ return;
1952
+ }
1953
+ process.stderr.write(`${message}
1954
+ `);
1955
+ }
1778
1956
  function encodeCredential(credential) {
1779
1957
  return JSON.stringify(credential);
1780
1958
  }
@@ -1802,6 +1980,68 @@ async function readStoredCredential(store, server) {
1802
1980
  const rawCredential = await store.getCredential(server);
1803
1981
  return rawCredential ? decodeCredential(rawCredential) : null;
1804
1982
  }
1983
+ function normalizeServerForCredentialLock(server) {
1984
+ try {
1985
+ return new URL(server).toString().replace(/\/$/, "");
1986
+ } catch {
1987
+ return server.trim();
1988
+ }
1989
+ }
1990
+ function credentialRefreshLockPath(config) {
1991
+ const normalizedServer = normalizeServerForCredentialLock(config.server);
1992
+ const serverHash = createHash("sha256").update(normalizedServer).digest("hex").slice(0, 24);
1993
+ return join(dirname(config.globalConfigPath), "locks", `credential-refresh-${serverHash}.lock`);
1994
+ }
1995
+ async function waitForCredentialRefreshLockPoll() {
1996
+ await new Promise((resolve) => setTimeout(resolve, credentialRefreshLockPollMs));
1997
+ }
1998
+ async function removeStaleCredentialRefreshLock(lockPath, now) {
1999
+ try {
2000
+ const lockStats = await stat(lockPath);
2001
+ if (now - lockStats.mtimeMs < credentialRefreshLockStaleMs) {
2002
+ return false;
2003
+ }
2004
+ await rm(lockPath, { recursive: true, force: true });
2005
+ return true;
2006
+ } catch (error) {
2007
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
2008
+ return false;
2009
+ }
2010
+ throw error;
2011
+ }
2012
+ }
2013
+ async function acquireCredentialRefreshLock(config) {
2014
+ const lockPath = credentialRefreshLockPath(config);
2015
+ const deadline = Date.now() + credentialRefreshLockTimeoutMs;
2016
+ await mkdir(dirname(lockPath), { recursive: true });
2017
+ while (true) {
2018
+ try {
2019
+ await mkdir(lockPath);
2020
+ await writeFile(join(lockPath, "holder.json"), `${JSON.stringify({
2021
+ pid: process.pid,
2022
+ server: normalizeServerForCredentialLock(config.server),
2023
+ acquiredAt: new Date().toISOString()
2024
+ }, null, 2)}
2025
+ `);
2026
+ return { path: lockPath };
2027
+ } catch (error) {
2028
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
2029
+ throw error;
2030
+ }
2031
+ const now = Date.now();
2032
+ const removedStaleLock = await removeStaleCredentialRefreshLock(lockPath, now);
2033
+ if (!removedStaleLock && now >= deadline) {
2034
+ throw new CliError("AUTH_REFRESH_LOCK_TIMEOUT", "Timed out waiting for another Cadence credential refresh to finish.", {
2035
+ server: config.server
2036
+ });
2037
+ }
2038
+ await waitForCredentialRefreshLockPoll();
2039
+ }
2040
+ }
2041
+ }
2042
+ async function releaseCredentialRefreshLock(lock) {
2043
+ await rm(lock.path, { recursive: true, force: true });
2044
+ }
1805
2045
  async function requireCredentialStore(store) {
1806
2046
  if (!await store.isAvailable()) {
1807
2047
  throw new CliError("CREDENTIAL_STORE_UNAVAILABLE", "OS secure credential storage is unavailable.");
@@ -1855,6 +2095,7 @@ function helpText() {
1855
2095
  " cadence auth login [--json]",
1856
2096
  " cadence auth status [--json]",
1857
2097
  " cadence auth logout [--json]",
2098
+ " cadence actors ensure-workspace-agent --agent-kind <kind> [--workspace-name <name>] [--workspace-ref <ref>] [--display-name <name>] [--project <project-id>] [--json]",
1858
2099
  " cadence status [--project <project-id>] [--json]",
1859
2100
  " cadence projects list [--json]",
1860
2101
  " cadence work overview [--project <project-id>] [--json]",
@@ -1864,36 +2105,103 @@ function helpText() {
1864
2105
  " cadence tickets attach <ticket-id> --from-intake <intake-id> --if-version <version> [--project <project-id>] [--json]",
1865
2106
  " cadence tickets create --title <text> [--from-intake <intake-id>] [--project <project-id>] [--json]",
1866
2107
  " cadence tickets update <ticket-id> --if-version <version> [--title <text>] [--description <text>] [--priority <priority>] [--status <status>] [--project <project-id>] [--json]",
1867
- " cadence tickets claim <ticket-id> --session <session-id> [--project <project-id>] [--json]",
2108
+ " cadence tickets claim <ticket-id> --session <session-id> [--actor <actor-id>] [--project <project-id>] [--json]",
1868
2109
  " cadence tickets release <ticket-id> --lease <lease-id> [--project <project-id>] [--json]",
2110
+ " cadence tickets log <ticket-id> --kind <intent|decision|rationale|action|verification|blocker|correction|note> --body <text> [--summary <text>] [--under <entry-id|ticket-last|session-last|last-decision|last-correction|last-action>] [--session <session-id>] [--changeset <changeset-id>] [--project <project-id>] [--json]",
1869
2111
  " cadence tickets complete <ticket-id> --if-version <version> [--summary <summary>] [--project <project-id>] [--json]",
1870
- " cadence sessions start --ticket <ticket-id> [--changeset <changeset-id>] [--project <project-id>] [--json]",
2112
+ " cadence sessions start --ticket <ticket-id> [--changeset <changeset-id>] [--actor <actor-id>] [--project <project-id>] [--json]",
1871
2113
  " cadence sessions end <session-id> --summary <summary> [--project <project-id>] [--json]",
2114
+ " cadence sessions files <session-id> --file <path> [--file <path>] [--kind <added|modified|deleted|renamed|unknown>] [--changeset <changeset-id>] [--project <project-id>] [--json]",
1872
2115
  " cadence changesets create --ticket <ticket-id> --branch <branch> [--base-branch <branch>] [--project <project-id>] [--json]",
1873
2116
  " cadence intake dismiss <intake-id> --reason <reason> [--project <project-id>] [--json]",
1874
2117
  " cadence sessions current [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--json]",
2118
+ " cadence changesets current [--branch current|<branch>] [--project <project-id>] [--json]",
1875
2119
  " cadence changesets get <changeset-id> [--project <project-id>] [--json]",
1876
2120
  " cadence changesets list [--project <project-id>] [--ticket <ticket-id>] [--status <status>] [--json]",
2121
+ " cadence changesets notes get [--changeset <id>|--branch current|<branch>] [--project <project-id>] [--json]",
2122
+ " cadence changesets notes put [--changeset <id>|--branch current|<branch>] --title <text> --body-file <path> [--head-sha <sha>] [--base-sha <sha>] [--pr-url <url>] [--pr-number <n>] [--project <project-id>] [--json]",
2123
+ " cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
1877
2124
  " cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
1878
2125
  "",
1879
2126
  "Global flags:",
1880
2127
  " --project <id> Cadence project ID or org/project slug",
2128
+ " --server <url> Cadence API server override",
1881
2129
  " --json Print stable JSON envelope",
1882
2130
  "",
2131
+ "Work log parent selectors:",
2132
+ " --under ticket-last attaches under the latest work-log entry on the Ticket.",
2133
+ " --under session-last attaches under the latest entry in --session.",
2134
+ " --under last-decision|last-correction|last-action attaches under the latest matching kind.",
2135
+ " --under last is only valid with --session; without a session use ticket-last or a kind selector.",
2136
+ "",
1883
2137
  "Auth options:",
1884
2138
  " --web-base-url <url> Browser login base URL"
1885
2139
  ].join(`
1886
2140
  `);
1887
2141
  }
1888
- async function createClient(config, options) {
2142
+ function credentialExpiresSoon(credential, now = new Date) {
2143
+ if (!credential.expiresAt) {
2144
+ return false;
2145
+ }
2146
+ const expiresAt = new Date(credential.expiresAt).getTime();
2147
+ if (Number.isNaN(expiresAt)) {
2148
+ return false;
2149
+ }
2150
+ return expiresAt <= now.getTime() + credentialRefreshSkewMs;
2151
+ }
2152
+ async function readFreshAccessToken(config, store, authClient) {
2153
+ const credential = await readStoredCredential(store, config.server);
2154
+ if (!credential) {
2155
+ return "";
2156
+ }
2157
+ if (!credentialExpiresSoon(credential)) {
2158
+ return credential.accessToken;
2159
+ }
2160
+ const lock = await acquireCredentialRefreshLock(config);
2161
+ try {
2162
+ const lockedCredential = await readStoredCredential(store, config.server);
2163
+ if (!lockedCredential) {
2164
+ return "";
2165
+ }
2166
+ if (!credentialExpiresSoon(lockedCredential)) {
2167
+ return lockedCredential.accessToken;
2168
+ }
2169
+ if (!lockedCredential.refreshToken) {
2170
+ throw new CliError("AUTH_REFRESH_REQUIRED", "Stored Cadence credential is expired and cannot be refreshed because no refresh token is stored.");
2171
+ }
2172
+ try {
2173
+ const refreshed = await authClient.auth.cli.refresh({
2174
+ refreshToken: lockedCredential.refreshToken
2175
+ });
2176
+ await store.setCredential(config.server, encodeCredential(refreshed));
2177
+ return refreshed.accessToken;
2178
+ } catch (error) {
2179
+ const recoveredCredential = await readStoredCredential(store, config.server);
2180
+ if (recoveredCredential && !credentialExpiresSoon(recoveredCredential)) {
2181
+ return recoveredCredential.accessToken;
2182
+ }
2183
+ throw new CliError("AUTH_REFRESH_FAILED", "Stored Cadence credential is expired and refresh failed. Run `cadence auth login` to reconnect.", {
2184
+ server: config.server,
2185
+ cause: error instanceof Error ? error.message : "Credential refresh failed."
2186
+ });
2187
+ }
2188
+ } finally {
2189
+ await releaseCredentialRefreshLock(lock);
2190
+ }
2191
+ }
2192
+ async function createClient(config, options, useStoredCredential = true) {
1889
2193
  if (options.client) {
1890
2194
  return options.client;
1891
2195
  }
1892
2196
  const store = getCredentialStore(options);
1893
- const credential = await readStoredCredential(store, config.server);
2197
+ const credential = useStoredCredential ? await readStoredCredential(store, config.server) : null;
2198
+ const authClient = createCadenceClient({
2199
+ baseUrl: config.server,
2200
+ ...options.fetch ? { fetch: options.fetch } : {}
2201
+ });
1894
2202
  return createCadenceClient({
1895
2203
  baseUrl: config.server,
1896
- ...credential ? { getAuthToken: async () => (await readStoredCredential(store, config.server))?.accessToken ?? "" } : {},
2204
+ ...credential ? { getAuthToken: async () => readFreshAccessToken(config, store, authClient) } : {},
1897
2205
  ...options.fetch ? { fetch: options.fetch } : {}
1898
2206
  });
1899
2207
  }
@@ -1901,6 +2209,7 @@ function commandMeta(parsed, config) {
1901
2209
  return {
1902
2210
  command: parsed.command.name,
1903
2211
  server: config.server,
2212
+ ...config.profile ? { profile: config.profile } : {},
1904
2213
  projectId: config.projectId
1905
2214
  };
1906
2215
  }
@@ -1962,6 +2271,55 @@ function parseTicketStatus(value) {
1962
2271
  }
1963
2272
  return value;
1964
2273
  }
2274
+ function parseSessionFileChangeKind(value) {
2275
+ if (!value) {
2276
+ return "unknown";
2277
+ }
2278
+ if (sessionFileChangeKinds.includes(value)) {
2279
+ return value;
2280
+ }
2281
+ throw new CliError("CLI_USAGE", "--kind must be one of added, modified, deleted, renamed, or unknown.");
2282
+ }
2283
+ function parseChangeSetPrNoteSource(value) {
2284
+ if (!value) {
2285
+ return;
2286
+ }
2287
+ if (changesetPrNoteSources.includes(value)) {
2288
+ return value;
2289
+ }
2290
+ throw new CliError("CLI_USAGE", "--source must be one of agent, human, system.");
2291
+ }
2292
+ function parseWorkLogEntryKind(value) {
2293
+ if (!value) {
2294
+ throw new CliError("CLI_USAGE", "--kind must be one of intent, decision, rationale, action, verification, blocker, correction, or note.");
2295
+ }
2296
+ if (workLogEntryKinds.includes(value)) {
2297
+ return value;
2298
+ }
2299
+ throw new CliError("CLI_USAGE", "--kind must be one of intent, decision, rationale, action, verification, blocker, correction, or note.");
2300
+ }
2301
+ var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2302
+ function parseWorkLogParent(value) {
2303
+ if (!value) {
2304
+ return {};
2305
+ }
2306
+ if (uuidPattern.test(value)) {
2307
+ return { parentEntryId: value };
2308
+ }
2309
+ if (workLogParentSelectors.includes(value)) {
2310
+ return { parentSelector: value };
2311
+ }
2312
+ throw new CliError("CLI_USAGE", "--under must be a work-log entry UUID or one of ticket-last, session-last, last-decision, last-correction, last-action, or last.");
2313
+ }
2314
+ function parseSessionFilePaths(parsed) {
2315
+ const value = requireOption(parsed, "file");
2316
+ const paths = value.split(`
2317
+ `).map((path) => path.trim()).filter(Boolean);
2318
+ if (paths.length === 0) {
2319
+ throw new CliError("CLI_USAGE", "sessions files requires at least one --file.");
2320
+ }
2321
+ return paths;
2322
+ }
1965
2323
  function leaseExpiresAt(ttlSeconds) {
1966
2324
  return new Date(Date.now() + ttlSeconds * 1000).toISOString();
1967
2325
  }
@@ -1974,20 +2332,73 @@ function commandMetadata() {
1974
2332
  source: "cli"
1975
2333
  };
1976
2334
  }
2335
+ function notesCommandMetadata() {
2336
+ return {
2337
+ occurredAt: new Date().toISOString(),
2338
+ eventSource: "cli"
2339
+ };
2340
+ }
2341
+ async function resolveCurrentBranch(options) {
2342
+ const injected = await options.resolveCurrentBranch?.();
2343
+ if (injected !== undefined) {
2344
+ const branch2 = injected.trim();
2345
+ if (!branch2) {
2346
+ throw new CliError("GIT_BRANCH_ERROR", "Current git branch is empty.");
2347
+ }
2348
+ return branch2;
2349
+ }
2350
+ const result = spawnSync("git", ["branch", "--show-current"], {
2351
+ cwd: options.cwd,
2352
+ encoding: "utf8"
2353
+ });
2354
+ if (result.status !== 0) {
2355
+ throw new CliError("GIT_BRANCH_ERROR", "Failed to resolve current git branch.", {
2356
+ stderr: result.stderr?.trim() ?? ""
2357
+ });
2358
+ }
2359
+ const branch = result.stdout.trim();
2360
+ if (!branch) {
2361
+ throw new CliError("GIT_BRANCH_ERROR", "Current git branch is empty.");
2362
+ }
2363
+ return branch;
2364
+ }
2365
+ async function changesetLookupFromOptions(parsed, options) {
2366
+ if (parsed.options.changeset && parsed.options.branch) {
2367
+ throw new CliError("CLI_USAGE", "Use either --changeset or --branch, not both.");
2368
+ }
2369
+ if (parsed.options.changeset) {
2370
+ return {
2371
+ changesetId: parsed.options.changeset
2372
+ };
2373
+ }
2374
+ const branchOption = parsed.options.branch ?? "current";
2375
+ const branchName = branchOption === "current" ? await resolveCurrentBranch(options) : branchOption;
2376
+ return {
2377
+ branchName
2378
+ };
2379
+ }
2380
+ async function readBodyFile(path, options) {
2381
+ const resolvedPath = isAbsolute(path) ? path : join(options.cwd ?? process.cwd(), path);
2382
+ return readFile(resolvedPath, "utf8");
2383
+ }
1977
2384
  async function runStatus(parsed, options) {
1978
2385
  const config = await resolveCliConfig(parsed.flags, options);
1979
2386
  const store = getCredentialStore(options);
1980
2387
  const credential = await readStoredCredential(store, config.server);
1981
2388
  const client = await createClient(config, options);
1982
2389
  const health = await client.health();
2390
+ const webBaseUrl = getCliWebBaseUrl(config, parsed, options);
1983
2391
  const data = {
1984
2392
  server: config.server,
2393
+ webBaseUrl,
2394
+ profile: config.profile,
1985
2395
  projectId: config.projectId,
1986
2396
  credentialConfigured: Boolean(credential),
1987
2397
  authTokenConfigured: Boolean(credential),
1988
2398
  health,
1989
2399
  config: {
1990
2400
  repoConfigPath: config.repoConfigPath,
2401
+ localConfigPath: config.localConfigPath,
1991
2402
  globalConfigPath: config.globalConfigPath
1992
2403
  }
1993
2404
  };
@@ -2116,7 +2527,7 @@ async function runAuthCommand(parsed, options) {
2116
2527
  switch (parsed.command.name) {
2117
2528
  case "auth.login":
2118
2529
  {
2119
- const client = await createClient(config, options);
2530
+ const client = await createClient(config, options, false);
2120
2531
  const challenge = await client.auth.cli.start({
2121
2532
  loginBaseUrl: getCliWebBaseUrl(config, parsed, options)
2122
2533
  });
@@ -2128,7 +2539,11 @@ async function runAuthCommand(parsed, options) {
2128
2539
  });
2129
2540
  }
2130
2541
  await requireCredentialStore(store);
2542
+ await writeInteractiveStatus("Opening browser to approve Cadence CLI access.", options);
2543
+ await writeInteractiveStatus(`Verification code: ${challenge.deviceCode}`, options);
2544
+ await writeInteractiveStatus(`Login URL: ${challenge.loginUrl}`, options);
2131
2545
  await openBrowser(challenge.loginUrl, options);
2546
+ await writeInteractiveStatus("Waiting for browser approval...", options);
2132
2547
  let poll = await client.auth.cli.poll({
2133
2548
  deviceId: challenge.deviceId,
2134
2549
  deviceCode: challenge.deviceCode
@@ -2148,6 +2563,7 @@ async function runAuthCommand(parsed, options) {
2148
2563
  }
2149
2564
  await store.setCredential(config.server, encodeCredential(poll.credential));
2150
2565
  await mergeConfigFile(config.globalConfigPath, { server: config.server });
2566
+ await writeInteractiveStatus("Cadence CLI login approved. Credential stored.", options);
2151
2567
  data = {
2152
2568
  server: config.server,
2153
2569
  credentialStored: true,
@@ -2252,6 +2668,12 @@ async function runReadCommand(parsed, options) {
2252
2668
  changesetId: requireArg(parsed, 0, "<changeset-id>")
2253
2669
  });
2254
2670
  break;
2671
+ case "changesets.current":
2672
+ data = await client.changesets.context({
2673
+ projectId,
2674
+ query: await changesetLookupFromOptions(parsed, options)
2675
+ });
2676
+ break;
2255
2677
  case "changesets.list":
2256
2678
  data = await client.changesets.list({
2257
2679
  projectId,
@@ -2263,6 +2685,12 @@ async function runReadCommand(parsed, options) {
2263
2685
  })
2264
2686
  });
2265
2687
  break;
2688
+ case "changesets.notes.get":
2689
+ data = await client.changesets.notes.get({
2690
+ projectId,
2691
+ query: await changesetLookupFromOptions(parsed, options)
2692
+ });
2693
+ break;
2266
2694
  default:
2267
2695
  throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
2268
2696
  }
@@ -2306,6 +2734,32 @@ async function runProjectCommand(parsed, options) {
2306
2734
  exitCode: 0
2307
2735
  };
2308
2736
  }
2737
+ async function runActorCommand(parsed, options) {
2738
+ const config = await resolveCliConfig(parsed.flags, options);
2739
+ const client = await createClient(config, options);
2740
+ const meta = commandMeta(parsed, config);
2741
+ let data;
2742
+ switch (parsed.command.name) {
2743
+ case "actors.ensure-workspace-agent":
2744
+ data = await ensureWorkspaceAgentIdentity(parsed, config, client, options);
2745
+ break;
2746
+ default:
2747
+ throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
2748
+ }
2749
+ if (parsed.flags.json) {
2750
+ return {
2751
+ stdout: formatJson(successEnvelope(data, meta)),
2752
+ stderr: "",
2753
+ exitCode: 0
2754
+ };
2755
+ }
2756
+ return {
2757
+ stdout: `${JSON.stringify(data, null, 2)}
2758
+ `,
2759
+ stderr: "",
2760
+ exitCode: 0
2761
+ };
2762
+ }
2309
2763
  async function runIntakeCommand(parsed, options) {
2310
2764
  const config = await resolveCliConfig(parsed.flags, options);
2311
2765
  const projectId = requireProjectId(config);
@@ -2402,6 +2856,7 @@ async function runIntakeCommand(parsed, options) {
2402
2856
  ticketId: requireArg(parsed, 0, "<ticket-id>"),
2403
2857
  sessionId: requireOption(parsed, "session"),
2404
2858
  ...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
2859
+ ...parsed.options.actor ? { actorId: parsed.options.actor } : {},
2405
2860
  expiresAt: leaseExpiresAt(parsePositiveInteger(parsed.options["ttl-seconds"], "--ttl-seconds") ?? defaultLeaseTtlSeconds),
2406
2861
  ...commandMetadata()
2407
2862
  }
@@ -2418,6 +2873,21 @@ async function runIntakeCommand(parsed, options) {
2418
2873
  }
2419
2874
  });
2420
2875
  break;
2876
+ case "tickets.log":
2877
+ data = await client.tickets.log({
2878
+ projectId,
2879
+ ticketId: requireArg(parsed, 0, "<ticket-id>"),
2880
+ entry: {
2881
+ entryKind: parseWorkLogEntryKind(parsed.options.kind),
2882
+ body: requireOption(parsed, "body"),
2883
+ ...parsed.options.summary ? { summary: parsed.options.summary } : {},
2884
+ ...parseWorkLogParent(parsed.options.under),
2885
+ ...parsed.options.session ? { sessionId: parsed.options.session } : {},
2886
+ ...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
2887
+ ...commandMetadata()
2888
+ }
2889
+ });
2890
+ break;
2421
2891
  case "tickets.complete":
2422
2892
  data = await client.tickets.complete({
2423
2893
  projectId,
@@ -2435,6 +2905,7 @@ async function runIntakeCommand(parsed, options) {
2435
2905
  session: {
2436
2906
  ticketId: requireOption(parsed, "ticket"),
2437
2907
  ...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
2908
+ ...parsed.options.actor ? { actorId: parsed.options.actor } : {},
2438
2909
  ...parsed.options["local-session-ref"] ? { localSessionRef: parsed.options["local-session-ref"] } : {},
2439
2910
  ...commandMetadata()
2440
2911
  }
@@ -2450,6 +2921,23 @@ async function runIntakeCommand(parsed, options) {
2450
2921
  }
2451
2922
  });
2452
2923
  break;
2924
+ case "sessions.files":
2925
+ {
2926
+ const changeKind = parseSessionFileChangeKind(parsed.options.kind);
2927
+ data = await client.sessions.files({
2928
+ projectId,
2929
+ sessionId: requireArg(parsed, 0, "<session-id>"),
2930
+ files: {
2931
+ ...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
2932
+ files: parseSessionFilePaths(parsed).map((path) => ({
2933
+ path,
2934
+ changeKind
2935
+ })),
2936
+ ...commandMetadata()
2937
+ }
2938
+ });
2939
+ }
2940
+ break;
2453
2941
  case "changesets.create":
2454
2942
  data = await client.changesets.create({
2455
2943
  projectId,
@@ -2462,6 +2950,41 @@ async function runIntakeCommand(parsed, options) {
2462
2950
  }
2463
2951
  });
2464
2952
  break;
2953
+ case "changesets.notes.put":
2954
+ {
2955
+ const source = parseChangeSetPrNoteSource(parsed.options.source);
2956
+ data = await client.changesets.notes.put({
2957
+ projectId,
2958
+ query: await changesetLookupFromOptions(parsed, options),
2959
+ notes: {
2960
+ title: requireOption(parsed, "title"),
2961
+ body: await readBodyFile(requireOption(parsed, "body-file"), options),
2962
+ ...source ? { source } : {},
2963
+ ...parsed.options.provider ? { prProvider: parsed.options.provider } : {},
2964
+ ...parsed.options["pr-number"] ? { prNumber: parseRequiredPositiveInteger(parsed.options["pr-number"], "--pr-number") } : {},
2965
+ ...parsed.options["pr-url"] ? { prUrl: parsed.options["pr-url"] } : {},
2966
+ ...parsed.options["base-branch"] ? { baseBranch: parsed.options["base-branch"] } : {},
2967
+ ...parsed.options["head-branch"] ? { headBranch: parsed.options["head-branch"] } : {},
2968
+ ...parsed.options["base-sha"] ? { baseSha: parsed.options["base-sha"] } : {},
2969
+ ...parsed.options["head-sha"] ? { headSha: parsed.options["head-sha"] } : {},
2970
+ ...notesCommandMetadata()
2971
+ }
2972
+ });
2973
+ }
2974
+ break;
2975
+ case "changesets.notes.apply":
2976
+ data = await client.changesets.notes.apply({
2977
+ projectId,
2978
+ query: await changesetLookupFromOptions(parsed, options),
2979
+ notes: {
2980
+ prProvider: requireOption(parsed, "provider"),
2981
+ prNumber: parseRequiredPositiveInteger(requireOption(parsed, "pr-number"), "--pr-number"),
2982
+ prUrl: requireOption(parsed, "pr-url"),
2983
+ ...parsed.options["head-sha"] ? { headSha: parsed.options["head-sha"] } : {},
2984
+ ...notesCommandMetadata()
2985
+ }
2986
+ });
2987
+ break;
2465
2988
  default:
2466
2989
  throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
2467
2990
  }
@@ -2523,16 +3046,19 @@ async function runCli(argv, options = {}) {
2523
3046
  if (parsed.command.name === "init") {
2524
3047
  return await runInit(parsed, options);
2525
3048
  }
3049
+ if (parsed.command.name === "actors.ensure-workspace-agent") {
3050
+ return await runActorCommand(parsed, options);
3051
+ }
2526
3052
  if (parsed.command.name === "auth.login" || parsed.command.name === "auth.status" || parsed.command.name === "auth.logout") {
2527
3053
  return await runAuthCommand(parsed, options);
2528
3054
  }
2529
- if (parsed.command.name === "events.list" || parsed.command.name === "work.overview" || parsed.command.name === "tickets.get" || parsed.command.name === "tickets.list" || parsed.command.name === "sessions.current" || parsed.command.name === "changesets.get" || parsed.command.name === "changesets.list") {
3055
+ if (parsed.command.name === "events.list" || parsed.command.name === "work.overview" || parsed.command.name === "tickets.get" || parsed.command.name === "tickets.list" || parsed.command.name === "sessions.current" || parsed.command.name === "changesets.get" || parsed.command.name === "changesets.current" || parsed.command.name === "changesets.list" || parsed.command.name === "changesets.notes.get") {
2530
3056
  return await runReadCommand(parsed, options);
2531
3057
  }
2532
3058
  if (parsed.command.name === "projects.list") {
2533
3059
  return await runProjectCommand(parsed, options);
2534
3060
  }
2535
- if (parsed.command.name === "intake" || parsed.command.name === "intake.dismiss" || parsed.command.name === "tickets.attach" || parsed.command.name === "tickets.create" || parsed.command.name === "tickets.update" || parsed.command.name === "tickets.claim" || parsed.command.name === "tickets.release" || parsed.command.name === "tickets.complete" || parsed.command.name === "sessions.start" || parsed.command.name === "sessions.end" || parsed.command.name === "changesets.create") {
3061
+ if (parsed.command.name === "intake" || parsed.command.name === "intake.dismiss" || parsed.command.name === "tickets.attach" || parsed.command.name === "tickets.create" || parsed.command.name === "tickets.update" || parsed.command.name === "tickets.claim" || parsed.command.name === "tickets.release" || parsed.command.name === "tickets.log" || parsed.command.name === "tickets.complete" || parsed.command.name === "sessions.start" || parsed.command.name === "sessions.end" || parsed.command.name === "sessions.files" || parsed.command.name === "changesets.create" || parsed.command.name === "changesets.notes.put" || parsed.command.name === "changesets.notes.apply") {
2536
3062
  return await runIntakeCommand(parsed, options);
2537
3063
  }
2538
3064
  throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trycadence/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {