codetime-cli 0.4.0 → 0.5.0

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/bin/codetime.mjs +143 -3
  2. package/package.json +1 -1
package/bin/codetime.mjs CHANGED
@@ -973,7 +973,7 @@ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
973
973
  import path2 from "node:path";
974
974
 
975
975
  // src/lib/constants.ts
976
- var PACKAGE_VERSION = true ? "0.4.0" : "0.1.1";
976
+ var PACKAGE_VERSION = true ? "0.5.0" : "0.1.1";
977
977
  var DEFAULT_API_URL = "https://codetime.dev";
978
978
  var DEFAULT_BACKFILL_BATCH_SIZE = 50;
979
979
  var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
@@ -4578,6 +4578,40 @@ async function logError(scope, error, meta = {}, home = homedir2()) {
4578
4578
  await writeLog({ scope, error, meta, level: "error" }, home);
4579
4579
  }
4580
4580
 
4581
+ // src/lib/login.ts
4582
+ function sleep(ms) {
4583
+ return new Promise((resolve) => setTimeout(resolve, ms));
4584
+ }
4585
+ function openBrowser(ctx, url) {
4586
+ const { command, args } = browserCommand(ctx.env.CODETIME_OS ?? process.platform, url);
4587
+ try {
4588
+ const child = ctx.spawn(command, args, { detached: true, stdio: "ignore" });
4589
+ child.unref?.();
4590
+ } catch {
4591
+ }
4592
+ }
4593
+ function browserCommand(platform, url) {
4594
+ if (platform === "darwin") {
4595
+ return { command: "open", args: [url] };
4596
+ }
4597
+ if (platform === "win32") {
4598
+ return { command: "cmd", args: ["/c", "start", "", url] };
4599
+ }
4600
+ return { command: "xdg-open", args: [url] };
4601
+ }
4602
+ function isHeadless(ctx) {
4603
+ if (ctx.env.CODETIME_NO_BROWSER) {
4604
+ return true;
4605
+ }
4606
+ if (ctx.env.SSH_CONNECTION || ctx.env.SSH_TTY) {
4607
+ return true;
4608
+ }
4609
+ if (process.platform === "linux") {
4610
+ return !ctx.env.DISPLAY && !ctx.env.WAYLAND_DISPLAY;
4611
+ }
4612
+ return false;
4613
+ }
4614
+
4581
4615
  // src/lib/progress.ts
4582
4616
  var BAR_WIDTH = 24;
