@build-astron-co/nimbus 0.2.0 → 0.3.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.
- package/bin/nimbus +26 -10
- package/package.json +13 -12
- package/src/__tests__/app.test.ts +1 -1
- package/src/__tests__/audit.test.ts +1 -1
- package/src/__tests__/circuit-breaker.test.ts +1 -1
- package/src/__tests__/cli-run.test.ts +1 -1
- package/src/__tests__/context-manager.test.ts +1 -1
- package/src/__tests__/context.test.ts +1 -1
- package/src/__tests__/enterprise.test.ts +1 -1
- package/src/__tests__/generator.test.ts +1 -1
- package/src/__tests__/hooks.test.ts +1 -1
- package/src/__tests__/init.test.ts +1 -1
- package/src/__tests__/intent-parser.test.ts +1 -1
- package/src/__tests__/llm-router.test.ts +1 -1
- package/src/__tests__/lsp.test.ts +1 -1
- package/src/__tests__/modes.test.ts +1 -1
- package/src/__tests__/permissions.test.ts +1 -1
- package/src/__tests__/serve.test.ts +1 -1
- package/src/__tests__/sessions.test.ts +1 -1
- package/src/__tests__/sharing.test.ts +53 -1
- package/src/__tests__/snapshots.test.ts +1 -1
- package/src/__tests__/state-db.test.ts +1 -1
- package/src/__tests__/stream-with-tools.test.ts +23 -25
- package/src/__tests__/subagents.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/tool-converter.test.ts +1 -1
- package/src/__tests__/tool-schemas.test.ts +1 -1
- package/src/__tests__/tools.test.ts +4 -3
- package/src/__tests__/version.test.ts +1 -1
- package/src/auth/oauth.ts +15 -5
- package/src/compat/runtime.ts +1 -1
- package/src/hooks/engine.ts +5 -4
- package/src/lsp/manager.ts +1 -1
- package/src/nimbus.ts +3 -17
- package/src/sharing/sync.ts +4 -0
- package/src/ui/streaming.ts +1 -1
- package/src/version.ts +1 -1
- package/src/wizard/ui.ts +1 -1
- package/tsconfig.json +2 -2
package/bin/nimbus
CHANGED
|
@@ -2,28 +2,44 @@
|
|
|
2
2
|
# Nimbus CLI launcher — prefers Bun, falls back to Node.js (>=18).
|
|
3
3
|
set -e
|
|
4
4
|
|
|
5
|
+
# Resolve the real script location, following symlinks so this works when npm
|
|
6
|
+
# installs the bin entry as a symlink (e.g. /opt/homebrew/bin/nimbus ->
|
|
7
|
+
# .../node_modules/@build-astron-co/nimbus/bin/nimbus).
|
|
5
8
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
-
|
|
9
|
+
REAL_SCRIPT="$SCRIPT_DIR/nimbus"
|
|
10
|
+
if [ -L "$REAL_SCRIPT" ]; then
|
|
11
|
+
LINK_TARGET="$(readlink "$REAL_SCRIPT")"
|
|
12
|
+
case "$LINK_TARGET" in
|
|
13
|
+
/*) REAL_SCRIPT="$LINK_TARGET" ;;
|
|
14
|
+
*) REAL_SCRIPT="$SCRIPT_DIR/$LINK_TARGET" ;;
|
|
15
|
+
esac
|
|
16
|
+
fi
|
|
17
|
+
PKG_ROOT="$(cd "$(dirname "$REAL_SCRIPT")/.." && pwd)"
|
|
18
|
+
ENTRY="$PKG_ROOT/src/nimbus.ts"
|
|
7
19
|
|
|
8
20
|
# Prefer Bun for best performance (native bun:sqlite, fast startup)
|
|
9
21
|
if command -v bun >/dev/null 2>&1; then
|
|
10
22
|
exec bun "$ENTRY" "$@"
|
|
11
23
|
fi
|
|
12
24
|
|
|
13
|
-
# Fallback: Node.js with tsx for TypeScript execution
|
|
25
|
+
# Fallback: Node.js with tsx for TypeScript execution.
|
|
26
|
+
# We use `node --loader tsx/esm` rather than `tsx` directly to ensure ESM
|
|
27
|
+
# output mode — required because yoga-layout (Ink dep) uses top-level await
|
|
28
|
+
# which is incompatible with tsx's default CJS output.
|
|
14
29
|
if command -v node >/dev/null 2>&1; then
|
|
15
30
|
NODE_VERSION=$(node -e "console.log(process.versions.node.split('.')[0])")
|
|
16
31
|
if [ "$NODE_VERSION" -ge 18 ] 2>/dev/null; then
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
if [ -
|
|
20
|
-
exec "$
|
|
32
|
+
# Prefer the tsx ESM loader bundled with this package
|
|
33
|
+
TSX_ESM="$PKG_ROOT/node_modules/tsx/dist/esm/index.mjs"
|
|
34
|
+
if [ -f "$TSX_ESM" ]; then
|
|
35
|
+
exec node --loader "$TSX_ESM" "$ENTRY" "$@"
|
|
21
36
|
fi
|
|
22
|
-
# Try global tsx
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
# Try global tsx ESM loader (npm global install)
|
|
38
|
+
TSX_GLOBAL_ESM="$(npm root -g 2>/dev/null)/tsx/dist/esm/index.mjs"
|
|
39
|
+
if [ -f "$TSX_GLOBAL_ESM" ]; then
|
|
40
|
+
exec node --loader "$TSX_GLOBAL_ESM" "$ENTRY" "$@"
|
|
25
41
|
fi
|
|
26
|
-
# Last resort:
|
|
42
|
+
# Last resort: node --import tsx (Node >= 18.19, tsx must be globally installed)
|
|
27
43
|
exec node --import tsx "$ENTRY" "$@"
|
|
28
44
|
fi
|
|
29
45
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@build-astron-co/nimbus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AI-Powered Cloud Engineering Agent",
|
|
5
5
|
"main": "src/nimbus.ts",
|
|
6
6
|
"bin": {
|
|
@@ -16,34 +16,35 @@
|
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "
|
|
20
|
-
"test:coverage": "
|
|
21
|
-
"test:watch": "
|
|
19
|
+
"test": "vitest run src/__tests__/",
|
|
20
|
+
"test:coverage": "vitest run src/__tests__/ --coverage",
|
|
21
|
+
"test:watch": "vitest src/__tests__/ --watch",
|
|
22
22
|
"lint": "eslint . --ext .ts,.tsx",
|
|
23
23
|
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
|
24
24
|
"format": "prettier --write .",
|
|
25
25
|
"format:check": "prettier --check .",
|
|
26
|
-
"type-check": "
|
|
27
|
-
"build": "
|
|
26
|
+
"type-check": "tsc --noEmit",
|
|
27
|
+
"build": "./scripts/build-binary.sh",
|
|
28
28
|
"build:binary": "./scripts/build-binary.sh",
|
|
29
29
|
"build:binary:all": "./scripts/build-binary.sh all",
|
|
30
|
-
"nimbus": "
|
|
31
|
-
"precommit": "
|
|
30
|
+
"nimbus": "node --loader ./node_modules/tsx/dist/esm/index.mjs src/nimbus.ts",
|
|
31
|
+
"precommit": "npm run lint && npm run format:check && npm run type-check"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/better-sqlite3": "^7.6.0",
|
|
35
|
-
"@types/bun": "latest",
|
|
36
35
|
"@types/js-yaml": "^4.0.9",
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
37
|
"@types/react": "^19.2.14",
|
|
38
38
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
39
39
|
"@typescript-eslint/parser": "^6.21.0",
|
|
40
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
40
41
|
"eslint": "^8.57.0",
|
|
41
42
|
"prettier": "^3.2.5",
|
|
42
|
-
"typescript": "^5.3.3"
|
|
43
|
+
"typescript": "^5.3.3",
|
|
44
|
+
"vitest": "^2.0.0"
|
|
43
45
|
},
|
|
44
46
|
"engines": {
|
|
45
|
-
"node": ">=18.0.0"
|
|
46
|
-
"bun": ">=1.0.0"
|
|
47
|
+
"node": ">=18.0.0"
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - shutdownApp() is idempotent (safe to call when already shut down)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { describe, it, expect, afterEach } from '
|
|
11
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
12
12
|
import { initApp, shutdownApp, getAppContext } from '../app';
|
|
13
13
|
|
|
14
14
|
describe('app lifecycle', () => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Activity Log
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
9
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
10
10
|
import * as fs from 'node:fs';
|
|
11
11
|
import * as path from 'node:path';
|
|
12
12
|
import * as os from 'node:os';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* structured RunOptions for the non-interactive nimbus run command.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { describe, test, expect } from '
|
|
8
|
+
import { describe, test, expect } from 'vitest';
|
|
9
9
|
import { parseRunArgs } from '../cli/run';
|
|
10
10
|
|
|
11
11
|
// ===========================================================================
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module __tests__/context-manager
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { describe, it, expect, beforeEach } from '
|
|
10
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
11
11
|
import { ContextManager, estimateTokens, estimateMessageTokens } from '../agent/context-manager';
|
|
12
12
|
import type { LLMMessage } from '../llm/types';
|
|
13
13
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* context injection formatting, and fuzzy file search.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
8
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
9
9
|
import * as fs from 'node:fs';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import * as os from 'node:os';
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* each test group and reset the db singleton afterwards via `closeDb()`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { describe, it, expect, beforeEach, afterEach } from '
|
|
16
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
17
17
|
import { getTestDb, closeDb } from '../state/db';
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* tests run entirely in-memory and complete in milliseconds.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { describe, it, expect } from '
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
13
|
import { TerraformProjectGenerator, type TerraformProjectConfig } from '../generator/terraform';
|
|
14
14
|
import {
|
|
15
15
|
KubernetesGenerator,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* with real hook scripts and hooks.yaml configuration files.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
12
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
13
13
|
import * as fs from 'node:fs';
|
|
14
14
|
import * as path from 'node:path';
|
|
15
15
|
import * as os from 'node:os';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* exercises the detection and init functions, and cleans up afterward.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
8
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
9
9
|
import * as fs from 'node:fs';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import * as os from 'node:os';
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* intentionally excluded from unit tests.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { describe, it, expect } from '
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
11
|
import { IntentParser, type ConversationalIntent } from '../generator/intent-parser';
|
|
12
12
|
|
|
13
13
|
// Helper: create a parser in pure-heuristic mode (no router)
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (no I/O, no network) and execute synchronously.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { describe, it, expect } from '
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
13
|
import { resolveModelAlias, getAliases } from '../llm/model-aliases';
|
|
14
14
|
import { detectProvider } from '../llm/provider-registry';
|
|
15
15
|
import { calculateCost, getPricingData, type CostResult } from '../llm/cost-calculator';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for LSP Manager, Client, Language configs, and Agent Loop integration.
|
|
3
3
|
*/
|
|
4
|
-
import { describe, it, expect, beforeEach, afterEach } from '
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
5
|
import { getLanguageForFile, getLanguagePriority, LANGUAGE_CONFIGS } from '../lsp/languages';
|
|
6
6
|
import { LSPManager, resetLSPManager } from '../lsp/manager';
|
|
7
7
|
import { severityLabel } from '../lsp/client';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for Multi-Session Manager
|
|
3
3
|
*/
|
|
4
|
-
import { describe, it, expect, beforeEach, afterEach } from '
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
5
|
import { Database } from '../compat/sqlite';
|
|
6
6
|
import { SessionManager } from '../sessions/manager';
|
|
7
7
|
import type { SessionEvent } from '../sessions/types';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for Session Sharing
|
|
3
3
|
*/
|
|
4
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
4
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
5
5
|
import {
|
|
6
6
|
shareSession,
|
|
7
7
|
getSharedSession,
|
|
@@ -39,7 +39,58 @@ const mockMessages = [
|
|
|
39
39
|
// Inject test dependencies (no module-level mocks — avoids cross-file leaks)
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
41
41
|
|
|
42
|
+
/** Minimal in-memory DB that mimics the SQLite API used by sharing/sync.ts */
|
|
43
|
+
function makeInMemoryDb() {
|
|
44
|
+
const rows = new Map<string, any>();
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
run(sql: string, params: any[]) {
|
|
48
|
+
if (/INSERT INTO shares/.test(sql)) {
|
|
49
|
+
const [id, session_id, name, messages, model, mode, cost_usd, token_count, is_live, write_token, created_at, expires_at] = params;
|
|
50
|
+
rows.set(id, { id, session_id, name, messages, model, mode, cost_usd, token_count, is_live, write_token, created_at, expires_at });
|
|
51
|
+
return { changes: 1 };
|
|
52
|
+
} else if (/UPDATE shares SET messages/.test(sql)) {
|
|
53
|
+
const [messages, id] = params;
|
|
54
|
+
const row = rows.get(id);
|
|
55
|
+
if (row) { row.messages = messages; return { changes: 1 }; }
|
|
56
|
+
return { changes: 0 };
|
|
57
|
+
} else if (/DELETE FROM shares WHERE expires_at/.test(sql)) {
|
|
58
|
+
const [now] = params;
|
|
59
|
+
let count = 0;
|
|
60
|
+
for (const [id, row] of rows) {
|
|
61
|
+
if (row.expires_at <= now) { rows.delete(id); count++; }
|
|
62
|
+
}
|
|
63
|
+
return { changes: count };
|
|
64
|
+
} else if (/DELETE FROM shares WHERE id/.test(sql)) {
|
|
65
|
+
const [id] = params;
|
|
66
|
+
const existed = rows.has(id);
|
|
67
|
+
rows.delete(id);
|
|
68
|
+
return { changes: existed ? 1 : 0 };
|
|
69
|
+
}
|
|
70
|
+
return { changes: 0 };
|
|
71
|
+
},
|
|
72
|
+
query(sql: string) {
|
|
73
|
+
return {
|
|
74
|
+
get(...params: any[]) {
|
|
75
|
+
if (/WHERE id = \? AND expires_at/.test(sql)) {
|
|
76
|
+
const [id, now] = params;
|
|
77
|
+
const row = rows.get(id);
|
|
78
|
+
return row && row.expires_at > now ? row : undefined;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
},
|
|
82
|
+
all(..._params: any[]) {
|
|
83
|
+
// listShares uses DELETE first then SELECT with ORDER BY (no WHERE params)
|
|
84
|
+
return [...rows.values()];
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
42
91
|
beforeEach(() => {
|
|
92
|
+
const db = makeInMemoryDb();
|
|
93
|
+
_deps.getDb = () => db;
|
|
43
94
|
_deps.getConversation = (id: string) =>
|
|
44
95
|
id === 'test-session-id' ? { messages: mockMessages } : null;
|
|
45
96
|
_deps.getSessionManager = () => ({
|
|
@@ -48,6 +99,7 @@ beforeEach(() => {
|
|
|
48
99
|
});
|
|
49
100
|
|
|
50
101
|
afterEach(() => {
|
|
102
|
+
_deps.getDb = undefined;
|
|
51
103
|
_deps.getConversation = undefined;
|
|
52
104
|
_deps.getSessionManager = undefined;
|
|
53
105
|
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* git write-tree / read-tree / checkout-index workflow.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
12
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
13
13
|
import * as fs from 'node:fs';
|
|
14
14
|
import * as path from 'node:path';
|
|
15
15
|
import * as os from 'node:os';
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* is never touched, and tests are fully isolated and fast.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { describe, it, expect, beforeEach } from '
|
|
13
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
14
14
|
import type { Database } from '../compat/sqlite';
|
|
15
15
|
import { getTestDb } from '../state/db';
|
|
16
16
|
import {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* All tests use mocks -- no real API calls are made.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { describe, test, expect,
|
|
17
|
+
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
|
18
18
|
import type { ToolCompletionRequest, StreamChunk } from '../llm/types';
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
@@ -120,7 +120,7 @@ describe('OllamaProvider.streamWithTools', () => {
|
|
|
120
120
|
'data: [DONE]\n\n',
|
|
121
121
|
];
|
|
122
122
|
|
|
123
|
-
globalThis.fetch =
|
|
123
|
+
globalThis.fetch = vi.fn(() =>
|
|
124
124
|
Promise.resolve(
|
|
125
125
|
new Response(buildReadableStream(sseLines), {
|
|
126
126
|
status: 200,
|
|
@@ -170,7 +170,7 @@ describe('OllamaProvider.streamWithTools', () => {
|
|
|
170
170
|
'data: [DONE]\n\n',
|
|
171
171
|
];
|
|
172
172
|
|
|
173
|
-
globalThis.fetch =
|
|
173
|
+
globalThis.fetch = vi.fn(() =>
|
|
174
174
|
Promise.resolve(
|
|
175
175
|
new Response(buildReadableStream(sseLines), {
|
|
176
176
|
status: 200,
|
|
@@ -202,7 +202,7 @@ describe('OllamaProvider.streamWithTools', () => {
|
|
|
202
202
|
test('fallback: when native streaming fails, falls back to completeWithTools', async () => {
|
|
203
203
|
let _callCount = 0;
|
|
204
204
|
|
|
205
|
-
globalThis.fetch =
|
|
205
|
+
globalThis.fetch = vi.fn((url: string | URL | Request) => {
|
|
206
206
|
_callCount++;
|
|
207
207
|
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url;
|
|
208
208
|
|
|
@@ -268,7 +268,7 @@ describe('OllamaProvider.streamWithTools', () => {
|
|
|
268
268
|
'data: [DONE]\n\n',
|
|
269
269
|
];
|
|
270
270
|
|
|
271
|
-
globalThis.fetch =
|
|
271
|
+
globalThis.fetch = vi.fn(() =>
|
|
272
272
|
Promise.resolve(
|
|
273
273
|
new Response(buildReadableStream(sseLines), {
|
|
274
274
|
status: 200,
|
|
@@ -311,7 +311,7 @@ describe('OpenRouterProvider.streamWithTools', () => {
|
|
|
311
311
|
},
|
|
312
312
|
]);
|
|
313
313
|
|
|
314
|
-
const mockCreate =
|
|
314
|
+
const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
|
|
315
315
|
|
|
316
316
|
const { OpenRouterProvider } = await import('../llm/providers/openrouter');
|
|
317
317
|
const provider = new OpenRouterProvider('test-api-key');
|
|
@@ -392,7 +392,7 @@ describe('OpenRouterProvider.streamWithTools', () => {
|
|
|
392
392
|
},
|
|
393
393
|
]);
|
|
394
394
|
|
|
395
|
-
const mockCreate =
|
|
395
|
+
const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
|
|
396
396
|
|
|
397
397
|
const { OpenRouterProvider } = await import('../llm/providers/openrouter');
|
|
398
398
|
const provider = new OpenRouterProvider('test-api-key');
|
|
@@ -415,7 +415,7 @@ describe('OpenRouterProvider.streamWithTools', () => {
|
|
|
415
415
|
});
|
|
416
416
|
|
|
417
417
|
test('fallback: when SDK stream creation throws, the generator yields nothing', async () => {
|
|
418
|
-
const mockCreate =
|
|
418
|
+
const mockCreate = vi.fn(() => Promise.reject(new Error('API unavailable')));
|
|
419
419
|
|
|
420
420
|
const { OpenRouterProvider } = await import('../llm/providers/openrouter');
|
|
421
421
|
const provider = new OpenRouterProvider('test-api-key');
|
|
@@ -462,7 +462,7 @@ describe('OpenRouterProvider.streamWithTools', () => {
|
|
|
462
462
|
},
|
|
463
463
|
]);
|
|
464
464
|
|
|
465
|
-
const mockCreate =
|
|
465
|
+
const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
|
|
466
466
|
|
|
467
467
|
const { OpenRouterProvider } = await import('../llm/providers/openrouter');
|
|
468
468
|
const provider = new OpenRouterProvider('test-api-key');
|
|
@@ -495,10 +495,8 @@ describe('OpenRouterProvider.streamWithTools', () => {
|
|
|
495
495
|
// ===========================================================================
|
|
496
496
|
|
|
497
497
|
describe('OpenAICompatibleProvider.streamWithTools', () => {
|
|
498
|
-
function createProvider() {
|
|
499
|
-
|
|
500
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
501
|
-
const { OpenAICompatibleProvider } = require('../llm/providers/openai-compatible');
|
|
498
|
+
async function createProvider() {
|
|
499
|
+
const { OpenAICompatibleProvider } = await import('../llm/providers/openai-compatible');
|
|
502
500
|
return new OpenAICompatibleProvider({
|
|
503
501
|
name: 'test-compat',
|
|
504
502
|
apiKey: 'test-key',
|
|
@@ -517,9 +515,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
|
|
|
517
515
|
},
|
|
518
516
|
]);
|
|
519
517
|
|
|
520
|
-
const mockCreate =
|
|
518
|
+
const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
|
|
521
519
|
|
|
522
|
-
const provider = createProvider();
|
|
520
|
+
const provider = await createProvider();
|
|
523
521
|
(provider as any).client = {
|
|
524
522
|
chat: { completions: { create: mockCreate } },
|
|
525
523
|
};
|
|
@@ -578,9 +576,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
|
|
|
578
576
|
},
|
|
579
577
|
]);
|
|
580
578
|
|
|
581
|
-
const mockCreate =
|
|
579
|
+
const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
|
|
582
580
|
|
|
583
|
-
const provider = createProvider();
|
|
581
|
+
const provider = await createProvider();
|
|
584
582
|
(provider as any).client = {
|
|
585
583
|
chat: { completions: { create: mockCreate } },
|
|
586
584
|
};
|
|
@@ -600,9 +598,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
|
|
|
600
598
|
});
|
|
601
599
|
|
|
602
600
|
test('fallback: when SDK stream creation throws, the error propagates', async () => {
|
|
603
|
-
const mockCreate =
|
|
601
|
+
const mockCreate = vi.fn(() => Promise.reject(new Error('Provider down')));
|
|
604
602
|
|
|
605
|
-
const provider = createProvider();
|
|
603
|
+
const provider = await createProvider();
|
|
606
604
|
(provider as any).client = {
|
|
607
605
|
chat: { completions: { create: mockCreate } },
|
|
608
606
|
};
|
|
@@ -655,9 +653,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
|
|
|
655
653
|
},
|
|
656
654
|
]);
|
|
657
655
|
|
|
658
|
-
const mockCreate =
|
|
656
|
+
const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
|
|
659
657
|
|
|
660
|
-
const provider = createProvider();
|
|
658
|
+
const provider = await createProvider();
|
|
661
659
|
(provider as any).client = {
|
|
662
660
|
chat: { completions: { create: mockCreate } },
|
|
663
661
|
};
|
|
@@ -686,9 +684,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
|
|
|
686
684
|
},
|
|
687
685
|
]);
|
|
688
686
|
|
|
689
|
-
const mockCreate =
|
|
687
|
+
const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
|
|
690
688
|
|
|
691
|
-
const provider = createProvider();
|
|
689
|
+
const provider = await createProvider();
|
|
692
690
|
(provider as any).client = {
|
|
693
691
|
chat: { completions: { create: mockCreate } },
|
|
694
692
|
};
|
|
@@ -709,9 +707,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
|
|
|
709
707
|
{ choices: [{ delta: { content: 'done' }, finish_reason: 'stop' }] },
|
|
710
708
|
]);
|
|
711
709
|
|
|
712
|
-
const mockCreate =
|
|
710
|
+
const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
|
|
713
711
|
|
|
714
|
-
const provider = createProvider();
|
|
712
|
+
const provider = await createProvider();
|
|
715
713
|
(provider as any).client = {
|
|
716
714
|
chat: { completions: { create: mockCreate } },
|
|
717
715
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* based on mode, tools, NIMBUS.md, subagent state, and environment context.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
8
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
9
9
|
import * as fs from 'node:fs';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import * as os from 'node:os';
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* tests fast and hermetic.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { describe, it, expect } from '
|
|
13
|
-
import {
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
14
15
|
import { FileSystemOperations } from '../tools/file-ops';
|
|
15
16
|
import { GitOperations } from '../tools/git-ops';
|
|
16
17
|
|
|
17
18
|
// The repository root is two levels above this test file: src/__tests__/ -> src/ -> repo-root
|
|
18
|
-
const REPO_ROOT = join(import.meta.
|
|
19
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// FileSystemOperations
|
package/src/auth/oauth.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* Fallback: Browser-based OAuth with local callback server
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
createServer,
|
|
9
|
+
type Server as HttpServer,
|
|
10
|
+
type IncomingMessage,
|
|
11
|
+
type ServerResponse,
|
|
12
|
+
} from 'node:http';
|
|
7
13
|
import type {
|
|
8
14
|
GitHubDeviceCodeResponse,
|
|
9
15
|
GitHubAccessTokenResponse,
|
|
@@ -229,7 +235,7 @@ export async function completeGitHubAuth(accessToken: string): Promise<GitHubIde
|
|
|
229
235
|
* Creates a temporary local server to receive the OAuth callback
|
|
230
236
|
*/
|
|
231
237
|
export class BrowserOAuthServer {
|
|
232
|
-
private server:
|
|
238
|
+
private server: HttpServer | null = null;
|
|
233
239
|
private clientId: string;
|
|
234
240
|
private codePromise: Promise<string> | null = null;
|
|
235
241
|
private codeResolve: ((code: string) => void) | null = null;
|
|
@@ -250,10 +256,14 @@ export class BrowserOAuthServer {
|
|
|
250
256
|
});
|
|
251
257
|
|
|
252
258
|
// Start the server
|
|
253
|
-
this.server =
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
this.server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
260
|
+
const url = new URL(req.url || '/', `http://localhost:${CALLBACK_PORT}`);
|
|
261
|
+
const webReq = new Request(url, { method: req.method || 'GET' });
|
|
262
|
+
const webRes = this.handleRequest(webReq);
|
|
263
|
+
res.writeHead(webRes.status, Object.fromEntries(webRes.headers.entries()));
|
|
264
|
+
webRes.text().then(body => res.end(body));
|
|
256
265
|
});
|
|
266
|
+
this.server.listen(CALLBACK_PORT);
|
|
257
267
|
|
|
258
268
|
// Build authorization URL
|
|
259
269
|
const params = new URLSearchParams({
|
|
@@ -292,7 +302,7 @@ export class BrowserOAuthServer {
|
|
|
292
302
|
*/
|
|
293
303
|
stop(): void {
|
|
294
304
|
if (this.server) {
|
|
295
|
-
this.server.
|
|
305
|
+
this.server.close();
|
|
296
306
|
this.server = null;
|
|
297
307
|
}
|
|
298
308
|
}
|
package/src/compat/runtime.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/** Whether the current runtime is Bun. */
|
|
9
|
-
export const isBun = typeof globalThis.Bun !== 'undefined';
|
|
9
|
+
export const isBun = typeof (globalThis as any).Bun !== 'undefined';
|
|
10
10
|
|
|
11
11
|
/** Whether the current runtime is Node.js (without Bun). */
|
|
12
12
|
export const isNode = !isBun && typeof process !== 'undefined' && !!process.versions?.node;
|
package/src/hooks/engine.ts
CHANGED
|
@@ -242,13 +242,14 @@ export class HookEngine {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
// Write context JSON to stdin
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
if (child.stdin) {
|
|
246
|
+
child.stdin.on('error', () => { /* EPIPE or other write errors — ignore */ });
|
|
247
|
+
try {
|
|
247
248
|
child.stdin.write(JSON.stringify(context));
|
|
248
249
|
child.stdin.end();
|
|
250
|
+
} catch {
|
|
251
|
+
// stdin may already be closed -- ignore
|
|
249
252
|
}
|
|
250
|
-
} catch {
|
|
251
|
-
// stdin may already be closed -- ignore
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
// Collect stdout and stderr
|
package/src/lsp/manager.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface LSPStatus {
|
|
|
28
28
|
|
|
29
29
|
export class LSPManager {
|
|
30
30
|
private clients = new Map<string, LSPClient>();
|
|
31
|
-
private idleTimers = new Map<string,
|
|
31
|
+
private idleTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
32
32
|
private rootUri: string;
|
|
33
33
|
private availabilityCache = new Map<string, boolean>();
|
|
34
34
|
private fileVersions = new Map<string, number>();
|
package/src/nimbus.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Nimbus CLI — Main Entry Point
|
|
4
4
|
*
|
|
@@ -185,23 +185,9 @@ async function main() {
|
|
|
185
185
|
}
|
|
186
186
|
} catch (error: any) {
|
|
187
187
|
const msg = error.message || String(error);
|
|
188
|
-
if (
|
|
189
|
-
console.error(
|
|
190
|
-
'Error: Nimbus requires the Bun runtime (for bun:sqlite and other built-in APIs).'
|
|
191
|
-
);
|
|
192
|
-
console.error('');
|
|
193
|
-
console.error('If you have Bun installed, run:');
|
|
194
|
-
console.error(' bun src/nimbus.ts');
|
|
195
|
-
console.error('');
|
|
196
|
-
console.error('To install Bun:');
|
|
197
|
-
console.error(' curl -fsSL https://bun.sh/install | bash');
|
|
198
|
-
console.error('');
|
|
199
|
-
console.error('Or install the pre-built binary (no Bun required):');
|
|
200
|
-
console.error(' brew install the-ai-project-co/tap/nimbus');
|
|
201
|
-
console.error(' # or download from GitHub Releases');
|
|
202
|
-
} else if (error.code === 'MODULE_NOT_FOUND') {
|
|
188
|
+
if (error.code === 'MODULE_NOT_FOUND') {
|
|
203
189
|
console.error(`Error: Missing module — ${msg}`);
|
|
204
|
-
console.error('Run "
|
|
190
|
+
console.error('Run "npm install" to install dependencies.');
|
|
205
191
|
} else {
|
|
206
192
|
console.error(`Error: ${msg}`);
|
|
207
193
|
}
|
package/src/sharing/sync.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { LLMMessage } from '../llm/types';
|
|
|
18
18
|
export const _deps = {
|
|
19
19
|
getConversation: undefined as ((id: string) => any) | undefined,
|
|
20
20
|
getSessionManager: undefined as (() => { get: (id: string) => any }) | undefined,
|
|
21
|
+
getDb: undefined as (() => any) | undefined,
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
function getConversation(id: string) {
|
|
@@ -60,6 +61,9 @@ export interface SharedSession {
|
|
|
60
61
|
* Lazily import the DB to avoid circular dependency.
|
|
61
62
|
*/
|
|
62
63
|
function getDb() {
|
|
64
|
+
if (_deps.getDb) {
|
|
65
|
+
return _deps.getDb();
|
|
66
|
+
}
|
|
63
67
|
try {
|
|
64
68
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
65
69
|
const { getDb: _getDb } = require('../state/db');
|
package/src/ui/streaming.ts
CHANGED
|
@@ -24,7 +24,7 @@ export class StreamingDisplay {
|
|
|
24
24
|
private options: Required<StreamingDisplayOptions>;
|
|
25
25
|
private buffer: string = '';
|
|
26
26
|
private lineStarted: boolean = false;
|
|
27
|
-
private cursorInterval?:
|
|
27
|
+
private cursorInterval?: ReturnType<typeof setTimeout>;
|
|
28
28
|
|
|
29
29
|
constructor(options: StreamingDisplayOptions = {}) {
|
|
30
30
|
this.options = {
|
package/src/version.ts
CHANGED
package/src/wizard/ui.ts
CHANGED
|
@@ -110,7 +110,7 @@ const boxChars = {
|
|
|
110
110
|
export class WizardUI {
|
|
111
111
|
private terminalWidth: number;
|
|
112
112
|
private spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
113
|
-
private spinnerInterval?:
|
|
113
|
+
private spinnerInterval?: ReturnType<typeof setTimeout>;
|
|
114
114
|
|
|
115
115
|
constructor() {
|
|
116
116
|
this.terminalWidth = process.stdout.columns || 80;
|
package/tsconfig.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"module": "ESNext",
|
|
5
5
|
"moduleResolution": "bundler",
|
|
6
6
|
"lib": ["ESNext"],
|
|
7
|
-
"types": ["
|
|
7
|
+
"types": ["node"],
|
|
8
8
|
"jsx": "react-jsx",
|
|
9
9
|
"jsxImportSource": "react",
|
|
10
10
|
"strict": true,
|
|
@@ -20,5 +20,5 @@
|
|
|
20
20
|
"rootDir": "."
|
|
21
21
|
},
|
|
22
22
|
"include": ["src/**/*", "scripts/**/*"],
|
|
23
|
-
"exclude": ["node_modules", "dist", "coverage", "**/node_modules", "**/dist"]
|
|
23
|
+
"exclude": ["node_modules", "dist", "coverage", "**/node_modules", "**/dist", "src/build.ts"]
|
|
24
24
|
}
|