@keepgoingdev/cli 1.4.0 → 1.6.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 (3) hide show
  1. package/README.md +18 -5
  2. package/dist/index.js +310 -135
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,14 +38,27 @@ Flags:
38
38
 
39
39
  ### `keepgoing save`
40
40
 
41
- Save a new checkpoint interactively. Prompts for:
42
-
43
- 1. What did you work on? (required)
44
- 2. What's your next step? (required)
45
- 3. Any blockers? (optional)
41
+ Save a new checkpoint. By default, auto-generates the summary and next step from recent git activity (commits and touched files). No interactive prompts.
46
42
 
47
43
  Git branch and touched files are auto-detected from the workspace.
48
44
 
45
+ Flags:
46
+
47
+ - `-m, --message <text>` — use a custom summary instead of auto-generating
48
+ - `-n, --next <text>` — use a custom next step instead of auto-generating
49
+ - `--force` — save even if a recent checkpoint exists or there are no detected changes
50
+ - `--json` — output raw JSON
51
+ - `--quiet` — suppress output
52
+ - `--cwd <path>` — override the working directory
53
+
54
+ Examples:
55
+
56
+ ```bash
57
+ keepgoing save # auto-generate from git
58
+ keepgoing save -m "Finished auth flow" # custom summary
59
+ keepgoing save --force # save even if no changes
60
+ ```
61
+
49
62
  ### `keepgoing hook install`
50
63
 
51
64
  Install a shell hook that runs `keepgoing status --quiet` automatically whenever you `cd` into a directory that contains `.keepgoing/`.
package/dist/index.js CHANGED
@@ -2801,96 +2801,7 @@ function renderEnrichedBriefingQuiet(briefing) {
2801
2801
  console.log(`KeepGoing \xB7 ${core.lastWorked} \xB7 Focus: ${core.currentFocus} \xB7 Next: ${core.smallNextStep}`);
2802
2802
  }
2803
2803
 
2804
- // src/updateCheck.ts
2805
- import { spawn } from "child_process";
2806
- import { readFileSync, existsSync } from "fs";
2807
- import path11 from "path";
2808
- import os5 from "os";
2809
- var CLI_VERSION = "1.4.0";
2810
- var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
2811
- var FETCH_TIMEOUT_MS = 5e3;
2812
- var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2813
- var CACHE_DIR = path11.join(os5.homedir(), ".keepgoing");
2814
- var CACHE_PATH = path11.join(CACHE_DIR, "update-check.json");
2815
- function isNewerVersion(current, latest) {
2816
- const cur = current.split(".").map(Number);
2817
- const lat = latest.split(".").map(Number);
2818
- for (let i = 0; i < 3; i++) {
2819
- if ((lat[i] ?? 0) > (cur[i] ?? 0)) return true;
2820
- if ((lat[i] ?? 0) < (cur[i] ?? 0)) return false;
2821
- }
2822
- return false;
2823
- }
2824
- function getCachedUpdateInfo() {
2825
- try {
2826
- if (!existsSync(CACHE_PATH)) return null;
2827
- const raw = readFileSync(CACHE_PATH, "utf-8");
2828
- const cache = JSON.parse(raw);
2829
- if (!cache.latest || !cache.checkedAt) return null;
2830
- const age = Date.now() - new Date(cache.checkedAt).getTime();
2831
- if (age > CHECK_INTERVAL_MS || cache.current !== CLI_VERSION) return null;
2832
- return {
2833
- current: CLI_VERSION,
2834
- latest: cache.latest,
2835
- updateAvailable: isNewerVersion(CLI_VERSION, cache.latest)
2836
- };
2837
- } catch {
2838
- return null;
2839
- }
2840
- }
2841
- function spawnBackgroundCheck() {
2842
- try {
2843
- if (existsSync(CACHE_PATH)) {
2844
- const raw = readFileSync(CACHE_PATH, "utf-8");
2845
- const cache = JSON.parse(raw);
2846
- const age = Date.now() - new Date(cache.checkedAt).getTime();
2847
- if (age < CHECK_INTERVAL_MS && cache.current === CLI_VERSION) return;
2848
- }
2849
- } catch {
2850
- }
2851
- const script = `
2852
- const https = require('https');
2853
- const fs = require('fs');
2854
- const path = require('path');
2855
- const os = require('os');
2856
-
2857
- const url = ${JSON.stringify(NPM_REGISTRY_URL)};
2858
- const cacheDir = path.join(os.homedir(), '.keepgoing');
2859
- const cachePath = path.join(cacheDir, 'update-check.json');
2860
- const currentVersion = ${JSON.stringify(CLI_VERSION)};
2861
-
2862
- const req = https.get(url, { timeout: ${FETCH_TIMEOUT_MS} }, (res) => {
2863
- let data = '';
2864
- res.on('data', (chunk) => { data += chunk; });
2865
- res.on('end', () => {
2866
- try {
2867
- const latest = JSON.parse(data).version;
2868
- if (!latest) process.exit(0);
2869
- if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
2870
- fs.writeFileSync(cachePath, JSON.stringify({
2871
- latest,
2872
- current: currentVersion,
2873
- checkedAt: new Date().toISOString(),
2874
- }));
2875
- } catch {}
2876
- process.exit(0);
2877
- });
2878
- });
2879
- req.on('error', () => process.exit(0));
2880
- req.on('timeout', () => { req.destroy(); process.exit(0); });
2881
- `;
2882
- const child = spawn(process.execPath, ["-e", script], {
2883
- detached: true,
2884
- stdio: "ignore",
2885
- env: { ...process.env }
2886
- });
2887
- child.unref();
2888
- }
2889
-
2890
2804
  // src/commands/status.ts
