@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 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.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": "node scripts/start-dino.mjs",
20
- "start:api": "node scripts/start-dino.mjs",
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"
@@ -1,8 +0,0 @@
1
- @echo off
2
- REM Convenience wrapper for Windows Command Prompt.
3
- REM Runs scripts\start-dino.mjs from the repository root.
4
- setlocal
5
- set "SCRIPT_DIR=%~dp0"
6
- set "REPO_DIR=%SCRIPT_DIR%.."
7
- cd /d "%REPO_DIR%"
8
- node scripts\start-dino.mjs %*
@@ -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();
@@ -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
@@ -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 "$@"