4583
4617
  var ProgressBar = class {
@@ -4737,6 +4771,28 @@ async function deleteRollupsBySource(remote, source, machine) {
4737
4771
  const body = await response.json();
4738
4772
  return Number(body.deleted) || 0;
4739
4773
  }
4774
+ async function startCliLink(remote) {
4775
+ const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/agent/cli/link/start"), {
4776
+ method: "POST",
4777
+ headers: buildHeaders(),
4778
+ body: "{}"
4779
+ });
4780
+ if (!response.ok) {
4781
+ throw new Error(`Failed to start login: ${response.status}`);
4782
+ }
4783
+ return await response.json();
4784
+ }
4785
+ async function pollCliLink(remote, deviceCode) {
4786
+ const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/agent/cli/link/poll"), {
4787
+ method: "POST",
4788
+ headers: buildHeaders(),
4789
+ body: JSON.stringify({ deviceCode })
4790
+ });
4791
+ if (!response.ok) {
4792
+ throw new Error(`Login poll failed: ${response.status}`);
4793
+ }
4794
+ return await response.json();
4795
+ }
4740
4796
  async function listMachines(remote) {
4741
4797
  const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/machines"), {
4742
4798
  headers: buildHeaders(remote.token)
@@ -4843,6 +4899,11 @@ function createCli(ctx, registry) {
4843
4899
  cli.command("sync-local-trigger", "Trigger one background local sync with throttle and locking").option("--min-interval <seconds>", "Minimum seconds between sync triggers").action((options) => syncLocalTriggerCommand(normalizeOptions(options), ctx, registry));
4844
4900
  cli.command("sync-local-runner", "Internal background local sync runner").option("--lock-file <path>", "Lock file for the active sync").option("--state-file <path>", "State file for trigger metadata").action((options) => syncLocalRunnerCommand(normalizeOptions(options), ctx, registry));
4845
4901
  cli.command("backfill [action]", "Inspect local history import candidates").option("--source <source>", "Backfill source").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--source-root <path>", "Override source history root").option("--include-source-path", "Include local source paths in output").option("--import-run <id>", "Import run id for verify/resume workflows").option("--limit <count>", "Maximum session files to parse").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--batch-bytes <bytes>", "Soft byte cap for the JSON body of a single ingest POST").option("--replace", "Replace conflicting records during import (default)").option("--skip-conflicts", "Skip conflicting records instead of replacing them").option("--force", "Force full re-import: clear watermark and re-process all files").action((action, options) => backfillCommand({ ...normalizeOptions(options), action }, ctx, registry));
4902
+ cli.command("sync", "Import and upload all local agent history (shorthand for `backfill import --source all`)").option("--source <source>", "Limit to one source (default: all)").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--force", "Force full re-import: clear watermark and re-process all files").option("--dry-run", "Print the planned import without uploading").action((options) => {
4903
+ const opts = normalizeOptions(options);
4904
+ return backfillCommand({ ...opts, action: "import", source: stringOption(opts.source) || "all" }, ctx, registry);
4905
+ });
4906
+ cli.command("login", "Authorize this machine by signing in through your browser").option("--remote <url>", "Override API base URL for this login").option("--no-browser", "Print the login URL instead of opening a browser").action((options) => loginCommand(normalizeOptions(options), ctx));
4846
4907
  cli.command("token [action] [value]", "Set, show, or clear the persisted API token").option("--remote <url>", "Override API base URL when setting a token").action((action, value, options) => tokenCommand(action, value, normalizeOptions(options), ctx));
4847
4908
  cli.command("machine [action]", "List or rename machines (requires login)").option("--name <name>", "New display name (used by `machine rename`)").option("--id <id>", "Machine id (defaults to current machine)").action((action, options) => machineCommand(action, normalizeOptions(options), ctx));
4848
4909
  return cli;
@@ -5716,6 +5777,80 @@ function maskToken(token) {
5716
5777
  }
5717
5778
  return `${token.slice(0, 3)}\u2026${token.slice(-4)}`;
5718
5779
  }