2891
- var RESET2 = "\x1B[0m";
2892
- var BOLD2 = "\x1B[1m";
2893
- var DIM2 = "\x1B[2m";
2894
2805
  async function statusCommand(opts) {
2895
2806
  const reader = new KeepGoingReader(opts.cwd);
2896
2807
  if (!reader.exists()) {
@@ -2918,15 +2829,10 @@ async function statusCommand(opts) {
2918
2829
  (Date.now() - new Date(lastSession.timestamp).getTime()) / (1e3 * 60 * 60 * 24)
2919
2830
  );
2920
2831
  renderCheckpoint(lastSession, daysSince);
2921
- const cached = getCachedUpdateInfo();
2922
- if (cached?.updateAvailable) {
2923
- console.log(`${DIM2}Update available: ${cached.current} \u2192 ${cached.latest}. Run: ${RESET2}${BOLD2}npm install -g @keepgoingdev/cli@latest${RESET2}`);
2924
- }
2925
- spawnBackgroundCheck();
2926
2832
  }
2927
2833
 
2928
2834
  // src/commands/save.ts
2929
- import path12 from "path";
2835
+ import path11 from "path";
2930
2836
  async function saveCommand(opts) {
2931
2837
  const { cwd, message, nextStepOverride, json, quiet, force } = opts;
2932
2838
  const isManual = !!message;
@@ -2955,9 +2861,9 @@ async function saveCommand(opts) {
2955
2861
  sessionStartTime: lastSession?.timestamp ?? now,
2956
2862
  lastActivityTime: now
2957
2863
  });
2958
- const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path12.basename(f)).join(", ")}`;
2864
+ const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path11.basename(f)).join(", ")}`;
2959
2865
  const nextStep = nextStepOverride ?? buildSmartNextStep(events);
2960
- const projectName = path12.basename(resolveStorageRoot(cwd));
2866
+ const projectName = path11.basename(resolveStorageRoot(cwd));
2961
2867
  const sessionId = generateSessionId({
2962
2868
  workspaceRoot: cwd,
2963
2869
  branch: gitBranch ?? void 0,
@@ -2991,16 +2897,16 @@ async function saveCommand(opts) {
2991
2897
 
2992
2898
  // src/commands/hook.ts
2993
2899
  import fs9 from "fs";
2994
- import path13 from "path";
2995
- import os6 from "os";
2900
+ import path12 from "path";
2901
+ import os5 from "os";
2996
2902
  import { execSync as execSync2 } from "child_process";
2997
2903
  var HOOK_MARKER_START = "# keepgoing-hook-start";
2998
2904
  var HOOK_MARKER_END = "# keepgoing-hook-end";
2999
2905
  var POST_COMMIT_MARKER_START = "# keepgoing-post-commit-start";
3000
2906
  var POST_COMMIT_MARKER_END = "# keepgoing-post-commit-end";
3001
- var KEEPGOING_HOOKS_DIR = path13.join(os6.homedir(), ".keepgoing", "hooks");
3002
- var POST_COMMIT_HOOK_PATH = path13.join(KEEPGOING_HOOKS_DIR, "post-commit");
3003
- var KEEPGOING_MANAGED_MARKER = path13.join(KEEPGOING_HOOKS_DIR, ".keepgoing-managed");
2907
+ var KEEPGOING_HOOKS_DIR = path12.join(os5.homedir(), ".keepgoing", "hooks");
2908
+ var POST_COMMIT_HOOK_PATH = path12.join(KEEPGOING_HOOKS_DIR, "post-commit");
2909
+ var KEEPGOING_MANAGED_MARKER = path12.join(KEEPGOING_HOOKS_DIR, ".keepgoing-managed");
3004
2910
  var POST_COMMIT_HOOK = `#!/bin/sh
3005
2911
  ${POST_COMMIT_MARKER_START}
3006
2912
  # Runs after every git commit. Detects high-signal decisions.
@@ -3041,7 +2947,7 @@ if command -v keepgoing >/dev/null 2>&1
3041
2947
  end
3042
2948
  ${HOOK_MARKER_END}`;
3043
2949
  function detectShellInfo(shellOverride) {
3044
- const home = os6.homedir();
2950
+ const home = os5.homedir();
3045
2951
  let shell;
3046
2952
  if (shellOverride) {
3047
2953
  shell = shellOverride.toLowerCase();
@@ -3064,14 +2970,14 @@ function detectShellInfo(shellOverride) {
3064
2970
  }
3065
2971
  }
3066
2972
  if (shell === "zsh") {
3067
- return { shell: "zsh", rcFile: path13.join(home, ".zshrc") };
2973
+ return { shell: "zsh", rcFile: path12.join(home, ".zshrc") };
3068
2974
  }
3069
2975
  if (shell === "bash") {
3070
- return { shell: "bash", rcFile: path13.join(home, ".bashrc") };
2976
+ return { shell: "bash", rcFile: path12.join(home, ".bashrc") };
3071
2977
  }
3072
2978
  if (shell === "fish") {
3073
- const xdgConfig = process.env["XDG_CONFIG_HOME"] || path13.join(home, ".config");
3074
- return { shell: "fish", rcFile: path13.join(xdgConfig, "fish", "config.fish") };
2979
+ const xdgConfig = process.env["XDG_CONFIG_HOME"] || path12.join(home, ".config");
2980
+ return { shell: "fish", rcFile: path12.join(xdgConfig, "fish", "config.fish") };
3075
2981
  }
3076
2982
  return void 0;
3077
2983
  }
@@ -3102,11 +3008,11 @@ function resolveGlobalGitignorePath() {
3102
3008
  stdio: ["pipe", "pipe", "pipe"]
3103
3009
  }).trim();
3104
3010
  if (configured) {
3105
- return configured.startsWith("~") ? path13.join(os6.homedir(), configured.slice(1)) : configured;
3011
+ return configured.startsWith("~") ? path12.join(os5.homedir(), configured.slice(1)) : configured;
3106
3012
  }
3107
3013
  } catch {
3108
3014
  }
3109
- return path13.join(os6.homedir(), ".gitignore_global");
3015
+ return path12.join(os5.homedir(), ".gitignore_global");
3110
3016
  }
