@bli-cockpit/cli 0.2.3 → 0.2.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.
package/README.md CHANGED
@@ -54,6 +54,7 @@ On a blank Mac, `npm i -g @bli-cockpit/cli && cockpit do-everything` is enough t
54
54
  | `cockpit status` | Prints install / sign-in / capture / upload health in one screen. | The "is it working?" command. Run it whenever you're unsure. |
55
55
  | `cockpit backfill --all` | Uploads your HISTORICAL Codex + Claude sessions (from before Cockpit existed on this Mac). | One-time catch-up so your past work counts too. "Backfill" = fill in the back-catalog. |
56
56
  | `cockpit sync` | Captures and uploads once, right now. This is what the background agent runs every 15 min — you almost never type it yourself. | Named for what it does: synchronize local session files up to the dashboard. |
57
+ | `cockpit analyze` | Runs a fresh sync, then queues batch analysis of your latest uploaded sessions. It returns immediately; status and results appear in My Work. | One command when you want newly finished work uploaded and judged now. |
57
58
  | `cockpit start --ticket <id>` | Tags your CURRENT work with a Linear ticket so sessions attribute to it. `--clear-ticket` returns to general capture. | "Start (working on) X." Agents usually run this for you per the agent rules. |
58
59
  | `cockpit login` / `cockpit pair` | Just the sign-in + device-registration step, standalone (onboard already includes it). Two names, one command: `login` is what humans guess, `pair` is what the dashboard's device screen calls it. | Recovery path when a session expires or you switch accounts. |
59
60
  | `cockpit logout` | Deletes this machine's session. | The undo of login. |
@@ -93,6 +94,23 @@ cockpit onboard --no-auth
93
94
 
94
95
  `cockpit update` installs the latest public CLI and reruns onboarding checks against saved roots. `cockpit upgrade` is the same command.
95
96
 
97
+ ## Analyze latest work
98
+
99
+ ```bash
100
+ cockpit analyze --workspace "$PWD"
101
+ ```
102
+
103
+ `cockpit analyze` first performs the same durable upload as `cockpit sync`. It
104
+ queues the analysis only after that upload succeeds, then exits without waiting
105
+ for the Fireworks batch. Use `--json` for a single machine-readable object that
106
+ contains both the sync receipt and queued job status. The dashboard button
107
+ analyzes the latest upload already stored by the background collector; run the
108
+ CLI command when the work ended too recently for the 15-minute background sync.
109
+ Each person can queue analysis once every three hours, subject to the team's
110
+ shared `$2` UTC-day judge cap. Completion can take several minutes plus the wait
111
+ for the next 15-minute collector poll; My Work shows queued, running, done, or
112
+ failed status while you wait.
113
+
96
114
  ## Parent Mode
97
115
 
98
116
  Use a parent folder such as `~/BLI` when it contains multiple repos. Cockpit scans child git repos/worktrees, creates one stable work context per worktree, and rolls them up under repo rows in the dashboard.
@@ -29,6 +29,8 @@ export function parseLocalArgs(argv) {
29
29
  return parseStartArgs(argv.slice(1));
30
30
  case "sync":
31
31
  return parseSyncArgs(argv.slice(1));
32
+ case "analyze":
33
+ return parseAnalyzeArgs(argv.slice(1));
32
34
  case "backfill":
33
35
  return parseBackfillArgs(argv.slice(1));
34
36
  case "status":
@@ -302,6 +304,12 @@ function parseStartArgs(args) {
302
304
  };
303
305
  }
