@maintainer-pro/ai-server 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.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @maintainer-pro/ai-server
2
+
3
+ Local Node **chat server** (sidecar) for Maintainer Pro. Start it with `npx` — no custom `server.mjs`.
4
+
5
+ It uses [`@maintainer-pro/ai-cli`](https://www.npmjs.com/package/@maintainer-pro/ai-cli) to talk to Cursor (`agent`), Claude (`claude`), or Antigravity (`agy`).
6
+
7
+ This process is **not** the host product backend. Run it **beside** the app in development. The sandbox setup guide uses port **3100** so it does not collide with the host (`:3000` / `:5173`). The binary default with no env is port `3000`.
8
+
9
+ It is **not** what you ship as the production website. Without this process, `localhost:3100` is down and a page that loads the widget from there will show no chat. Production should serve `ai-ui.iife.js` as a static file and call a real chat API — see [`@maintainer-pro/ai-ui`](https://www.npmjs.com/package/@maintainer-pro/ai-ui).
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ npx --yes @maintainer-pro/ai-server
15
+ ```
16
+
17
+ Put a `.env` in the current directory (from the admin setup guide or by hand). CLI flags override env.
18
+
19
+ Requires **Node.js 22+**, [`@maintainer-pro/ai-ui`](https://www.npmjs.com/package/@maintainer-pro/ai-ui) next to this package (for `/ai-ui.iife.js`), and one authenticated coding-agent CLI.
20
+
21
+ ## Routes
22
+
23
+ | Path | Purpose |
24
+ |------|---------|
25
+ | `GET` / `POST /api/chat` | Chat (agent + Maintainer Pro store) |
26
+ | `GET /embed-config.js` | Client keys for the widget (`window.__MAINTAINER_PRO__`) |
27
+ | `GET /ai-ui.iife.js` | Embed widget from `node_modules/@maintainer-pro/ai-ui` (unless a file exists in `AI_SERVER_UI`) |
28
+ | other paths | Static files from `AI_SERVER_UI` |
29
+
30
+ A copied `ai-ui.iife.js` in the UI folder is served first and will not update when you `npm update @maintainer-pro/ai-ui`. Prefer the `node_modules` copy, then restart this process.
31
+
32
+ ## Config
33
+
34
+ | Env / flag | Purpose |
35
+ |------------|---------|
36
+ | `PORT` / `--port` | Listen port (binary default `3000`; setup guide uses `3100`) |
37
+ | `AI_CLI_WORKSPACE` / `--workspace` | Folder the agent may edit |
38
+ | `AI_SERVER_UI` / `--ui` | Static UI folder |
39
+ | `CORS_ORIGIN` / `--cors-origin` | Browser origin of the **host app** |
40
+ | `--env-file` | Env file path (default `./.env`) |
41
+ | `MAINTAINER_PRO_URL` | Admin base URL |
42
+ | `MAINTAINER_PRO_API_KEY` | Server API key |
43
+ | `MAINTAINER_PRO_CLIENT_API_KEY` | Client key (served at `/embed-config.js`) |
44
+ | `AI_CLI_PROVIDER` | `auto` \| `claude` \| `cursor` \| `antigravity` |
45
+ | `AI_SERVER_PRODUCT_DESCRIPTION` | Optional product text for the agent prompt |
46
+
47
+ ```bash
48
+ npx --yes @maintainer-pro/ai-server --port 3100 --ui . --workspace .
49
+ ```
50
+
51
+ ## Related packages
52
+
53
+ | Package | Role |
54
+ |---------|------|
55
+ | [`@maintainer-pro/ai-cli`](https://www.npmjs.com/package/@maintainer-pro/ai-cli) | Talks to coding agents |
56
+ | [`@maintainer-pro/ai-ui`](https://www.npmjs.com/package/@maintainer-pro/ai-ui) | Embed widget (`/ai-ui.iife.js`) |
57
+ | [`@maintainer-pro/setup`](https://www.npmjs.com/package/@maintainer-pro/setup) | Download the sandbox setup guide |
package/bin/cli.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * npx --yes @maintainer-pro/ai-server
4
+ */
5
+ import "../src/server.mjs";
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@maintainer-pro/ai-server",
3
+ "version": "0.1.0",
4
+ "description": "Node HTTP server for Maintainer Pro chat. Uses @maintainer-pro/ai-cli to talk to coding agents.",
5
+ "keywords": [
6
+ "maintainer-pro",
7
+ "ai",
8
+ "chat",
9
+ "server",
10
+ "npx",
11
+ "cursor",
12
+ "claude"
13
+ ],
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "bin": {
17
+ "ai-server": "bin/cli.js",
18
+ "maintainer-pro-ai-server": "bin/cli.js"
19
+ },
20
+ "files": [
21
+ "bin",
22
+ "src",
23
+ "README.md"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "@maintainer-pro/ai-cli": "^0.1.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=22"
33
+ }
34
+ }
package/src/server.mjs ADDED
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Maintainer Pro chat HTTP server.
4
+ * Talks to coding agents through @maintainer-pro/ai-cli.
5
+ *
6
+ * Config from .env (or --env-file). CLI flags override env.
7
+ */
8
+ import fs from "node:fs";
9
+ import http from "node:http";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import {
13
+ createChatHandler,
14
+ createDefaultSystemPrompt,
15
+ createMaintainerProStoreFromEnv,
16
+ } from "@maintainer-pro/ai-cli";
17
+
18
+ const log = (msg) => console.log(`[ai-server] ${msg}`);
19
+ const fail = (msg) => {
20
+ console.error(`[ai-server] ${msg}`);
21
+ process.exit(1);
22
+ };
23
+
24
+ function parseArgs(argv) {
25
+ /** @type {Record<string, string | boolean>} */
26
+ const out = {};
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const arg = argv[i];
29
+ if (arg === "--help" || arg === "-h") {
30
+ out.help = true;
31
+ continue;
32
+ }
33
+ if (arg.startsWith("--") && i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
34
+ const key = arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
35
+ out[key] = argv[++i];
36
+ continue;
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
42
+ function loadEnvFile(file) {
43
+ if (!file || !fs.existsSync(file)) return;
44
+ const text = fs.readFileSync(file, "utf8");
45
+ for (const raw of text.split(/\r?\n/)) {
46
+ const line = raw.trim();
47
+ if (!line || line.startsWith("#")) continue;
48
+ const eq = line.indexOf("=");
49
+ if (eq < 1) continue;
50
+ const key = line.slice(0, eq).trim();
51
+ let value = line.slice(eq + 1).trim();
52
+ if (
53
+ (value.startsWith('"') && value.endsWith('"')) ||
54
+ (value.startsWith("'") && value.endsWith("'"))
55
+ ) {
56
+ value = value.slice(1, -1);
57
+ }
58
+ if (process.env[key] === undefined) process.env[key] = value;
59
+ }
60
+ }
61
+
62
+ function printHelp() {
63
+ console.log(`Maintainer Pro AI server
64
+
65
+ Usage:
66
+ npx @maintainer-pro/ai-server
67
+ npx @maintainer-pro/ai-server --port 3000 --ui . --workspace .
68
+
69
+ Flags override .env. Preferred config is env (from the admin setup guide).
70
+
71
+ --port <n> Listen port (PORT)
72
+ --ui <dir> Static UI folder (AI_SERVER_UI)
73
+ --workspace <dir> Agent edit root (AI_CLI_WORKSPACE)
74
+ --env-file <path> Env file (default: ./.env)
75
+ --cors-origin <url> CORS origin (CORS_ORIGIN)
76
+ `);
77
+ }
78
+
79
+ function contentType(file) {
80
+ if (file.endsWith(".html")) return "text/html; charset=utf-8";
81
+ if (file.endsWith(".js")) return "text/javascript; charset=utf-8";
82
+ if (file.endsWith(".css")) return "text/css; charset=utf-8";
83
+ if (file.endsWith(".svg")) return "image/svg+xml";
84
+ if (file.endsWith(".json")) return "application/json";
85
+ if (file.endsWith(".map")) return "application/json";
86
+ return "application/octet-stream";
87
+ }
88
+
89
+ function resolveUiFile(uiDir, pathname) {
90
+ const rel = decodeURIComponent(pathname === "/" ? "/index.html" : pathname);
91
+ const file = path.normalize(path.join(uiDir, rel));
92
+ const root = path.resolve(uiDir);
93
+ if (!file.startsWith(root)) return null;
94
+ if (fs.existsSync(file) && fs.statSync(file).isFile()) return file;
95
+ if (pathname === "/ai-ui.iife.js") {
96
+ const bundled = findIife();
97
+ if (bundled) return bundled;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function findIife() {
103
+ const here = path.dirname(fileURLToPath(import.meta.url));
104
+ const candidates = [
105
+ path.resolve(process.cwd(), "node_modules/@maintainer-pro/ai-ui/dist/ai-ui.iife.js"),
106
+ path.resolve(here, "../../ai-ui/dist/ai-ui.iife.js"),
107
+ path.resolve(here, "../../../node_modules/@maintainer-pro/ai-ui/dist/ai-ui.iife.js"),
108
+ ];
109
+ return candidates.find((file) => fs.existsSync(file)) ?? null;
110
+ }
111
+
112
+ async function toFetchRequest(req) {
113
+ const host = req.headers.host ?? "localhost";
114
+ const url = `http://${host}${req.url ?? "/"}`;
115
+ const chunks = [];
116
+ for await (const chunk of req) chunks.push(chunk);
117
+ const body = Buffer.concat(chunks);
118
+ const headers = new Headers();
119
+ for (const [key, value] of Object.entries(req.headers)) {
120
+ if (value === undefined) continue;
121
+ headers.set(key, Array.isArray(value) ? value.join(", ") : String(value));
122
+ }
123
+ /** @type {RequestInit} */
124
+ const init = { method: req.method, headers };
125
+ if (req.method !== "GET" && req.method !== "HEAD" && body.length) {
126
+ init.body = body;
127
+ init.duplex = "half";
128
+ }
129
+ return new Request(url, init);
130
+ }
131
+
132
+ async function send(res, response) {
133
+ res.statusCode = response.status;
134
+ response.headers.forEach((value, key) => {
135
+ if (key.toLowerCase() === "transfer-encoding") return;
136
+ res.setHeader(key, value);
137
+ });
138
+ res.end(Buffer.from(await response.arrayBuffer()));
139
+ }
140
+
141
+ const args = parseArgs(process.argv.slice(2));
142
+ if (args.help) {
143
+ printHelp();
144
+ process.exit(0);
145
+ }
146
+
147
+ const envFile = path.resolve(
148
+ typeof args.envFile === "string" ? args.envFile : path.join(process.cwd(), ".env")
149
+ );
150
+ loadEnvFile(envFile);
151
+
152
+ const port = Number(args.port || process.env.PORT || 3000);
153
+ const workspaceDir = path.resolve(
154
+ args.workspace || process.env.AI_CLI_WORKSPACE || process.cwd()
155
+ );
156
+ const uiDir = path.resolve(
157
+ args.ui || process.env.AI_SERVER_UI || process.cwd()
158
+ );
159
+ const corsOrigin =
160
+ (typeof args.corsOrigin === "string" ? args.corsOrigin : process.env.CORS_ORIGIN) ||
161
+ "*";
162
+
163
+ process.env.AI_CLI_WORKSPACE = workspaceDir;
164
+
165
+ const db = createMaintainerProStoreFromEnv();
166
+ if (!db) {
167
+ fail(
168
+ `Set MAINTAINER_PRO_URL and MAINTAINER_PRO_API_KEY (env file: ${envFile}).`
169
+ );
170
+ }
171
+
172
+ const productDescription =
173
+ process.env.AI_SERVER_PRODUCT_DESCRIPTION?.trim() ||
174
+ "This product uses Maintainer Pro. When the user asks for a change, edit source files in the workspace using the coding agent CLI. Do not invent JSON tool calls.";
175
+
176
+ const handlers = createChatHandler({
177
+ systemPrompt: createDefaultSystemPrompt({ productDescription }),
178
+ workspaceDir,
179
+ db,
180
+ });
181
+
182
+ const server = http.createServer((req, res) => {
183
+ const pathname = (req.url ?? "/").split("?")[0];
184
+ res.setHeader("Access-Control-Allow-Origin", corsOrigin);
185
+ res.setHeader("Access-Control-Allow-Headers", "content-type, authorization");
186
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
187
+ if (req.method === "OPTIONS") {
188
+ res.statusCode = 204;
189
+ res.end();
190
+ return;
191
+ }
192
+ if (pathname === "/embed-config.js") {
193
+ const payload = {
194
+ maintainerProUrl: process.env.MAINTAINER_PRO_URL ?? "",
195
+ maintainerProApiKey:
196
+ process.env.MAINTAINER_PRO_CLIENT_API_KEY ??
197
+ process.env.NEXT_PUBLIC_MAINTAINER_PRO_CLIENT_API_KEY ??
198
+ "",
199
+ };
200
+ res.setHeader("Content-Type", "text/javascript; charset=utf-8");
201
+ res.end(`window.__MAINTAINER_PRO__=${JSON.stringify(payload)};`);
202
+ return;
203
+ }
204
+ if (pathname === "/api/chat") {
205
+ const handler = req.method === "GET" ? handlers.GET : handlers.POST;
206
+ void toFetchRequest(req)
207
+ .then((request) => handler(request))
208
+ .then((response) => send(res, response))
209
+ .catch((err) => {
210
+ console.error(err);
211
+ res.statusCode = 500;
212
+ res.end("Chat handler failed");
213
+ });
214
+ return;
215
+ }
216
+ const file = resolveUiFile(uiDir, pathname);
217
+ if (!file) {
218
+ res.statusCode = 404;
219
+ res.end("Not found");
220
+ return;
221
+ }
222
+ res.setHeader("Content-Type", contentType(file));
223
+ fs.createReadStream(file).pipe(res);
224
+ });
225
+
226
+ server.listen(port, () => {
227
+ log(`chat → http://localhost:${port}/api/chat`);
228
+ log(`ui → http://localhost:${port}/`);
229
+ log(`workspace ${workspaceDir}`);
230
+ });