nebula-ai 0.0.17 → 0.0.19

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
@@ -18,7 +18,7 @@ npx nebula-ai # interactive TUI
18
18
  npx nebula-ai chat "summarize the open issues in repo X"
19
19
  ```
20
20
 
21
- At login you pick where the auth token lives — your **system keychain** (recommended; uses `Bun.secrets` → macOS Keychain / Windows Credential Manager / libsecret) or a **local file** at `~/.nebula/.auth-token` (0600). Keychain failures fall back transparently to the file store so you never lose a session you just authenticated.
21
+ The auth token is persisted as an encrypted file at `~/.nebula/auth-token` (mode `0600`). The encryption key is derived from your hostname and OS user via HKDF-SHA256, so backups/syncs of `~/.nebula/` to a different machine yield ciphertext that won't decrypt on the target — the binding is intentional, not a bug. See [ADR-0054](../../docs/adr/0054-encrypted-file-token-store-no-keychain.md) for the threat-model details.
22
22
 
23
23
  **Supported platforms:** macOS (arm64, x64), Linux (x64, arm64), Windows (x64).
24
24
 
@@ -40,6 +40,10 @@ The Homebrew channel ships prod releases only. Dev builds remain on npm — inst
40
40
 
41
41
  If you've installed via both `brew` and `npm i -g`, whichever prefix appears first on your `PATH` wins — this is normal for multi-source CLIs.
42
42
 
43
+ ### Bundled with Nebula Desktop
44
+
45
+ The [Nebula Desktop app](https://github.com/agent-labs-dev/nebula-desktop/releases) bundles this same CLI binary inside its `.app`. From **Settings → Install Shell Command** in the desktop app, you can expose `nebula-ai` on your `$PATH` without a separate npm/brew install — the shim re-execs the bundled binary, so desktop updates automatically refresh the CLI version. Both surfaces share `~/.nebula/auth-token`: a session you sign into in one is immediately usable from the other.
46
+
43
47
  ## Interactive TUI
44
48
 
45
49
  The TUI gives you a full dashboard with agents, channels, and settings — all navigable from the keyboard.
@@ -149,6 +153,14 @@ No events are sent when telemetry is disabled.
149
153
  | `NEBULA_AI_DOWNLOAD_BASE` | Use an alternate mirror for the prebuilt binary download (useful behind restrictive firewalls). |
150
154
  | `NEBULA_AI_NO_UPDATE_CHECK` | Set to `1` to suppress the daily update check. Already suppressed under `npx`, CI, or a non-TTY stderr. |
151
155
  | `API_URL` | Override the Nebula API base URL (alternative to the `--api-url` flag). |
152
- | `NEBULA_AUTH_TOKEN` | Use a pre-minted auth token instead of the stored credential. Scripting / CI escape hatch — takes precedence over both keychain and file stores. |
156
+ | `NEBULA_AUTH_TOKEN` | Use a pre-minted auth token instead of the stored credential. Scripting / CI escape hatch — takes precedence over the encrypted on-disk store. |
153
157
  | `NEBULA_DEBUG` / `DEBUG=nebula-cli` | Enable verbose logging (same effect as `-v/--verbose`). |
154
158
  | `DO_NOT_TRACK` / `NEBULA_DO_NOT_TRACK` | Set to `1` to disable product telemetry. |
159
+
160
+ ## Development
161
+
162
+ This CLI is developed in the [`nebula-desktop`](https://github.com/agent-labs-dev/nebula-desktop) Bun-workspaces umbrella alongside the desktop app and shared packages. Contributor docs:
163
+
164
+ - [`AGENTS.md`](./AGENTS.md) — CLI-specific build, dev, and architecture conventions.
165
+ - [`docs/`](./docs/) — working notes (local-device backend contracts, RPC port mappings, sandbox image directives).
166
+ - The [workspace README](../../README.md) and [root `AGENTS.md`](../../AGENTS.md) cover umbrella-level conventions (gates, releases, packages).
package/bin/nebula-ai.js CHANGED
@@ -46,23 +46,23 @@ import { homedir } from "node:os";
46
46
  import { dirname, join } from "node:path";
47
47
  import { pipeline } from "node:stream/promises";
48
48
  import { fileURLToPath } from "node:url";
49
- import { detectInstallMethod, getChannel, getUpdateCheckCachePath, getUpgradeCommand, isNewerVersion, isNpx, isUpdateCheckDisabled, UPDATE_CHECK_TTL_MS, } from "./update-check-lib.js";
49
+ import { detectInstallMethod, distTagForChannel, getChannel, getUpdateCheckCachePath, getUpgradeCommand, isNewerVersion, isNpx, isUpdateCheckDisabled, UPDATE_CHECK_TTL_MS, } from "./update-check-lib.js";
50
50
  // Re-export the pure update-check helpers so older tests / downstream
51
51
  // importers that still reach for `./launcher.js` keep working.
52
- export { detectInstallMethod, getChannel, getUpdateCheckCachePath, getUpgradeCommand, isNewerVersion, isNpx, isUpdateCheckDisabled, } from "./update-check-lib.js";
52
+ export { detectInstallMethod, distTagForChannel, getChannel, getUpdateCheckCachePath, getUpgradeCommand, isNewerVersion, isNpx, isUpdateCheckDisabled, } from "./update-check-lib.js";
53
53
  // The public half of the ed25519 key that signs `checksums.json`. The
54
54
  // private half lives only in the `NEBULA_AI_SIGNING_PRIVATE_KEY` GitHub
55
55
  // Actions secret.
56
56
  const EMBEDDED_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
57
- MCowBQYDK2VwAyEAd8QFvCTSMY0HmSsJgL6jtFQas00WedvSZhkc9bxdAl0=
57
+ MCowBQYDK2VwAyEAyAvta5v665Vd82jfFmuCEw3WpneUQboj936gYHQE4/0=
58
58
  -----END PUBLIC KEY-----
59
59
  `;