5780
+ var LOGIN_MAX_WAIT_MS = 15 * 60 * 1e3;
5781
+ async function loginCommand(options, ctx) {
5782
+ const home = resolveHome(options, ctx);
5783
+ const remoteOverride = stringOption(options.remote) || stringOption(options["api-url"]);
5784
+ const existing = readConfig(home);
5785
+ const baseUrl = (remoteOverride || ctx.env.CODETIME_API_URL || existing.remoteUrl || DEFAULT_API_URL).replace(/\/$/, "");
5786
+ const remote = resolveRemote({
5787
+ apiUrl: baseUrl,
5788
+ env: ctx.env,
5789
+ fetch: ctx.fetch,
5790
+ homeOverride: home
5791
+ });
5792
+ if (!remote) {
5793
+ write(ctx.stderr, "No fetch implementation available.\n");
5794
+ return 1;
5795
+ }
5796
+ let link;
5797
+ try {
5798
+ link = await startCliLink(remote);
5799
+ } catch (error) {
5800
+ write(ctx.stderr, `${error.message}
5801
+ `);
5802
+ return 1;
5803
+ }
5804
+ const authUrl = `${baseUrl}/cli/auth?code=${encodeURIComponent(link.userCode)}`;
5805
+ const noBrowser = options.browser === false;
5806
+ const headless = isHeadless(ctx);
5807
+ write(ctx.stdout, `
5808
+ To sign in, visit:
5809
+
5810
+ ${authUrl}
5811
+
5812
+ and confirm this code: ${link.userCode}
5813
+
5814
+ `);
5815
+ if (!noBrowser && !headless) {
5816
+ openBrowser(ctx, authUrl);
5817
+ }
5818
+ write(ctx.stdout, "Waiting for authorization\u2026\n");
5819
+ const intervalMs = Math.max(1, link.interval || 4) * 1e3;
5820
+ const deadline = Date.now() + Math.min(LOGIN_MAX_WAIT_MS, Math.max(1, link.expiresIn || 600) * 1e3);
5821
+ while (Date.now() < deadline) {
5822
+ let poll;
5823
+ try {
5824
+ poll = await pollCliLink(remote, link.deviceCode);
5825
+ } catch (error) {
5826
+ void error;
5827
+ await sleep(intervalMs);
5828
+ continue;
5829
+ }
5830
+ if (poll.status === "pending") {
5831
+ await sleep(intervalMs);
5832
+ continue;
5833
+ }
5834
+ if (poll.status === "expired") {
5835
+ write(ctx.stderr, "Login code expired before it was approved. Re-run `codetime login`.\n");
5836
+ return 1;
5837
+ }
5838
+ writeConfig({
5839
+ ...existing,
5840
+ token: poll.token,
5841
+ ...poll.userId == null ? {} : { userId: String(poll.userId) },
5842
+ // Persist the host only when explicitly overridden, matching
5843
+ // `token set` so a default-host login never clobbers an earlier
5844
+ // --remote choice.
5845
+ ...remoteOverride ? { remoteUrl: remoteOverride } : {}
5846
+ }, home);
5847
+ write(ctx.stdout, `Logged in. Token saved (${maskToken(poll.token)}).
5848
+ `);
5849
+ return 0;
5850
+ }
5851
+ write(ctx.stderr, "Timed out waiting for authorization. Re-run `codetime login`.\n");
5852
+ return 1;
5853
+ }
5719
5854
  async function tokenCommand(action, value, options, ctx) {
5720
5855
  const verb = action || "show";
5721
5856
  const home = resolveHome(options, ctx);
@@ -5827,20 +5962,25 @@ Usage:
5827
5962
  codetime detect [--json] [--home <path>]
5828
5963
  codetime install [--target codex,claude,opencode,pi] [--all] [--dry-run] [--force] [--home <path>]
5829
5964
  codetime hook --agent <name>
5965
+ codetime sync [--source <source>] [--force] [--dry-run]
5830
5966
  codetime backfill discover|plan|import|verify --source codex|claude-code|opencode|pi|all --dry-run [--json] [--batch-size <count>]
5967
+ codetime login [--no-browser] [--remote <url>]
5831
5968
  codetime token set <token>
5832
5969
  codetime token show
5833
5970
  codetime token clear
5834
5971
 
5835
5972
  Setup:
5836
- Copy your upload token from https://codetime.dev/dashboard/settings,
5837
- then run: codetime token set <token>
5973
+ Run: codetime login (signs in through your browser)
5974
+ Or copy your upload token from https://codetime.dev/dashboard/settings
5975
+ and run: codetime token set <token>
5838
5976
 
5839
5977
  Commands:
5840
5978
  detect Show supported local targets and install status.
5841
5979
  install Install integration files into detected or requested targets.
5842
5980
  hook Read agent hook JSON from stdin and report a throttled event.
5981
+ sync Import and upload all local agent history (backfill import --source all).
5843
5982
  backfill Discover local history and create metadata-only import plans.
5983
+ login Authorize this machine by signing in through your browser.
5844
5984
  token Set, show, or clear the persisted API token.
5845
5985
  machine List your machines (read-only).
5846
5986
  version Print CLI version.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codetime-cli",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.5.0",
5
5
  "description": "codetime CLI — install AI-agent hooks (Claude Code, Codex, OpenCode, Pi) and report activity to codetime.dev.",
6
6
  "license": "MIT",
7
7
  "publishConfig": {