@assertrx/dino 1.0.0 → 1.0.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 +152 -0
- package/package.json +11 -3
- package/scripts/api-server.mjs +507 -0
- package/scripts/setup-dino-api.bat +101 -0
- package/scripts/setup-dino-api.sh +64 -0
- package/scripts/start-dino-server.bat +31 -0
- package/scripts/start-dino-server.sh +16 -0
- package/scripts/start-dino.cmd +0 -8
- package/scripts/start-dino.mjs +0 -226
- package/scripts/start-dino.ps1 +0 -9
- package/scripts/start-dino.sh +0 -10
package/README.md
CHANGED
|
@@ -291,3 +291,155 @@ npx @mablhq/mabl-cli auth clear
|
|
|
291
291
|
|
|
292
292
|
- `/mabl-auth` — Check mabl OAuth status, run `mabl auth login --auto` if needed, and start the mabl MCP server so the model can use the mabl tools.
|
|
293
293
|
|
|
294
|
+
## Dino API Server
|
|
295
|
+
|
|
296
|
+
Dino ships with a Fastify API server that exposes every prompt workflow as REST endpoints. This lets you trigger Dino prompts from CI pipelines, web UIs, or any HTTP client.
|
|
297
|
+
|
|
298
|
+
### Quick start (fresh system)
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Windows — double-click or run:
|
|
302
|
+
scripts\setup-dino-api.bat
|
|
303
|
+
|
|
304
|
+
# macOS / Linux — run:
|
|
305
|
+
chmod +x scripts/setup-dino-api.sh
|
|
306
|
+
./scripts/setup-dino-api.sh
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
This single script:
|
|
310
|
+
1. Checks that Node.js is installed
|
|
311
|
+
2. Installs `pi` globally if missing
|
|
312
|
+
3. Installs `@assertrx/dino` via `pi install` if missing
|
|
313
|
+
4. Starts the API server on port 3000
|
|
314
|
+
|
|
315
|
+
### Manual setup
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
# 1. Install pi (if not already installed)
|
|
319
|
+
npm install -g @earendil-works/pi-coding-agent
|
|
320
|
+
|
|
321
|
+
# 2. Install Dino via pi
|
|
322
|
+
pi install npm:@assertrx/dino --approve
|
|
323
|
+
|
|
324
|
+
# 3. Set an API key (any supported provider works)
|
|
325
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
326
|
+
# e.g. for OpenAI instead: export OPENAI_API_KEY=sk-...
|
|
327
|
+
|
|
328
|
+
# 4. Start the server
|
|
329
|
+
node ~/.pi/agent/npm/node_modules/@assertrx/dino/scripts/api-server.mjs
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Endpoints
|
|
333
|
+
|
|
334
|
+
| Method | Path | Description |
|
|
335
|
+
|--------|------|-------------|
|
|
336
|
+
| `GET` | `/api/health` | Health check |
|
|
337
|
+
| `GET` | `/api/providers` | List all providers with auth status and model counts |
|
|
338
|
+
| `GET` | `/api/models` | List models available with current credentials (`?provider=<id>` to filter) |
|
|
339
|
+
| `POST` | `/api/models` | List models for given credentials — body accepts `provider`+`apiKey` or `apiKeys` (validates the key and unlocks that provider's catalog) |
|
|
340
|
+
| `GET` | `/api/prompts` | List all available prompts with parameters |
|
|
341
|
+
| `GET` | `/api/prompts/:name` | Get raw markdown content of a prompt |
|
|
342
|
+
| `POST` | `/api/prompts/:name` | **Execute** a prompt via the pi agent |
|
|
343
|
+
|
|
344
|
+
### Providers, models & API keys
|
|
345
|
+
|
|
346
|
+
The server supports every pi provider (Anthropic, OpenAI, Google, DeepSeek, Groq, Mistral, xAI, OpenRouter, and many more). Credentials can come from three places, in this order of precedence:
|
|
347
|
+
|
|
348
|
+
1. **Per-request keys** — pass `apiKeys` (a map of provider → key) or `apiKey` (+ optional `provider`) in the POST body. These keys are scoped to that single request's session.
|
|
349
|
+
2. **Environment variables** — any provider's standard env var (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`).
|
|
350
|
+
3. **Stored credentials** — logins created via `pi /login` in `~/.pi/agent/auth.json`.
|
|
351
|
+
|
|
352
|
+
Use `GET /api/providers` to discover which providers are configured and `GET /api/models` to list models available to the server right now.
|
|
353
|
+
|
|
354
|
+
**Selecting a model:** pass `model` in the POST body as either `"provider/model-id"` (e.g. `"openai/gpt-5.2"`) or `{ "provider": "openai", "id": "gpt-5.2" }`. Without `model`, the session uses the default model resolution (configured provider defaults). Requesting a model whose provider has no credentials returns a clear error.
|
|
355
|
+
|
|
356
|
+
### Prompt execution endpoints
|
|
357
|
+
|
|
358
|
+
All POST bodies accept auth/model options, `cwd` (workspace directory, defaults to current), and `stream` (default: `true` for SSE):
|
|
359
|
+
|
|
360
|
+
| Body param | Description |
|
|
361
|
+
|------------|-------------|
|
|
362
|
+
| `apiKeys` | Map of provider → API key, e.g. `{"anthropic": "sk-...", "openai": "sk-..."}` |
|
|
363
|
+
| `apiKey` | Single API key. Pair with `provider` to target one explicitly; otherwise applied to the first available provider (anthropic fallback) |
|
|
364
|
+
| `provider` | Provider id (e.g. `"openai"`) to pair with a single `apiKey` |
|
|
365
|
+
| `model` | Model selection: `"provider/model-id"`, `"model-id"`, or `{"provider": ..., "id": ...}` |
|
|
366
|
+
|
|
367
|
+
| Prompt name | Required params |
|
|
368
|
+
|-------------|---------------|
|
|
369
|
+
| `setup-playwright` | _none_ |
|
|
370
|
+
| `create-pr` | _none_ |
|
|
371
|
+
| `generate-pom-from-url` | `url` |
|
|
372
|
+
| `generate-pom-from-url-cli` | `url` |
|
|
373
|
+
| `generate-bdd-from-url` | `url` |
|
|
374
|
+
| `generate-spec-from-feature` | `url`, `featureFile`, `pomDir` |
|
|
375
|
+
| `heal-playwright-failures` | _none_ (optional: `testPattern`, `maxIterations`) |
|
|
376
|
+
|
|
377
|
+
### Example requests
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
# List all prompts
|
|
381
|
+
curl http://localhost:3000/api/prompts
|
|
382
|
+
|
|
383
|
+
# Discover providers and their auth status
|
|
384
|
+
curl http://localhost:3000/api/providers
|
|
385
|
+
|
|
386
|
+
# List models available to the server (optionally per provider)
|
|
387
|
+
curl http://localhost:3000/api/models
|
|
388
|
+
curl "http://localhost:3000/api/models?provider=anthropic"
|
|
389
|
+
|
|
390
|
+
# Execute a prompt (SSE streaming, auth from env vars)
|
|
391
|
+
curl -X POST http://localhost:3000/api/prompts/setup-playwright \
|
|
392
|
+
-H "Content-Type: application/json" \
|
|
393
|
+
-d '{"cwd": "/path/to/workspace"}'
|
|
394
|
+
|
|
395
|
+
# Execute with a single provider key + model selection
|
|
396
|
+
# (env: export OPENAI_API_KEY=sk-... then:)
|
|
397
|
+
curl -X POST http://localhost:3000/api/prompts/setup-playwright \
|
|
398
|
+
-H "Content-Type: application/json" \
|
|
399
|
+
-d '{"cwd": "/path/to/workspace", "model": "openai/gpt-5.2"}'
|
|
400
|
+
|
|
401
|
+
# Execute with multiple per-request API keys and an explicit model
|
|
402
|
+
curl -X POST http://localhost:3000/api/prompts/setup-playwright \
|
|
403
|
+
-H "Content-Type: application/json" \
|
|
404
|
+
-d '{
|
|
405
|
+
"cwd": "/path/to/workspace",
|
|
406
|
+
"apiKeys": {"anthropic": "sk-ant-...", "openai": "sk-ov-..."},
|
|
407
|
+
"model": "anthropic/claude-sonnet-4-5"
|
|
408
|
+
}'
|
|
409
|
+
|
|
410
|
+
# Execute with URL parameter (non-streaming JSON response)
|
|
411
|
+
curl -X POST http://localhost:3000/api/prompts/generate-pom-from-url-cli \
|
|
412
|
+
-H "Content-Type: application/json" \
|
|
413
|
+
-d '{"url": "https://example.com", "outputDir": "tests/pom", "stream": false}'
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Response formats
|
|
417
|
+
|
|
418
|
+
**SSE streaming** (`stream: true`, default):
|
|
419
|
+
```
|
|
420
|
+
event: start → {"prompt": "..."}
|
|
421
|
+
event: text → {"delta": "..."} # response tokens
|
|
422
|
+
event: thinking → {"delta": "..."} # thinking output
|
|
423
|
+
event: tool_start → {"tool": "bash"} # tool execution started
|
|
424
|
+
event: tool_end → {"tool": "bash", ...} # tool execution finished
|
|
425
|
+
event: done → {} # session complete
|
|
426
|
+
event: error → {"message": "..."} # error
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Non-streaming** (`stream: false`):
|
|
430
|
+
```json
|
|
431
|
+
{
|
|
432
|
+
"prompt": "setup-playwright",
|
|
433
|
+
"response": "...full agent response text...",
|
|
434
|
+
"toolCalls": [{"name": "bash", "status": "success"}]
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Environment variables
|
|
439
|
+
|
|
440
|
+
| Variable | Default | Description |
|
|
441
|
+
|----------|---------|-------------|
|
|
442
|
+
| `PORT` | `3000` | Server port |
|
|
443
|
+
| `HOST` | `0.0.0.0` | Server host |
|
|
444
|
+
| Provider env vars | — | Any provider's standard env var (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`); per-request `apiKeys`/`apiKey` override these |
|
|
445
|
+
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assertrx/dino",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A friendly purple dinosaur pi package with a custom TUI header, theme, and Playwright skills.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
7
7
|
],
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"author": "",
|
|
10
|
+
"bin": {
|
|
11
|
+
"dino-api": "scripts/api-server.mjs"
|
|
12
|
+
},
|
|
10
13
|
"files": [
|
|
11
14
|
"extensions",
|
|
12
15
|
"prompts",
|
|
@@ -16,15 +19,20 @@
|
|
|
16
19
|
"scripts"
|
|
17
20
|
],
|
|
18
21
|
"scripts": {
|
|
19
|
-
"start:all": "
|
|
20
|
-
"start:api": "node scripts/
|
|
22
|
+
"start:all": "npm run start:api",
|
|
23
|
+
"start:api": "node scripts/api-server.mjs",
|
|
24
|
+
"start:api:dev": "node --watch scripts/api-server.mjs",
|
|
21
25
|
"postinstall": "npx playwright install chromium"
|
|
22
26
|
},
|
|
23
27
|
"dependencies": {
|
|
28
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
29
|
+
"@fastify/cors": "^11.3.0",
|
|
24
30
|
"@mablhq/mabl-cli": "^2.127.20",
|
|
25
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
26
32
|
"@playwright/cli": "^0.1.18",
|
|
27
33
|
"@playwright/mcp": "^0.0.79",
|
|
34
|
+
"fastify": "^5.2.1",
|
|
35
|
+
"pino-pretty": "^13.0.0",
|
|
28
36
|
"typebox": "^1.1.38"
|
|
29
37
|
},
|
|
30
38
|
"peerDependencies": {
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import cors from "@fastify/cors";
|
|
4
|
+
import {
|
|
5
|
+
createAgentSession,
|
|
6
|
+
ModelRuntime,
|
|
7
|
+
SessionManager,
|
|
8
|
+
DefaultResourceLoader,
|
|
9
|
+
getAgentDir,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { resolve, dirname } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
|
|
16
|
+
// ── Resolve paths ──────────────────────────────────────────────────────────
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const PROMPTS_DIR = resolve(__dirname, "..", "prompts");
|
|
19
|
+
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
20
|
+
|
|
21
|
+
// ── Prompt registry ────────────────────────────────────────────────────────
|
|
22
|
+
// Each entry maps a prompt name to its markdown file and expected body params.
|
|
23
|
+
const PROMPT_REGISTRY = {
|
|
24
|
+
"setup-playwright": {
|
|
25
|
+
file: "setup-playwright.md",
|
|
26
|
+
params: [],
|
|
27
|
+
description: "Set up a vanilla Playwright project in the current workspace",
|
|
28
|
+
},
|
|
29
|
+
"create-pr": {
|
|
30
|
+
file: "create-pr.md",
|
|
31
|
+
params: [],
|
|
32
|
+
description: "Create a GitHub pull request for changes in the current workspace",
|
|
33
|
+
},
|
|
34
|
+
"generate-pom-from-url": {
|
|
35
|
+
file: "generate-pom-from-url.md",
|
|
36
|
+
params: [
|
|
37
|
+
{ name: "url", required: true, description: "Target website URL" },
|
|
38
|
+
{ name: "outputDir", required: false, description: "Output directory (default: tests/pom/)" },
|
|
39
|
+
{ name: "authEnvFile", required: false, description: "Auth env file for login credentials" },
|
|
40
|
+
],
|
|
41
|
+
description: "Generate Playwright POMs from a live URL using Playwright MCP bridge (fallback only)",
|
|
42
|
+
},
|
|
43
|
+
"generate-pom-from-url-cli": {
|
|
44
|
+
file: "generate-pom-from-url-cli.md",
|
|
45
|
+
params: [
|
|
46
|
+
{ name: "url", required: true, description: "Target website URL" },
|
|
47
|
+
{ name: "outputDir", required: false, description: "Output directory (default: tests/pom/)" },
|
|
48
|
+
{ name: "authEnvFile", required: false, description: "Auth env file for login credentials" },
|
|
49
|
+
],
|
|
50
|
+
description: "Generate Playwright POMs from a live URL using playwright-cli (preferred)",
|
|
51
|
+
},
|
|
52
|
+
"generate-bdd-from-url": {
|
|
53
|
+
file: "generate-bdd-from-url.md",
|
|
54
|
+
params: [
|
|
55
|
+
{ name: "url", required: true, description: "Target website URL" },
|
|
56
|
+
{ name: "outputDir", required: false, description: "Output directory (default: tests/features/)" },
|
|
57
|
+
],
|
|
58
|
+
description: "Generate Gherkin BDD scenarios for a page by inspecting it with playwright-cli",
|
|
59
|
+
},
|
|
60
|
+
"generate-spec-from-feature": {
|
|
61
|
+
file: "generate-spec-from-feature.md",
|
|
62
|
+
params: [
|
|
63
|
+
{ name: "url", required: true, description: "URL of the page under test" },
|
|
64
|
+
{ name: "featureFile", required: true, description: "Path to the .feature file to implement" },
|
|
65
|
+
{ name: "pomDir", required: true, description: "Directory containing existing POM .ts files" },
|
|
66
|
+
{ name: "outputDir", required: false, description: "Output directory (default: tests/specs/)" },
|
|
67
|
+
],
|
|
68
|
+
description: "Generate a Playwright test spec from a Gherkin feature file",
|
|
69
|
+
},
|
|
70
|
+
"heal-playwright-failures": {
|
|
71
|
+
file: "heal-playwright-failures.md",
|
|
72
|
+
params: [
|
|
73
|
+
{ name: "testPattern", required: false, description: "Test file pattern to run (e.g., tests/login.spec.ts)" },
|
|
74
|
+
{ name: "maxIterations", required: false, description: "Max auto-heal iterations (default: 3)" },
|
|
75
|
+
],
|
|
76
|
+
description: "Detect, diagnose, and auto-heal failed Playwright tests",
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/** Load a prompt template and substitute $1..$N positional arguments. */
|
|
83
|
+
async function loadPrompt(name, params = {}) {
|
|
84
|
+
const entry = PROMPT_REGISTRY[name];
|
|
85
|
+
if (!entry) throw new Error(`Unknown prompt: ${name}`);
|
|
86
|
+
|
|
87
|
+
const filePath = resolve(PROMPTS_DIR, entry.file);
|
|
88
|
+
if (!existsSync(filePath)) throw new Error(`Prompt file not found: ${entry.file}`);
|
|
89
|
+
|
|
90
|
+
let content = await readFile(filePath, "utf-8");
|
|
91
|
+
|
|
92
|
+
// Build positional arg array from ordered entry.params
|
|
93
|
+
const positional = entry.params.map((p) => params[p.name] ?? "");
|
|
94
|
+
// Substitute $1, $2, $3, $4, ${1:-default}, ${2:-default}, etc.
|
|
95
|
+
content = content.replace(
|
|
96
|
+
/\$\{?(\d+)(?:\s*:-\s*([^}]*))?\}?/g,
|
|
97
|
+
(_, idx, fallback) => positional[parseInt(idx) - 1] || fallback || ""
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return content;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create a fresh ModelRuntime.
|
|
105
|
+
*
|
|
106
|
+
* Runtime API keys set on this instance are isolated to the current request.
|
|
107
|
+
*/
|
|
108
|
+
async function createModelRuntime() {
|
|
109
|
+
return ModelRuntime.create({
|
|
110
|
+
allowModelNetwork: false,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve runtime API keys from the request body.
|
|
116
|
+
*
|
|
117
|
+
* Supports three styles:
|
|
118
|
+
* - `apiKeys`: { "anthropic": "...", "openai": "..." }
|
|
119
|
+
* - `provider` + `apiKey`: set one key for an explicit provider
|
|
120
|
+
* - `apiKey` only (legacy): set key for the first available provider, falling back to anthropic
|
|
121
|
+
*/
|
|
122
|
+
function resolveApiKeys(body, availableModels) {
|
|
123
|
+
const keys = [];
|
|
124
|
+
|
|
125
|
+
if (body.apiKeys && typeof body.apiKeys === "object") {
|
|
126
|
+
for (const [providerId, key] of Object.entries(body.apiKeys)) {
|
|
127
|
+
if (typeof key === "string" && key.trim().length > 0) {
|
|
128
|
+
keys.push({ provider: providerId, key });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return keys;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (body.apiKey && typeof body.apiKey === "string" && body.apiKey.trim().length > 0) {
|
|
135
|
+
const provider = body.provider && typeof body.provider === "string"
|
|
136
|
+
? body.provider
|
|
137
|
+
: availableModels.length > 0
|
|
138
|
+
? availableModels[0].provider
|
|
139
|
+
: "anthropic";
|
|
140
|
+
keys.push({ provider, key: body.apiKey });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return keys;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve a model object from the request body.
|
|
148
|
+
*
|
|
149
|
+
* Accepts:
|
|
150
|
+
* - string: "anthropic/claude-opus-4-5" or "claude-opus-4-5"
|
|
151
|
+
* - object: { provider: "anthropic", id: "claude-opus-4-5" }
|
|
152
|
+
*
|
|
153
|
+
* If no model is requested, returns undefined so the agent picks a default.
|
|
154
|
+
*/
|
|
155
|
+
function resolveModel(modelRuntime, modelSelector, configuredProviders) {
|
|
156
|
+
if (!modelSelector) return undefined;
|
|
157
|
+
|
|
158
|
+
let providerId;
|
|
159
|
+
let modelId;
|
|
160
|
+
|
|
161
|
+
if (typeof modelSelector === "string") {
|
|
162
|
+
const parts = modelSelector.split("/");
|
|
163
|
+
if (parts.length === 2) {
|
|
164
|
+
[providerId, modelId] = parts;
|
|
165
|
+
} else {
|
|
166
|
+
modelId = modelSelector;
|
|
167
|
+
providerId = configuredProviders[0] ?? "anthropic";
|
|
168
|
+
}
|
|
169
|
+
} else if (typeof modelSelector === "object") {
|
|
170
|
+
providerId = modelSelector.provider;
|
|
171
|
+
modelId = modelSelector.id ?? modelSelector.modelId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!providerId || !modelId) {
|
|
175
|
+
throw new Error(`Invalid model selector: ${JSON.stringify(modelSelector)}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const model = modelRuntime.getModel(providerId, modelId);
|
|
179
|
+
if (!model) {
|
|
180
|
+
throw new Error(`Model not found: ${providerId}/${modelId}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!modelRuntime.hasConfiguredAuth(providerId)) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Provider "${providerId}" is not configured with an API key. ` +
|
|
186
|
+
`Provide an apiKey or apiKeys for this provider.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return model;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Create a single-use pi agent session. */
|
|
194
|
+
async function createSession({ cwd, apiKey, apiKeys, provider, model: modelSelector }) {
|
|
195
|
+
const modelRuntime = await createModelRuntime();
|
|
196
|
+
|
|
197
|
+
// Resolve keys before selecting a model so we know which providers are configured.
|
|
198
|
+
const availableBeforeKeys = modelRuntime.getAvailableSnapshot();
|
|
199
|
+
const keysToSet = resolveApiKeys({ apiKey, apiKeys, provider }, availableBeforeKeys);
|
|
200
|
+
|
|
201
|
+
// Set all runtime API keys in parallel.
|
|
202
|
+
await Promise.all(
|
|
203
|
+
keysToSet.map(({ provider: providerId, key }) =>
|
|
204
|
+
modelRuntime.setRuntimeApiKey(providerId, key).catch((err) => {
|
|
205
|
+
throw new Error(`Failed to set API key for ${providerId}: ${err.message}`);
|
|
206
|
+
})
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Refresh availability after keys are set so the model picker sees configured providers.
|
|
211
|
+
await modelRuntime.getAvailable();
|
|
212
|
+
|
|
213
|
+
const configuredProviders = keysToSet.map((entry) => entry.provider);
|
|
214
|
+
const model = resolveModel(modelRuntime, modelSelector, configuredProviders);
|
|
215
|
+
|
|
216
|
+
const agentDir = getAgentDir();
|
|
217
|
+
|
|
218
|
+
const loader = new DefaultResourceLoader({
|
|
219
|
+
cwd,
|
|
220
|
+
agentDir,
|
|
221
|
+
});
|
|
222
|
+
await loader.reload();
|
|
223
|
+
|
|
224
|
+
const { session } = await createAgentSession({
|
|
225
|
+
cwd,
|
|
226
|
+
agentDir,
|
|
227
|
+
modelRuntime,
|
|
228
|
+
resourceLoader: loader,
|
|
229
|
+
sessionManager: SessionManager.inMemory(),
|
|
230
|
+
...(model ? { model } : {}),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return session;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Fastify app ────────────────────────────────────────────────────────────
|
|
237
|
+
const app = Fastify({ logger: { level: "info" } });
|
|
238
|
+
|
|
239
|
+
// Register CORS for browser clients
|
|
240
|
+
await app.register(cors, { origin: true, credentials: true });
|
|
241
|
+
|
|
242
|
+
// Graceful shutdown
|
|
243
|
+
const shutdown = async () => {
|
|
244
|
+
app.log.info("Shutting down...");
|
|
245
|
+
await app.close();
|
|
246
|
+
process.exit(0);
|
|
247
|
+
};
|
|
248
|
+
process.on("SIGINT", shutdown);
|
|
249
|
+
process.on("SIGTERM", shutdown);
|
|
250
|
+
|
|
251
|
+
// Health check
|
|
252
|
+
app.get("/api/health", async () => ({ status: "ok", package: "@assertrx/dino" }));
|
|
253
|
+
|
|
254
|
+
// List all available prompts
|
|
255
|
+
app.get("/api/prompts", async () => {
|
|
256
|
+
const prompts = Object.entries(PROMPT_REGISTRY).map(([name, entry]) => ({
|
|
257
|
+
name,
|
|
258
|
+
description: entry.description,
|
|
259
|
+
params: entry.params,
|
|
260
|
+
endpoint: `/api/prompts/${name}`,
|
|
261
|
+
}));
|
|
262
|
+
return { prompts };
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Get prompt details (raw content)
|
|
266
|
+
app.get("/api/prompts/:name", async (request, reply) => {
|
|
267
|
+
const { name } = request.params;
|
|
268
|
+
const entry = PROMPT_REGISTRY[name];
|
|
269
|
+
if (!entry) {
|
|
270
|
+
reply.code(404);
|
|
271
|
+
return { error: `Prompt not found: ${name}` };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const content = await readFile(resolve(PROMPTS_DIR, entry.file), "utf-8");
|
|
275
|
+
return {
|
|
276
|
+
name,
|
|
277
|
+
description: entry.description,
|
|
278
|
+
params: entry.params,
|
|
279
|
+
content,
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// List providers and their auth/configuration status
|
|
284
|
+
app.get("/api/providers", async (request, reply) => {
|
|
285
|
+
try {
|
|
286
|
+
const modelRuntime = await createModelRuntime();
|
|
287
|
+
const providers = modelRuntime.getProviders().map((provider) => ({
|
|
288
|
+
id: provider.id,
|
|
289
|
+
name: provider.name ?? provider.id,
|
|
290
|
+
authStatus: modelRuntime.getProviderAuthStatus(provider.id),
|
|
291
|
+
modelCount: modelRuntime.getModels(provider.id).length,
|
|
292
|
+
}));
|
|
293
|
+
return { providers };
|
|
294
|
+
} catch (err) {
|
|
295
|
+
reply.code(500);
|
|
296
|
+
return { error: `Failed to list providers: ${err.message}` };
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// List available models. Optionally filter with ?provider=<id>.
|
|
301
|
+
app.get("/api/models", async (request, reply) => {
|
|
302
|
+
try {
|
|
303
|
+
const { provider } = request.query;
|
|
304
|
+
const modelRuntime = await createModelRuntime();
|
|
305
|
+
await modelRuntime.getAvailable();
|
|
306
|
+
const models = modelRuntime.getAvailableSnapshot();
|
|
307
|
+
const filtered = provider
|
|
308
|
+
? models.filter((model) => model.provider === provider)
|
|
309
|
+
: models;
|
|
310
|
+
return {
|
|
311
|
+
models: filtered.map((model) => ({
|
|
312
|
+
provider: model.provider,
|
|
313
|
+
id: model.id,
|
|
314
|
+
name: model.name ?? model.id,
|
|
315
|
+
thinking: model.thinking,
|
|
316
|
+
})),
|
|
317
|
+
};
|
|
318
|
+
} catch (err) {
|
|
319
|
+
reply.code(500);
|
|
320
|
+
return { error: `Failed to list models: ${err.message}` };
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// List models available for the provided credentials (per-request keys).
|
|
325
|
+
// Accepts provider+apiKey or an apiKeys map; without keys, lists models from
|
|
326
|
+
// env/stored credentials. Used by clients to validate a key and pick a model.
|
|
327
|
+
app.post("/api/models", async (request, reply) => {
|
|
328
|
+
try {
|
|
329
|
+
const { provider, apiKey, apiKeys } = request.body || {};
|
|
330
|
+
const modelRuntime = await createModelRuntime();
|
|
331
|
+
|
|
332
|
+
const availableBeforeKeys = modelRuntime.getAvailableSnapshot();
|
|
333
|
+
const keysToSet = resolveApiKeys({ provider, apiKey, apiKeys }, availableBeforeKeys);
|
|
334
|
+
|
|
335
|
+
await Promise.all(
|
|
336
|
+
keysToSet.map(({ provider: providerId, key }) =>
|
|
337
|
+
modelRuntime.setRuntimeApiKey(providerId, key).catch((err) => {
|
|
338
|
+
throw new Error(`Failed to set API key for ${providerId}: ${err.message}`);
|
|
339
|
+
})
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
await modelRuntime.getAvailable();
|
|
344
|
+
|
|
345
|
+
let models = modelRuntime.getAvailableSnapshot();
|
|
346
|
+
if (provider) {
|
|
347
|
+
models = models.filter((model) => model.provider === provider);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
providers: [...new Set(keysToSet.map((entry) => entry.provider))],
|
|
352
|
+
models: models.map((model) => ({
|
|
353
|
+
provider: model.provider,
|
|
354
|
+
id: model.id,
|
|
355
|
+
name: model.name ?? model.id,
|
|
356
|
+
thinking: model.thinking,
|
|
357
|
+
})),
|
|
358
|
+
};
|
|
359
|
+
} catch (err) {
|
|
360
|
+
reply.code(500);
|
|
361
|
+
return { error: `Failed to list models: ${err.message}` };
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Execute a prompt (SSE streaming)
|
|
366
|
+
app.post("/api/prompts/:name", async (request, reply) => {
|
|
367
|
+
const { name } = request.params;
|
|
368
|
+
const entry = PROMPT_REGISTRY[name];
|
|
369
|
+
if (!entry) {
|
|
370
|
+
reply.code(404);
|
|
371
|
+
return { error: `Prompt not found: ${name}` };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const {
|
|
375
|
+
apiKey,
|
|
376
|
+
apiKeys,
|
|
377
|
+
provider,
|
|
378
|
+
model,
|
|
379
|
+
cwd = process.cwd(),
|
|
380
|
+
stream = true,
|
|
381
|
+
...params
|
|
382
|
+
} = request.body || {};
|
|
383
|
+
|
|
384
|
+
// Validate required params
|
|
385
|
+
for (const p of entry.params) {
|
|
386
|
+
if (p.required && !params[p.name]) {
|
|
387
|
+
reply.code(400);
|
|
388
|
+
return { error: `Missing required parameter: ${p.name}` };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Build the prompt
|
|
393
|
+
let promptContent;
|
|
394
|
+
try {
|
|
395
|
+
promptContent = await loadPrompt(name, params);
|
|
396
|
+
} catch (err) {
|
|
397
|
+
reply.code(500);
|
|
398
|
+
return { error: `Failed to load prompt: ${err.message}` };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Create session
|
|
402
|
+
let session;
|
|
403
|
+
try {
|
|
404
|
+
session = await createSession({
|
|
405
|
+
cwd: resolve(cwd),
|
|
406
|
+
apiKey,
|
|
407
|
+
apiKeys,
|
|
408
|
+
provider,
|
|
409
|
+
model,
|
|
410
|
+
});
|
|
411
|
+
} catch (err) {
|
|
412
|
+
reply.code(500);
|
|
413
|
+
return { error: `Failed to create agent session: ${err.message}` };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!stream) {
|
|
417
|
+
// ── Non-streaming: collect full response ──────────────────────────────
|
|
418
|
+
const messages = [];
|
|
419
|
+
const toolCalls = [];
|
|
420
|
+
|
|
421
|
+
session.subscribe((event) => {
|
|
422
|
+
if (event.type === "message_update") {
|
|
423
|
+
if (event.assistantMessageEvent.type === "text_delta") {
|
|
424
|
+
messages.push(event.assistantMessageEvent.delta);
|
|
425
|
+
}
|
|
426
|
+
} else if (event.type === "tool_execution_start") {
|
|
427
|
+
toolCalls.push({ name: event.toolName, status: "started" });
|
|
428
|
+
} else if (event.type === "tool_execution_end") {
|
|
429
|
+
const last = toolCalls[toolCalls.length - 1];
|
|
430
|
+
if (last) last.status = event.isError ? "error" : "success";
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
await session.prompt(promptContent);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
prompt: name,
|
|
438
|
+
response: messages.join(""),
|
|
439
|
+
toolCalls,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── Streaming SSE ──────────────────────────────────────────────────────
|
|
444
|
+
reply.raw.writeHead(200, {
|
|
445
|
+
"Content-Type": "text/event-stream",
|
|
446
|
+
"Cache-Control": "no-cache",
|
|
447
|
+
Connection: "keep-alive",
|
|
448
|
+
"X-Accel-Buffering": "no",
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const sendSSE = (event, data) => {
|
|
452
|
+
reply.raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
sendSSE("start", { prompt: name });
|
|
456
|
+
|
|
457
|
+
session.subscribe((event) => {
|
|
458
|
+
switch (event.type) {
|
|
459
|
+
case "message_update":
|
|
460
|
+
if (event.assistantMessageEvent.type === "text_delta") {
|
|
461
|
+
sendSSE("text", { delta: event.assistantMessageEvent.delta });
|
|
462
|
+
} else if (event.assistantMessageEvent.type === "thinking_delta") {
|
|
463
|
+
sendSSE("thinking", { delta: event.assistantMessageEvent.delta });
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
|
|
467
|
+
case "tool_execution_start":
|
|
468
|
+
sendSSE("tool_start", { tool: event.toolName });
|
|
469
|
+
break;
|
|
470
|
+
|
|
471
|
+
case "tool_execution_update":
|
|
472
|
+
sendSSE("tool_output", { tool: event.toolName, output: event.output });
|
|
473
|
+
break;
|
|
474
|
+
|
|
475
|
+
case "tool_execution_end":
|
|
476
|
+
sendSSE("tool_end", { tool: event.toolName, error: event.isError });
|
|
477
|
+
break;
|
|
478
|
+
|
|
479
|
+
case "agent_end":
|
|
480
|
+
sendSSE("done", {});
|
|
481
|
+
reply.raw.end();
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
await session.prompt(promptContent);
|
|
488
|
+
} catch (err) {
|
|
489
|
+
sendSSE("error", { message: err.message });
|
|
490
|
+
reply.raw.end();
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// ── Start server ───────────────────────────────────────────────────────────
|
|
495
|
+
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
496
|
+
const HOST = process.env.HOST || "0.0.0.0";
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
await app.listen({ port: PORT, host: HOST });
|
|
500
|
+
console.log(`🦕 Dino API server running at http://${HOST}:${PORT}`);
|
|
501
|
+
console.log(` Prompts: http://${HOST}:${PORT}/api/prompts`);
|
|
502
|
+
console.log(` Providers: http://${HOST}:${PORT}/api/providers`);
|
|
503
|
+
console.log(` Models: http://${HOST}:${PORT}/api/models`);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
app.log.error(err);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal enabledelayedexpansion
|
|
3
|
+
|
|
4
|
+
echo ============================================
|
|
5
|
+
echo Dino API Server - Setup ^& Start
|
|
6
|
+
echo ============================================
|
|
7
|
+
echo.
|
|
8
|
+
|
|
9
|
+
:: ── 1. Check Node.js ──────────────────────────────────────────────────
|
|
10
|
+
echo [1/4] Checking Node.js...
|
|
11
|
+
where node >nul 2>&1
|
|
12
|
+
if !ERRORLEVEL! NEQ 0 (
|
|
13
|
+
echo ERROR: Node.js is not installed.
|
|
14
|
+
echo Please install Node.js from https://nodejs.org/ and re-run this script.
|
|
15
|
+
pause
|
|
16
|
+
exit /b 1
|
|
17
|
+
)
|
|
18
|
+
for /f "tokens=*" %%v in ('node -v') do echo Found Node.js %%v
|
|
19
|
+
|
|
20
|
+
:: ── 2. Install pi (if missing) ────────────────────────────────────────
|
|
21
|
+
echo [2/4] Checking pi CLI...
|
|
22
|
+
where pi >nul 2>&1
|
|
23
|
+
if !ERRORLEVEL! EQU 0 (
|
|
24
|
+
for /f "tokens=*" %%v in ('pi --version 2^>^&1') do echo Found pi %%v
|
|
25
|
+
) else (
|
|
26
|
+
echo Installing pi...
|
|
27
|
+
call npm install -g @earendil-works/pi-coding-agent
|
|
28
|
+
if !ERRORLEVEL! NEQ 0 (
|
|
29
|
+
echo ERROR: Failed to install pi.
|
|
30
|
+
pause
|
|
31
|
+
exit /b 1
|
|
32
|
+
)
|
|
33
|
+
echo pi installed successfully.
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
:: ── 3. Install Dino via pi (if missing) ───────────────────────────────
|
|
37
|
+
echo [3/4] Checking Dino package...
|
|
38
|
+
set "DINO_DIR=%USERPROFILE%\.pi\agent\npm\node_modules\@assertrx\dino"
|
|
39
|
+
if not exist "%DINO_DIR%" (
|
|
40
|
+
echo Installing @assertrx/dino via pi...
|
|
41
|
+
call pi install npm:@assertrx/dino --approve
|
|
42
|
+
if !ERRORLEVEL! NEQ 0 (
|
|
43
|
+
echo ERROR: Failed to install Dino package.
|
|
44
|
+
pause
|
|
45
|
+
exit /b 1
|
|
46
|
+
)
|
|
47
|
+
echo Dino package installed.
|
|
48
|
+
) else (
|
|
49
|
+
echo Dino already installed at %DINO_DIR%
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
:: ── 4. Start server ───────────────────────────────────────────────────
|
|
53
|
+
echo [4/4] Starting Dino API server...
|
|
54
|
+
echo.
|
|
55
|
+
|
|
56
|
+
:: Defaults — override via env vars
|
|
57
|
+
if "%PORT%"=="" set PORT=3000
|
|
58
|
+
if "%HOST%"=="" set HOST=0.0.0.0
|
|
59
|
+
|
|
60
|
+
echo Port: %PORT%
|
|
61
|
+
echo Host: %HOST%
|
|
62
|
+
echo.
|
|
63
|
+
|
|
64
|
+
:: Use ANTHROPIC_API_KEY from env if set
|
|
65
|
+
if defined ANTHROPIC_API_KEY (
|
|
66
|
+
echo API key: loaded from ANTHROPIC_API_KEY
|
|
67
|
+
) else (
|
|
68
|
+
echo WARNING: No ANTHROPIC_API_KEY set. Set it or pass apiKey in request body.
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
echo.
|
|
72
|
+
echo Endpoints:
|
|
73
|
+
echo GET http://%HOST%:%PORT%/api/health
|
|
74
|
+
echo GET http://%HOST%:%PORT%/api/prompts
|
|
75
|
+
echo POST http://%HOST%:%PORT%/api/prompts/:name
|
|
76
|
+
echo.
|
|
77
|
+
echo ============================================
|
|
78
|
+
echo.
|
|
79
|
+
|
|
80
|
+
:: The published npm package may not include api-server.mjs, so fall back
|
|
81
|
+
:: to the local source copy when this script is run from the Dino repo.
|
|
82
|
+
set "LOCAL_API=%~dp0..\scripts\api-server.mjs"
|
|
83
|
+
set "INSTALLED_API=%DINO_DIR%\scripts\api-server.mjs"
|
|
84
|
+
|
|
85
|
+
if exist "%INSTALLED_API%" (
|
|
86
|
+
echo Using installed api-server.mjs
|
|
87
|
+
node "%INSTALLED_API%"
|
|
88
|
+
) else if exist "%LOCAL_API%" (
|
|
89
|
+
echo Installed api-server.mjs not found.
|
|
90
|
+
echo Falling back to local source: %LOCAL_API%
|
|
91
|
+
node "%LOCAL_API%"
|
|
92
|
+
) else (
|
|
93
|
+
echo ERROR: api-server.mjs not found at either of the following locations:
|
|
94
|
+
echo %INSTALLED_API%
|
|
95
|
+
echo %LOCAL_API%
|
|
96
|
+
echo.
|
|
97
|
+
echo The published @assertrx/dino package appears to be missing api-server.mjs.
|
|
98
|
+
echo Run this script from the Dino source repository, or republish the package.
|
|
99
|
+
pause
|
|
100
|
+
exit /b 1
|
|
101
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
echo "============================================"
|
|
5
|
+
echo " Dino API Server - Setup & Start"
|
|
6
|
+
echo "============================================"
|
|
7
|
+
echo ""
|
|
8
|
+
|
|
9
|
+
# ── 1. Check Node.js ──────────────────────────────────────────────────
|
|
10
|
+
echo "[1/4] Checking Node.js..."
|
|
11
|
+
if ! command -v node &>/dev/null; then
|
|
12
|
+
echo "ERROR: Node.js is not installed."
|
|
13
|
+
echo "Please install Node.js from https://nodejs.org/ and re-run this script."
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
echo " Found Node.js $(node -v)"
|
|
17
|
+
|
|
18
|
+
# ── 2. Install pi (if missing) ────────────────────────────────────────
|
|
19
|
+
echo "[2/4] Checking pi CLI..."
|
|
20
|
+
if command -v pi &>/dev/null; then
|
|
21
|
+
echo " Found pi $(pi --version 2>&1 || echo '(unknown version)')"
|
|
22
|
+
else
|
|
23
|
+
echo " Installing pi..."
|
|
24
|
+
npm install -g @earendil-works/pi-coding-agent
|
|
25
|
+
echo " pi installed successfully."
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# ── 3. Install Dino via pi (if missing) ───────────────────────────────
|
|
29
|
+
echo "[3/4] Checking Dino package..."
|
|
30
|
+
DINO_DIR="${HOME}/.pi/agent/npm/node_modules/@assertrx/dino"
|
|
31
|
+
if [ ! -d "${DINO_DIR}" ]; then
|
|
32
|
+
echo " Installing @assertrx/dino via pi..."
|
|
33
|
+
pi install "npm:@assertrx/dino" --approve
|
|
34
|
+
echo " Dino package installed."
|
|
35
|
+
else
|
|
36
|
+
echo " Dino already installed at ${DINO_DIR}"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# ── 4. Start server ───────────────────────────────────────────────────
|
|
40
|
+
echo "[4/4] Starting Dino API server..."
|
|
41
|
+
echo ""
|
|
42
|
+
|
|
43
|
+
PORT="${PORT:-3000}"
|
|
44
|
+
HOST="${HOST:-0.0.0.0}"
|
|
45
|
+
|
|
46
|
+
echo " Port: ${PORT}"
|
|
47
|
+
echo " Host: ${HOST}"
|
|
48
|
+
|
|
49
|
+
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
|
|
50
|
+
echo " API key: loaded from ANTHROPIC_API_KEY"
|
|
51
|
+
else
|
|
52
|
+
echo " WARNING: No ANTHROPIC_API_KEY set. Set it or pass apiKey in request body."
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
echo ""
|
|
56
|
+
echo " Endpoints:"
|
|
57
|
+
echo " GET http://${HOST}:${PORT}/api/health"
|
|
58
|
+
echo " GET http://${HOST}:${PORT}/api/prompts"
|
|
59
|
+
echo " POST http://${HOST}:${PORT}/api/prompts/:name"
|
|
60
|
+
echo ""
|
|
61
|
+
echo "============================================"
|
|
62
|
+
echo ""
|
|
63
|
+
|
|
64
|
+
exec node "${DINO_DIR}/scripts/api-server.mjs"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
:: Quick start for Dino API server (assumes pi + Dino already installed)
|
|
3
|
+
:: Usage: start-dino-server.bat
|
|
4
|
+
|
|
5
|
+
set "DINO_DIR=%USERPROFILE%\.pi\agent\npm\node_modules\@assertrx\dino"
|
|
6
|
+
|
|
7
|
+
if not exist "%DINO_DIR%" (
|
|
8
|
+
echo Dino package not found at %DINO_DIR%
|
|
9
|
+
echo Run setup-dino-api.bat first to install.
|
|
10
|
+
exit /b 1
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
echo Starting Dino API server on port %PORT% (default: 3000)...
|
|
14
|
+
|
|
15
|
+
:: The published npm package may not include api-server.mjs, so fall back
|
|
16
|
+
:: to the local source copy when this script is run from the Dino repo.
|
|
17
|
+
set "LOCAL_API=%~dp0..\scripts\api-server.mjs"
|
|
18
|
+
set "INSTALLED_API=%DINO_DIR%\scripts\api-server.mjs"
|
|
19
|
+
|
|
20
|
+
if exist "%INSTALLED_API%" (
|
|
21
|
+
node "%INSTALLED_API%"
|
|
22
|
+
) else if exist "%LOCAL_API%" (
|
|
23
|
+
echo Installed api-server.mjs not found. Using local source: %LOCAL_API%
|
|
24
|
+
node "%LOCAL_API%"
|
|
25
|
+
) else (
|
|
26
|
+
echo ERROR: api-server.mjs not found at either of the following locations:
|
|
27
|
+
echo %INSTALLED_API%
|
|
28
|
+
echo %LOCAL_API%
|
|
29
|
+
pause
|
|
30
|
+
exit /b 1
|
|
31
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Quick start for Dino API server (assumes pi + Dino already installed)
|
|
3
|
+
# Usage: ./start-dino-server.sh
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
DINO_DIR="${HOME}/.pi/agent/npm/node_modules/@assertrx/dino"
|
|
8
|
+
|
|
9
|
+
if [ ! -d "${DINO_DIR}" ]; then
|
|
10
|
+
echo "Dino package not found at ${DINO_DIR}"
|
|
11
|
+
echo "Run setup-dino-api.sh first to install."
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
echo "Starting Dino API server on port ${PORT:-3000}..."
|
|
16
|
+
exec node "${DINO_DIR}/scripts/api-server.mjs"
|
package/scripts/start-dino.cmd
DELETED
package/scripts/start-dino.mjs
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* One-shot bootstrapper for the Dino pi package + dino-api Fastify server.
|
|
4
|
-
*
|
|
5
|
-
* What it does:
|
|
6
|
-
* 1. Verifies the `pi` CLI is installed.
|
|
7
|
-
* 2. Installs the local Dino pi package (`pi install ./ --approve`).
|
|
8
|
-
* 3. Installs dependencies and builds `dino-api`.
|
|
9
|
-
* 4. Starts the Fastify API and polls `/health` until it is ready.
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* node scripts/start-dino.mjs
|
|
13
|
-
*
|
|
14
|
-
* Optional env vars:
|
|
15
|
-
* DINO_API_DIR Path to dino-api (default: sibling ../dino-api)
|
|
16
|
-
* PORT Port the API will listen on (default: 3000)
|
|
17
|
-
*
|
|
18
|
-
* Optional flags:
|
|
19
|
-
* --skip-pi-install Skip `pi install ./`
|
|
20
|
-
* --skip-install Skip `npm install` in dino-api
|
|
21
|
-
* --skip-build Skip `npm run build` in dino-api
|
|
22
|
-
*/
|
|
23
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
24
|
-
import { fileURLToPath } from "node:url";
|
|
25
|
-
import http from "node:http";
|
|
26
|
-
import path from "node:path";
|
|
27
|
-
|
|
28
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
-
const DINO_DIR = path.resolve(__dirname, "..");
|
|
30
|
-
const API_DIR =
|
|
31
|
-
process.env.DINO_API_DIR ?? path.resolve(DINO_DIR, "..", "dino-api");
|
|
32
|
-
const PORT = Number(process.env.PORT ?? "3000");
|
|
33
|
-
const HOST = process.env.HOST ?? "localhost";
|
|
34
|
-
|
|
35
|
-
const FLAGS = {
|
|
36
|
-
skipPiInstall: process.argv.includes("--skip-pi-install"),
|
|
37
|
-
skipInstall: process.argv.includes("--skip-install"),
|
|
38
|
-
skipBuild: process.argv.includes("--skip-build"),
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
function run(command, args, options = {}) {
|
|
42
|
-
// On Windows, .cmd files (npm, pi) need a shell. On Unix they don't, and
|
|
43
|
-
// passing args with shell: true triggers a Node deprecation warning.
|
|
44
|
-
const shell = process.platform === "win32";
|
|
45
|
-
const fullCommand = shell ? [command, ...args].join(" ") : command;
|
|
46
|
-
const spawnArgs = shell ? [] : args;
|
|
47
|
-
|
|
48
|
-
return new Promise((resolve, reject) => {
|
|
49
|
-
const child = spawn(fullCommand, spawnArgs, {
|
|
50
|
-
cwd: options.cwd,
|
|
51
|
-
shell,
|
|
52
|
-
stdio: options.stdio ?? "inherit",
|
|
53
|
-
env: { ...process.env, ...options.env },
|
|
54
|
-
});
|
|
55
|
-
child.on("error", reject);
|
|
56
|
-
child.on("close", (code) => {
|
|
57
|
-
if (code === 0) {
|
|
58
|
-
resolve();
|
|
59
|
-
} else {
|
|
60
|
-
reject(
|
|
61
|
-
new Error(`"${command} ${args.join(" ")}" exited with ${code}`)
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function checkPiInstalled() {
|
|
69
|
-
// Run through a shell so this works on both Windows (pi.cmd) and Unix.
|
|
70
|
-
const result = spawnSync("pi --version", [], {
|
|
71
|
-
shell: true,
|
|
72
|
-
encoding: "utf-8",
|
|
73
|
-
});
|
|
74
|
-
if (result.error || result.status !== 0) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
"pi CLI not found. Install it first:\n" +
|
|
77
|
-
" npm install -g @earendil-works/pi-coding-agent\n" +
|
|
78
|
-
"Then re-run this script."
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
const version = result.stdout?.trim() || "unknown";
|
|
82
|
-
console.log(`✓ pi CLI detected (${version})`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function assertPortFree(port, host = "0.0.0.0") {
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
const server = http.createServer();
|
|
88
|
-
server.once("error", (err) => {
|
|
89
|
-
if (err.code === "EADDRINUSE") {
|
|
90
|
-
reject(
|
|
91
|
-
new Error(
|
|
92
|
-
`Port ${port} is already in use on ${host}. ` +
|
|
93
|
-
`Stop the other process or set a different PORT (e.g. PORT=3001).`
|
|
94
|
-
)
|
|
95
|
-
);
|
|
96
|
-
} else {
|
|
97
|
-
reject(err);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
server.once("listening", () => {
|
|
101
|
-
server.close(() => resolve());
|
|
102
|
-
});
|
|
103
|
-
server.listen({ port, host });
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function installDinoPackage() {
|
|
108
|
-
if (FLAGS.skipPiInstall) {
|
|
109
|
-
console.log("⏭ Skipping pi install (--skip-pi-install)");
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
console.log("📦 Installing Dino pi package...");
|
|
113
|
-
await run("pi", ["install", "./", "--approve"], { cwd: DINO_DIR });
|
|
114
|
-
console.log("✓ Dino pi package installed");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function buildDinoApi() {
|
|
118
|
-
console.log(`🔨 Building dino-api at ${API_DIR} ...`);
|
|
119
|
-
if (!FLAGS.skipInstall) {
|
|
120
|
-
console.log(" → npm install");
|
|
121
|
-
await run("npm", ["install"], { cwd: API_DIR });
|
|
122
|
-
} else {
|
|
123
|
-
console.log(" ⏭ Skipping npm install (--skip-install)");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!FLAGS.skipBuild) {
|
|
127
|
-
console.log(" → npm run build");
|
|
128
|
-
await run("npm", ["run", "build"], { cwd: API_DIR });
|
|
129
|
-
} else {
|
|
130
|
-
console.log(" ⏭ Skipping npm run build (--skip-build)");
|
|
131
|
-
}
|
|
132
|
-
console.log("✓ dino-api built");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function waitForApi(retries = 120, intervalMs = 500) {
|
|
136
|
-
const url = new URL("/health", `http://${HOST}:${PORT}`);
|
|
137
|
-
return new Promise((resolve, reject) => {
|
|
138
|
-
let attempt = 0;
|
|
139
|
-
const tryOnce = () => {
|
|
140
|
-
attempt += 1;
|
|
141
|
-
const req = http.get(
|
|
142
|
-
url,
|
|
143
|
-
{ timeout: 2000 },
|
|
144
|
-
(res) => {
|
|
145
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
146
|
-
resolve();
|
|
147
|
-
} else {
|
|
148
|
-
retry();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
);
|
|
152
|
-
req.on("error", retry);
|
|
153
|
-
req.on("timeout", () => req.destroy());
|
|
154
|
-
};
|
|
155
|
-
const retry = () => {
|
|
156
|
-
if (attempt >= retries) {
|
|
157
|
-
reject(
|
|
158
|
-
new Error(
|
|
159
|
-
`API did not become ready on ${url} after ${retries * intervalMs}ms`
|
|
160
|
-
)
|
|
161
|
-
);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
process.stdout.write(
|
|
165
|
-
`\r ⏳ Waiting for API at ${url} ... (${attempt}/${retries})`
|
|
166
|
-
);
|
|
167
|
-
setTimeout(tryOnce, intervalMs);
|
|
168
|
-
};
|
|
169
|
-
tryOnce();
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function startDinoApi() {
|
|
174
|
-
console.log("🚀 Starting dino-api...");
|
|
175
|
-
await assertPortFree(PORT);
|
|
176
|
-
|
|
177
|
-
const shell = process.platform === "win32";
|
|
178
|
-
const command = shell ? "npm start" : "npm";
|
|
179
|
-
const args = shell ? [] : ["start"];
|
|
180
|
-
const child = spawn(command, args, {
|
|
181
|
-
cwd: API_DIR,
|
|
182
|
-
shell,
|
|
183
|
-
stdio: "inherit",
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const cleanup = (signal) => {
|
|
187
|
-
console.log(`\n⚙ Received ${signal}, shutting down dino-api...`);
|
|
188
|
-
child.kill(signal === "SIGINT" ? "SIGINT" : "SIGTERM");
|
|
189
|
-
};
|
|
190
|
-
process.on("SIGINT", cleanup);
|
|
191
|
-
process.on("SIGTERM", cleanup);
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
await waitForApi();
|
|
195
|
-
process.stdout.write("\r"); // clear progress line
|
|
196
|
-
console.log(`✅ dino-api is ready at http://${HOST}:${PORT}`);
|
|
197
|
-
console.log(` Health check: http://${HOST}:${PORT}/health`);
|
|
198
|
-
console.log(` Chat endpoint: POST http://${HOST}:${PORT}/chat`);
|
|
199
|
-
} catch (err) {
|
|
200
|
-
console.error("\n" + err.message);
|
|
201
|
-
child.kill("SIGTERM");
|
|
202
|
-
throw err;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return new Promise((resolve) => {
|
|
206
|
-
child.on("close", (code) => {
|
|
207
|
-
console.log(`\ndino-api exited with code ${code ?? 0}`);
|
|
208
|
-
resolve(code ?? 0);
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function main() {
|
|
214
|
-
console.log("🦖 Dino bootstrapper\n");
|
|
215
|
-
try {
|
|
216
|
-
await checkPiInstalled();
|
|
217
|
-
await installDinoPackage();
|
|
218
|
-
await buildDinoApi();
|
|
219
|
-
await startDinoApi();
|
|
220
|
-
} catch (err) {
|
|
221
|
-
console.error("\n❌", err.message);
|
|
222
|
-
process.exit(1);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
main();
|
package/scripts/start-dino.ps1
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Convenience wrapper for PowerShell (run with -ExecutionPolicy Bypass if needed).
|
|
2
|
-
# Runs scripts/start-dino.mjs from the repository root.
|
|
3
|
-
$ErrorActionPreference = 'Stop'
|
|
4
|
-
|
|
5
|
-
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
6
|
-
$repoDir = Split-Path -Parent $scriptDir
|
|
7
|
-
|
|
8
|
-
Set-Location $repoDir
|
|
9
|
-
& node scripts/start-dino.mjs @args
|
package/scripts/start-dino.sh
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Convenience wrapper for Unix-like shells (Git Bash, WSL, macOS, Linux).
|
|
3
|
-
# Runs scripts/start-dino.mjs from the repository root.
|
|
4
|
-
set -euo pipefail
|
|
5
|
-
|
|
6
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
8
|
-
|
|
9
|
-
cd "${REPO_DIR}"
|
|
10
|
-
node scripts/start-dino.mjs "$@"
|