@studio-foundation/api 0.3.0-beta.1 → 0.3.0-beta.5
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/package.json +7 -4
- package/ARCHITECTURE.md +0 -52
- package/src/.gitkeep +0 -0
- package/src/api.ts +0 -8
- package/src/bootstrap.ts +0 -259
- package/src/event-bus.ts +0 -64
- package/src/index.ts +0 -58
- package/src/integration-runtime.ts +0 -180
- package/src/integration-store.ts +0 -125
- package/src/integrations/linear/failure-handler.ts +0 -93
- package/src/integrations/linear/webhook-handler.ts +0 -156
- package/src/integrations/registry.ts +0 -12
- package/src/integrations/types.ts +0 -37
- package/src/launcher.ts +0 -214
- package/src/logger.ts +0 -50
- package/src/plans.ts +0 -43
- package/src/routes/agents.ts +0 -131
- package/src/routes/config.ts +0 -154
- package/src/routes/contracts.ts +0 -205
- package/src/routes/pipelines.ts +0 -146
- package/src/routes/projects.ts +0 -237
- package/src/routes/runs.ts +0 -468
- package/src/routes/skills.ts +0 -136
- package/src/routes/tools.ts +0 -222
- package/src/routes/users.ts +0 -169
- package/src/routes/validate.ts +0 -196
- package/src/routes/webhooks.ts +0 -120
- package/src/server.ts +0 -155
- package/src/spawners/http-api-spawner.ts +0 -96
- package/src/user-store-pg.ts +0 -138
- package/src/user-store.ts +0 -125
- package/src/utils/repo-resolver.ts +0 -3
- package/src/webhook-dispatcher.ts +0 -142
- package/src/webhook-store.ts +0 -141
- package/tests/agents.test.ts +0 -164
- package/tests/cancel.test.ts +0 -120
- package/tests/config.test.ts +0 -196
- package/tests/contracts.test.ts +0 -302
- package/tests/event-bus.test.ts +0 -53
- package/tests/http-api-spawner.test.ts +0 -158
- package/tests/integration-runtime.test.ts +0 -199
- package/tests/integration-store.test.ts +0 -66
- package/tests/integrations/linear/failure-handler.test.ts +0 -113
- package/tests/integrations/linear/webhook-handler.test.ts +0 -191
- package/tests/launcher.test.ts +0 -380
- package/tests/linear-webhook.test.ts +0 -390
- package/tests/pipelines.test.ts +0 -166
- package/tests/projects.test.ts +0 -283
- package/tests/runs.test.ts +0 -379
- package/tests/server.test.ts +0 -208
- package/tests/skills.test.ts +0 -149
- package/tests/sse.test.ts +0 -129
- package/tests/tools.test.ts +0 -233
- package/tests/user-store.test.ts +0 -93
- package/tests/users.test.ts +0 -189
- package/tests/utils/repo-resolver.test.ts +0 -105
- package/tests/validate.test.ts +0 -233
- package/tests/webhook-dispatcher.test.ts +0 -214
- package/tests/webhook-store.test.ts +0 -98
- package/tests/webhooks.test.ts +0 -176
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studio-foundation/api",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.5",
|
|
4
4
|
"description": "HTTP API for Studio v7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/api.js",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"bin": {
|
|
22
22
|
"studio-api": "./dist/index.js"
|
|
23
23
|
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
24
27
|
"keywords": [
|
|
25
28
|
"studio",
|
|
26
29
|
"api"
|
|
@@ -36,9 +39,9 @@
|
|
|
36
39
|
"fastify": "^5.3.2",
|
|
37
40
|
"js-yaml": "^4.1.1",
|
|
38
41
|
"pg": "^8.19.0",
|
|
39
|
-
"@studio-foundation/
|
|
40
|
-
"@studio-foundation/
|
|
41
|
-
"@studio-foundation/
|
|
42
|
+
"@studio-foundation/engine": "0.3.0-beta.5",
|
|
43
|
+
"@studio-foundation/runner": "0.3.0-beta.5",
|
|
44
|
+
"@studio-foundation/contracts": "0.3.0-beta.5"
|
|
42
45
|
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@types/better-sqlite3": "^7.6.8",
|
package/ARCHITECTURE.md
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# @studio-foundation/api
|
|
2
|
-
|
|
3
|
-
Serveur HTTP REST pour Studio. Même engine que le CLI, interface machine-to-machine.
|
|
4
|
-
|
|
5
|
-
## Concept
|
|
6
|
-
|
|
7
|
-
`bootstrap(cwd)` trouve `.studio/`, câble engine + store + launcher + integrations.
|
|
8
|
-
`buildServer()` crée le serveur Fastify avec toutes les routes et le Swagger UI.
|
|
9
|
-
Résultat : Linear webhook → pipeline run → SSE streaming → webhook dispatch.
|
|
10
|
-
|
|
11
|
-
## Règles
|
|
12
|
-
|
|
13
|
-
- Le API est aussi un **composition root** — même pattern que CLI, dépend de `@studio-foundation/engine` et `@studio-foundation/runner`
|
|
14
|
-
- Toute route Fastify **DOIT** avoir un schema Swagger complet (tags, summary, params, response). Sans ça, la route n'apparaît pas dans Swagger UI.
|
|
15
|
-
- Le engine est appelé via `InProcessLauncher` — pas de HTTP round-trip interne sauf pour `HttpApiSpawner`
|
|
16
|
-
- Swagger UI désactivé en production (`NODE_ENV=production`)
|
|
17
|
-
- Auth Bearer optionnelle : si `api.key` dans `config.yaml`, toutes les routes l'exigent
|
|
18
|
-
- DB configurable : `db.type: sqlite | postgres | inmemory` dans `config.yaml`
|
|
19
|
-
|
|
20
|
-
## Fichiers clés
|
|
21
|
-
|
|
22
|
-
- `bootstrap.ts` — `bootstrap(cwd)` : trouve `.studio/`, crée store + engine + launcher + integrations
|
|
23
|
-
- `server.ts` — `buildServer()` : Fastify factory, monte toutes les routes
|
|
24
|
-
- `launcher.ts` — `InProcessLauncher` : lance les pipelines en arrière-plan, publie sur le bus d'événements
|
|
25
|
-
- `event-bus.ts` — `RunEventBus` : bus interne SSE (run events → clients connectés)
|
|
26
|
-
- `routes/runs.ts` — `GET/POST /api/runs`, SSE `/api/runs/:id/stream`, cancel
|
|
27
|
-
- `routes/pipelines.ts`, `agents.ts`, `contracts.ts`, `skills.ts` — CRUD YAML
|
|
28
|
-
- `routes/tools.ts` — `GET /api/tools`
|
|
29
|
-
- `routes/projects.ts` — `GET /api/projects`
|
|
30
|
-
- `routes/config.ts` — `GET/PUT /api/config`
|
|
31
|
-
- `routes/validate.ts` — `POST /api/validate`
|
|
32
|
-
- `routes/webhooks.ts` — enregistrement webhooks
|
|
33
|
-
- `webhook-store.ts`, `webhook-dispatcher.ts` — persistence + dispatch webhooks
|
|
34
|
-
- `integration-store.ts`, `integration-runtime.ts` — lifecycle intégrations
|
|
35
|
-
- `integrations/linear/webhook-handler.ts` — handler Linear (drag → run → comment → move)
|
|
36
|
-
- `spawners/http-api-spawner.ts` — `HttpApiSpawner` : spawner auto-référentiel pour `studio_run`
|
|
37
|
-
|
|
38
|
-
## Séquence bootstrap
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
findStudioDir(cwd)
|
|
42
|
-
→ loadConfig (config.yaml + ${ENV_VAR} substitution)
|
|
43
|
-
→ createRunStore (SQLite | PostgreSQL | InMemory)
|
|
44
|
-
→ createProviderRegistry + ToolRegistry
|
|
45
|
-
→ loadProjectTools (.tool.yaml) + loadPlugins (MCP)
|
|
46
|
-
→ new HttpApiSpawner → EngineConfig → InProcessLauncher
|
|
47
|
-
→ WebhookStore + IntegrationStore + IntegrationRuntime
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Dépendances
|
|
51
|
-
|
|
52
|
-
`@studio-foundation/engine`, `@studio-foundation/runner`, `@studio-foundation/contracts`
|
package/src/.gitkeep
DELETED
|
File without changes
|
package/src/api.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// Named exports for programmatic use (CLI, tests)
|
|
2
|
-
export { bootstrap } from './bootstrap.js';
|
|
3
|
-
export { buildServer } from './server.js';
|
|
4
|
-
export type { ServerDeps, ApiConfig } from './server.js';
|
|
5
|
-
export type { RunLauncher, LaunchConfig } from './launcher.js';
|
|
6
|
-
export { InProcessLauncher, generateRunId } from './launcher.js';
|
|
7
|
-
export { cloneRepo, resolveRepoPath } from './utils/repo-resolver.js';
|
|
8
|
-
export type { RepoResolveOptions } from './utils/repo-resolver.js';
|
package/src/bootstrap.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
// Bootstrap — finds .studio/, loads config, creates engine + store + launcher
|
|
2
|
-
// Same pattern as CLI but without terminal output
|
|
3
|
-
|
|
4
|
-
import { resolve, join, dirname } from 'node:path';
|
|
5
|
-
import { access, readFile } from 'node:fs/promises';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import yaml from 'js-yaml';
|
|
8
|
-
import {
|
|
9
|
-
type EngineConfig,
|
|
10
|
-
SQLiteRunStore,
|
|
11
|
-
InMemoryRunStore,
|
|
12
|
-
PgRunStore,
|
|
13
|
-
type AnyRunStore,
|
|
14
|
-
} from '@studio-foundation/engine';
|
|
15
|
-
import {
|
|
16
|
-
createDefaultRegistry,
|
|
17
|
-
ToolRegistry,
|
|
18
|
-
loadProjectTools,
|
|
19
|
-
loadPlugins,
|
|
20
|
-
loadProjectIntegrations,
|
|
21
|
-
MCPClient,
|
|
22
|
-
type SkillContent,
|
|
23
|
-
} from '@studio-foundation/runner';
|
|
24
|
-
import { InProcessLauncher, type RunLauncher } from './launcher.js';
|
|
25
|
-
import { RunEventBus } from './event-bus.js';
|
|
26
|
-
import type { MaskedConfig } from './server.js';
|
|
27
|
-
import { WebhookStore } from './webhook-store.js';
|
|
28
|
-
import { WebhookDispatcher } from './webhook-dispatcher.js';
|
|
29
|
-
import { IntegrationStore } from './integration-store.js';
|
|
30
|
-
import { IntegrationRuntime } from './integration-runtime.js';
|
|
31
|
-
import { HttpApiSpawner } from './spawners/http-api-spawner.js';
|
|
32
|
-
import { resolvePlans, type PlansConfig } from './plans.js';
|
|
33
|
-
import { UserStore } from './user-store.js';
|
|
34
|
-
import { PgUserStore } from './user-store-pg.js';
|
|
35
|
-
|
|
36
|
-
export interface StudioApiConfig {
|
|
37
|
-
providers?: {
|
|
38
|
-
openai?: { apiKey: string };
|
|
39
|
-
anthropic?: { apiKey: string };
|
|
40
|
-
ollama?: { baseUrl?: string }; // ADD THIS
|
|
41
|
-
};
|
|
42
|
-
paths?: { projects_dir?: string };
|
|
43
|
-
defaults?: { provider?: string; model?: string };
|
|
44
|
-
api?: { key?: string; port?: number };
|
|
45
|
-
integrations?: Record<string, Record<string, unknown>>;
|
|
46
|
-
db?: {
|
|
47
|
-
type?: 'sqlite' | 'postgres' | 'inmemory';
|
|
48
|
-
url?: string;
|
|
49
|
-
};
|
|
50
|
-
plans?: Record<string, {
|
|
51
|
-
runs_per_day?: number;
|
|
52
|
-
max_concurrent?: number;
|
|
53
|
-
max_tokens_per_run?: number;
|
|
54
|
-
rate_limit_per_minute?: number;
|
|
55
|
-
}>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface BootstrapResult {
|
|
59
|
-
store: AnyRunStore;
|
|
60
|
-
launcher: RunLauncher;
|
|
61
|
-
configsDir: string;
|
|
62
|
-
/** Raw projects_dir from config (may contain ~). Used by route handlers for repo cloning. */
|
|
63
|
-
projectsDir?: string;
|
|
64
|
-
projectName: string;
|
|
65
|
-
apiConfig: { key?: string; port?: number };
|
|
66
|
-
cleanup: () => Promise<void>;
|
|
67
|
-
studioVersion: string;
|
|
68
|
-
maskedConfig: MaskedConfig;
|
|
69
|
-
webhookStore: WebhookStore;
|
|
70
|
-
integrationStore: IntegrationStore;
|
|
71
|
-
integrationRuntime: IntegrationRuntime;
|
|
72
|
-
userStore: UserStore | PgUserStore;
|
|
73
|
-
plans: PlansConfig;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function findStudioDir(startDir: string): Promise<string | null> {
|
|
77
|
-
let current = resolve(startDir);
|
|
78
|
-
while (true) {
|
|
79
|
-
const candidate = join(current, '.studio');
|
|
80
|
-
try {
|
|
81
|
-
await access(candidate);
|
|
82
|
-
return candidate;
|
|
83
|
-
} catch {
|
|
84
|
-
// not here, go up
|
|
85
|
-
}
|
|
86
|
-
const parent = dirname(current);
|
|
87
|
-
if (parent === current) return null;
|
|
88
|
-
current = parent;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export async function bootstrap(cwd: string = process.cwd()): Promise<BootstrapResult> {
|
|
93
|
-
const studioDir = await findStudioDir(cwd);
|
|
94
|
-
if (!studioDir) {
|
|
95
|
-
throw new Error(`No .studio/ directory found from ${cwd}. Run 'studio init' first.`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Load config
|
|
99
|
-
let config: StudioApiConfig = {};
|
|
100
|
-
try {
|
|
101
|
-
const raw = await readFile(join(studioDir, 'config.yaml'), 'utf-8');
|
|
102
|
-
// Resolve env vars
|
|
103
|
-
const resolved = raw.replace(/\$\{([^}]+)\}/g, (_m: string, v: string) => process.env[v.trim()] ?? '');
|
|
104
|
-
config = (yaml.load(resolved) as StudioApiConfig) ?? {};
|
|
105
|
-
} catch {
|
|
106
|
-
// No config.yaml — use defaults
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Read studio version from api/package.json
|
|
110
|
-
const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
111
|
-
const pkgRaw = await readFile(pkgPath, 'utf-8');
|
|
112
|
-
const studioVersion = (JSON.parse(pkgRaw) as { version: string }).version;
|
|
113
|
-
|
|
114
|
-
// Build masked config — provider names only, no API keys
|
|
115
|
-
const maskedConfig = {
|
|
116
|
-
defaults: config.defaults,
|
|
117
|
-
providers: Object.keys(config.providers ?? {}),
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const runsDir = join(studioDir, 'runs');
|
|
121
|
-
|
|
122
|
-
const dbType = config.db?.type ?? 'sqlite';
|
|
123
|
-
|
|
124
|
-
// WebhookStore and IntegrationStore are always SQLite — they are separate from RunStore
|
|
125
|
-
const dbPath = join(studioDir, 'runs', 'runs.db');
|
|
126
|
-
|
|
127
|
-
let store: AnyRunStore;
|
|
128
|
-
if (dbType === 'postgres') {
|
|
129
|
-
const url = config.db?.url;
|
|
130
|
-
if (!url) throw new Error('db.url is required when db.type is postgres');
|
|
131
|
-
store = new PgRunStore(url);
|
|
132
|
-
} else if (dbType === 'inmemory') {
|
|
133
|
-
store = new InMemoryRunStore();
|
|
134
|
-
} else {
|
|
135
|
-
store = new SQLiteRunStore(dbPath);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// UserStore — same DB as run store
|
|
139
|
-
let userStore: UserStore | PgUserStore;
|
|
140
|
-
if (dbType === 'postgres') {
|
|
141
|
-
const url = config.db?.url;
|
|
142
|
-
if (!url) throw new Error('db.url is required when db.type is postgres');
|
|
143
|
-
userStore = new PgUserStore(url);
|
|
144
|
-
} else {
|
|
145
|
-
userStore = new UserStore(dbPath);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const plans = resolvePlans(config.plans as Record<string, import('./plans.js').PlanLimits> | undefined);
|
|
149
|
-
|
|
150
|
-
const providerRegistry = createDefaultRegistry({
|
|
151
|
-
openai: config.providers?.openai ? { apiKey: config.providers.openai.apiKey } : undefined,
|
|
152
|
-
anthropic: config.providers?.anthropic ? { apiKey: config.providers.anthropic.apiKey } : undefined,
|
|
153
|
-
ollama: config.providers?.ollama ? { baseUrl: config.providers.ollama.baseUrl } : undefined,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const toolsDir = join(studioDir, 'tools');
|
|
157
|
-
const loadedPlugins = await loadProjectTools(toolsDir, cwd);
|
|
158
|
-
const toolRegistry = new ToolRegistry();
|
|
159
|
-
for (const plugin of loadedPlugins) {
|
|
160
|
-
toolRegistry.registerPlugin(plugin.name, plugin.tools, plugin.promptSnippet);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Load MCP plugins
|
|
164
|
-
const pluginsDir = join(studioDir, 'plugins');
|
|
165
|
-
const pluginManifests = await loadPlugins(pluginsDir);
|
|
166
|
-
const mcpClients: InstanceType<typeof MCPClient>[] = [];
|
|
167
|
-
const pluginSkills: Record<string, string[]> = {};
|
|
168
|
-
|
|
169
|
-
for (const manifest of pluginManifests) {
|
|
170
|
-
for (const [serverName, serverDef] of Object.entries(manifest.mcpServers)) {
|
|
171
|
-
const client = new MCPClient(manifest.name, serverName, serverDef);
|
|
172
|
-
try {
|
|
173
|
-
await client.start();
|
|
174
|
-
const mcpTools = await client.getTools();
|
|
175
|
-
toolRegistry.registerPlugin(`${manifest.name}-${serverName}`, mcpTools);
|
|
176
|
-
mcpClients.push(client);
|
|
177
|
-
} catch {
|
|
178
|
-
// Plugin failed to start — skip silently for API
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (manifest.skills.length > 0) {
|
|
182
|
-
pluginSkills[manifest.name] = manifest.skills.map(
|
|
183
|
-
(s: SkillContent) => `## Skill: ${s.name}\n\n${s.content}`
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Self-referential spawner: allows pipelines to spawn child runs via the API
|
|
189
|
-
// Must match DEFAULT_PORT in index.ts (3700) — was 3000 which collides with common dev servers
|
|
190
|
-
const apiPort = config.api?.port ?? 3700;
|
|
191
|
-
const spawner = new HttpApiSpawner(`http://localhost:${apiPort}`, config.api?.key);
|
|
192
|
-
|
|
193
|
-
const engineConfig: EngineConfig = {
|
|
194
|
-
configsDir: studioDir,
|
|
195
|
-
providerRegistry,
|
|
196
|
-
toolRegistry,
|
|
197
|
-
db: store,
|
|
198
|
-
pluginSkills,
|
|
199
|
-
spawner,
|
|
200
|
-
maxDepth: 3,
|
|
201
|
-
defaultProvider: config.defaults?.provider,
|
|
202
|
-
defaultModel: config.defaults?.model,
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const bus = new RunEventBus();
|
|
206
|
-
const launcher = new InProcessLauncher(engineConfig, store, runsDir, bus);
|
|
207
|
-
|
|
208
|
-
// Derive project name from the directory containing .studio/
|
|
209
|
-
const projectName = studioDir.split('/').slice(-2, -1)[0] ?? 'studio-project';
|
|
210
|
-
|
|
211
|
-
const webhookStore = new WebhookStore(dbPath);
|
|
212
|
-
const integrationStore = new IntegrationStore(dbPath);
|
|
213
|
-
const integrationsDir = join(studioDir, 'integrations');
|
|
214
|
-
const loadedIntegrations = await loadProjectIntegrations(integrationsDir);
|
|
215
|
-
const integrationConfigs = (config.integrations ?? {}) as Record<string, Record<string, unknown>>;
|
|
216
|
-
const integrationRuntime = new IntegrationRuntime({
|
|
217
|
-
integrations: loadedIntegrations,
|
|
218
|
-
store: integrationStore,
|
|
219
|
-
launcher,
|
|
220
|
-
configsDir: studioDir,
|
|
221
|
-
projectsDir: config.paths?.projects_dir,
|
|
222
|
-
apiConfig: config.api ?? {},
|
|
223
|
-
integrationConfigs,
|
|
224
|
-
});
|
|
225
|
-
integrationRuntime.setupEventBus(bus);
|
|
226
|
-
const webhookDispatcher = new WebhookDispatcher(webhookStore, projectName);
|
|
227
|
-
bus.subscribeAll((runId, event) => {
|
|
228
|
-
void webhookDispatcher.handleBusEvent(runId, event.type, event.data);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
store,
|
|
233
|
-
launcher,
|
|
234
|
-
configsDir: studioDir,
|
|
235
|
-
projectsDir: config.paths?.projects_dir,
|
|
236
|
-
projectName,
|
|
237
|
-
apiConfig: config.api ?? {},
|
|
238
|
-
studioVersion,
|
|
239
|
-
maskedConfig,
|
|
240
|
-
webhookStore,
|
|
241
|
-
integrationStore,
|
|
242
|
-
integrationRuntime,
|
|
243
|
-
userStore,
|
|
244
|
-
plans,
|
|
245
|
-
cleanup: async () => {
|
|
246
|
-
for (const client of mcpClients) {
|
|
247
|
-
try { await client.close(); } catch { /* ignore */ }
|
|
248
|
-
}
|
|
249
|
-
if ('close' in store && typeof (store as AnyRunStore & { close?: () => unknown }).close === 'function') {
|
|
250
|
-
await (store as AnyRunStore & { close(): Promise<void> | void }).close();
|
|
251
|
-
}
|
|
252
|
-
if (userStore && 'close' in userStore && typeof userStore.close === 'function') {
|
|
253
|
-
await userStore.close();
|
|
254
|
-
}
|
|
255
|
-
webhookStore.close();
|
|
256
|
-
integrationStore.close();
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
}
|
package/src/event-bus.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
export type SseEventType =
|
|
2
|
-
| 'pipeline_start'
|
|
3
|
-
| 'stage_start'
|
|
4
|
-
| 'stage_complete'
|
|
5
|
-
| 'stage_retry'
|
|
6
|
-
| 'group_start'
|
|
7
|
-
| 'group_iteration'
|
|
8
|
-
| 'group_feedback'
|
|
9
|
-
| 'group_complete'
|
|
10
|
-
| 'pipeline_complete'
|
|
11
|
-
| 'pipeline_cancelled'
|
|
12
|
-
| 'stage_context'
|
|
13
|
-
| 'tool_call_start'
|
|
14
|
-
| 'tool_call_complete'
|
|
15
|
-
| 'done';
|
|
16
|
-
|
|
17
|
-
export interface BusEvent {
|
|
18
|
-
type: SseEventType;
|
|
19
|
-
data: unknown;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type BusListener = (event: BusEvent) => void;
|
|
23
|
-
export type GlobalBusListener = (runId: string, event: BusEvent) => void;
|
|
24
|
-
|
|
25
|
-
export class RunEventBus {
|
|
26
|
-
private subs = new Map<string, Set<BusListener>>();
|
|
27
|
-
private globalListeners = new Set<GlobalBusListener>();
|
|
28
|
-
|
|
29
|
-
subscribe(runId: string, listener: BusListener): () => void {
|
|
30
|
-
if (!this.subs.has(runId)) {
|
|
31
|
-
this.subs.set(runId, new Set());
|
|
32
|
-
}
|
|
33
|
-
this.subs.get(runId)!.add(listener);
|
|
34
|
-
return () => {
|
|
35
|
-
this.subs.get(runId)?.delete(listener);
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Global subscription — receives all events across all runs
|
|
40
|
-
subscribeAll(listener: GlobalBusListener): () => void {
|
|
41
|
-
this.globalListeners.add(listener);
|
|
42
|
-
return () => {
|
|
43
|
-
this.globalListeners.delete(listener);
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
emit(runId: string, type: SseEventType, data: unknown): void {
|
|
48
|
-
const event: BusEvent = { type, data };
|
|
49
|
-
const listeners = this.subs.get(runId);
|
|
50
|
-
if (listeners) {
|
|
51
|
-
for (const listener of listeners) {
|
|
52
|
-
listener(event);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
for (const listener of this.globalListeners) {
|
|
56
|
-
listener(runId, event);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
close(runId: string): void {
|
|
61
|
-
this.emit(runId, 'done', {});
|
|
62
|
-
this.subs.delete(runId);
|
|
63
|
-
}
|
|
64
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Standalone entrypoint for @studio/api
|
|
3
|
-
// PM2: pm2 start node_modules/.bin/studio-api --env STUDIO_CWD=/path/to/project
|
|
4
|
-
// systemd: ExecStart=/usr/bin/node /path/to/api/dist/index.js
|
|
5
|
-
|
|
6
|
-
import { bootstrap } from './bootstrap.js';
|
|
7
|
-
import { buildServer } from './server.js';
|
|
8
|
-
|
|
9
|
-
const DEFAULT_PORT = 3700;
|
|
10
|
-
|
|
11
|
-
async function main() {
|
|
12
|
-
const cwd = process.env['STUDIO_CWD'] ?? process.cwd();
|
|
13
|
-
|
|
14
|
-
let result: Awaited<ReturnType<typeof bootstrap>>;
|
|
15
|
-
try {
|
|
16
|
-
result = await bootstrap(cwd);
|
|
17
|
-
} catch (err) {
|
|
18
|
-
console.error('Bootstrap failed:', err instanceof Error ? err.message : String(err));
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const { store, launcher, configsDir, projectName, apiConfig, cleanup, studioVersion, maskedConfig, webhookStore, integrationStore, integrationRuntime, userStore, plans } = result;
|
|
23
|
-
const port = apiConfig.port ?? DEFAULT_PORT;
|
|
24
|
-
|
|
25
|
-
let hasUsers = false;
|
|
26
|
-
try {
|
|
27
|
-
// listUsers() is sync on UserStore and async on PgUserStore; await handles both
|
|
28
|
-
const users = await userStore.listUsers();
|
|
29
|
-
hasUsers = users.length > 0;
|
|
30
|
-
} catch {
|
|
31
|
-
// DB unavailable at startup — default to open/dev mode
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const server = buildServer({ store, launcher, configsDir, projectName, apiConfig, studioVersion, maskedConfig, webhookStore, integrationStore, integrationRuntime, userStore, plans, hasUsers });
|
|
35
|
-
|
|
36
|
-
// Graceful shutdown
|
|
37
|
-
const shutdown = async () => {
|
|
38
|
-
await server.close();
|
|
39
|
-
await cleanup();
|
|
40
|
-
process.exit(0);
|
|
41
|
-
};
|
|
42
|
-
process.on('SIGTERM', () => void shutdown());
|
|
43
|
-
process.on('SIGINT', () => void shutdown());
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
await server.listen({ port, host: '0.0.0.0' });
|
|
47
|
-
console.log(`Studio API listening on port ${port}`);
|
|
48
|
-
if (!apiConfig.key) {
|
|
49
|
-
console.log('Warning: no api.key configured — running without auth');
|
|
50
|
-
}
|
|
51
|
-
} catch (err) {
|
|
52
|
-
console.error('Failed to start server:', err);
|
|
53
|
-
await cleanup();
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
void main();
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
// api/src/integration-runtime.ts
|
|
2
|
-
import type { FastifyInstance } from 'fastify';
|
|
3
|
-
import type { IntegrationPluginDef } from '@studio-foundation/contracts';
|
|
4
|
-
import type { IntegrationStore } from './integration-store.js';
|
|
5
|
-
import type { RunLauncher } from './launcher.js';
|
|
6
|
-
import type { RunEventBus } from './event-bus.js';
|
|
7
|
-
import type { ApiConfig } from './server.js';
|
|
8
|
-
import { WEBHOOK_HANDLERS, FAILURE_HANDLERS } from './integrations/registry.js';
|
|
9
|
-
import type { FailureHandlerContext } from './integrations/types.js';
|
|
10
|
-
import type { GroupFeedbackEvent } from '@studio-foundation/engine';
|
|
11
|
-
|
|
12
|
-
export interface IntegrationRuntimeDeps {
|
|
13
|
-
integrations: IntegrationPluginDef[];
|
|
14
|
-
store: IntegrationStore;
|
|
15
|
-
launcher: RunLauncher;
|
|
16
|
-
configsDir: string;
|
|
17
|
-
projectsDir?: string;
|
|
18
|
-
apiConfig: ApiConfig;
|
|
19
|
-
integrationConfigs: Record<string, Record<string, unknown>>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class IntegrationRuntime {
|
|
23
|
-
constructor(private deps: IntegrationRuntimeDeps) {}
|
|
24
|
-
|
|
25
|
-
setupEventBus(bus: RunEventBus): void {
|
|
26
|
-
bus.subscribeAll((runId, event) => {
|
|
27
|
-
if (event.type !== 'pipeline_complete') return;
|
|
28
|
-
|
|
29
|
-
const data = event.data as {
|
|
30
|
-
status: string;
|
|
31
|
-
duration_ms: number;
|
|
32
|
-
meta?: Record<string, unknown>;
|
|
33
|
-
last_group_feedback?: GroupFeedbackEvent;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
if (data.status === 'success') return;
|
|
37
|
-
|
|
38
|
-
for (const integration of this.deps.integrations) {
|
|
39
|
-
if (!integration.on_failure?.handler) continue;
|
|
40
|
-
const handler = FAILURE_HANDLERS[integration.on_failure.handler];
|
|
41
|
-
if (!handler) continue;
|
|
42
|
-
|
|
43
|
-
const ctx: FailureHandlerContext = {
|
|
44
|
-
runId,
|
|
45
|
-
durationMs: data.duration_ms,
|
|
46
|
-
status: data.status,
|
|
47
|
-
meta: data.meta ?? {},
|
|
48
|
-
lastGroupFeedback: data.last_group_feedback,
|
|
49
|
-
integration,
|
|
50
|
-
integrationConfig: this.deps.integrationConfigs[integration.name] ?? {},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
void handler.handleFailure(ctx);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
registerRoutes(fastify: FastifyInstance, prefix: string): void {
|
|
59
|
-
for (const integration of this.deps.integrations) {
|
|
60
|
-
if (!integration.webhook?.handler) continue;
|
|
61
|
-
const webhookHandler = WEBHOOK_HANDLERS[integration.webhook.handler];
|
|
62
|
-
if (!webhookHandler) continue;
|
|
63
|
-
|
|
64
|
-
const name = integration.name;
|
|
65
|
-
const { store, launcher, configsDir, projectsDir, apiConfig, integrationConfigs } = this.deps;
|
|
66
|
-
const integrationConfig = integrationConfigs[name] ?? {};
|
|
67
|
-
const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
|
|
68
|
-
|
|
69
|
-
void fastify.register(async (scope) => {
|
|
70
|
-
scope.addContentTypeParser(
|
|
71
|
-
'application/json',
|
|
72
|
-
{ parseAs: 'buffer' },
|
|
73
|
-
(_req, body, done) => done(null, body),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
// GET /api/integrations/{name}
|
|
77
|
-
scope.get(`${prefix}/integrations/${name}`, {
|
|
78
|
-
schema: {
|
|
79
|
-
tags: ['integrations'],
|
|
80
|
-
summary: `Get ${name} integration config and trigger log`,
|
|
81
|
-
response: {
|
|
82
|
-
200: {
|
|
83
|
-
type: 'object',
|
|
84
|
-
properties: {
|
|
85
|
-
webhook_url: { type: 'string' },
|
|
86
|
-
pipeline: { type: ['string', 'null'] },
|
|
87
|
-
active: { type: 'boolean' },
|
|
88
|
-
triggers: { type: 'array' },
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
}, async (request, reply) => {
|
|
94
|
-
const config = store.getConfig(name);
|
|
95
|
-
const triggers = store.listTriggers(name, 50);
|
|
96
|
-
const baseUrl = process.env['STUDIO_BASE_URL'] ?? `${request.protocol}://${request.hostname}`;
|
|
97
|
-
return reply.status(200).send({
|
|
98
|
-
webhook_url: `${baseUrl}/api/integrations/${name}/webhook`,
|
|
99
|
-
pipeline: config.pipeline ?? null,
|
|
100
|
-
active: config.active,
|
|
101
|
-
triggers,
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// PATCH /api/integrations/{name}
|
|
106
|
-
scope.patch(`${prefix}/integrations/${name}`, {
|
|
107
|
-
schema: {
|
|
108
|
-
tags: ['integrations'],
|
|
109
|
-
summary: `Update ${name} integration config`,
|
|
110
|
-
body: {
|
|
111
|
-
type: 'object',
|
|
112
|
-
properties: {
|
|
113
|
-
pipeline: { type: 'string' },
|
|
114
|
-
active: { type: 'boolean' },
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
response: {
|
|
118
|
-
200: {
|
|
119
|
-
type: 'object',
|
|
120
|
-
properties: {
|
|
121
|
-
webhook_url: { type: 'string' },
|
|
122
|
-
pipeline: { type: ['string', 'null'] },
|
|
123
|
-
active: { type: 'boolean' },
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
400: errorSchema,
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
}, async (request, reply) => {
|
|
130
|
-
let data: { pipeline?: string; active?: boolean };
|
|
131
|
-
try {
|
|
132
|
-
data = JSON.parse((request.body as Buffer).toString('utf-8')) as typeof data;
|
|
133
|
-
} catch {
|
|
134
|
-
return reply.status(400).send({ error: 'Invalid JSON' });
|
|
135
|
-
}
|
|
136
|
-
store.patchConfig(name, data);
|
|
137
|
-
const updated = store.getConfig(name);
|
|
138
|
-
const baseUrl = process.env['STUDIO_BASE_URL'] ?? `${request.protocol}://${request.hostname}`;
|
|
139
|
-
return reply.status(200).send({
|
|
140
|
-
webhook_url: `${baseUrl}/api/integrations/${name}/webhook`,
|
|
141
|
-
pipeline: updated.pipeline ?? null,
|
|
142
|
-
active: updated.active,
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// POST /api/integrations/{name}/webhook
|
|
147
|
-
scope.post(`${prefix}/integrations/${name}/webhook`, {
|
|
148
|
-
schema: {
|
|
149
|
-
tags: ['integrations'],
|
|
150
|
-
summary: `Receive ${name} webhook event`,
|
|
151
|
-
response: {
|
|
152
|
-
202: {
|
|
153
|
-
type: 'object',
|
|
154
|
-
properties: { run_id: { type: 'string' }, stream_url: { type: 'string' } },
|
|
155
|
-
},
|
|
156
|
-
200: {
|
|
157
|
-
type: 'object',
|
|
158
|
-
properties: { ignored: { type: 'boolean' }, reason: { type: 'string' } },
|
|
159
|
-
},
|
|
160
|
-
400: errorSchema,
|
|
161
|
-
401: errorSchema,
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
}, async (request, reply) => {
|
|
165
|
-
return webhookHandler.handle({
|
|
166
|
-
rawBody: request.body as Buffer,
|
|
167
|
-
headers: request.headers,
|
|
168
|
-
integration,
|
|
169
|
-
store,
|
|
170
|
-
launcher,
|
|
171
|
-
configsDir,
|
|
172
|
-
projectsDir,
|
|
173
|
-
apiConfig,
|
|
174
|
-
integrationConfig,
|
|
175
|
-
}, reply);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|