3111
3017
  function performGlobalGitignore() {
3112
3018
  const ignorePath = resolveGlobalGitignorePath();
@@ -3426,12 +3332,12 @@ async function briefingCommand(opts) {
3426
3332
  }
3427
3333
 
3428
3334
  // src/commands/init.ts
3429
- var RESET3 = "\x1B[0m";
3430
- var BOLD3 = "\x1B[1m";
3335
+ var RESET2 = "\x1B[0m";
3336
+ var BOLD2 = "\x1B[1m";
3431
3337
  var GREEN2 = "\x1B[32m";
3432
3338
  var YELLOW2 = "\x1B[33m";
3433
3339
  var CYAN2 = "\x1B[36m";
3434
- var DIM3 = "\x1B[2m";
3340
+ var DIM2 = "\x1B[2m";
3435
3341
  function initCommand(options) {
3436
3342
  const scope = options.scope === "user" ? "user" : "project";
3437
3343
  const result = setupProject({
@@ -3439,7 +3345,7 @@ function initCommand(options) {
3439
3345
  scope
3440
3346
  });
3441
3347
  console.log(`
3442
- ${BOLD3}KeepGoing Init${RESET3} ${DIM3}(${scope} scope)${RESET3}
3348
+ ${BOLD2}KeepGoing Init${RESET2} ${DIM2}(${scope} scope)${RESET2}
3443
3349
  `);
3444
3350
  for (const msg of result.messages) {
3445
3351
  const colonIdx = msg.indexOf(":");
@@ -3450,16 +3356,16 @@ ${BOLD3}KeepGoing Init${RESET3} ${DIM3}(${scope} scope)${RESET3}
3450
3356
  const label = msg.slice(0, colonIdx + 1);
3451
3357
  const body = msg.slice(colonIdx + 1);
3452
3358
  if (label.startsWith("Warning")) {
3453
- console.log(` ${YELLOW2}${label}${RESET3}${body}`);
3359
+ console.log(` ${YELLOW2}${label}${RESET2}${body}`);
3454
3360
  } else if (body.includes("Added")) {
3455
- console.log(` ${GREEN2}${label}${RESET3}${body}`);
3361
+ console.log(` ${GREEN2}${label}${RESET2}${body}`);
3456
3362
  } else {
3457
- console.log(` ${CYAN2}${label}${RESET3}${body}`);
3363
+ console.log(` ${CYAN2}${label}${RESET2}${body}`);
3458
3364
  }
3459
3365
  }
3460
3366
  if (result.changed) {
3461
3367
  console.log(`
3462
- ${GREEN2}Done!${RESET3} KeepGoing is set up for this project.
3368
+ ${GREEN2}Done!${RESET2} KeepGoing is set up for this project.
3463
3369
  `);
3464
3370
  } else {
3465
3371
  console.log(`
@@ -3470,8 +3376,8 @@ Everything was already configured. No changes made.
3470
3376
 
3471
3377
  // src/commands/setup.ts
3472
3378
  import fs10 from "fs";
3473
- import path14 from "path";
3474
- import os7 from "os";
3379
+ import path13 from "path";
3380
+ import os6 from "os";
3475
3381
  import { execSync as execSync3, exec as exec2 } from "child_process";
3476
3382
  import { promisify as promisify2 } from "util";
3477
3383
 
@@ -5396,14 +5302,15 @@ function cancelAndExit() {
5396
5302
 
5397
5303
  // src/commands/setup.ts
5398
5304
  var execAsync = promisify2(exec2);
5399
- var VALID_PRESETS = ["vscode", "claude", "copilot", "cursor", "windsurf", "jetbrains"];
5305
+ var VALID_PRESETS = ["vscode", "claude", "copilot", "cursor", "windsurf", "jetbrains", "desktop-tray"];
5400
5306
  var PRESET_DEFAULTS = {
5401
5307
  vscode: { ide: ["vscode"] },
5402
5308
  jetbrains: { ide: ["jetbrains"] },
5403
5309
  claude: { tools: ["claude-code"] },
5404
5310
  copilot: { tools: ["copilot"] },
5405
5311
  cursor: { tools: ["cursor"] },
5406
- windsurf: { tools: ["windsurf"] }
5312
+ windsurf: { tools: ["windsurf"] },
5313
+ "desktop-tray": { desktopTray: true }
5407
5314
  };
5408
5315
  var PRESET_LABELS = {
5409
5316
  vscode: "VS Code",
@@ -5411,7 +5318,8 @@ var PRESET_LABELS = {
5411
5318
  claude: "Claude Code",
5412
5319
  copilot: "GitHub Copilot",
5413
5320
  cursor: "Cursor",
5414
- windsurf: "Windsurf"
5321
+ windsurf: "Windsurf",
5322
+ "desktop-tray": "Desktop Tray"
5415
5323
  };
5416
5324
  var JETBRAINS_PLUGIN_URL = "https://plugins.jetbrains.com/plugin/30449-keepgoing";
5417
5325
  var JETBRAINS_PLUGIN_ID = "com.keepgoing.plugin";
@@ -5433,14 +5341,14 @@ function detectJetBrainsIdes() {
5433
5341
  if (process.platform !== "darwin") return [];
5434
5342
  return JETBRAINS_IDES.filter((ide) => {
5435
5343
  try {
5436
- return fs10.statSync(path14.join("/Applications", ide.app)).isDirectory();
5344
+ return fs10.statSync(path13.join("/Applications", ide.app)).isDirectory();
5437
5345
  } catch {
5438
5346
  return false;
5439
5347
  }
5440
5348
  });
5441
5349
  }
5442
5350
  async function setupCommand(options) {
5443
- const displayPath = options.cwd.startsWith(os7.homedir()) ? "~" + options.cwd.slice(os7.homedir().length) : options.cwd;
5351
+ const displayPath = options.cwd.startsWith(os6.homedir()) ? "~" + options.cwd.slice(os6.homedir().length) : options.cwd;
5444
5352
  const presetDefaults = options.preset ? PRESET_DEFAULTS[options.preset] : void 0;
5445
5353
  const presetLabel = options.preset ? PRESET_LABELS[options.preset] : void 0;
5446
5354
  dist_exports.intro(presetLabel ? `KeepGoing Setup - ${presetLabel}` : "KeepGoing Setup Wizard");
@@ -5461,7 +5369,7 @@ async function setupCommand(options) {
5461
5369
  tools = presetDefaults.tools;
5462
5370
  dist_exports.log.step(`AI tool: ${tools.map((t2) => t2 === "claude-code" ? "Claude Code" : t2.charAt(0).toUpperCase() + t2.slice(1)).join(", ")}`);
5463
5371
  const addMore = await dist_exports.multiselect({
5464
- message: "Also configure any of these?",
5372
+ message: "Also configure any of these? (enter to skip)",
5465
5373
  options: [
5466
5374
  ...!tools.includes("claude-code") ? [{ label: "Claude Code", value: "claude-code" }] : [],
5467
5375
  ...!tools.includes("copilot") ? [{ label: "GitHub Copilot", value: "copilot" }] : [],
@@ -5500,12 +5408,14 @@ async function setupCommand(options) {
5500
5408
  if (isCancel(pluginAnswer)) cancelAndExit();
5501
5409
  claudePlugin = pluginAnswer === "plugin";
5502
5410
  }
5411
+ const IDE_TOOLS = ["cursor", "windsurf"];
5412
+ const hasIdeTool = tools.some((t2) => IDE_TOOLS.includes(t2));
5503
5413
  let ide;
5504
5414
  if (presetDefaults?.ide) {
5505
5415
  ide = presetDefaults.ide;
5506
5416
  dist_exports.log.step(`IDE: ${ide.map((i) => i === "vscode" ? "VS Code" : i === "jetbrains" ? "JetBrains" : "Terminal").join(", ")}`);
5507
5417
  const addMoreIde = await dist_exports.multiselect({
5508
- message: "Also configure any of these?",
5418
+ message: "Also configure any of these? (enter to skip)",
5509
5419
  options: [
5510
5420
  ...!ide.includes("vscode") ? [{ label: "VS Code", value: "vscode" }] : [],
5511
5421
  ...!ide.includes("jetbrains") ? [{ label: "JetBrains", hint: "IntelliJ, WebStorm, PyCharm, etc.", value: "jetbrains" }] : [],
@@ -5516,6 +5426,19 @@ async function setupCommand(options) {
5516
5426
  if (!isCancel(addMoreIde) && addMoreIde.length > 0) {
5517
5427
  ide = [...ide, ...addMoreIde];
5518
5428
  }
5429
+ } else if (hasIdeTool) {
5430
+ ide = [];
5431
+ const addIdeExtensions = await dist_exports.multiselect({
5432
+ message: "Also configure IDE extensions? (enter to skip)",
5433
+ options: [
5434
+ { label: "VS Code", value: "vscode" },
5435
+ { label: "JetBrains", hint: "IntelliJ, WebStorm, PyCharm, etc.", value: "jetbrains" }
5436
+ ],
5437
+ required: false
5438
+ });
5439
+ if (!isCancel(addIdeExtensions) && addIdeExtensions.length > 0) {
5440
+ ide = addIdeExtensions;
5441
+ }
5519
5442
  } else {
5520
5443
  const ideAnswer = await dist_exports.multiselect({
5521
5444
  message: "Which IDE(s) do you use?",
@@ -5735,6 +5658,83 @@ async function setupCommand(options) {
5735
5658
  }
5736
5659
  }
5737
5660
  }
5661
+ let installDesktopTray = false;
5662
+ const trayPreset = presetDefaults?.desktopTray === true;
5663
+ if (process.platform === "darwin") {
5664
+ const alreadyInstalled = fs10.existsSync("/Applications/KeepGoing.app");
5665
+ if (alreadyInstalled) {
5666
+ dist_exports.log.message(`${DIM5}Desktop tray: Already installed${RESET5}`);
5667
+ installDesktopTray = true;
5668
+ } else {
5669
+ if (!trayPreset) {
5670
+ dist_exports.note(
5671
+ "A menubar app that shows re-entry briefings, active sessions,\nand momentum across all your projects. Editor-agnostic, free.",
5672
+ "Desktop Tray"
5673
+ );
5674
+ }
5675
+ const wantsTray = trayPreset || await (async () => {
5676
+ const trayAnswer = await dist_exports.confirm({
5677
+ message: "Install the desktop tray app?",
5678
+ initialValue: trayPreset,
5679
+ active: "Yes",
5680
+ inactive: "No, skip"
5681
+ });
5682
+ return !isCancel(trayAnswer) && trayAnswer;
5683
+ })();
5684
+ if (wantsTray) {
5685
+ installDesktopTray = true;
5686
+ let installed = false;
5687
+ let hasHomebrew = false;
5688
+ try {
5689
+ execSync3("brew --version", { stdio: "pipe" });
5690
+ hasHomebrew = true;
5691
+ } catch {
5692
+ }
5693
+ if (hasHomebrew) {
5694
+ const ts = dist_exports.spinner();
5695
+ ts.start("Installing desktop tray via Homebrew...");
5696
+ try {
5697
+ const { stderr } = await execAsync(
5698
+ "brew tap keepgoing-dev/tap && brew install --cask keepgoing",
5699
+ { timeout: 12e4 }
5700
+ );
5701
+ if (stderr?.includes("already installed")) {
5702
+ ts.stop("Desktop tray: Already installed via Homebrew");
5703
+ } else {
5704
+ ts.stop("Desktop tray installed");
5705
+ }
5706
+ installed = true;
5707
+ try {
5708
+ execSync3("open -a KeepGoing", { stdio: "pipe" });
5709
+ } catch {
5710
+ }
5711
+ } catch {
5712
+ ts.stop("Homebrew install failed");
5713
+ }
5714
+ }
5715
+ if (!installed) {
5716
+ dist_exports.log.info(
5717
+ "Install manually:\n 1. Download from https://github.com/keepgoing-dev/releases/releases/latest\n 2. Drag KeepGoing.app to /Applications\n 3. Run: xattr -cr /Applications/KeepGoing.app\n 4. Open KeepGoing from Applications"
5718
+ );
5719
+ }
5720
+ } else {
5721
+ dist_exports.log.info("Desktop tray: Install anytime with:\n brew tap keepgoing-dev/tap && brew install --cask keepgoing");
5722
+ }
5723
+ }
5724
+ if (installDesktopTray) {
5725
+ try {
5726
+ const existing = readTrayConfigProjects();
5727
+ if (!existing.includes(options.cwd)) {
5728
+ const trayConfigDir = path13.join(os6.homedir(), ".keepgoing");
5729
+ fs10.mkdirSync(trayConfigDir, { recursive: true });
5730
+ const trayConfigPath = path13.join(trayConfigDir, "tray-config.json");
5731
+ const updated = { projects: [...existing, options.cwd] };
5732
+ fs10.writeFileSync(trayConfigPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
5733
+ }
5734
+ } catch {
5735
+ }
5736
+ }
5737
+ }
5738
5738
  if (hasLicense) {
5739
5739
  const key = await dist_exports.text({
5740
5740
  message: "Enter your license key",
@@ -5791,12 +5791,13 @@ async function setupCommand(options) {
5791
5791
  scope,
5792
5792
  shellHook: installShellHook,
5793
5793
  claudePlugin,
5794
+ desktopTray: installDesktopTray,
5794
5795
  licensed
5795
5796
  };
5796
- const keepgoingDir = path14.join(os7.homedir(), ".keepgoing");
5797
+ const keepgoingDir = path13.join(os6.homedir(), ".keepgoing");
5797
5798
  fs10.mkdirSync(keepgoingDir, { recursive: true });
5798
5799
  fs10.writeFileSync(
5799
- path14.join(keepgoingDir, "setup-profile.json"),
5800
+ path13.join(keepgoingDir, "setup-profile.json"),
5800
5801
  JSON.stringify(profile, null, 2) + "\n",
5801
5802
  "utf-8"
5802
5803
  );
@@ -6007,8 +6008,8 @@ function filterDecisions(decisions, opts) {
6007
6008
  }
6008
6009
 
6009
6010
  // src/commands/log.ts
6010
- var RESET4 = "\x1B[0m";
6011
- var DIM4 = "\x1B[2m";
6011
+ var RESET3 = "\x1B[0m";
6012
+ var DIM3 = "\x1B[2m";
6012
6013
  function logSessions(reader, opts) {
6013
6014
  const { effectiveBranch } = reader.resolveBranchScope(opts.branch || void 0);
6014
6015
  let sessions = reader.getSessions();
@@ -6019,7 +6020,7 @@ function logSessions(reader, opts) {
6019
6020
  sessions = filterSessions(sessions, opts);
6020
6021
  const totalFiltered = sessions.length;
6021
6022
  if (totalFiltered === 0) {
6022
- console.log(`${DIM4}No checkpoints match the given filters.${RESET4}`);
6023
+ console.log(`${DIM3}No checkpoints match the given filters.${RESET3}`);
6023
6024
  return;
6024
6025
  }
6025
6026
  const displayed = sessions.slice(0, opts.count);
@@ -6043,7 +6044,7 @@ function logSessions(reader, opts) {
6043
6044
  }
6044
6045
  }
6045
6046
  if (totalFiltered > opts.count) {
6046
- console.log(`${DIM4}(showing ${displayed.length} of ${totalFiltered} checkpoints)${RESET4}`);
6047
+ console.log(`${DIM3}(showing ${displayed.length} of ${totalFiltered} checkpoints)${RESET3}`);
6047
6048
  }
6048
6049
  }
6049
6050
  function renderGrouped(sessions, showStat) {
@@ -6082,7 +6083,7 @@ async function logDecisions(reader, opts) {
6082
6083
  decisions = filterDecisions(decisions, opts);
6083
6084
  const totalFiltered = decisions.length;
6084
6085
  if (totalFiltered === 0) {
6085
- console.log(`${DIM4}No decisions match the given filters.${RESET4}`);
6086
+ console.log(`${DIM3}No decisions match the given filters.${RESET3}`);
6086
6087
  return;
6087
6088
  }
6088
6089
  const displayed = decisions.slice(0, opts.count);
@@ -6104,7 +6105,7 @@ async function logDecisions(reader, opts) {
6104
6105
  }
6105
6106
  }
6106
6107
  if (totalFiltered > opts.count) {
6107
- console.log(`${DIM4}(showing ${displayed.length} of ${totalFiltered} decisions)${RESET4}`);
6108
+ console.log(`${DIM3}(showing ${displayed.length} of ${totalFiltered} decisions)${RESET3}`);
6108
6109
  }
6109
6110
  }
6110
6111
  async function logCommand(opts) {
@@ -6245,6 +6246,154 @@ function hotCommand(opts) {
6245
6246
  }
6246
6247
  }
6247
6248
 
6249
+ // src/commands/update.ts
6250
+ import { execSync as execSync5 } from "child_process";
6251
+
6252
+ // src/updateCheck.ts
6253
+ import { spawn } from "child_process";
6254
+ import { readFileSync, existsSync } from "fs";
6255
+ import path14 from "path";
6256
+ import os7 from "os";
6257
+ var CLI_VERSION = "1.6.0";
6258
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
6259
+ var FETCH_TIMEOUT_MS = 5e3;
6260
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
6261
+ var CACHE_DIR = path14.join(os7.homedir(), ".keepgoing");
6262
+ var CACHE_PATH = path14.join(CACHE_DIR, "update-check.json");
6263
+ function isNewerVersion(current, latest) {
6264
+ const cur = current.split(".").map(Number);
6265
+ const lat = latest.split(".").map(Number);
6266
+ for (let i = 0; i < 3; i++) {
6267
+ if ((lat[i] ?? 0) > (cur[i] ?? 0)) return true;
6268
+ if ((lat[i] ?? 0) < (cur[i] ?? 0)) return false;
6269
+ }
6270
+ return false;
6271
+ }
6272
+ function getCachedUpdateInfo() {
6273
+ try {
6274
+ if (!existsSync(CACHE_PATH)) return null;
6275
+ const raw = readFileSync(CACHE_PATH, "utf-8");
6276
+ const cache = JSON.parse(raw);
6277
+ if (!cache.latest || !cache.checkedAt) return null;
6278
+ const age = Date.now() - new Date(cache.checkedAt).getTime();
6279
+ if (age > CHECK_INTERVAL_MS || cache.current !== CLI_VERSION) return null;
6280
+ return {
6281
+ current: CLI_VERSION,
6282
+ latest: cache.latest,
6283
+ updateAvailable: isNewerVersion(CLI_VERSION, cache.latest)
6284
+ };
6285
+ } catch {
6286
+ return null;
6287
+ }
6288
+ }
6289
+ function spawnBackgroundCheck() {
6290
+ try {
6291
+ if (existsSync(CACHE_PATH)) {
6292
+ const raw = readFileSync(CACHE_PATH, "utf-8");
6293
+ const cache = JSON.parse(raw);
6294
+ const age = Date.now() - new Date(cache.checkedAt).getTime();
6295
+ if (age < CHECK_INTERVAL_MS && cache.current === CLI_VERSION) return;
6296
+ }
6297
+ } catch {
6298
+ }
6299
+ const script = `
6300
+ const https = require('https');
6301
+ const fs = require('fs');
6302
+ const path = require('path');
6303
+ const os = require('os');
6304
+
6305
+ const url = ${JSON.stringify(NPM_REGISTRY_URL)};
6306
+ const cacheDir = path.join(os.homedir(), '.keepgoing');
6307
+ const cachePath = path.join(cacheDir, 'update-check.json');
6308
+ const currentVersion = ${JSON.stringify(CLI_VERSION)};
6309
+
6310
+ const req = https.get(url, { timeout: ${FETCH_TIMEOUT_MS} }, (res) => {
6311
+ let data = '';
6312
+ res.on('data', (chunk) => { data += chunk; });
6313
+ res.on('end', () => {
6314
+ try {
6315
+ const latest = JSON.parse(data).version;
6316
+ if (!latest) process.exit(0);
6317
+ if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
6318
+ fs.writeFileSync(cachePath, JSON.stringify({
6319
+ latest,
6320
+ current: currentVersion,
6321
+ checkedAt: new Date().toISOString(),
6322
+ }));
6323
+ } catch {}
6324
+ process.exit(0);
6325
+ });
6326
+ });
6327
+ req.on('error', () => process.exit(0));
6328
+ req.on('timeout', () => { req.destroy(); process.exit(0); });
6329
+ `;
6330
+ const child = spawn(process.execPath, ["-e", script], {
6331
+ detached: true,
6332
+ stdio: "ignore",
6333
+ env: { ...process.env }
6334
+ });
6335
+ child.unref();
6336
+ }
6337
+
6338
+ // src/commands/update.ts
6339
+ var RESET4 = "\x1B[0m";
6340
+ var BOLD3 = "\x1B[1m";
6341
+ var DIM4 = "\x1B[2m";
6342
+ var GREEN3 = "\x1B[32m";
6343
+ var YELLOW3 = "\x1B[33m";
6344
+ var CLI_VERSION2 = "1.6.0";
6345
+ async function updateCommand() {
6346
+ console.log(`
6347
+ ${BOLD3}KeepGoing CLI${RESET4} ${DIM4}v${CLI_VERSION2}${RESET4}
6348
+ `);
6349
+ console.log(`${DIM4}Checking for updates...${RESET4}`);
6350
+ let latest;
6351
+ try {
6352
+ latest = execSync5("npm view @keepgoingdev/cli version", {
6353
+ encoding: "utf-8",
6354
+ timeout: 1e4,
6355
+ stdio: ["pipe", "pipe", "pipe"]
6356
+ }).trim();
6357
+ } catch {
6358
+ console.error(`${YELLOW3}Could not reach the npm registry. Check your network connection.${RESET4}
6359
+ `);
6360
+ process.exit(1);
6361
+ }
6362
+ if (!latest) {
6363
+ console.error(`${YELLOW3}Could not determine the latest version.${RESET4}
6364
+ `);
6365
+ process.exit(1);
6366
+ }
6367
+ const current = CLI_VERSION2;
6368
+ const updateAvailable = isNewerVersion(current, latest);
6369
+ if (!updateAvailable) {
6370
+ console.log(`${GREEN3}Already up to date.${RESET4}
6371
+ `);
6372
+ return;
6373
+ }
6374
+ console.log(`${YELLOW3}Update available:${RESET4} ${DIM4}${current}${RESET4} -> ${BOLD3}${latest}${RESET4}
6375
+ `);
6376
+ console.log(`${DIM4}Installing @keepgoingdev/cli@${latest}...${RESET4}
6377
+ `);
6378
+ try {
6379
+ execSync5(`npm install -g @keepgoingdev/cli@${latest}`, {
6380
+ encoding: "utf-8",
6381
+ timeout: 6e4,
6382
+ stdio: "inherit"
6383
+ });
6384
+ console.log(`
6385
+ ${GREEN3}Updated to v${latest}${RESET4}
6386
+ `);
6387
+ } catch {
6388
+ console.error(`
6389
+ ${YELLOW3}Update failed.${RESET4} Try manually:
6390
+ `);
6391
+ console.error(` ${BOLD3}npm install -g @keepgoingdev/cli@latest${RESET4}
6392
+ `);
6393
+ process.exit(1);
6394
+ }
6395
+ }
6396
+
6248
6397
  // src/index.ts
6249
6398
  var HELP_TEXT = `
6250
6399
  keepgoing: resume side projects without the mental friction
@@ -6264,6 +6413,7 @@ Commands:
6264
6413
  continue Export context for use in another AI tool
6265
6414
  save Save a checkpoint (auto-generates from git)
6266
6415
  hook Manage the shell hook (zsh, bash, fish)
6416
+ update Update the CLI to the latest version
6267
6417
  activate <key> Activate a Pro license on this device
6268
6418
  deactivate Deactivate the Pro license from this device
6269
6419
 
@@ -6301,6 +6451,7 @@ Presets:
6301
6451
  windsurf Pre-select Windsurf
6302
6452
  vscode Pre-select VS Code IDE
6303
6453
  jetbrains Pre-select JetBrains IDE
6454
+ desktop-tray Install the desktop tray app
6304
6455
 
6305
6456
  Options:
6306
6457
  --cwd <path> Override the working directory
@@ -6428,6 +6579,13 @@ Usage: keepgoing activate <key>
6428
6579
 
6429
6580
  Example:
6430
6581
  keepgoing activate XXXX-XXXX-XXXX-XXXX
6582
+ `,
6583
+ update: `
6584
+ keepgoing update: Update the CLI to the latest version
6585
+
6586
+ Usage: keepgoing update
6587
+
6588
+ Checks the npm registry for a newer version and installs it globally.
6431
6589
  `,
6432
6590
  deactivate: `
6433
6591
  keepgoing deactivate: Deactivate the Pro license from this device
@@ -6709,8 +6867,11 @@ async function main() {
6709
6867
  console.log(COMMAND_HELP.hook);
6710
6868
  }
6711
6869
  break;
6870
+ case "update":
6871
+ await updateCommand();
6872
+ break;
6712
6873
  case "version":
6713
- console.log(`keepgoing v${"1.4.0"}`);
6874
+ console.log(`keepgoing v${"1.6.0"}`);
6714
6875
  break;
6715
6876
  case "activate":
6716
6877
  await activateCommand({ licenseKey: subcommand });
@@ -6725,6 +6886,20 @@ async function main() {
6725
6886
  console.error(`Unknown command: "${command}". Run "keepgoing --help" for usage.`);
6726
6887
  process.exit(1);
6727
6888
  }
6889
+ if (command && command !== "update" && command !== "version" && command !== "help" && !json && !quiet && !parsed.help) {
6890
+ showUpdateHint();
6891
+ }
6892
+ }
6893
+ function showUpdateHint() {
6894
+ const cached = getCachedUpdateInfo();
6895
+ if (cached?.updateAvailable) {
6896
+ const DIM5 = "\x1B[2m";
6897
+ const BOLD4 = "\x1B[1m";
6898
+ const RESET5 = "\x1B[0m";
6899
+ console.log(`
6900
+ ${DIM5}Update available: ${cached.current} -> ${cached.latest}. Run:${RESET5} ${BOLD4}keepgoing update${RESET5}`);
6901
+ }
6902
+ spawnBackgroundCheck();
6728
6903
  }
6729
6904
  main().catch((err) => {
6730
6905
  console.error(err instanceof Error ? err.message : String(err));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepgoingdev/cli",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Terminal CLI for KeepGoing. Resume side projects without the mental friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",