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 +14 -2
- package/bin/nebula-ai.js +22 -20
- package/bin/update-check-lib.js +54 -29
- package/package.json +5 -4
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
|
-
|
|
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
|
|
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
|
-
|
|
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://
|
|
65
|
-
const DOWNLOAD_BASE_URL = process.env
|
|
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
|
|
101
|
-
throw new Error(`unsupported manifest algorithm: ${m
|
|
100
|
+
if (m["algorithm"] !== "sha256") {
|
|
101
|
+
throw new Error(`unsupported manifest algorithm: ${m["algorithm"] ?? "(missing)"}`);
|
|
102
102
|
}
|
|
103
|
-
if (!m
|
|
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
|
|
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
|
|
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
|
|
140
|
+
const base = process.env["NEBULA_AI_CACHE_DIR"] ||
|
|
141
141
|
(process.platform === "win32"
|
|
142
|
-
? process.env
|
|
143
|
-
: process.env
|
|
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
|
|
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
|
-
:
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
368
|
-
npmUserAgent: process.env
|
|
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",
|
package/bin/update-check-lib.js
CHANGED
|
@@ -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
|
|
30
|
+
const base = process.env["NEBULA_AI_CACHE_DIR"] ||
|
|
31
31
|
(process.platform === "win32"
|
|
32
|
-
? process.env
|
|
33
|
-
: process.env
|
|
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
|
-
*
|
|
38
|
+
* Pre-stable versions look like `x.y.z-<channel>.YYYYMMDDHHMMSS.sha`.
|
|
40
39
|
*/
|
|
41
40
|
export function getChannel(version) {
|
|
42
|
-
|
|
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
|
-
*
|
|
49
|
-
* every
|
|
50
|
-
* "update available" — including the common case
|
|
51
|
-
* locally-built
|
|
52
|
-
* (e.g. a PR branch that's ahead of main).
|
|
53
|
-
* both sides and compare those instead;
|
|
54
|
-
* string doesn't match the expected
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
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
|
|
114
|
+
const ci = process.env["CI"]?.toLowerCase();
|
|
102
115
|
const inCI = !!ci && ci !== "false" && ci !== "0";
|
|
103
|
-
return (process.env
|
|
104
|
-
process.env
|
|
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
|
|
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"
|
|
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
|
|
214
|
-
// suffixes Homebrew mishandles). If
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
31
|
+
"url": "https://github.com/agent-labs-dev/nebula-desktop/issues"
|
|
31
32
|
},
|
|
32
33
|
"publishConfig": {
|
|
33
34
|
"access": "public"
|