@assertrx/dino 1.0.0 → 1.0.1

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,113 @@ 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 API key
325
+ export ANTHROPIC_API_KEY=sk-ant-...
326
+
327
+ # 4. Start the server
328
+ node ~/.pi/agent/npm/node_modules/@assertrx/dino/scripts/api-server.mjs
329
+ ```
330
+
331
+ ### Endpoints
332
+
333
+ | Method | Path | Description |
334
+ |--------|------|-------------|
335
+ | `GET` | `/api/health` | Health check |
336
+ | `GET` | `/api/prompts` | List all available prompts with parameters |
337
+ | `GET` | `/api/prompts/:name` | Get raw markdown content of a prompt |
338
+ | `POST` | `/api/prompts/:name` | **Execute** a prompt via the pi agent |
339
+
340
+ ### Prompt execution endpoints
341
+
342
+ All POST bodies accept `apiKey` (optional, falls back to `ANTHROPIC_API_KEY` env var), `cwd` (workspace directory, defaults to current), and `stream` (default: `true` for SSE).
343
+
344
+ | Prompt name | Required params |
345
+ |-------------|---------------|
346
+ | `setup-playwright` | _none_ |
347
+ | `create-pr` | _none_ |
348
+ | `generate-pom-from-url` | `url` |
349
+ | `generate-pom-from-url-cli` | `url` |
350
+ | `generate-bdd-from-url` | `url` |
351
+ | `generate-spec-from-feature` | `url`, `featureFile`, `pomDir` |
352
+ | `heal-playwright-failures` | _none_ (optional: `testPattern`, `maxIterations`) |
353
+
354
+ ### Example requests
355
+
356
+ ```bash
357
+ # List all prompts
358
+ curl http://localhost:3000/api/prompts
359
+
360
+ # Get prompt details
361
+ curl http://localhost:3000/api/prompts/setup-playwright
362
+
363
+ # Execute a prompt (SSE streaming)
364
+ curl -X POST http://localhost:3000/api/prompts/setup-playwright \
365
+ -H "Content-Type: application/json" \
366
+ -d '{"cwd": "/path/to/workspace"}'
367
+
368
+ # Execute with URL parameter (non-streaming JSON response)
369
+ curl -X POST http://localhost:3000/api/prompts/generate-pom-from-url-cli \
370
+ -H "Content-Type: application/json" \
371
+ -d '{"url": "https://example.com", "outputDir": "tests/pom", "stream": false}'
372
+ ```
373
+
374
+ ### Response formats
375
+
376
+ **SSE streaming** (`stream: true`, default):
377
+ ```
378
+ event: start → {"prompt": "..."}
379
+ event: text → {"delta": "..."} # response tokens
380
+ event: thinking → {"delta": "..."} # thinking output
381
+ event: tool_start → {"tool": "bash"} # tool execution started
382
+ event: tool_end → {"tool": "bash", ...} # tool execution finished
383
+ event: done → {} # session complete
384
+ event: error → {"message": "..."} # error
385
+ ```
386
+
387
+ **Non-streaming** (`stream: false`):
388
+ ```json
389
+ {
390
+ "prompt": "setup-playwright",
391
+ "response": "...full agent response text...",
392
+ "toolCalls": [{"name": "bash", "status": "success"}]
393
+ }
394
+ ```
395
+
396
+ ### Environment variables
397
+
398
+ | Variable | Default | Description |
399
+ |----------|---------|-------------|
400
+ | `PORT` | `3000` | Server port |
401
+ | `HOST` | `0.0.0.0` | Server host |
402
+ | `ANTHROPIC_API_KEY` | — | API key (or pass `apiKey` in request body) |
403
+
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@assertrx/dino",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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,319 @@
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
+ /** Create a single-use pi agent session. */
104
+ async function createSession({ cwd, apiKey }) {
105
+ const modelRuntime = await ModelRuntime.create({
106
+ allowModelNetwork: false,
107
+ });
108
+
109
+ // Set API key if provided (uses env vars otherwise)
110
+ if (apiKey) {
111
+ // Try to set for the first available provider (anthropic preferred)
112
+ const available = await modelRuntime.getAvailable();
113
+ if (available.length > 0) {
114
+ const providerId = available[0].model.provider;
115
+ await modelRuntime.setRuntimeApiKey(providerId, apiKey);
116
+ } else {
117
+ // Blindly try anthropic
118
+ await modelRuntime.setRuntimeApiKey("anthropic", apiKey);
119
+ }
120
+ }
121
+
122
+ const agentDir = getAgentDir();
123
+
124
+ const loader = new DefaultResourceLoader({
125
+ cwd,
126
+ agentDir,
127
+ });
128
+ await loader.reload();
129
+
130
+ const { session } = await createAgentSession({
131
+ cwd,
132
+ agentDir,
133
+ modelRuntime,
134
+ resourceLoader: loader,
135
+ sessionManager: SessionManager.inMemory(),
136
+ });
137
+
138
+ return session;
139
+ }
140
+
141
+ // ── Fastify app ────────────────────────────────────────────────────────────
142
+ const app = Fastify({ logger: { level: "info" } });
143
+
144
+ // Register CORS for browser clients
145
+ await app.register(cors, { origin: true, credentials: true });
146
+
147
+ // Graceful shutdown
148
+ const shutdown = async () => {
149
+ app.log.info("Shutting down...");
150
+ await app.close();
151
+ process.exit(0);
152
+ };
153
+ process.on("SIGINT", shutdown);
154
+ process.on("SIGTERM", shutdown);
155
+
156
+ // Health check
157
+ app.get("/api/health", async () => ({ status: "ok", package: "@assertrx/dino" }));
158
+
159
+ // List all available prompts
160
+ app.get("/api/prompts", async () => {
161
+ const prompts = Object.entries(PROMPT_REGISTRY).map(([name, entry]) => ({
162
+ name,
163
+ description: entry.description,
164
+ params: entry.params,
165
+ endpoint: `/api/prompts/${name}`,
166
+ }));
167
+ return { prompts };
168
+ });
169
+
170
+ // Get prompt details (raw content)
171
+ app.get("/api/prompts/:name", async (request, reply) => {
172
+ const { name } = request.params;
173
+ const entry = PROMPT_REGISTRY[name];
174
+ if (!entry) {
175
+ reply.code(404);
176
+ return { error: `Prompt not found: ${name}` };
177
+ }
178
+
179
+ const content = await readFile(resolve(PROMPTS_DIR, entry.file), "utf-8");
180
+ return {
181
+ name,
182
+ description: entry.description,
183
+ params: entry.params,
184
+ content,
185
+ };
186
+ });
187
+
188
+ // Execute a prompt (SSE streaming)
189
+ app.post("/api/prompts/:name", async (request, reply) => {
190
+ const { name } = request.params;
191
+ const entry = PROMPT_REGISTRY[name];
192
+ if (!entry) {
193
+ reply.code(404);
194
+ return { error: `Prompt not found: ${name}` };
195
+ }
196
+
197
+ const {
198
+ apiKey,
199
+ cwd = process.cwd(),
200
+ stream = true,
201
+ ...params
202
+ } = request.body || {};
203
+
204
+ // Validate required params
205
+ for (const p of entry.params) {
206
+ if (p.required && !params[p.name]) {
207
+ reply.code(400);
208
+ return { error: `Missing required parameter: ${p.name}` };
209
+ }
210
+ }
211
+
212
+ // Build the prompt
213
+ let promptContent;
214
+ try {
215
+ promptContent = await loadPrompt(name, params);
216
+ } catch (err) {
217
+ reply.code(500);
218
+ return { error: `Failed to load prompt: ${err.message}` };
219
+ }
220
+
221
+ // Create session
222
+ let session;
223
+ try {
224
+ session = await createSession({ cwd: resolve(cwd), apiKey });
225
+ } catch (err) {
226
+ reply.code(500);
227
+ return { error: `Failed to create agent session: ${err.message}` };
228
+ }
229
+
230
+ if (!stream) {
231
+ // ── Non-streaming: collect full response ──────────────────────────────
232
+ const messages = [];
233
+ const toolCalls = [];
234
+
235
+ session.subscribe((event) => {
236
+ if (event.type === "message_update") {
237
+ if (event.assistantMessageEvent.type === "text_delta") {
238
+ messages.push(event.assistantMessageEvent.delta);
239
+ }
240
+ } else if (event.type === "tool_execution_start") {
241
+ toolCalls.push({ name: event.toolName, status: "started" });
242
+ } else if (event.type === "tool_execution_end") {
243
+ const last = toolCalls[toolCalls.length - 1];
244
+ if (last) last.status = event.isError ? "error" : "success";
245
+ }
246
+ });
247
+
248
+ await session.prompt(promptContent);
249
+
250
+ return {
251
+ prompt: name,
252
+ response: messages.join(""),
253
+ toolCalls,
254
+ };
255
+ }
256
+
257
+ // ── Streaming SSE ──────────────────────────────────────────────────────
258
+ reply.raw.writeHead(200, {
259
+ "Content-Type": "text/event-stream",
260
+ "Cache-Control": "no-cache",
261
+ Connection: "keep-alive",
262
+ "X-Accel-Buffering": "no",
263
+ });
264
+
265
+ const sendSSE = (event, data) => {
266
+ reply.raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
267
+ };
268
+
269
+ sendSSE("start", { prompt: name });
270
+
271
+ session.subscribe((event) => {
272
+ switch (event.type) {
273
+ case "message_update":
274
+ if (event.assistantMessageEvent.type === "text_delta") {
275
+ sendSSE("text", { delta: event.assistantMessageEvent.delta });
276
+ } else if (event.assistantMessageEvent.type === "thinking_delta") {
277
+ sendSSE("thinking", { delta: event.assistantMessageEvent.delta });
278
+ }
279
+ break;
280
+
281
+ case "tool_execution_start":
282
+ sendSSE("tool_start", { tool: event.toolName });
283
+ break;
284
+
285
+ case "tool_execution_update":
286
+ sendSSE("tool_output", { tool: event.toolName, output: event.output });
287
+ break;
288
+
289
+ case "tool_execution_end":
290
+ sendSSE("tool_end", { tool: event.toolName, error: event.isError });
291
+ break;
292
+
293
+ case "agent_end":
294
+ sendSSE("done", {});
295
+ reply.raw.end();
296
+ break;
297
+ }
298
+ });
299
+
300
+ try {
301
+ await session.prompt(promptContent);
302
+ } catch (err) {
303
+ sendSSE("error", { message: err.message });
304
+ reply.raw.end();
305
+ }
306
+ });
307
+
308
+ // ── Start server ───────────────────────────────────────────────────────────
309
+ const PORT = parseInt(process.env.PORT || "3000", 10);
310
+ const HOST = process.env.HOST || "0.0.0.0";
311
+
312
+ try {
313
+ await app.listen({ port: PORT, host: HOST });
314
+ console.log(`🦕 Dino API server running at http://${HOST}:${PORT}`);
315
+ console.log(` Prompts: http://${HOST}:${PORT}/api/prompts`);
316
+ } catch (err) {
317
+ app.log.error(err);
318
+ process.exit(1);
319
+ }
@@ -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 "$@"