@nexical/ai 0.1.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.
@@ -0,0 +1,34 @@
1
+ name: Deploy Nexical AI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+ name: Build & Publish
12
+ steps:
13
+ - name: Checkout Repo
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Setup Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: 22
20
+ registry-url: 'https://registry.npmjs.org'
21
+
22
+ - name: Install Dependencies
23
+ run: npm install
24
+
25
+ - name: Build
26
+ run: npm run build
27
+
28
+ - name: Test
29
+ run: npm run test
30
+
31
+ - name: Publish to NPM
32
+ run: npm publish --access public
33
+ env:
34
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ npx lint-staged
@@ -0,0 +1,10 @@
1
+ import { GeminiCLI } from './providers/GeminiCLI.js';
2
+ export class AiClientFactory {
3
+ static create(config) {
4
+ const provider = config?.provider || 'gemini-cli';
5
+ if (provider === 'gemini-cli') {
6
+ return new GeminiCLI(config || {});
7
+ }
8
+ throw new Error(`Unsupported AI Client provider: ${provider}`);
9
+ }
10
+ }
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './providers/GeminiCLI.js';
3
+ export * from './AiClientFactory.js';
@@ -0,0 +1,52 @@
1
+ import { spawn } from 'node:child_process';
2
+ import chalk from 'chalk';
3
+ export class GeminiCLI {
4
+ commandTemplate;
5
+ constructor(config) {
6
+ this.commandTemplate = config.commandTemplate || 'gemini --yolo -p "" --model {model}';
7
+ }
8
+ run(model, input) {
9
+ return new Promise((resolve) => {
10
+ const command = this.commandTemplate.replace('{model}', model);
11
+ const start = Date.now();
12
+ const child = spawn(command, {
13
+ shell: true,
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
+ });
16
+ let stdoutData = '';
17
+ let stderrData = '';
18
+ child.stdout?.on('data', (data) => {
19
+ const chunk = data.toString();
20
+ process.stdout.write(chalk.yellow(chunk));
21
+ stdoutData += chunk;
22
+ });
23
+ child.stderr?.on('data', (data) => {
24
+ const chunk = data.toString();
25
+ stderrData += chunk;
26
+ });
27
+ child.stdin.write(input);
28
+ child.stdin.end();
29
+ child.on('close', (code) => {
30
+ const duration = Date.now() - start;
31
+ const exitCode = code ?? 1;
32
+ const isExhausted = stderrData.includes('429') ||
33
+ stderrData.includes('exhausted your capacity') ||
34
+ stderrData.includes('ResourceExhausted');
35
+ if (exitCode !== 0 && isExhausted) {
36
+ console.warn(`[Agent] Model ${model} exhausted (429). Duration: ${duration}ms`);
37
+ resolve({ code: exitCode, shouldRetry: true, output: stdoutData });
38
+ }
39
+ else {
40
+ if (exitCode !== 0 && stderrData) {
41
+ process.stderr.write(stderrData);
42
+ }
43
+ resolve({ code: exitCode, shouldRetry: false, output: stdoutData });
44
+ }
45
+ });
46
+ child.on('error', (err) => {
47
+ console.error(`[Agent] Failed to spawn Gemini (${model}): ${err instanceof Error ? err.message : String(err)}`);
48
+ resolve({ code: 1, shouldRetry: false, output: '' });
49
+ });
50
+ });
51
+ }
52
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import eslintPluginAstro from 'eslint-plugin-astro';
2
+ import eslintPluginReact from 'eslint-plugin-react';
3
+ import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
4
+ import eslintPluginJsxA11y from 'eslint-plugin-jsx-a11y';
5
+ import eslintConfigPrettier from 'eslint-config-prettier';
6
+ import tseslint from 'typescript-eslint';
7
+ import globals from 'globals';
8
+ import js from '@eslint/js';
9
+
10
+ export default tseslint.config(
11
+ // Global ignore
12
+ {
13
+ ignores: ['**/dist/**', '**/node_modules/**', '**/coverage/**', '**/.astro/**', '**/.agent/**'],
14
+ },
15
+
16
+ // Base JS/TS configuration
17
+ js.configs.recommended,
18
+ ...tseslint.configs.recommended,
19
+
20
+ // React configuration
21
+ {
22
+ files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
23
+ plugins: {
24
+ react: eslintPluginReact,
25
+ 'react-hooks': eslintPluginReactHooks,
26
+ 'jsx-a11y': eslintPluginJsxA11y,
27
+ },
28
+ languageOptions: {
29
+ parserOptions: {
30
+ ecmaFeatures: {
31
+ jsx: true,
32
+ },
33
+ },
34
+ globals: {
35
+ ...globals.browser,
36
+ ...globals.node,
37
+ },
38
+ },
39
+ settings: {
40
+ react: {
41
+ version: 'detect',
42
+ },
43
+ },
44
+ rules: {
45
+ ...eslintPluginReact.configs.recommended.rules,
46
+ ...eslintPluginReactHooks.configs.recommended.rules,
47
+ ...eslintPluginJsxA11y.configs.recommended.rules,
48
+ 'react/react-in-jsx-scope': 'off', // Not needed in React 17+ or Astro
49
+ 'react/no-unknown-property': 'off', // Conflicts with some Astro/Web Component patterns
50
+ },
51
+ },
52
+
53
+ // Astro configuration
54
+ ...eslintPluginAstro.configs.recommended,
55
+
56
+ // Custom rules override
57
+ {
58
+ rules: {
59
+ '@typescript-eslint/no-unused-vars': ['error', { args: 'none', varsIgnorePattern: '^_' }],
60
+ '@typescript-eslint/no-explicit-any': 'warn', // Downgrade to warning for now as codebase has some anys
61
+ 'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
62
+ },
63
+ },
64
+
65
+ // Prettier must be last to override conflicting rules
66
+ eslintConfigPrettier,
67
+ );
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@nexical/ai",
3
+ "version": "0.1.0",
4
+ "description": "General purpose AI Client interfaces and providers for the Nexical applications",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.build.json",
9
+ "format": "prettier --write .",
10
+ "lint": "eslint .",
11
+ "lint:fix": "eslint . --fix",
12
+ "prepare": "husky",
13
+ "prepublishOnly": "npm run build",
14
+ "test": "vitest run --passWithNoTests",
15
+ "test:unit": "vitest run --passWithNoTests"
16
+ },
17
+ "lint-staged": {
18
+ "**/*": [
19
+ "prettier --write --ignore-unknown"
20
+ ],
21
+ "**/*.{js,jsx,ts,tsx,astro}": [
22
+ "eslint --fix"
23
+ ]
24
+ },
25
+ "dependencies": {
26
+ "chalk": "^5.3.0"
27
+ },
28
+ "devDependencies": {
29
+ "@eslint/js": "^9.39.2",
30
+ "@types/node": "^22.0.0",
31
+ "eslint": "^9.39.2",
32
+ "eslint-config-prettier": "^10.0.1",
33
+ "globals": "^15.9.0",
34
+ "husky": "^9.1.5",
35
+ "lint-staged": "^15.2.10",
36
+ "prettier": "^3.3.3",
37
+ "typescript": "^5.5.4",
38
+ "typescript-eslint": "^8.3.0",
39
+ "vitest": "^2.1.0"
40
+ },
41
+ "types": "dist/index.d.ts"
42
+ }
@@ -0,0 +1,14 @@
1
+ import { AiClient, AiClientConfig } from './types.js';
2
+ import { GeminiCLI } from './providers/GeminiCLI.js';
3
+
4
+ export class AiClientFactory {
5
+ static create(config?: AiClientConfig): AiClient {
6
+ const provider = config?.provider || 'gemini-cli';
7
+
8
+ if (provider === 'gemini-cli') {
9
+ return new GeminiCLI(config || {});
10
+ }
11
+
12
+ throw new Error(`Unsupported AI Client provider: ${provider}`);
13
+ }
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './providers/GeminiCLI.js';
3
+ export * from './AiClientFactory.js';
@@ -0,0 +1,66 @@
1
+ import { spawn } from 'node:child_process';
2
+ import chalk from 'chalk';
3
+ import { AiClient, AiClientConfig, AiClientResult } from '../types.js';
4
+
5
+ export class GeminiCLI implements AiClient {
6
+ private commandTemplate: string;
7
+
8
+ constructor(config: AiClientConfig) {
9
+ this.commandTemplate = config.commandTemplate || 'gemini --yolo -p "" --model {model}';
10
+ }
11
+
12
+ run(model: string, input: string): Promise<AiClientResult> {
13
+ return new Promise((resolve) => {
14
+ const command = this.commandTemplate.replace('{model}', model);
15
+ const start = Date.now();
16
+
17
+ const child = spawn(command, {
18
+ shell: true,
19
+ stdio: ['pipe', 'pipe', 'pipe'],
20
+ });
21
+
22
+ let stdoutData = '';
23
+ let stderrData = '';
24
+
25
+ child.stdout?.on('data', (data) => {
26
+ const chunk = data.toString();
27
+ process.stdout.write(chalk.yellow(chunk));
28
+ stdoutData += chunk;
29
+ });
30
+
31
+ child.stderr?.on('data', (data) => {
32
+ const chunk = data.toString();
33
+ stderrData += chunk;
34
+ });
35
+
36
+ child.stdin.write(input);
37
+ child.stdin.end();
38
+
39
+ child.on('close', (code) => {
40
+ const duration = Date.now() - start;
41
+ const exitCode = code ?? 1;
42
+ const isExhausted =
43
+ stderrData.includes('429') ||
44
+ stderrData.includes('exhausted your capacity') ||
45
+ stderrData.includes('ResourceExhausted');
46
+
47
+ if (exitCode !== 0 && isExhausted) {
48
+ console.warn(`[Agent] Model ${model} exhausted (429). Duration: ${duration}ms`);
49
+ resolve({ code: exitCode, shouldRetry: true, output: stdoutData });
50
+ } else {
51
+ if (exitCode !== 0 && stderrData) {
52
+ process.stderr.write(stderrData);
53
+ }
54
+ resolve({ code: exitCode, shouldRetry: false, output: stdoutData });
55
+ }
56
+ });
57
+
58
+ child.on('error', (err) => {
59
+ console.error(
60
+ `[Agent] Failed to spawn Gemini (${model}): ${err instanceof Error ? err.message : String(err)}`,
61
+ );
62
+ resolve({ code: 1, shouldRetry: false, output: '' });
63
+ });
64
+ });
65
+ }
66
+ }
package/src/types.ts ADDED
@@ -0,0 +1,15 @@
1
+ export interface AiClientResult {
2
+ code: number;
3
+ shouldRetry: boolean;
4
+ output: string;
5
+ }
6
+
7
+ export interface AiClientConfig {
8
+ provider?: string;
9
+ commandTemplate?: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ export interface AiClient {
14
+ run(model: string, input: string): Promise<AiClientResult>;
15
+ }
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { GeminiCLI } from '../../src/providers/GeminiCLI.js';
3
+ import { spawn } from 'node:child_process';
4
+
5
+ vi.mock('node:child_process', () => ({
6
+ spawn: vi.fn(),
7
+ }));
8
+
9
+ describe('GeminiCLI', () => {
10
+ beforeEach(() => {
11
+ vi.resetAllMocks();
12
+ // Prevent stdout/stderr intercepts from breaking the test output
13
+ vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
14
+ vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
15
+ });
16
+
17
+ it('should run a prompt and succeed', async () => {
18
+ const mockChild = {
19
+ stdout: { on: vi.fn((event, cb) => cb('mock response')) },
20
+ stderr: { on: vi.fn() },
21
+ stdin: { write: vi.fn(), end: vi.fn() },
22
+ on: vi.fn((event, callback) => {
23
+ if (event === 'close') callback(0);
24
+ }),
25
+ };
26
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
27
+
28
+ const client = new GeminiCLI({});
29
+ const result = await client.run('gemini-3-pro-preview', 'Hello World');
30
+
31
+ expect(spawn).toHaveBeenCalledWith(
32
+ expect.stringContaining('gemini --yolo -p "" --model'),
33
+ expect.any(Object),
34
+ );
35
+ expect(result.code).toBe(0);
36
+ expect(result.shouldRetry).toBe(false);
37
+ expect(result.output).toBe('mock response');
38
+ });
39
+
40
+ it('should return retry status on 429 ResourceExhausted', async () => {
41
+ const mockChild = {
42
+ stdout: { on: vi.fn() },
43
+ stderr: {
44
+ on: vi.fn((event, cb) => {
45
+ if (event === 'data') cb('Error: ResourceExhausted (429)');
46
+ }),
47
+ },
48
+ stdin: { write: vi.fn(), end: vi.fn() },
49
+ on: vi.fn((event, callback) => {
50
+ if (event === 'close') callback(1);
51
+ }),
52
+ };
53
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
54
+
55
+ const client = new GeminiCLI({});
56
+ const result = await client.run('gemini-3-pro-preview', 'Hello World');
57
+
58
+ expect(result.code).toBe(1);
59
+ expect(result.shouldRetry).toBe(true);
60
+ expect(result.output).toBe('');
61
+ });
62
+
63
+ it('should return failure without retry on other errors', async () => {
64
+ const mockChild = {
65
+ stdout: { on: vi.fn() },
66
+ stderr: {
67
+ on: vi.fn((event, cb) => {
68
+ if (event === 'data') cb('Other fatal model error');
69
+ }),
70
+ },
71
+ stdin: { write: vi.fn(), end: vi.fn() },
72
+ on: vi.fn((event, callback) => {
73
+ if (event === 'close') callback(2);
74
+ }),
75
+ };
76
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
77
+
78
+ const client = new GeminiCLI({});
79
+ const result = await client.run('gemini-3-pro-preview', 'Hello World');
80
+
81
+ expect(result.code).toBe(2);
82
+ expect(result.shouldRetry).toBe(false);
83
+ });
84
+
85
+ it('should catch spawn errors', async () => {
86
+ const mockChild = {
87
+ stdout: { on: vi.fn() },
88
+ stderr: { on: vi.fn() },
89
+ stdin: { write: vi.fn(), end: vi.fn() },
90
+ on: vi.fn((event, callback) => {
91
+ if (event === 'error') callback(new Error('Spawn failed'));
92
+ }),
93
+ };
94
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
95
+
96
+ const client = new GeminiCLI({});
97
+ const result = await client.run('gemini-3-pro-preview', 'Hello World');
98
+
99
+ expect(result.code).toBe(1);
100
+ expect(result.shouldRetry).toBe(false);
101
+ expect(result.output).toBe('');
102
+ });
103
+
104
+ it('should use a custom command template', async () => {
105
+ const mockChild = {
106
+ stdout: { on: vi.fn() },
107
+ stderr: { on: vi.fn() },
108
+ stdin: { write: vi.fn(), end: vi.fn() },
109
+ on: vi.fn((event, callback) => {
110
+ if (event === 'close') callback(0);
111
+ }),
112
+ };
113
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
114
+
115
+ const client = new GeminiCLI({ commandTemplate: 'echo {model}' });
116
+ await client.run('gemini-3-pro-preview', 'Hello World');
117
+
118
+ expect(spawn).toHaveBeenCalledWith('echo gemini-3-pro-preview', expect.any(Object));
119
+ });
120
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": [
4
+ "tests/**/*",
5
+ "**/*.test.ts"
6
+ ]
7
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": [
7
+ "ESNext"
8
+ ],
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "outDir": "dist",
15
+ "rootDir": "src"
16
+ },
17
+ "include": [
18
+ "src/**/*"
19
+ ]
20
+ }