@studio-foundation/api 0.3.0-beta.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/ARCHITECTURE.md +52 -0
- package/LICENSE +663 -0
- package/README.md +161 -0
- package/dist/api.d.ts +8 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +6 -0
- package/dist/api.js.map +1 -0
- package/dist/bootstrap.d.ts +66 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +193 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/event-bus.d.ts +16 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +37 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/integration-runtime.d.ts +22 -0
- package/dist/integration-runtime.d.ts.map +1 -0
- package/dist/integration-runtime.js +150 -0
- package/dist/integration-runtime.js.map +1 -0
- package/dist/integration-store.d.ts +26 -0
- package/dist/integration-store.d.ts.map +1 -0
- package/dist/integration-store.js +80 -0
- package/dist/integration-store.js.map +1 -0
- package/dist/integrations/linear/failure-handler.d.ts +5 -0
- package/dist/integrations/linear/failure-handler.d.ts.map +1 -0
- package/dist/integrations/linear/failure-handler.js +73 -0
- package/dist/integrations/linear/failure-handler.js.map +1 -0
- package/dist/integrations/linear/webhook-handler.d.ts +6 -0
- package/dist/integrations/linear/webhook-handler.d.ts.map +1 -0
- package/dist/integrations/linear/webhook-handler.js +131 -0
- package/dist/integrations/linear/webhook-handler.js.map +1 -0
- package/dist/integrations/registry.d.ts +4 -0
- package/dist/integrations/registry.d.ts.map +1 -0
- package/dist/integrations/registry.js +9 -0
- package/dist/integrations/registry.js.map +1 -0
- package/dist/integrations/types.d.ts +33 -0
- package/dist/integrations/types.d.ts.map +1 -0
- package/dist/integrations/types.js +2 -0
- package/dist/integrations/types.js.map +1 -0
- package/dist/launcher.d.ts +45 -0
- package/dist/launcher.d.ts.map +1 -0
- package/dist/launcher.js +149 -0
- package/dist/launcher.js.map +1 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +37 -0
- package/dist/logger.js.map +1 -0
- package/dist/plans.d.ts +13 -0
- package/dist/plans.d.ts.map +1 -0
- package/dist/plans.js +33 -0
- package/dist/plans.js.map +1 -0
- package/dist/routes/agents.d.ts +6 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +118 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/config.d.ts +6 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +129 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/contracts.d.ts +6 -0
- package/dist/routes/contracts.d.ts.map +1 -0
- package/dist/routes/contracts.js +188 -0
- package/dist/routes/contracts.js.map +1 -0
- package/dist/routes/pipelines.d.ts +6 -0
- package/dist/routes/pipelines.d.ts.map +1 -0
- package/dist/routes/pipelines.js +133 -0
- package/dist/routes/pipelines.js.map +1 -0
- package/dist/routes/projects.d.ts +6 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +220 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/runs.d.ts +6 -0
- package/dist/routes/runs.d.ts.map +1 -0
- package/dist/routes/runs.js +427 -0
- package/dist/routes/runs.js.map +1 -0
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +123 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/tools.d.ts +6 -0
- package/dist/routes/tools.d.ts.map +1 -0
- package/dist/routes/tools.js +193 -0
- package/dist/routes/tools.js.map +1 -0
- package/dist/routes/users.d.ts +6 -0
- package/dist/routes/users.d.ts.map +1 -0
- package/dist/routes/users.js +152 -0
- package/dist/routes/users.js.map +1 -0
- package/dist/routes/validate.d.ts +6 -0
- package/dist/routes/validate.d.ts.map +1 -0
- package/dist/routes/validate.js +171 -0
- package/dist/routes/validate.js.map +1 -0
- package/dist/routes/webhooks.d.ts +8 -0
- package/dist/routes/webhooks.d.ts.map +1 -0
- package/dist/routes/webhooks.js +102 -0
- package/dist/routes/webhooks.js.map +1 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +101 -0
- package/dist/server.js.map +1 -0
- package/dist/spawners/http-api-spawner.d.ts +10 -0
- package/dist/spawners/http-api-spawner.d.ts.map +1 -0
- package/dist/spawners/http-api-spawner.js +88 -0
- package/dist/spawners/http-api-spawner.js.map +1 -0
- package/dist/user-store-pg.d.ts +21 -0
- package/dist/user-store-pg.d.ts.map +1 -0
- package/dist/user-store-pg.js +97 -0
- package/dist/user-store-pg.js.map +1 -0
- package/dist/user-store.d.ts +29 -0
- package/dist/user-store.d.ts.map +1 -0
- package/dist/user-store.js +97 -0
- package/dist/user-store.js.map +1 -0
- package/dist/utils/repo-resolver.d.ts +3 -0
- package/dist/utils/repo-resolver.d.ts.map +1 -0
- package/dist/utils/repo-resolver.js +3 -0
- package/dist/utils/repo-resolver.js.map +1 -0
- package/dist/webhook-dispatcher.d.ts +13 -0
- package/dist/webhook-dispatcher.d.ts.map +1 -0
- package/dist/webhook-dispatcher.js +108 -0
- package/dist/webhook-dispatcher.js.map +1 -0
- package/dist/webhook-store.d.ts +31 -0
- package/dist/webhook-store.d.ts.map +1 -0
- package/dist/webhook-store.js +91 -0
- package/dist/webhook-store.js.map +1 -0
- package/package.json +59 -0
- package/src/.gitkeep +0 -0
- package/src/api.ts +8 -0
- package/src/bootstrap.ts +259 -0
- package/src/event-bus.ts +64 -0
- package/src/index.ts +58 -0
- package/src/integration-runtime.ts +180 -0
- package/src/integration-store.ts +125 -0
- package/src/integrations/linear/failure-handler.ts +93 -0
- package/src/integrations/linear/webhook-handler.ts +156 -0
- package/src/integrations/registry.ts +12 -0
- package/src/integrations/types.ts +37 -0
- package/src/launcher.ts +214 -0
- package/src/logger.ts +50 -0
- package/src/plans.ts +43 -0
- package/src/routes/agents.ts +131 -0
- package/src/routes/config.ts +154 -0
- package/src/routes/contracts.ts +205 -0
- package/src/routes/pipelines.ts +146 -0
- package/src/routes/projects.ts +237 -0
- package/src/routes/runs.ts +468 -0
- package/src/routes/skills.ts +136 -0
- package/src/routes/tools.ts +222 -0
- package/src/routes/users.ts +169 -0
- package/src/routes/validate.ts +196 -0
- package/src/routes/webhooks.ts +120 -0
- package/src/server.ts +155 -0
- package/src/spawners/http-api-spawner.ts +96 -0
- package/src/user-store-pg.ts +138 -0
- package/src/user-store.ts +125 -0
- package/src/utils/repo-resolver.ts +3 -0
- package/src/webhook-dispatcher.ts +142 -0
- package/src/webhook-store.ts +141 -0
- package/tests/agents.test.ts +164 -0
- package/tests/cancel.test.ts +120 -0
- package/tests/config.test.ts +196 -0
- package/tests/contracts.test.ts +302 -0
- package/tests/event-bus.test.ts +53 -0
- package/tests/http-api-spawner.test.ts +158 -0
- package/tests/integration-runtime.test.ts +199 -0
- package/tests/integration-store.test.ts +66 -0
- package/tests/integrations/linear/failure-handler.test.ts +113 -0
- package/tests/integrations/linear/webhook-handler.test.ts +191 -0
- package/tests/launcher.test.ts +380 -0
- package/tests/linear-webhook.test.ts +390 -0
- package/tests/pipelines.test.ts +166 -0
- package/tests/projects.test.ts +283 -0
- package/tests/runs.test.ts +379 -0
- package/tests/server.test.ts +208 -0
- package/tests/skills.test.ts +149 -0
- package/tests/sse.test.ts +129 -0
- package/tests/tools.test.ts +233 -0
- package/tests/user-store.test.ts +93 -0
- package/tests/users.test.ts +189 -0
- package/tests/utils/repo-resolver.test.ts +105 -0
- package/tests/validate.test.ts +233 -0
- package/tests/webhook-dispatcher.test.ts +214 -0
- package/tests/webhook-store.test.ts +98 -0
- package/tests/webhooks.test.ts +176 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-store.js","sourceRoot":"","sources":["../src/user-store.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,2DAA2D;AAC3D,4DAA4D;AAC5D,gEAAgE;AAEhE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiB5C,MAAM,OAAO,SAAS;IACZ,EAAE,CAAoC;IAE9C,YAAY,MAAc;QACxB,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,MAAM,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAA4D,CAAC;QACvG,IAAI,CAAC,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACrC,IAAI,CAAC,UAAU,EAAE,CAAC;IACpB,CAAC;IAEO,UAAU;QAChB,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;KAgBZ,CAAC,CAAC;IACL,CAAC;IAED,eAAe,CAAC,MAAc;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAAC,0EAA0E,CAAC;aACnF,GAAG,CAAC,MAAM,CAAqB,CAAC;QACnC,OAAO,GAAG,IAAI,IAAI,CAAC;IACrB,CAAC;IAED,WAAW,CAAC,EAAU;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAAC,qEAAqE,CAAC;aAC9E,GAAG,CAAC,EAAE,CAAqB,CAAC;QAC/B,OAAO,GAAG,IAAI,IAAI,CAAC;IACrB,CAAC;IAED,cAAc,CAAC,KAAa;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAAC,wEAAwE,CAAC;aACjF,GAAG,CAAC,KAAK,CAAqB,CAAC;QAClC,OAAO,GAAG,IAAI,IAAI,CAAC;IACrB,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,EAAE;aACX,OAAO,CAAC,gFAAgF,CAAC;aACzF,GAAG,EAAY,CAAC;IACrB,CAAC;IAED,QAAQ,CAAC,IAAU;QACjB,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;;;;;KAQf,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IACxE,CAAC;IAED,UAAU,CAAC,EAAU;QACnB,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,aAAa,CAAC,MAAc,EAAE,IAAY;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAAC,yFAAyF,CAAC;aAClG,GAAG,CAAC,MAAM,EAAE,IAAI,CAA2B,CAAC;QAC/C,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IACzE,CAAC;IAED,aAAa,CAAC,MAAc,EAAE,IAAY;QACxC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;;KAKf,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACvB,CAAC;IAED,eAAe,CAAC,MAAc,EAAE,IAAY,EAAE,MAAc;QAC1D,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;;KAKf,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-resolver.d.ts","sourceRoot":"","sources":["../../src/utils/repo-resolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACvE,YAAY,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-resolver.js","sourceRoot":"","sources":["../../src/utils/repo-resolver.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SseEventType } from './event-bus.js';
|
|
2
|
+
import type { WebhookStore } from './webhook-store.js';
|
|
3
|
+
export type WebhookEventType = 'pipeline_start' | 'pipeline_complete' | 'stage_complete' | 'stage_rejected' | 'stage_failed' | 'group_feedback';
|
|
4
|
+
export declare class WebhookDispatcher {
|
|
5
|
+
private readonly store;
|
|
6
|
+
private readonly projectName;
|
|
7
|
+
private readonly fetcher;
|
|
8
|
+
constructor(store: WebhookStore, projectName: string, fetcher?: typeof fetch);
|
|
9
|
+
handleBusEvent(runId: string, sseType: SseEventType, data: unknown): Promise<void>;
|
|
10
|
+
private dispatch;
|
|
11
|
+
private handleFailure;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=webhook-dispatcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-dispatcher.d.ts","sourceRoot":"","sources":["../src/webhook-dispatcher.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,gBAAgB,GACxB,gBAAgB,GAChB,mBAAmB,GACnB,gBAAgB,GAChB,gBAAgB,GAChB,cAAc,GACd,gBAAgB,CAAC;AA8BrB,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAFP,KAAK,EAAE,YAAY,EACnB,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,OAAO,KAAa;IAG1C,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;YAa1E,QAAQ;IAwDtB,OAAO,CAAC,aAAa;CAsBtB"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// WebhookDispatcher — listens to pipeline events and delivers them to registered webhooks
|
|
2
|
+
// Handles HMAC-SHA256 signing, retry logic (30s / 5min / 15min), and failure tracking
|
|
3
|
+
import { createHmac, randomUUID } from 'node:crypto';
|
|
4
|
+
// Retry delays in ms: attempt 1 failed → 30s, attempt 2 failed → 5min, attempt 3 failed → 15min
|
|
5
|
+
const RETRY_DELAYS = [30_000, 5 * 60_000, 15 * 60_000];
|
|
6
|
+
function mapToWebhookEvent(sseType, data) {
|
|
7
|
+
switch (sseType) {
|
|
8
|
+
case 'pipeline_start':
|
|
9
|
+
return 'pipeline_start';
|
|
10
|
+
case 'pipeline_complete':
|
|
11
|
+
return 'pipeline_complete';
|
|
12
|
+
case 'stage_complete': {
|
|
13
|
+
const status = data.status;
|
|
14
|
+
if (status === 'rejected')
|
|
15
|
+
return 'stage_rejected';
|
|
16
|
+
if (status === 'failed')
|
|
17
|
+
return 'stage_failed';
|
|
18
|
+
return 'stage_complete';
|
|
19
|
+
}
|
|
20
|
+
case 'group_feedback':
|
|
21
|
+
return 'group_feedback';
|
|
22
|
+
default:
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function sign(body, secret) {
|
|
27
|
+
const hmac = createHmac('sha256', secret);
|
|
28
|
+
hmac.update(body, 'utf-8');
|
|
29
|
+
return `sha256=${hmac.digest('hex')}`;
|
|
30
|
+
}
|
|
31
|
+
export class WebhookDispatcher {
|
|
32
|
+
store;
|
|
33
|
+
projectName;
|
|
34
|
+
fetcher;
|
|
35
|
+
constructor(store, projectName, fetcher = fetch) {
|
|
36
|
+
this.store = store;
|
|
37
|
+
this.projectName = projectName;
|
|
38
|
+
this.fetcher = fetcher;
|
|
39
|
+
}
|
|
40
|
+
async handleBusEvent(runId, sseType, data) {
|
|
41
|
+
const webhookEvent = mapToWebhookEvent(sseType, data);
|
|
42
|
+
if (!webhookEvent)
|
|
43
|
+
return;
|
|
44
|
+
const webhooks = this.store.listWebhooks().filter(w => w.status === 'active' && w.events.includes(webhookEvent));
|
|
45
|
+
await Promise.all(webhooks.map(webhook => this.dispatch(webhook.id, webhookEvent, runId, data, 1)));
|
|
46
|
+
}
|
|
47
|
+
async dispatch(webhookId, event, runId, sourceData, attempt) {
|
|
48
|
+
const webhook = this.store.getWebhook(webhookId);
|
|
49
|
+
if (!webhook || webhook.status === 'failed')
|
|
50
|
+
return;
|
|
51
|
+
const payload = {
|
|
52
|
+
event,
|
|
53
|
+
run_id: runId,
|
|
54
|
+
project: this.projectName,
|
|
55
|
+
ts: new Date().toISOString(),
|
|
56
|
+
...sourceData,
|
|
57
|
+
};
|
|
58
|
+
const body = JSON.stringify(payload);
|
|
59
|
+
const headers = {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
'X-Studio-Event': event,
|
|
62
|
+
};
|
|
63
|
+
if (webhook.secret) {
|
|
64
|
+
headers['X-Studio-Signature'] = sign(body, webhook.secret);
|
|
65
|
+
}
|
|
66
|
+
const deliveryId = randomUUID();
|
|
67
|
+
this.store.saveDelivery({
|
|
68
|
+
id: deliveryId,
|
|
69
|
+
webhook_id: webhookId,
|
|
70
|
+
event,
|
|
71
|
+
run_id: runId,
|
|
72
|
+
status: 'pending',
|
|
73
|
+
attempt,
|
|
74
|
+
created_at: new Date().toISOString(),
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
const response = await this.fetcher(webhook.url, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers,
|
|
80
|
+
body,
|
|
81
|
+
signal: AbortSignal.timeout(10_000),
|
|
82
|
+
});
|
|
83
|
+
if (response.ok) {
|
|
84
|
+
this.store.updateDelivery(deliveryId, 'success');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.handleFailure(webhookId, event, runId, sourceData, attempt, deliveryId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
this.handleFailure(webhookId, event, runId, sourceData, attempt, deliveryId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
handleFailure(webhookId, event, runId, sourceData, attempt, deliveryId) {
|
|
95
|
+
const maxAttempts = RETRY_DELAYS.length + 1; // 4 total (1 original + 3 retries)
|
|
96
|
+
if (attempt >= maxAttempts) {
|
|
97
|
+
this.store.updateDelivery(deliveryId, 'failed');
|
|
98
|
+
this.store.markWebhookFailed(webhookId);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.store.updateDelivery(deliveryId, 'retrying');
|
|
102
|
+
const delay = RETRY_DELAYS[attempt - 1];
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
void this.dispatch(webhookId, event, runId, sourceData, attempt + 1);
|
|
105
|
+
}, delay);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=webhook-dispatcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-dispatcher.js","sourceRoot":"","sources":["../src/webhook-dispatcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface WebhookRegistration {
|
|
2
|
+
id: string;
|
|
3
|
+
url: string;
|
|
4
|
+
events: string[];
|
|
5
|
+
secret?: string;
|
|
6
|
+
status: 'active' | 'failed';
|
|
7
|
+
created_at: string;
|
|
8
|
+
}
|
|
9
|
+
export interface WebhookDelivery {
|
|
10
|
+
id: string;
|
|
11
|
+
webhook_id: string;
|
|
12
|
+
event: string;
|
|
13
|
+
run_id: string;
|
|
14
|
+
status: 'pending' | 'retrying' | 'success' | 'failed';
|
|
15
|
+
attempt: number;
|
|
16
|
+
created_at: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class WebhookStore {
|
|
19
|
+
private db;
|
|
20
|
+
constructor(dbPath: string);
|
|
21
|
+
private initSchema;
|
|
22
|
+
saveWebhook(webhook: WebhookRegistration): void;
|
|
23
|
+
getWebhook(id: string): WebhookRegistration | null;
|
|
24
|
+
listWebhooks(): WebhookRegistration[];
|
|
25
|
+
deleteWebhook(id: string): boolean;
|
|
26
|
+
markWebhookFailed(id: string): void;
|
|
27
|
+
saveDelivery(delivery: WebhookDelivery): void;
|
|
28
|
+
updateDelivery(id: string, status: WebhookDelivery['status']): void;
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=webhook-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-store.d.ts","sourceRoot":"","sources":["../src/webhook-store.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,EAAE,CAAoC;gBAElC,MAAM,EAAE,MAAM;IAQ1B,OAAO,CAAC,UAAU;IAwBlB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI;IAmB/C,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAgBlD,YAAY,IAAI,mBAAmB,EAAE;IAerC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAKlC,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAInC,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAe7C,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,GAAG,IAAI;IAInE,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// WebhookStore — SQLite persistence for webhook registrations and deliveries
|
|
2
|
+
// Uses the same DB file as the run store (.studio/runs/runs.db)
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
export class WebhookStore {
|
|
5
|
+
db;
|
|
6
|
+
constructor(dbPath) {
|
|
7
|
+
const _require = createRequire(import.meta.url);
|
|
8
|
+
const Database = _require('better-sqlite3');
|
|
9
|
+
this.db = new Database(dbPath);
|
|
10
|
+
this.db.pragma('journal_mode = WAL');
|
|
11
|
+
this.initSchema();
|
|
12
|
+
}
|
|
13
|
+
initSchema() {
|
|
14
|
+
this.db.exec(`
|
|
15
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
url TEXT NOT NULL,
|
|
18
|
+
events TEXT NOT NULL,
|
|
19
|
+
secret TEXT,
|
|
20
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
21
|
+
created_at TEXT NOT NULL
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE TABLE IF NOT EXISTS webhook_deliveries (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
webhook_id TEXT NOT NULL,
|
|
27
|
+
event TEXT NOT NULL,
|
|
28
|
+
run_id TEXT NOT NULL,
|
|
29
|
+
status TEXT NOT NULL,
|
|
30
|
+
attempt INTEGER NOT NULL,
|
|
31
|
+
created_at TEXT NOT NULL,
|
|
32
|
+
FOREIGN KEY (webhook_id) REFERENCES webhooks(id)
|
|
33
|
+
);
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
saveWebhook(webhook) {
|
|
37
|
+
this.db.prepare(`
|
|
38
|
+
INSERT INTO webhooks (id, url, events, secret, status, created_at)
|
|
39
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
40
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
41
|
+
url = excluded.url,
|
|
42
|
+
events = excluded.events,
|
|
43
|
+
secret = excluded.secret,
|
|
44
|
+
status = excluded.status
|
|
45
|
+
`).run(webhook.id, webhook.url, JSON.stringify(webhook.events), webhook.secret ?? null, webhook.status, webhook.created_at);
|
|
46
|
+
}
|
|
47
|
+
getWebhook(id) {
|
|
48
|
+
const row = this.db.prepare('SELECT id, url, events, secret, status, created_at FROM webhooks WHERE id = ?').get(id);
|
|
49
|
+
if (!row)
|
|
50
|
+
return null;
|
|
51
|
+
return {
|
|
52
|
+
id: row.id,
|
|
53
|
+
url: row.url,
|
|
54
|
+
events: JSON.parse(row.events),
|
|
55
|
+
...(row.secret != null ? { secret: row.secret } : {}),
|
|
56
|
+
status: row.status,
|
|
57
|
+
created_at: row.created_at,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
listWebhooks() {
|
|
61
|
+
const rows = this.db.prepare('SELECT id, url, events, secret, status, created_at FROM webhooks ORDER BY created_at DESC').all();
|
|
62
|
+
return rows.map(row => ({
|
|
63
|
+
id: row.id,
|
|
64
|
+
url: row.url,
|
|
65
|
+
events: JSON.parse(row.events),
|
|
66
|
+
...(row.secret != null ? { secret: row.secret } : {}),
|
|
67
|
+
status: row.status,
|
|
68
|
+
created_at: row.created_at,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
deleteWebhook(id) {
|
|
72
|
+
const result = this.db.prepare('DELETE FROM webhooks WHERE id = ?').run(id);
|
|
73
|
+
return result.changes > 0;
|
|
74
|
+
}
|
|
75
|
+
markWebhookFailed(id) {
|
|
76
|
+
this.db.prepare("UPDATE webhooks SET status = 'failed' WHERE id = ?").run(id);
|
|
77
|
+
}
|
|
78
|
+
saveDelivery(delivery) {
|
|
79
|
+
this.db.prepare(`
|
|
80
|
+
INSERT INTO webhook_deliveries (id, webhook_id, event, run_id, status, attempt, created_at)
|
|
81
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
82
|
+
`).run(delivery.id, delivery.webhook_id, delivery.event, delivery.run_id, delivery.status, delivery.attempt, delivery.created_at);
|
|
83
|
+
}
|
|
84
|
+
updateDelivery(id, status) {
|
|
85
|
+
this.db.prepare('UPDATE webhook_deliveries SET status = ? WHERE id = ?').run(status, id);
|
|
86
|
+
}
|
|
87
|
+
close() {
|
|
88
|
+
this.db.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=webhook-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-store.js","sourceRoot":"","sources":["../src/webhook-store.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@studio-foundation/api",
|
|
3
|
+
"version": "0.3.0-beta.1",
|
|
4
|
+
"description": "HTTP API for Studio v7",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/api.js",
|
|
7
|
+
"types": "./dist/api.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/api.js",
|
|
11
|
+
"types": "./dist/api.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./standalone": {
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./user-store": {
|
|
17
|
+
"import": "./dist/user-store.js",
|
|
18
|
+
"types": "./dist/user-store.d.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"studio-api": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"studio",
|
|
26
|
+
"api"
|
|
27
|
+
],
|
|
28
|
+
"author": "Ariane Guay",
|
|
29
|
+
"license": "AGPL-3.0-only",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@fastify/cors": "^10.0.2",
|
|
32
|
+
"@fastify/rate-limit": "^10.3.0",
|
|
33
|
+
"@fastify/swagger": "^9.7.0",
|
|
34
|
+
"@fastify/swagger-ui": "^5.2.5",
|
|
35
|
+
"better-sqlite3": "^9.0.0",
|
|
36
|
+
"fastify": "^5.3.2",
|
|
37
|
+
"js-yaml": "^4.1.1",
|
|
38
|
+
"pg": "^8.19.0",
|
|
39
|
+
"@studio-foundation/contracts": "0.3.0-beta.1",
|
|
40
|
+
"@studio-foundation/engine": "0.3.0-beta.1",
|
|
41
|
+
"@studio-foundation/runner": "0.3.0-beta.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/better-sqlite3": "^7.6.8",
|
|
45
|
+
"@types/js-yaml": "^4.0.9",
|
|
46
|
+
"@types/node": "^25.2.3",
|
|
47
|
+
"@types/pg": "^8.18.0",
|
|
48
|
+
"typescript": "^5.3.0",
|
|
49
|
+
"vitest": "^4.0.18"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc",
|
|
53
|
+
"dev": "tsc --watch",
|
|
54
|
+
"clean": "rm -rf dist",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/.gitkeep
ADDED
|
File without changes
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
}
|