60
60
  // Stamped by `scripts/publish/prepare.mjs` at wrapper staging time with
61
61
  // the channel-appropriate base URL (dev vs prod). The `__NEBULA_AI_…__`
62
62
  // sentinel lets us keep the checked-in source runnable for local tests
63
63
  // without pointing at any real host.
64
- const STAMPED_DOWNLOAD_BASE_URL = "https://cli-assets.nebula-dev.ai/production";
65
- const DOWNLOAD_BASE_URL = process.env.NEBULA_AI_DOWNLOAD_BASE ?? STAMPED_DOWNLOAD_BASE_URL;
64
+ const STAMPED_DOWNLOAD_BASE_URL = "https://app-assets.nebula.gg/cli/stable";
65
+ const DOWNLOAD_BASE_URL = process.env["NEBULA_AI_DOWNLOAD_BASE"] ?? STAMPED_DOWNLOAD_BASE_URL;
66
66
  const SUPPORTED_PLATFORMS = new Set([
67
67
  "darwin-arm64",
68
68
  "darwin-x64",
@@ -97,10 +97,10 @@ export function verifyManifest(manifestBytes, signatureBase64, publicKeyPem) {
97
97
  throw new Error("manifest is not a JSON object");
98
98
  }
99
99
  const m = manifest;
100
- if (m.algorithm !== "sha256") {
101
- throw new Error(`unsupported manifest algorithm: ${m.algorithm ?? "(missing)"}`);
100
+ if (m["algorithm"] !== "sha256") {
101
+ throw new Error(`unsupported manifest algorithm: ${m["algorithm"] ?? "(missing)"}`);
102
102
  }
103
- if (!m.binaries || typeof m.binaries !== "object") {
103
+ if (!m["binaries"] || typeof m["binaries"] !== "object") {
104
104
  throw new Error("manifest is missing the `binaries` map");
105
105
  }
106
106
  return manifest;
@@ -129,18 +129,18 @@ async function readVersion() {
129
129
  const pkgPath = join(here, "..", "package.json");
130
130
  const raw = await readFile(pkgPath, "utf8");
131
131
  const pkg = JSON.parse(raw);
132
- if (typeof pkg.version !== "string") {
132
+ if (typeof pkg["version"] !== "string") {
133
133
  throw new Error("nebula-ai: package.json is missing a version field");
134
134
  }
135
- return pkg.version;
135
+ return pkg["version"];
136
136
  }
137
137
  function getCacheBaseDir() {
138
138
  // Respect XDG_CACHE_HOME on linux, honor LOCALAPPDATA on windows, default
139
139
  // to ~/.cache elsewhere. Keeps behavior predictable across platforms.
140
- const base = process.env.NEBULA_AI_CACHE_DIR ||
140
+ const base = process.env["NEBULA_AI_CACHE_DIR"] ||
141
141
  (process.platform === "win32"
142
- ? process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local")
143
- : process.env.XDG_CACHE_HOME || join(homedir(), ".cache"));
142
+ ? process.env["LOCALAPPDATA"] || join(homedir(), "AppData", "Local")
143
+ : process.env["XDG_CACHE_HOME"] || join(homedir(), ".cache"));
144
144
  return join(base, "nebula-ai");
145
145
  }
146
146
  function getCacheDir(version) {
@@ -242,7 +242,7 @@ function maybeSpawnUpdateCheck(currentVersion) {
242
242
  // File missing — fall through to spawn.
243
243
  }
244
244
  const channel = getChannel(currentVersion);
245
- const distTag = channel === "dev" ? "dev" : "latest";
245
+ const distTag = distTagForChannel(channel);
246
246
  const script = `
247
247
  const fs = require("node:fs");
248
248
  const path = require("node:path");
@@ -293,7 +293,9 @@ function maybeShowUpdateNotification(currentVersion, installMethod) {
293
293
  const cmd = getUpgradeCommand(installMethod, channel) ||
294
294
  (channel === "dev"
295
295
  ? "npm install -g nebula-ai@dev"
296
- : "npm install -g nebula-ai");
296
+ : channel === "nightly"
297
+ ? "npm install -g nebula-ai@nightly"
298
+ : "npm install -g nebula-ai");
297
299
  process.stderr.write(`\nnebula-ai: update available ${currentVersion} \u2192 ${cached.latest}\n` +
298
300
  ` Run: ${cmd}\n`);
299
301
  }
@@ -310,7 +312,7 @@ async function main() {
310
312
  " npm install -g nebula-ai\n" +
311
313
  "\n" +
312
314
  "If the problem persists, please open an issue:\n" +
313
- " https://github.com/agent-labs-dev/nebula-cli/issues");
315
+ " https://github.com/agent-labs-dev/nebula-desktop/issues");
314
316
  process.exit(1);
315
317
  }
316
318
  if (!SUPPORTED_PLATFORMS.has(platformKey)) {
@@ -319,7 +321,7 @@ async function main() {
319
321
  `Supported platforms: ${[...SUPPORTED_PLATFORMS].join(", ")}.\n` +
320
322
  `\n` +
321
323
  `Please open an issue to request support:\n` +
322
- ` https://github.com/agent-labs-dev/nebula-cli/issues`);
324
+ ` https://github.com/agent-labs-dev/nebula-desktop/issues`);
323
325
  process.exit(1);
324
326
  }
325
327
  let version;
@@ -347,7 +349,7 @@ async function main() {
347
349
  `If the problem persists:\n` +
348
350
  ` • Clear the download cache: rm -rf ${getCacheDir(version)}\n` +
349
351
  ` • Reinstall: npm install -g nebula-ai\n` +
350
- ` • Open an issue: https://github.com/agent-labs-dev/nebula-cli/issues`);
352
+ ` • Open an issue: https://github.com/agent-labs-dev/nebula-desktop/issues`);
351
353
  process.exit(1);
352
354
  }
353
355
  // New version downloaded — purge stale version directories.
@@ -364,8 +366,8 @@ async function main() {
364
366
  const installMethod = detectInstallMethod({
365
367
  execPath: fileURLToPath(import.meta.url),
366
368
  argv0: process.argv[0],
367
- npmCommand: process.env.npm_command,
368
- npmUserAgent: process.env.npm_config_user_agent,
369
+ npmCommand: process.env["npm_command"],
370
+ npmUserAgent: process.env["npm_config_user_agent"],
369
371
  });
370
372
  const result = spawnSync(binaryPath, process.argv.slice(2), {
371
373
  stdio: "inherit",
@@ -27,37 +27,50 @@ export const UPDATE_CHECK_TTL_MS = 4 * 60 * 60 * 1000;
27
27
  * across version upgrades.
28
28
  */
29
29
  export function getUpdateCheckCachePath() {
30
- const base = process.env.NEBULA_AI_CACHE_DIR ||
30
+ const base = process.env["NEBULA_AI_CACHE_DIR"] ||
31
31
  (process.platform === "win32"
32
- ? process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local")
33
- : process.env.XDG_CACHE_HOME || join(homedir(), ".cache"));
32
+ ? process.env["LOCALAPPDATA"] || join(homedir(), "AppData", "Local")
33
+ : process.env["XDG_CACHE_HOME"] || join(homedir(), ".cache"));
34
34
  return join(base, "nebula-ai", "update-check.json");
35
35
  }
36
- // ─── version helpers ───────────────────────────────────────────────────────
37
36
  /**
38
37
  * Determine the release channel from a version string.
39
- * Dev versions look like `x.y.z-dev.YYYYMMDDHHMMSS.sha`.
38
+ * Pre-stable versions look like `x.y.z-<channel>.YYYYMMDDHHMMSS.sha`.
40
39
  */
41
40
  export function getChannel(version) {
42
- return version.includes("-dev.") ? "dev" : "prod";
41
+ if (version.includes("-dev."))
42
+ return "dev";
43
+ if (version.includes("-nightly."))
44
+ return "nightly";
45
+ return "prod";
46
+ }
47
+ /** Map a channel back to the npm dist-tag we publish under. */
48
+ export function distTagForChannel(channel) {
49
+ if (channel === "dev")
50
+ return "dev";
51
+ if (channel === "nightly")
52
+ return "nightly";
53
+ return "latest";
43
54
  }
44
55
  /**
45
56
  * Return true if `latest` is a newer semver than `current` (prod), or a
46
- * strictly newer build timestamp (dev).
57
+ * strictly newer build timestamp (dev/nightly).
47
58
  *
48
- * Dev versions look like `x.y.z-dev.YYYYMMDDHHMMSS.sha`, produced by CI on
49
- * every merge. Comparing with `!==` would falsely flag any mismatch as an
50
- * "update available" — including the common case where a contributor runs a
51
- * locally-built dev binary that's ahead of the registry's `dev` dist-tag
52
- * (e.g. a PR branch that's ahead of main). Parse the 14-digit stamp out of
53
- * both sides and compare those instead; fall back to `!==` only if either
54
- * string doesn't match the expected shape.
59
+ * Pre-stable versions look like `x.y.z-<channel>.YYYYMMDDHHMMSS.sha`,
60
+ * produced by CI on every release run. Comparing with `!==` would falsely
61
+ * flag any mismatch as an "update available" — including the common case
62
+ * where a contributor runs a locally-built pre-stable binary that's ahead
63
+ * of the registry's dist-tag (e.g. a PR branch that's ahead of main).
64
+ * Parse the 14-digit stamp out of both sides and compare those instead;
65
+ * fall back to `!==` only if either string doesn't match the expected
66
+ * shape.
55
67
  */
56
68
  export function isNewerVersion(latest, current) {
57
- if (current.includes("-dev.")) {
58
- const devStamp = /-dev\.(\d{14})\./;
59
- const latestStamp = latest.match(devStamp)?.[1];
60
- const currentStamp = current.match(devStamp)?.[1];
69
+ const currentChannel = getChannel(current);
70
+ if (currentChannel === "dev" || currentChannel === "nightly") {
71
+ const stamp = new RegExp(`-${currentChannel}\\.(\\d{14})\\.`);
72
+ const latestStamp = latest.match(stamp)?.[1];
73
+ const currentStamp = current.match(stamp)?.[1];
61
74
  if (latestStamp && currentStamp)
62
75
  return latestStamp > currentStamp;
63
76
  return latest !== current;
@@ -98,10 +111,10 @@ export function isUpdateCheckDisabled(stream = process.stderr) {
98
111
  // but the long tail of CI runners uses `CI=1`, `CI=yes`, etc. Treat any
99
112
  // non-empty value as "in CI" unless it's explicitly opted out
100
113
  // (`CI=false` / `CI=0`), matching npm's is-ci heuristic.
101
- const ci = process.env.CI?.toLowerCase();
114
+ const ci = process.env["CI"]?.toLowerCase();
102
115
  const inCI = !!ci && ci !== "false" && ci !== "0";
103
- return (process.env.NEBULA_AI_NO_UPDATE_CHECK === "1" ||
104
- process.env.NEBULA_AI_NO_UPDATE_CHECK === "true" ||
116
+ return (process.env["NEBULA_AI_NO_UPDATE_CHECK"] === "1" ||
117
+ process.env["NEBULA_AI_NO_UPDATE_CHECK"] === "true" ||
105
118
  inCI ||
106
119
  !stream.isTTY);
107
120
  }
@@ -124,7 +137,7 @@ export function isUpdateCheckDisabled(stream = process.stderr) {
124
137
  * path-based signal (like `detectInstallMethod`) layer that check on top.
125
138
  */
126
139
  export function isNpx() {
127
- const cmd = process.env.npm_command;
140
+ const cmd = process.env["npm_command"];
128
141
  return cmd === "exec" || cmd === "dlx";
129
142
  }
130
143
  /**
@@ -151,6 +164,9 @@ export function detectInstallMethod(input) {
151
164
  // Normalize Windows separators so our substring checks work uniformly.
152
165
  p.replace(/\\/g, "/"));
153
166
  const anyMatch = (...needles) => paths.some((p) => needles.some((n) => p.includes(n)));
167
+ if (anyMatch(".app/Contents/Resources/")) {
168
+ return "desktop-bundle";
169
+ }
154
170
  // Homebrew formula name is `nebula-ai`, so a brew-installed binary lives
155
171
  // under `.../Cellar/nebula-ai/<version>/bin/nebula-ai`. Matching the
156
172
  // formula segment specifically avoids misdetecting an npm global under a
@@ -195,7 +211,11 @@ export function detectInstallMethod(input) {
195
211
  * single-shot upgrade makes sense (npx / unknown).
196
212
  */
197
213
  export function getUpgradeCommand(method, channel) {
198
- const pkg = channel === "dev" ? "nebula-ai@dev" : "nebula-ai";
214
+ const pkg = channel === "dev"
215
+ ? "nebula-ai@dev"
216
+ : channel === "nightly"
217
+ ? "nebula-ai@nightly"
218
+ : "nebula-ai";
199
219
  switch (method) {
200
220
  case "npm":
201
221
  return `npm install -g ${pkg}`;
@@ -210,12 +230,17 @@ export function getUpgradeCommand(method, channel) {
210
230
  case "bun":
211
231
  return `bun add -g ${pkg}`;
212
232
  case "brew":
213
- // The brew tap is prod-only (dev versions use `-dev.<stamp>.<sha>`
214
- // suffixes Homebrew mishandles). If someone somehow ended up on dev
215
- // via brew, redirect them to npm for the dev channel.
216
- return channel === "dev"
217
- ? `npm install -g nebula-ai@dev`
218
- : `brew upgrade nebula-ai`;
233
+ // The brew tap is stable-only (pre-stable versions use
234
+ // `-<channel>.<stamp>.<sha>` suffixes Homebrew mishandles). If
235
+ // someone somehow ended up on a pre-stable build via brew,
236
+ // redirect them to npm for the matching channel.
237
+ if (channel === "dev")
238
+ return "npm install -g nebula-ai@dev";
239
+ if (channel === "nightly")
240
+ return "npm install -g nebula-ai@nightly";
241
+ return "brew upgrade nebula-ai";
242
+ case "desktop-bundle":
243
+ return "Update via Nebula.app -> Settings";
219
244
  case "npx":
220
245
  case "unknown":
221
246
  return "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nebula-ai",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "CLI/TUI for the nebula.gg AI task orchestration platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,13 +21,14 @@
21
21
  "tasks"
22
22
  ],
23
23
  "license": "MIT",
24
- "homepage": "https://github.com/agent-labs-dev/nebula-cli#readme",
24
+ "homepage": "https://github.com/agent-labs-dev/nebula-desktop/tree/main/apps/cli#readme",
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "git+https://github.com/agent-labs-dev/nebula-cli.git"
27
+ "url": "git+https://github.com/agent-labs-dev/nebula-desktop.git",
28
+ "directory": "apps/cli"
28
29
  },
29
30
  "bugs": {
30
- "url": "https://github.com/agent-labs-dev/nebula-cli/issues"
31
+ "url": "https://github.com/agent-labs-dev/nebula-desktop/issues"
31
32
  },
32
33
  "publishConfig": {
33
34
  "access": "public"