bhooai-nexus 0.1.2
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 +184 -0
- package/apps/admin/index.html +12 -0
- package/apps/admin/package.json +25 -0
- package/apps/admin/postcss.config.js +6 -0
- package/apps/admin/public/anime-girl.svg +84 -0
- package/apps/admin/public/bhooai-nexus-logo.png +0 -0
- package/apps/admin/public/bhooai-nexus-logo.svg +135 -0
- package/apps/admin/public/favicon.ico +0 -0
- package/apps/admin/src/main.tsx +10 -0
- package/apps/admin/src/vite-env.d.ts +18 -0
- package/apps/admin/tailwind.config.js +10 -0
- package/apps/admin/tsconfig.json +17 -0
- package/apps/admin/vite.config.ts +68 -0
- package/bin/nexus.js +52 -0
- package/package.json +108 -0
- package/packages/nexus-admin/package.json +32 -0
- package/packages/nexus-admin/src/App.tsx +4878 -0
- package/packages/nexus-admin/src/alertCenter.tsx +150 -0
- package/packages/nexus-admin/src/api.ts +601 -0
- package/packages/nexus-admin/src/assets/bhooai-nexus-logo.svg +25 -0
- package/packages/nexus-admin/src/index.css +3635 -0
- package/packages/nexus-admin/src/vite-env.d.ts +19 -0
- package/packages/nexus-ads/package.json +20 -0
- package/packages/nexus-ads/src/GaqlBuilder.ts +72 -0
- package/packages/nexus-ads/src/GoogleAdsClient.ts +121 -0
- package/packages/nexus-ads/src/fetchTransport.ts +10 -0
- package/packages/nexus-ads/src/index.ts +20 -0
- package/packages/nexus-ads/src/types.ts +41 -0
- package/packages/nexus-ai-client/package.json +18 -0
- package/packages/nexus-ai-client/src/client.ts +159 -0
- package/packages/nexus-ai-client/src/errors.ts +21 -0
- package/packages/nexus-ai-client/src/index.ts +4 -0
- package/packages/nexus-ai-client/src/sse.ts +53 -0
- package/packages/nexus-ai-client/src/types.ts +174 -0
- package/packages/nexus-auth/package.json +22 -0
- package/packages/nexus-auth/src/auth.ts +116 -0
- package/packages/nexus-auth/src/bcryptjs.d.ts +10 -0
- package/packages/nexus-auth/src/cors.ts +91 -0
- package/packages/nexus-auth/src/csrf.ts +158 -0
- package/packages/nexus-auth/src/headers.ts +55 -0
- package/packages/nexus-auth/src/index.ts +11 -0
- package/packages/nexus-auth/src/jwt.ts +65 -0
- package/packages/nexus-auth/src/middleware.ts +94 -0
- package/packages/nexus-auth/src/oauth.ts +199 -0
- package/packages/nexus-auth/src/password.ts +33 -0
- package/packages/nexus-auth/src/rateLimit.ts +59 -0
- package/packages/nexus-auth/src/rbac.ts +94 -0
- package/packages/nexus-auth/src/session.ts +91 -0
- package/packages/nexus-cache/package.json +21 -0
- package/packages/nexus-cache/src/Cache.ts +155 -0
- package/packages/nexus-cache/src/PubSub.ts +98 -0
- package/packages/nexus-cache/src/RateLimiter.ts +43 -0
- package/packages/nexus-cache/src/index.ts +9 -0
- package/packages/nexus-cache/src/types.ts +47 -0
- package/packages/nexus-cli/package.json +25 -0
- package/packages/nexus-cli/src/commands/cluster.ts +133 -0
- package/packages/nexus-cli/src/commands/dev.ts +200 -0
- package/packages/nexus-cli/src/commands/doctor.ts +199 -0
- package/packages/nexus-cli/src/commands/init.ts +1009 -0
- package/packages/nexus-cli/src/commands/node.ts +106 -0
- package/packages/nexus-cli/src/commands/pysetup.ts +142 -0
- package/packages/nexus-cli/src/commands/sync.ts +116 -0
- package/packages/nexus-cli/src/commands/uninstall.ts +287 -0
- package/packages/nexus-cli/src/config-sync.ts +384 -0
- package/packages/nexus-cli/src/dotenv.ts +39 -0
- package/packages/nexus-cli/src/index.ts +94 -0
- package/packages/nexus-cli/src/supervisor.ts +394 -0
- package/packages/nexus-cli/src/util.ts +132 -0
- package/packages/nexus-cli/src/wizard.ts +149 -0
- package/packages/nexus-cli/templates/Dockerfile +60 -0
- package/packages/nexus-cli/templates/README.md +69 -0
- package/packages/nexus-cli/templates/apps/admin/index.html +12 -0
- package/packages/nexus-cli/templates/apps/admin/package.json +24 -0
- package/packages/nexus-cli/templates/apps/admin/postcss.config.js +6 -0
- package/packages/nexus-cli/templates/apps/admin/src/main.tsx +10 -0
- package/packages/nexus-cli/templates/apps/admin/src/vite-env.d.ts +18 -0
- package/packages/nexus-cli/templates/apps/admin/tailwind.config.js +9 -0
- package/packages/nexus-cli/templates/apps/admin/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/apps/admin/vite.config.ts +64 -0
- package/packages/nexus-cli/templates/apps/ai-server/main.py +43 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/base.py +111 -0
- package/packages/nexus-cli/templates/apps/ai-server/requirements.txt +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/chat.py +47 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/embeddings.py +30 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/lint.py +167 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/models.py +23 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/preflight.py +169 -0
- package/packages/nexus-cli/templates/apps/ai-server/settings.py +48 -0
- package/packages/nexus-cli/templates/apps/backend/package.json +34 -0
- package/packages/nexus-cli/templates/apps/backend/src/main.ts +375 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/adminRoutes.ts +732 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/clusterRoutes.ts +391 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/databaseRoutes.ts +161 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/lintProxy.ts +89 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/preflightProxy.ts +242 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/roleCatalog.ts +78 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/schemaRoutes.ts +449 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/ai/aiProxy.ts +265 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/auth/authRoutes.ts +220 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentRoutes.ts +100 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentStore.ts +172 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/requests/requestLog.ts +175 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userGraph.ts +83 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userModel.ts +88 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/CronScheduler.ts +69 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/loadPlugins.ts +107 -0
- package/packages/nexus-cli/templates/apps/backend/tsconfig.json +14 -0
- package/packages/nexus-cli/templates/apps/frontend/index.html +12 -0
- package/packages/nexus-cli/templates/apps/frontend/package.json +19 -0
- package/packages/nexus-cli/templates/apps/frontend/src/main.tsx +64 -0
- package/packages/nexus-cli/templates/apps/frontend/vite.config.ts +72 -0
- package/packages/nexus-cli/templates/bin/nexus.js +35 -0
- package/packages/nexus-cli/templates/bin/serve-all.mjs +45 -0
- package/packages/nexus-cli/templates/dockerignore +15 -0
- package/packages/nexus-cli/templates/gitignore +12 -0
- package/packages/nexus-cli/templates/nexus.config.ts +69 -0
- package/packages/nexus-cli/templates/package.json +47 -0
- package/packages/nexus-cli/templates/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/uploads/.gitkeep +0 -0
- package/packages/nexus-core/package.json +38 -0
- package/packages/nexus-core/src/config/ConfigLoader.ts +161 -0
- package/packages/nexus-core/src/config/defaults.ts +155 -0
- package/packages/nexus-core/src/config/env.ts +113 -0
- package/packages/nexus-core/src/config/index.ts +6 -0
- package/packages/nexus-core/src/config/merge.ts +28 -0
- package/packages/nexus-core/src/config/schema.ts +184 -0
- package/packages/nexus-core/src/config/types.ts +352 -0
- package/packages/nexus-core/src/di/Container.ts +98 -0
- package/packages/nexus-core/src/di/index.ts +1 -0
- package/packages/nexus-core/src/errors.ts +62 -0
- package/packages/nexus-core/src/http/Router.ts +149 -0
- package/packages/nexus-core/src/http/Server.ts +145 -0
- package/packages/nexus-core/src/http/bodyParser.ts +129 -0
- package/packages/nexus-core/src/http/context.ts +112 -0
- package/packages/nexus-core/src/http/index.ts +6 -0
- package/packages/nexus-core/src/http/static.ts +85 -0
- package/packages/nexus-core/src/http/uploads.ts +94 -0
- package/packages/nexus-core/src/index.ts +4 -0
- package/packages/nexus-crypto/package.json +18 -0
- package/packages/nexus-crypto/src/csr.ts +165 -0
- package/packages/nexus-crypto/src/der.ts +262 -0
- package/packages/nexus-crypto/src/https.ts +36 -0
- package/packages/nexus-crypto/src/index.ts +5 -0
- package/packages/nexus-crypto/src/keypair.ts +59 -0
- package/packages/nexus-crypto/src/x509.ts +156 -0
- package/packages/nexus-data/package.json +27 -0
- package/packages/nexus-data/src/connection/Connection.ts +57 -0
- package/packages/nexus-data/src/connection/ConnectionManager.ts +37 -0
- package/packages/nexus-data/src/errors.ts +17 -0
- package/packages/nexus-data/src/index.ts +66 -0
- package/packages/nexus-data/src/model/Document.ts +126 -0
- package/packages/nexus-data/src/model/Model.ts +189 -0
- package/packages/nexus-data/src/model/hydrate.ts +12 -0
- package/packages/nexus-data/src/populate/populate.ts +114 -0
- package/packages/nexus-data/src/projects.ts +185 -0
- package/packages/nexus-data/src/query/Query.ts +158 -0
- package/packages/nexus-data/src/schema/Schema.ts +173 -0
- package/packages/nexus-data/src/schema/SchemaType.ts +80 -0
- package/packages/nexus-data/src/schema/validators.ts +88 -0
- package/packages/nexus-email/package.json +22 -0
- package/packages/nexus-email/src/EmailService.ts +76 -0
- package/packages/nexus-email/src/TemplateEngine.ts +62 -0
- package/packages/nexus-email/src/index.ts +37 -0
- package/packages/nexus-email/src/providers.ts +63 -0
- package/packages/nexus-email/src/queue.ts +103 -0
- package/packages/nexus-email/src/types.ts +49 -0
- package/packages/nexus-graphql/package.json +24 -0
- package/packages/nexus-graphql/src/federation/composeSupergraph.ts +153 -0
- package/packages/nexus-graphql/src/federation/createFederatedGateway.ts +36 -0
- package/packages/nexus-graphql/src/federation/executor.ts +305 -0
- package/packages/nexus-graphql/src/federation/parseMetadata.ts +87 -0
- package/packages/nexus-graphql/src/gateway/createGateway.ts +77 -0
- package/packages/nexus-graphql/src/gateway/fieldResolver.ts +45 -0
- package/packages/nexus-graphql/src/gateway/graphqlHttpHandler.ts +90 -0
- package/packages/nexus-graphql/src/gateway/publicSchema.ts +26 -0
- package/packages/nexus-graphql/src/index.ts +14 -0
- package/packages/nexus-graphql/src/subgraph/defineSubgraph.ts +115 -0
- package/packages/nexus-graphql/src/subgraph/federationDirectives.ts +47 -0
- package/packages/nexus-graphql/src/subgraph/parseKeys.ts +25 -0
- package/packages/nexus-graphql/src/subscriptions/PubSub.ts +64 -0
- package/packages/nexus-graphql/src/subscriptions/SubscriptionServer.ts +203 -0
- package/packages/nexus-graphql/src/types.ts +77 -0
- package/packages/nexus-payments/package.json +20 -0
- package/packages/nexus-payments/src/errors.ts +15 -0
- package/packages/nexus-payments/src/http.ts +53 -0
- package/packages/nexus-payments/src/index.ts +106 -0
- package/packages/nexus-payments/src/providers/PayPalProvider.ts +192 -0
- package/packages/nexus-payments/src/providers/PayUProvider.ts +164 -0
- package/packages/nexus-payments/src/providers/PayoneerProvider.ts +122 -0
- package/packages/nexus-payments/src/providers/RazorpayProvider.ts +184 -0
- package/packages/nexus-payments/src/providers/SkrillProvider.ts +117 -0
- package/packages/nexus-payments/src/signature.ts +24 -0
- package/packages/nexus-payments/src/types.ts +150 -0
- package/packages/nexus-payments/src/webhooks.ts +58 -0
- package/packages/nexus-plugins/package.json +20 -0
- package/packages/nexus-plugins/src/HookBus.ts +42 -0
- package/packages/nexus-plugins/src/admin/adminExtensions.ts +53 -0
- package/packages/nexus-plugins/src/host.ts +105 -0
- package/packages/nexus-plugins/src/index.ts +8 -0
- package/packages/nexus-plugins/src/manifest.ts +73 -0
- package/packages/nexus-plugins/src/registry.ts +123 -0
- package/packages/nexus-plugins/src/runtime/TrustedPlugin.ts +134 -0
- package/packages/nexus-plugins/src/sandbox/SandboxedPlugin.ts +279 -0
- package/packages/nexus-plugins/src/sandbox/capabilityPolicy.ts +68 -0
- package/packages/nexus-plugins/src/sandbox/workerBootstrap.mjs +211 -0
- package/packages/nexus-realtime/package.json +26 -0
- package/packages/nexus-realtime/src/adapter/PubSubAdapter.ts +42 -0
- package/packages/nexus-realtime/src/adapter/RedisPubSubAdapter.ts +59 -0
- package/packages/nexus-realtime/src/index.ts +5 -0
- package/packages/nexus-realtime/src/mediasoup/MediasoupAdapter.ts +176 -0
- package/packages/nexus-realtime/src/server/RealtimeServer.ts +252 -0
- package/packages/nexus-realtime/src/types.ts +52 -0
- package/packages/nexus-telemetry/package.json +20 -0
- package/packages/nexus-telemetry/src/index.ts +3 -0
- package/packages/nexus-telemetry/src/logger/Logger.ts +178 -0
- package/packages/nexus-telemetry/src/metrics/Metrics.ts +123 -0
- package/packages/nexus-telemetry/src/trace.ts +28 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Ambient declarations that keep `@bhooai/admin` self-contained: it does
|
|
2
|
+
// not depend on the consumer having `vite/client` types installed. Vite /
|
|
3
|
+
// esbuild inject `import.meta.env` and resolve asset imports at runtime; the
|
|
4
|
+
// host project's build (vite build) handles actual bundling.
|
|
5
|
+
interface ImportMetaEnv {
|
|
6
|
+
readonly VITE_SUPERVISOR_URL?: string;
|
|
7
|
+
readonly [key: string]: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
interface ImportMeta {
|
|
10
|
+
readonly env: ImportMetaEnv;
|
|
11
|
+
}
|
|
12
|
+
declare module '*.svg' {
|
|
13
|
+
const src: string;
|
|
14
|
+
export default src;
|
|
15
|
+
}
|
|
16
|
+
declare module '*.png' {
|
|
17
|
+
const src: string;
|
|
18
|
+
export default src;
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhooai/nexus-ads",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@bhooai/nexus-core": "^0.1.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^22.5.0",
|
|
17
|
+
"typescript": "^5.6.2",
|
|
18
|
+
"vitest": "^2.1.1"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GAQL (Google Ads Query Language) builder. Produces queries like:
|
|
3
|
+
* SELECT campaign.id, campaign.name FROM campaign WHERE campaign.status = 'ENABLED' ORDER BY campaign.id LIMIT 50
|
|
4
|
+
*/
|
|
5
|
+
export class GaqlBuilder {
|
|
6
|
+
private fields: string[] = [];
|
|
7
|
+
private resource: string = '';
|
|
8
|
+
private conditions: string[] = [];
|
|
9
|
+
private order?: { field: string; dir: 'ASC' | 'DESC' };
|
|
10
|
+
private limitN?: number;
|
|
11
|
+
|
|
12
|
+
select(...fields: string[]): this {
|
|
13
|
+
this.fields.push(...fields);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
from(resource: string): this {
|
|
17
|
+
this.resource = resource;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
/** Add a condition, e.g. `where("campaign.status = 'ENABLED'")`. */
|
|
21
|
+
where(condition: string): this {
|
|
22
|
+
this.conditions.push(condition);
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
orderBy(field: string, dir: 'ASC' | 'DESC' = 'ASC'): this {
|
|
26
|
+
this.order = { field, dir };
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
limit(n: number): this {
|
|
30
|
+
this.limitN = n;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
build(): string {
|
|
35
|
+
if (!this.resource) throw new Error('[nexus-ads] GaqlBuilder: FROM resource required');
|
|
36
|
+
if (this.fields.length === 0) throw new Error('[nexus-ads] GaqlBuilder: at least one field required');
|
|
37
|
+
let q = `SELECT ${this.fields.join(', ')} FROM ${this.resource}`;
|
|
38
|
+
if (this.conditions.length) q += ` WHERE ${this.conditions.join(' AND ')}`;
|
|
39
|
+
if (this.order) q += ` ORDER BY ${this.order.field} ${this.order.dir}`;
|
|
40
|
+
if (this.limitN != null) q += ` LIMIT ${this.limitN}`;
|
|
41
|
+
return q;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Convenience: standard campaign listing query. */
|
|
46
|
+
export function campaignsQuery(limit = 50): string {
|
|
47
|
+
return new GaqlBuilder()
|
|
48
|
+
.select('campaign.id', 'campaign.name', 'campaign.status', 'campaign.advertising_channel_type')
|
|
49
|
+
.from('campaign')
|
|
50
|
+
.orderBy('campaign.id')
|
|
51
|
+
.limit(limit)
|
|
52
|
+
.build();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Convenience: campaign performance metrics for a date range (YYYY-MM-DD). */
|
|
56
|
+
export function campaignMetricsQuery(fromDate: string, toDate: string, limit = 50): string {
|
|
57
|
+
return new GaqlBuilder()
|
|
58
|
+
.select(
|
|
59
|
+
'campaign.id',
|
|
60
|
+
'campaign.name',
|
|
61
|
+
'metrics.impressions',
|
|
62
|
+
'metrics.clicks',
|
|
63
|
+
'metrics.cost_micros',
|
|
64
|
+
'metrics.conversions',
|
|
65
|
+
)
|
|
66
|
+
.from('campaign')
|
|
67
|
+
.where(`segments.date >= '${fromDate}'`)
|
|
68
|
+
.where(`segments.date <= '${toDate}'`)
|
|
69
|
+
.orderBy('campaign.id')
|
|
70
|
+
.limit(limit)
|
|
71
|
+
.build();
|
|
72
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { GoogleAdsConfig, HttpTransport, AdsRow, Campaign } from './types.js';
|
|
2
|
+
|
|
3
|
+
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Google Ads REST client (no grpc/native deps). Uses the Google Ads API REST
|
|
7
|
+
* endpoint `googleAds:search` with GAQL. OAuth2 access tokens are acquired via
|
|
8
|
+
* the refresh-token grant and cached until near expiry. The HTTP transport is
|
|
9
|
+
* injectable so tests run without live credentials.
|
|
10
|
+
*/
|
|
11
|
+
export class GoogleAdsClient {
|
|
12
|
+
private readonly cfg: GoogleAdsConfig;
|
|
13
|
+
private readonly transport: HttpTransport;
|
|
14
|
+
private readonly apiVersion: string;
|
|
15
|
+
private token: { value: string; expiresAt: number } | null = null;
|
|
16
|
+
|
|
17
|
+
constructor(cfg: GoogleAdsConfig, transport: HttpTransport, apiVersion = 'v17') {
|
|
18
|
+
this.cfg = cfg;
|
|
19
|
+
this.transport = transport;
|
|
20
|
+
this.apiVersion = apiVersion;
|
|
21
|
+
if (!cfg.developerToken || !cfg.clientId || !cfg.clientSecret || !cfg.refreshToken) {
|
|
22
|
+
throw new Error('[nexus-ads] developerToken, clientId, clientSecret, refreshToken all required');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private baseUrl(): string {
|
|
27
|
+
return `https://googleads.googleapis.com/${this.apiVersion}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** OAuth2 refresh-token → access token, cached with a 60s safety margin. */
|
|
31
|
+
private async accessToken(): Promise<string> {
|
|
32
|
+
if (this.token && Date.now() < this.token.expiresAt - 60_000) return this.token.value;
|
|
33
|
+
const body = new URLSearchParams({
|
|
34
|
+
client_id: this.cfg.clientId,
|
|
35
|
+
client_secret: this.cfg.clientSecret,
|
|
36
|
+
refresh_token: this.cfg.refreshToken,
|
|
37
|
+
grant_type: 'refresh_token',
|
|
38
|
+
}).toString();
|
|
39
|
+
const res = await this.transport({
|
|
40
|
+
method: 'POST',
|
|
41
|
+
url: TOKEN_URL,
|
|
42
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
43
|
+
body,
|
|
44
|
+
});
|
|
45
|
+
const parsed = safeJson(res.body);
|
|
46
|
+
if (res.status !== 200 || !parsed?.access_token) {
|
|
47
|
+
throw new Error(`[nexus-ads] token refresh failed: HTTP ${res.status} ${res.body}`);
|
|
48
|
+
}
|
|
49
|
+
this.token = { value: parsed.access_token, expiresAt: Date.now() + (parsed.expires_in ?? 3600) * 1000 };
|
|
50
|
+
return this.token.value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async searchHeaders(): Promise<Record<string, string>> {
|
|
54
|
+
return {
|
|
55
|
+
authorization: `Bearer ${await this.accessToken()}`,
|
|
56
|
+
'developer-token': this.cfg.developerToken,
|
|
57
|
+
'content-type': 'application/json',
|
|
58
|
+
...(this.cfg.loginCustomerId ? { 'login-customer-id': normalizeId(this.cfg.loginCustomerId) } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Run a GAQL query against a customer and return all rows (handles pagination). */
|
|
63
|
+
async search(customerId: string, query: string): Promise<AdsRow[]> {
|
|
64
|
+
const headers = await this.searchHeaders();
|
|
65
|
+
let pageToken: string | undefined;
|
|
66
|
+
const rows: AdsRow[] = [];
|
|
67
|
+
do {
|
|
68
|
+
const body = JSON.stringify({ query, ...(pageToken ? { pageToken } : {}) });
|
|
69
|
+
const res = await this.transport({
|
|
70
|
+
method: 'POST',
|
|
71
|
+
url: `${this.baseUrl()}/customers/${normalizeId(customerId)}/googleAds:search`,
|
|
72
|
+
headers,
|
|
73
|
+
body,
|
|
74
|
+
});
|
|
75
|
+
const parsed = safeJson(res.body);
|
|
76
|
+
if (res.status !== 200) {
|
|
77
|
+
throw new Error(`[nexus-ads] search failed: HTTP ${res.status} ${res.body}`);
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(parsed?.results)) rows.push(...parsed.results);
|
|
80
|
+
pageToken = parsed?.nextPageToken;
|
|
81
|
+
} while (pageToken);
|
|
82
|
+
return rows;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** List campaigns on the default customer. */
|
|
86
|
+
async listCampaigns(customerId = this.cfg.customerId, limit = 50): Promise<Campaign[]> {
|
|
87
|
+
const rows = await this.search(customerId, listCampaignsGaql(limit));
|
|
88
|
+
return rows.map(rowToCampaign);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Campaign metrics for a date range (YYYY-MM-DD). */
|
|
92
|
+
async campaignMetrics(fromDate: string, toDate: string, customerId = this.cfg.customerId, limit = 50): Promise<AdsRow[]> {
|
|
93
|
+
const rows = await this.search(customerId, metricsGaql(fromDate, toDate, limit));
|
|
94
|
+
return rows;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function listCampaignsGaql(limit: number): string {
|
|
99
|
+
return `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type FROM campaign ORDER BY campaign.id LIMIT ${limit}`;
|
|
100
|
+
}
|
|
101
|
+
function metricsGaql(from: string, to: string, limit: number): string {
|
|
102
|
+
return `SELECT campaign.id, campaign.name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date >= '${from}' AND segments.date <= '${to}' ORDER BY campaign.id LIMIT ${limit}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function rowToCampaign(row: AdsRow): Campaign {
|
|
106
|
+
const c = (row.campaign ?? {}) as Record<string, unknown>;
|
|
107
|
+
return {
|
|
108
|
+
id: String(c.id ?? ''),
|
|
109
|
+
name: String(c.name ?? ''),
|
|
110
|
+
status: String(c.status ?? ''),
|
|
111
|
+
advertisingChannelType: c.advertisingChannelType != null ? String(c.advertisingChannelType) : undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeId(id: string): string {
|
|
116
|
+
return id.replace(/-/g, '');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function safeJson(body: string): any {
|
|
120
|
+
try { return JSON.parse(body); } catch { return undefined; }
|
|
121
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HttpTransport, HttpRequest, HttpResponse } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** Default HTTP transport built on the global `fetch` (Node >= 18). */
|
|
4
|
+
export const fetchTransport: HttpTransport = async (req: HttpRequest): Promise<HttpResponse> => {
|
|
5
|
+
const res = await fetch(req.url, { method: req.method, headers: req.headers, body: req.body });
|
|
6
|
+
const body = await res.text();
|
|
7
|
+
const headers: Record<string, string> = {};
|
|
8
|
+
res.headers.forEach((v, k) => { headers[k] = v; });
|
|
9
|
+
return { status: res.status, body, headers };
|
|
10
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './GaqlBuilder.js';
|
|
3
|
+
export * from './GoogleAdsClient.js';
|
|
4
|
+
|
|
5
|
+
import { GoogleAdsClient } from './GoogleAdsClient.js';
|
|
6
|
+
import { fetchTransport } from './fetchTransport.js';
|
|
7
|
+
import type { GoogleAdsConfig, HttpTransport } from './types.js';
|
|
8
|
+
|
|
9
|
+
/** Default fetch-based transport (Node >= 18). */
|
|
10
|
+
export { fetchTransport };
|
|
11
|
+
|
|
12
|
+
export interface CreateAdsOptions {
|
|
13
|
+
transport?: HttpTransport;
|
|
14
|
+
apiVersion?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Build a Google Ads client. Throws at construction if required creds are missing. */
|
|
18
|
+
export function createGoogleAds(config: GoogleAdsConfig, opts: CreateAdsOptions = {}): GoogleAdsClient {
|
|
19
|
+
return new GoogleAdsClient(config, opts.transport ?? fetchTransport, opts.apiVersion);
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Injectable HTTP transport so the client is testable without live Google creds. */
|
|
2
|
+
export interface HttpRequest {
|
|
3
|
+
method: 'GET' | 'POST';
|
|
4
|
+
url: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
body?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface HttpResponse {
|
|
9
|
+
status: number;
|
|
10
|
+
body: string;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export type HttpTransport = (req: HttpRequest) => Promise<HttpResponse>;
|
|
14
|
+
|
|
15
|
+
/** Google Ads config as it appears in nexus.config.ts `ads`. */
|
|
16
|
+
export interface GoogleAdsConfig {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
/** Google Ads API developer token. */
|
|
19
|
+
developerToken: string;
|
|
20
|
+
/** OAuth2 client credentials. */
|
|
21
|
+
clientId: string;
|
|
22
|
+
clientSecret: string;
|
|
23
|
+
/** OAuth2 refresh token (long-lived). */
|
|
24
|
+
refreshToken: string;
|
|
25
|
+
/** Login customer ID (MCC account, hyphenated or plain). Required for MCC. */
|
|
26
|
+
loginCustomerId?: string;
|
|
27
|
+
/** Default customer ID to query. */
|
|
28
|
+
customerId: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Minimal campaign shape from `campaign` resource. */
|
|
32
|
+
export interface Campaign {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
status: string;
|
|
36
|
+
advertisingChannelType?: string;
|
|
37
|
+
budget?: { amountMicros?: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A row from a GAQL search: a record mapping resource name → fields. */
|
|
41
|
+
export type AdsRow = Record<string, unknown>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhooai/nexus-ai-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^22.5.0",
|
|
15
|
+
"typescript": "^5.6.2",
|
|
16
|
+
"vitest": "^2.1.1"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AiClientOptions,
|
|
3
|
+
ChatCompletionRequest,
|
|
4
|
+
ChatCompletionResponse,
|
|
5
|
+
ChatCompletionChunk,
|
|
6
|
+
EmbeddingRequest,
|
|
7
|
+
EmbeddingResponse,
|
|
8
|
+
ModelsResponse,
|
|
9
|
+
ImageGenRequest,
|
|
10
|
+
ImageGenResponse,
|
|
11
|
+
ImageGenModelsResponse,
|
|
12
|
+
AgentImageRequest,
|
|
13
|
+
AgentImageEvent,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
import { AiError, isRetryable } from './errors.js';
|
|
16
|
+
import { parseSseStream } from './sse.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Node client for the BhooAI Nexus AI server (Python FastAPI). OpenAI-compatible.
|
|
20
|
+
* Supports non-streaming + streaming chat completions, embeddings, and model
|
|
21
|
+
* listing, with timeout (AbortController) and exponential-backoff retries on
|
|
22
|
+
* transient failures (network errors, 5xx, 429).
|
|
23
|
+
*/
|
|
24
|
+
export class AiClient {
|
|
25
|
+
private readonly serverUrl: string;
|
|
26
|
+
private readonly timeoutMs: number;
|
|
27
|
+
private readonly maxRetries: number;
|
|
28
|
+
private readonly retryDelayMs: number;
|
|
29
|
+
private readonly authToken?: string;
|
|
30
|
+
|
|
31
|
+
constructor(opts: AiClientOptions) {
|
|
32
|
+
this.serverUrl = opts.serverUrl.replace(/\/+$/, '');
|
|
33
|
+
this.timeoutMs = opts.timeoutMs ?? 60_000;
|
|
34
|
+
this.maxRetries = opts.maxRetries ?? 2;
|
|
35
|
+
this.retryDelayMs = opts.retryDelayMs ?? 500;
|
|
36
|
+
this.authToken = opts.authToken;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private headers(extra: Record<string, string> = {}): Record<string, string> {
|
|
40
|
+
const h: Record<string, string> = { 'content-type': 'application/json', accept: 'application/json', ...extra };
|
|
41
|
+
if (this.authToken) h.authorization = `Bearer ${this.authToken}`;
|
|
42
|
+
return h;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async request<T>(path: string, init: RequestInit, { stream = false } = {}): Promise<T> {
|
|
46
|
+
let lastErr: unknown;
|
|
47
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(`${this.serverUrl}${path}`, {
|
|
52
|
+
...init,
|
|
53
|
+
signal: controller.signal,
|
|
54
|
+
headers: this.headers(init.headers as Record<string, string> | undefined),
|
|
55
|
+
});
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
let message = `AI server ${res.status}`;
|
|
59
|
+
let code: string | undefined;
|
|
60
|
+
try {
|
|
61
|
+
const body = await res.json() as { error?: { message?: string; code?: string } };
|
|
62
|
+
message = body?.error?.message ?? message;
|
|
63
|
+
code = body?.error?.code;
|
|
64
|
+
} catch { /* non-JSON error body */ }
|
|
65
|
+
const err = new AiError(message, { status: res.status, code });
|
|
66
|
+
if (isRetryable(err) && attempt < this.maxRetries) {
|
|
67
|
+
lastErr = err;
|
|
68
|
+
await this.backoff(attempt);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
return (stream ? res : await res.json() as T) as T;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
// Abort → timeout; network errors are retryable.
|
|
77
|
+
const isAbort = e instanceof Error && e.name === 'AbortError';
|
|
78
|
+
const err = e instanceof AiError ? e : new AiError(isAbort ? 'AI request timed out' : String((e as Error)?.message ?? e));
|
|
79
|
+
if (isRetryable(err) && attempt < this.maxRetries) {
|
|
80
|
+
lastErr = err;
|
|
81
|
+
await this.backoff(attempt);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw lastErr instanceof Error ? lastErr : new AiError('AI request failed');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async backoff(attempt: number): Promise<void> {
|
|
91
|
+
const delay = this.retryDelayMs * Math.pow(2, attempt);
|
|
92
|
+
// Tiny jitter without Math.random (deterministic): fold attempt into the delay.
|
|
93
|
+
await new Promise((r) => setTimeout(r, delay + attempt));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Non-streaming chat completion. */
|
|
97
|
+
async chat(req: ChatCompletionRequest): Promise<ChatCompletionResponse> {
|
|
98
|
+
const body = { ...req, stream: false };
|
|
99
|
+
return this.request<ChatCompletionResponse>('/chat/completions', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
body: JSON.stringify(body),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Streaming chat completion — yields chunks until the stream ends. */
|
|
106
|
+
async *chatStream(req: ChatCompletionRequest): AsyncGenerator<ChatCompletionChunk, void, unknown> {
|
|
107
|
+
const body = { ...req, stream: true };
|
|
108
|
+
const res = await this.request<Response>('/chat/completions', {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
body: JSON.stringify(body),
|
|
111
|
+
headers: { accept: 'text/event-stream' },
|
|
112
|
+
}, { stream: true });
|
|
113
|
+
if (!res.body) throw new AiError('streaming response has no body');
|
|
114
|
+
for await (const chunk of parseSseStream(res.body as unknown as ReadableStream<Uint8Array>)) {
|
|
115
|
+
yield chunk as ChatCompletionChunk;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Create embeddings for one or more inputs. */
|
|
120
|
+
async embeddings(req: EmbeddingRequest): Promise<EmbeddingResponse> {
|
|
121
|
+
return this.request<EmbeddingResponse>('/embeddings', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
body: JSON.stringify(req),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** List available models (optionally for a specific provider). */
|
|
128
|
+
async listModels(provider?: string): Promise<ModelsResponse> {
|
|
129
|
+
const qs = provider ? `?provider=${encodeURIComponent(provider)}` : '';
|
|
130
|
+
return this.request<ModelsResponse>(`/models${qs}`, { method: 'GET' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Generate images via the local Stable Diffusion HTTP server. */
|
|
134
|
+
async images(req: ImageGenRequest): Promise<ImageGenResponse> {
|
|
135
|
+
return this.request<ImageGenResponse>('/images/generations', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
body: JSON.stringify(req),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** List available image-generation models (SD checkpoints). */
|
|
142
|
+
async listImageModels(): Promise<ImageGenModelsResponse> {
|
|
143
|
+
return this.request<ImageGenModelsResponse>('/images/models', { method: 'GET' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Agentic image loop — streams reasoning/tool/image/done events.
|
|
147
|
+
* Drives the Ollama LLM (reasoning) + local SD server (pixels). */
|
|
148
|
+
async *agentImageStream(req: AgentImageRequest): AsyncGenerator<AgentImageEvent, void, unknown> {
|
|
149
|
+
const res = await this.request<Response>('/agent/image', {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
body: JSON.stringify(req),
|
|
152
|
+
headers: { accept: 'text/event-stream' },
|
|
153
|
+
}, { stream: true });
|
|
154
|
+
if (!res.body) throw new AiError('agent stream response has no body');
|
|
155
|
+
for await (const chunk of parseSseStream(res.body as unknown as ReadableStream<Uint8Array>)) {
|
|
156
|
+
yield chunk as AgentImageEvent;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** AI client errors. */
|
|
2
|
+
|
|
3
|
+
export class AiError extends Error {
|
|
4
|
+
readonly status?: number;
|
|
5
|
+
readonly code?: string;
|
|
6
|
+
constructor(message: string, opts: { status?: number; code?: string } = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'AiError';
|
|
9
|
+
this.status = opts.status;
|
|
10
|
+
this.code = opts.code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** True for transient errors worth retrying: network failures + 5xx + 429. */
|
|
15
|
+
export function isRetryable(err: unknown): boolean {
|
|
16
|
+
if (err instanceof AiError) {
|
|
17
|
+
if (err.status === undefined) return true; // network-level failure
|
|
18
|
+
return err.status >= 500 || err.status === 429;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SSE parser for the streaming chat endpoint. Reads a web
|
|
3
|
+
* ReadableStream<Uint8Array>, buffers bytes, splits events on blank lines, and
|
|
4
|
+
* yields the parsed `data:` JSON payload of each event. Stops at `[DONE]`.
|
|
5
|
+
*/
|
|
6
|
+
export async function* parseSseStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<any, void, unknown> {
|
|
7
|
+
const reader = stream.getReader();
|
|
8
|
+
const decoder = new TextDecoder();
|
|
9
|
+
let buffer = '';
|
|
10
|
+
try {
|
|
11
|
+
while (true) {
|
|
12
|
+
const { done, value } = await reader.read();
|
|
13
|
+
if (done) break;
|
|
14
|
+
buffer += decoder.decode(value, { stream: true });
|
|
15
|
+
// SSE events are separated by a blank line (\n\n). Process complete ones.
|
|
16
|
+
let idx: number;
|
|
17
|
+
while ((idx = buffer.indexOf('\n\n')) !== -1) {
|
|
18
|
+
const rawEvent = buffer.slice(0, idx);
|
|
19
|
+
buffer = buffer.slice(idx + 2);
|
|
20
|
+
const payload = extractData(rawEvent);
|
|
21
|
+
if (payload === undefined) continue;
|
|
22
|
+
if (payload === '[DONE]') return;
|
|
23
|
+
try {
|
|
24
|
+
yield JSON.parse(payload);
|
|
25
|
+
} catch {
|
|
26
|
+
/* skip malformed */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Flush any trailing event without a blank-line terminator.
|
|
31
|
+
if (buffer.trim()) {
|
|
32
|
+
const payload = extractData(buffer);
|
|
33
|
+
if (payload && payload !== '[DONE]') {
|
|
34
|
+
try { yield JSON.parse(payload); } catch { /* skip */ }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
reader.releaseLock();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractData(event: string): string | undefined {
|
|
43
|
+
// An event may have multiple lines; collect `data:` fields (concatenated).
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
for (const line of event.split('\n')) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (trimmed.startsWith('data:')) {
|
|
48
|
+
parts.push(trimmed.slice(5).trimStart());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (parts.length === 0) return undefined;
|
|
52
|
+
return parts.join('\n');
|
|
53
|
+
}
|