304
306
  function parseSyncArgs(args) {
307
+ return parseSyncLikeArgs(args, "sync");
308
+ }
309
+ function parseAnalyzeArgs(args) {
310
+ return parseSyncLikeArgs(args, "analyze");
311
+ }
312
+ function parseSyncLikeArgs(args, command) {
305
313
  const values = parseNamedArgs(args, {
306
314
  allowedFlags: [
307
315
  "--home",
@@ -321,9 +329,9 @@ function parseSyncArgs(args) {
321
329
  "--max-repos",
322
330
  ],
323
331
  });
324
- assertNoPositionals(values.positionals, "sync");
332
+ assertNoPositionals(values.positionals, command);
325
333
  return {
326
- kind: "sync",
334
+ kind: command,
327
335
  homeDir: optionalNonEmpty(values.flags.get("--home")),
328
336
  repoRoot: optionalNonEmpty(workRootFlagValue(values)),
329
337
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
@@ -30,6 +30,7 @@ export const rootCommandNames = new Set([
30
30
  "logout",
31
31
  "start",
32
32
  "sync",
33
+ "analyze",
33
34
  "backfill",
34
35
  "status",
35
36
  "sessions",
@@ -76,6 +77,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
76
77
  return await runStart(command, io);
77
78
  case "sync":
78
79
  return await runSync(command, io);
80
+ case "analyze":
81
+ return await runAnalyze(command, io);
79
82
  case "backfill":
80
83
  return await runBackfillCommand(command, io);
81
84
  case "status":
@@ -112,6 +115,7 @@ export function localCommandHelp(command) {
112
115
  " cockpit logout",
113
116
  " cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
114
117
  " cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
118
+ " cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
115
119
  " cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--yes] [--workspace <path>] [--json]",
116
120
  " cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
117
121
  " cockpit sessions [--source codex|claude] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
@@ -236,6 +240,17 @@ function localSubcommandHelp(command) {
236
240
  "--max-depth and --max-repos.",
237
241
  ],
238
242
  ],
243
+ [
244
+ "analyze",
245
+ [
246
+ "Usage: cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--json]",
247
+ "",
248
+ "Uploads the latest local ambient evidence, then queues one analysis job.",
249
+ "The command returns after the batch is queued; view status and results in My Work.",
250
+ "Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
251
+ "`--repo <path>` remains supported as a backward-compatible alias.",
252
+ ],
253
+ ],
239
254
  [
240
255
  "backfill",
241
256
  [
@@ -1825,6 +1840,137 @@ async function runSyncLocked(command, io) {
1825
1840
  writeLine(io.stderr, `Retry: ${result.retry_command}`);
1826
1841
  return 1;
1827
1842
  }
1843
+ async function runAnalyze(command, io) {
1844
+ const syncStdout = [];
1845
+ const syncStderr = [];
1846
+ const syncIo = {
1847
+ ...io,
1848
+ stdout: bufferedWritable(syncStdout),
1849
+ stderr: bufferedWritable(syncStderr),
1850
+ };
1851
+ const syncCommand = {
1852
+ ...command,
1853
+ kind: "sync",
1854
+ json: true,
1855
+ };
1856
+ const syncExitCode = await runSync(syncCommand, syncIo);
1857
+ const syncOutput = parseCapturedJson(syncStdout);
1858
+ if (syncExitCode !== 0 || !isUploadedSyncResult(syncOutput)) {
1859
+ if (command.json) {
1860
+ writeLine(io.stdout, JSON.stringify({
1861
+ status: syncExitCode === 0 ? "sync_not_completed" : "sync_failed",
1862
+ sync: syncOutput,
1863
+ error: syncStderr.join("").trim()
1864
+ || (syncExitCode === 0
1865
+ ? "Cockpit sync did not upload fresh evidence."
1866
+ : "Cockpit sync failed."),
1867
+ }, null, 2));
1868
+ }
1869
+ else {
1870
+ replayCaptured(io.stderr, syncStderr);
1871
+ writeLine(io.stderr, syncExitCode === 0
1872
+ ? "Analysis was not queued because a fresh upload did not complete."
1873
+ : "Analysis was not queued because the latest upload failed.");
1874
+ }
1875
+ return syncExitCode === 0 ? 1 : syncExitCode;
1876
+ }
1877
+ const paths = getCollectorRuntimePaths(command.homeDir);
1878
+ const session = await readLocalCollectorSessionFile(paths).catch(() => {
1879
+ throw new Error("Cockpit is not signed in. Run `cockpit onboard` or `cockpit login` first.");
1880
+ });
1881
+ const dashboardUrl = normalizeUrl(command.dashboardUrl ?? session.dashboard_url ?? DEFAULT_DASHBOARD_URL);
1882
+ const response = await io.fetch(`${dashboardUrl}/api/ambient/analyze`, {
1883
+ method: "POST",
1884
+ headers: {
1885
+ authorization: `Bearer ${session.device_token}`,
1886
+ "content-type": "application/json",
1887
+ },
1888
+ body: "{}",
1889
+ });
1890
+ const body = await readAnalyzeApiResponse(response);
1891
+ if (!response.ok) {
1892
+ const message = typeof body.message === "string"
1893
+ ? body.message
1894
+ : `Analysis request failed with HTTP ${response.status}.`;
1895
+ if (command.json) {
1896
+ writeLine(io.stdout, JSON.stringify({
1897
+ status: "analyze_failed",
1898
+ sync: syncOutput,
1899
+ http_status: response.status,
1900
+ code: typeof body.code === "string" ? body.code : null,
1901
+ message,
1902
+ retry_after_seconds: typeof body.retry_after_seconds === "number"
1903
+ ? body.retry_after_seconds
1904
+ : null,
1905
+ }, null, 2));
1906
+ }
1907
+ else {
1908
+ replayCaptured(io.stderr, syncStderr);
1909
+ writeLine(io.stderr, `Analysis was not queued: ${message}`);
1910
+ }
1911
+ return 1;
1912
+ }
1913
+ const jobId = typeof body.job?.id === "string" ? body.job.id : null;
1914
+ const jobStatus = typeof body.job?.status === "string" ? body.job.status : "pending";
1915
+ if (command.json) {
1916
+ writeLine(io.stdout, JSON.stringify({
1917
+ status: "queued",
1918
+ sync: syncOutput,
1919
+ job: body.job ?? null,
1920
+ dashboard_url: `${dashboardUrl}/my-work`,
1921
+ }, null, 2));
1922
+ return 0;
1923
+ }
1924
+ replayCaptured(io.stderr, syncStderr);
1925
+ writeLine(io.stdout, "Cockpit latest evidence uploaded.");
1926
+ writeLine(io.stdout, "Cockpit analysis queued.");
1927
+ if (jobId)
1928
+ writeLine(io.stdout, `Job: ${jobId}`);
1929
+ writeLine(io.stdout, `Status: ${jobStatus === "pending" ? "queued" : jobStatus}`);
1930
+ writeLine(io.stdout, `Results: ${dashboardUrl}/my-work`);
1931
+ return 0;
1932
+ }
1933
+ async function readAnalyzeApiResponse(response) {
1934
+ const text = await response.text();
1935
+ if (!text)
1936
+ return {};
1937
+ try {
1938
+ const value = JSON.parse(text);
1939
+ return value && typeof value === "object" ? value : {};
1940
+ }
1941
+ catch {
1942
+ return {};
1943
+ }
1944
+ }
1945
+ function parseCapturedJson(chunks) {
1946
+ const text = chunks.join("").trim();
1947
+ if (!text)
1948
+ return null;
1949
+ try {
1950
+ return JSON.parse(text);
1951
+ }
1952
+ catch {
1953
+ return text;
1954
+ }
1955
+ }
1956
+ function isUploadedSyncResult(value) {
1957
+ return Boolean(value
1958
+ && typeof value === "object"
1959
+ && !Array.isArray(value)
1960
+ && value["status"] === "uploaded");
1961
+ }
1962
+ function bufferedWritable(chunks) {
1963
+ return {
1964
+ write(chunk) {
1965
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
1966
+ return true;
1967
+ },
1968
+ };
1969
+ }
1970
+ function replayCaptured(stream, chunks) {
1971
+ for (const chunk of chunks)
1972
+ stream.write(chunk);
1973
+ }
1828
1974
  async function runSyncRawEvidenceGc(command, io) {
1829
1975
  return runRawEvidenceLocalGc(getCollectorRuntimePaths(command.homeDir), io.env);
1830
1976
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {