@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.
Files changed (188) hide show
  1. package/ARCHITECTURE.md +52 -0
  2. package/LICENSE +663 -0
  3. package/README.md +161 -0
  4. package/dist/api.d.ts +8 -0
  5. package/dist/api.d.ts.map +1 -0
  6. package/dist/api.js +6 -0
  7. package/dist/api.js.map +1 -0
  8. package/dist/bootstrap.d.ts +66 -0
  9. package/dist/bootstrap.d.ts.map +1 -0
  10. package/dist/bootstrap.js +193 -0
  11. package/dist/bootstrap.js.map +1 -0
  12. package/dist/event-bus.d.ts +16 -0
  13. package/dist/event-bus.d.ts.map +1 -0
  14. package/dist/event-bus.js +37 -0
  15. package/dist/event-bus.js.map +1 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +52 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/integration-runtime.d.ts +22 -0
  21. package/dist/integration-runtime.d.ts.map +1 -0
  22. package/dist/integration-runtime.js +150 -0
  23. package/dist/integration-runtime.js.map +1 -0
  24. package/dist/integration-store.d.ts +26 -0
  25. package/dist/integration-store.d.ts.map +1 -0
  26. package/dist/integration-store.js +80 -0
  27. package/dist/integration-store.js.map +1 -0
  28. package/dist/integrations/linear/failure-handler.d.ts +5 -0
  29. package/dist/integrations/linear/failure-handler.d.ts.map +1 -0
  30. package/dist/integrations/linear/failure-handler.js +73 -0
  31. package/dist/integrations/linear/failure-handler.js.map +1 -0
  32. package/dist/integrations/linear/webhook-handler.d.ts +6 -0
  33. package/dist/integrations/linear/webhook-handler.d.ts.map +1 -0
  34. package/dist/integrations/linear/webhook-handler.js +131 -0
  35. package/dist/integrations/linear/webhook-handler.js.map +1 -0
  36. package/dist/integrations/registry.d.ts +4 -0
  37. package/dist/integrations/registry.d.ts.map +1 -0
  38. package/dist/integrations/registry.js +9 -0
  39. package/dist/integrations/registry.js.map +1 -0
  40. package/dist/integrations/types.d.ts +33 -0
  41. package/dist/integrations/types.d.ts.map +1 -0
  42. package/dist/integrations/types.js +2 -0
  43. package/dist/integrations/types.js.map +1 -0
  44. package/dist/launcher.d.ts +45 -0
  45. package/dist/launcher.d.ts.map +1 -0
  46. package/dist/launcher.js +149 -0
  47. package/dist/launcher.js.map +1 -0
  48. package/dist/logger.d.ts +7 -0
  49. package/dist/logger.d.ts.map +1 -0
  50. package/dist/logger.js +37 -0
  51. package/dist/logger.js.map +1 -0
  52. package/dist/plans.d.ts +13 -0
  53. package/dist/plans.d.ts.map +1 -0
  54. package/dist/plans.js +33 -0
  55. package/dist/plans.js.map +1 -0
  56. package/dist/routes/agents.d.ts +6 -0
  57. package/dist/routes/agents.d.ts.map +1 -0
  58. package/dist/routes/agents.js +118 -0
  59. package/dist/routes/agents.js.map +1 -0
  60. package/dist/routes/config.d.ts +6 -0
  61. package/dist/routes/config.d.ts.map +1 -0
  62. package/dist/routes/config.js +129 -0
  63. package/dist/routes/config.js.map +1 -0
  64. package/dist/routes/contracts.d.ts +6 -0
  65. package/dist/routes/contracts.d.ts.map +1 -0
  66. package/dist/routes/contracts.js +188 -0
  67. package/dist/routes/contracts.js.map +1 -0
  68. package/dist/routes/pipelines.d.ts +6 -0
  69. package/dist/routes/pipelines.d.ts.map +1 -0
  70. package/dist/routes/pipelines.js +133 -0
  71. package/dist/routes/pipelines.js.map +1 -0
  72. package/dist/routes/projects.d.ts +6 -0
  73. package/dist/routes/projects.d.ts.map +1 -0
  74. package/dist/routes/projects.js +220 -0
  75. package/dist/routes/projects.js.map +1 -0
  76. package/dist/routes/runs.d.ts +6 -0
  77. package/dist/routes/runs.d.ts.map +1 -0
  78. package/dist/routes/runs.js +427 -0
  79. package/dist/routes/runs.js.map +1 -0
  80. package/dist/routes/skills.d.ts +6 -0
  81. package/dist/routes/skills.d.ts.map +1 -0
  82. package/dist/routes/skills.js +123 -0
  83. package/dist/routes/skills.js.map +1 -0
  84. package/dist/routes/tools.d.ts +6 -0
  85. package/dist/routes/tools.d.ts.map +1 -0
  86. package/dist/routes/tools.js +193 -0
  87. package/dist/routes/tools.js.map +1 -0
  88. package/dist/routes/users.d.ts +6 -0
  89. package/dist/routes/users.d.ts.map +1 -0
  90. package/dist/routes/users.js +152 -0
  91. package/dist/routes/users.js.map +1 -0
  92. package/dist/routes/validate.d.ts +6 -0
  93. package/dist/routes/validate.d.ts.map +1 -0
  94. package/dist/routes/validate.js +171 -0
  95. package/dist/routes/validate.js.map +1 -0
  96. package/dist/routes/webhooks.d.ts +8 -0
  97. package/dist/routes/webhooks.d.ts.map +1 -0
  98. package/dist/routes/webhooks.js +102 -0
  99. package/dist/routes/webhooks.js.map +1 -0
  100. package/dist/server.d.ts +46 -0
  101. package/dist/server.d.ts.map +1 -0
  102. package/dist/server.js +101 -0
  103. package/dist/server.js.map +1 -0
  104. package/dist/spawners/http-api-spawner.d.ts +10 -0
  105. package/dist/spawners/http-api-spawner.d.ts.map +1 -0
  106. package/dist/spawners/http-api-spawner.js +88 -0
  107. package/dist/spawners/http-api-spawner.js.map +1 -0
  108. package/dist/user-store-pg.d.ts +21 -0
  109. package/dist/user-store-pg.d.ts.map +1 -0
  110. package/dist/user-store-pg.js +97 -0
  111. package/dist/user-store-pg.js.map +1 -0
  112. package/dist/user-store.d.ts +29 -0
  113. package/dist/user-store.d.ts.map +1 -0
  114. package/dist/user-store.js +97 -0
  115. package/dist/user-store.js.map +1 -0
  116. package/dist/utils/repo-resolver.d.ts +3 -0
  117. package/dist/utils/repo-resolver.d.ts.map +1 -0
  118. package/dist/utils/repo-resolver.js +3 -0
  119. package/dist/utils/repo-resolver.js.map +1 -0
  120. package/dist/webhook-dispatcher.d.ts +13 -0
  121. package/dist/webhook-dispatcher.d.ts.map +1 -0
  122. package/dist/webhook-dispatcher.js +108 -0
  123. package/dist/webhook-dispatcher.js.map +1 -0
  124. package/dist/webhook-store.d.ts +31 -0
  125. package/dist/webhook-store.d.ts.map +1 -0
  126. package/dist/webhook-store.js +91 -0
  127. package/dist/webhook-store.js.map +1 -0
  128. package/package.json +59 -0
  129. package/src/.gitkeep +0 -0
  130. package/src/api.ts +8 -0
  131. package/src/bootstrap.ts +259 -0
  132. package/src/event-bus.ts +64 -0
  133. package/src/index.ts +58 -0
  134. package/src/integration-runtime.ts +180 -0
  135. package/src/integration-store.ts +125 -0
  136. package/src/integrations/linear/failure-handler.ts +93 -0
  137. package/src/integrations/linear/webhook-handler.ts +156 -0
  138. package/src/integrations/registry.ts +12 -0
  139. package/src/integrations/types.ts +37 -0
  140. package/src/launcher.ts +214 -0
  141. package/src/logger.ts +50 -0
  142. package/src/plans.ts +43 -0
  143. package/src/routes/agents.ts +131 -0
  144. package/src/routes/config.ts +154 -0
  145. package/src/routes/contracts.ts +205 -0
  146. package/src/routes/pipelines.ts +146 -0
  147. package/src/routes/projects.ts +237 -0
  148. package/src/routes/runs.ts +468 -0
  149. package/src/routes/skills.ts +136 -0
  150. package/src/routes/tools.ts +222 -0
  151. package/src/routes/users.ts +169 -0
  152. package/src/routes/validate.ts +196 -0
  153. package/src/routes/webhooks.ts +120 -0
  154. package/src/server.ts +155 -0
  155. package/src/spawners/http-api-spawner.ts +96 -0
  156. package/src/user-store-pg.ts +138 -0
  157. package/src/user-store.ts +125 -0
  158. package/src/utils/repo-resolver.ts +3 -0
  159. package/src/webhook-dispatcher.ts +142 -0
  160. package/src/webhook-store.ts +141 -0
  161. package/tests/agents.test.ts +164 -0
  162. package/tests/cancel.test.ts +120 -0
  163. package/tests/config.test.ts +196 -0
  164. package/tests/contracts.test.ts +302 -0
  165. package/tests/event-bus.test.ts +53 -0
  166. package/tests/http-api-spawner.test.ts +158 -0
  167. package/tests/integration-runtime.test.ts +199 -0
  168. package/tests/integration-store.test.ts +66 -0
  169. package/tests/integrations/linear/failure-handler.test.ts +113 -0
  170. package/tests/integrations/linear/webhook-handler.test.ts +191 -0
  171. package/tests/launcher.test.ts +380 -0
  172. package/tests/linear-webhook.test.ts +390 -0
  173. package/tests/pipelines.test.ts +166 -0
  174. package/tests/projects.test.ts +283 -0
  175. package/tests/runs.test.ts +379 -0
  176. package/tests/server.test.ts +208 -0
  177. package/tests/skills.test.ts +149 -0
  178. package/tests/sse.test.ts +129 -0
  179. package/tests/tools.test.ts +233 -0
  180. package/tests/user-store.test.ts +93 -0
  181. package/tests/users.test.ts +189 -0
  182. package/tests/utils/repo-resolver.test.ts +105 -0
  183. package/tests/validate.test.ts +233 -0
  184. package/tests/webhook-dispatcher.test.ts +214 -0
  185. package/tests/webhook-store.test.ts +98 -0
  186. package/tests/webhooks.test.ts +176 -0
  187. package/tsconfig.json +24 -0
  188. package/vitest.config.ts +7 -0
@@ -0,0 +1,205 @@
1
+ import { readdir, readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import * as yaml from 'js-yaml';
4
+ import type { FastifyInstance } from 'fastify';
5
+ import type { ServerDeps } from '../server.js';
6
+ import { loadContract, validateOutput } from '@studio-foundation/engine';
7
+ import type { ToolCall } from '@studio-foundation/contracts';
8
+
9
+ export async function contractsRoutes(
10
+ fastify: FastifyInstance,
11
+ options: { deps: ServerDeps }
12
+ ): Promise<void> {
13
+ const contractsDir = join(options.deps.configsDir, 'contracts');
14
+ const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
15
+
16
+ // GET /api/contracts
17
+ fastify.get('/contracts', {
18
+ schema: {
19
+ tags: ['contracts'],
20
+ summary: 'List all contract names',
21
+ response: {
22
+ 200: {
23
+ type: 'object',
24
+ properties: {
25
+ contracts: { type: 'array', items: { type: 'string' } },
26
+ },
27
+ },
28
+ },
29
+ },
30
+ }, async (_request, reply) => {
31
+ let entries: string[];
32
+ try {
33
+ entries = await readdir(contractsDir);
34
+ } catch {
35
+ return reply.send({ contracts: [] });
36
+ }
37
+ const contracts = entries
38
+ .filter(f => f.endsWith('.contract.yaml'))
39
+ .map(f => f.slice(0, -'.contract.yaml'.length));
40
+ return reply.send({ contracts });
41
+ });
42
+
43
+ // GET /api/contracts/:name
44
+ fastify.get<{ Params: { name: string } }>('/contracts/:name', {
45
+ schema: {
46
+ tags: ['contracts'],
47
+ summary: 'Get a contract by name',
48
+ params: {
49
+ type: 'object',
50
+ properties: { name: { type: 'string' } },
51
+ required: ['name'],
52
+ },
53
+ response: {
54
+ 200: { type: 'object', additionalProperties: true },
55
+ 404: errorSchema,
56
+ },
57
+ },
58
+ }, async (request, reply) => {
59
+ const filePath = join(contractsDir, `${request.params.name}.contract.yaml`);
60
+ let content: string;
61
+ try {
62
+ content = await readFile(filePath, 'utf-8');
63
+ } catch {
64
+ return reply.status(404).send({ error: 'Contract not found' });
65
+ }
66
+ return reply.send(yaml.load(content));
67
+ });
68
+
69
+ // PUT /api/contracts/:name
70
+ fastify.put<{
71
+ Params: { name: string };
72
+ Body: Record<string, unknown>;
73
+ }>('/contracts/:name', {
74
+ schema: {
75
+ tags: ['contracts'],
76
+ summary: 'Create or update a contract',
77
+ params: {
78
+ type: 'object',
79
+ properties: { name: { type: 'string' } },
80
+ required: ['name'],
81
+ },
82
+ body: { type: 'object' },
83
+ response: {
84
+ 200: {
85
+ type: 'object',
86
+ properties: {
87
+ name: { type: 'string' },
88
+ content: { type: 'object', additionalProperties: true },
89
+ },
90
+ },
91
+ 400: errorSchema,
92
+ },
93
+ },
94
+ }, async (request, reply) => {
95
+ const body = request.body;
96
+
97
+ if (!body['name'] || typeof body['name'] !== 'string') {
98
+ return reply.status(400).send({ error: "Contract must have a 'name' field (string)" });
99
+ }
100
+ if (body['version'] === undefined) {
101
+ return reply.status(400).send({ error: "Contract must have a 'version' field" });
102
+ }
103
+
104
+ await mkdir(contractsDir, { recursive: true });
105
+ const filePath = join(contractsDir, `${request.params.name}.contract.yaml`);
106
+ await writeFile(filePath, yaml.dump(body), 'utf-8');
107
+
108
+ return reply.send({ name: request.params.name, content: body });
109
+ });
110
+
111
+ // DELETE /api/contracts/:name
112
+ fastify.delete<{ Params: { name: string } }>('/contracts/:name', {
113
+ schema: {
114
+ tags: ['contracts'],
115
+ summary: 'Delete a contract',
116
+ params: {
117
+ type: 'object',
118
+ properties: { name: { type: 'string' } },
119
+ required: ['name'],
120
+ },
121
+ response: {
122
+ 204: { type: 'null', description: 'No content' },
123
+ 404: errorSchema,
124
+ },
125
+ },
126
+ }, async (request, reply) => {
127
+ const filePath = join(contractsDir, `${request.params.name}.contract.yaml`);
128
+ try {
129
+ await unlink(filePath);
130
+ } catch {
131
+ return reply.status(404).send({ error: 'Contract not found' });
132
+ }
133
+ return reply.status(204).send();
134
+ });
135
+
136
+ // POST /api/contracts/:name/validate
137
+ fastify.post<{
138
+ Params: { name: string };
139
+ Body: { output: unknown; tool_calls?: ToolCall[] };
140
+ }>('/contracts/:name/validate', {
141
+ schema: {
142
+ tags: ['contracts'],
143
+ summary: 'Validate an output against a contract without running a pipeline',
144
+ params: {
145
+ type: 'object',
146
+ properties: { name: { type: 'string' } },
147
+ required: ['name'],
148
+ },
149
+ body: {
150
+ type: 'object',
151
+ required: ['output'],
152
+ properties: {
153
+ output: { type: 'object', additionalProperties: true },
154
+ tool_calls: {
155
+ type: 'array',
156
+ items: {
157
+ type: 'object',
158
+ properties: {
159
+ id: { type: 'string' },
160
+ name: { type: 'string' },
161
+ arguments: { type: 'object', additionalProperties: true },
162
+ result: {},
163
+ error: { type: 'string' },
164
+ },
165
+ required: ['id', 'name'],
166
+ },
167
+ },
168
+ },
169
+ },
170
+ response: {
171
+ 200: {
172
+ type: 'object',
173
+ required: ['valid', 'errors', 'warnings', 'post_validation'],
174
+ properties: {
175
+ valid: { type: 'boolean' },
176
+ errors: { type: 'array', items: { type: 'string' } },
177
+ warnings: { type: 'array', items: { type: 'string' } },
178
+ post_validation: {
179
+ type: 'object',
180
+ required: ['accepted'],
181
+ properties: {
182
+ accepted: { type: 'boolean' },
183
+ rejection_reason: { type: 'string' },
184
+ rejection_details: { type: 'array', items: { type: 'string' } },
185
+ },
186
+ },
187
+ },
188
+ },
189
+ 400: errorSchema,
190
+ 404: errorSchema,
191
+ },
192
+ },
193
+ }, async (request, reply) => {
194
+ let contract;
195
+ try {
196
+ contract = await loadContract(request.params.name, contractsDir);
197
+ } catch {
198
+ return reply.status(404).send({ error: 'Contract not found' });
199
+ }
200
+
201
+ const { output, tool_calls = [] } = request.body;
202
+ const result = validateOutput(contract, output, tool_calls);
203
+ return reply.send(result);
204
+ });
205
+ }
@@ -0,0 +1,146 @@
1
+ import { readdir, readFile, writeFile, unlink } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import yaml from 'js-yaml';
4
+ import type { FastifyInstance } from 'fastify';
5
+ import type { ServerDeps } from '../server.js';
6
+
7
+ function pipelinePath(configsDir: string, name: string): string {
8
+ return join(configsDir, 'pipelines', `${name}.pipeline.yaml`);
9
+ }
10
+
11
+ export async function pipelinesRoutes(
12
+ fastify: FastifyInstance,
13
+ options: { deps: ServerDeps }
14
+ ): Promise<void> {
15
+ const { configsDir } = options.deps;
16
+
17
+ // GET /api/pipelines — list all pipeline names
18
+ fastify.get('/pipelines', {
19
+ schema: {
20
+ tags: ['pipelines'],
21
+ summary: 'List all pipeline names',
22
+ response: {
23
+ 200: {
24
+ type: 'object',
25
+ properties: {
26
+ pipelines: { type: 'array', items: { type: 'string' } },
27
+ },
28
+ },
29
+ },
30
+ },
31
+ }, async (_request, reply) => {
32
+ const pipelinesDir = join(configsDir, 'pipelines');
33
+ let entries: string[];
34
+ try {
35
+ entries = await readdir(pipelinesDir);
36
+ } catch {
37
+ return reply.send({ pipelines: [] });
38
+ }
39
+ const pipelines = entries
40
+ .filter(f => f.endsWith('.pipeline.yaml'))
41
+ .map(f => f.replace('.pipeline.yaml', ''));
42
+ return reply.send({ pipelines });
43
+ });
44
+
45
+ const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
46
+
47
+ // GET /api/pipelines/:name — read a pipeline (YAML parsed to JSON)
48
+ fastify.get<{ Params: { name: string } }>('/pipelines/:name', {
49
+ schema: {
50
+ tags: ['pipelines'],
51
+ summary: 'Get a pipeline by name',
52
+ params: {
53
+ type: 'object',
54
+ properties: { name: { type: 'string' } },
55
+ required: ['name'],
56
+ },
57
+ response: {
58
+ 200: { type: 'object', additionalProperties: true },
59
+ 404: errorSchema,
60
+ },
61
+ },
62
+ }, async (request, reply) => {
63
+ const { name } = request.params;
64
+ let content: string;
65
+ try {
66
+ content = await readFile(pipelinePath(configsDir, name), 'utf-8');
67
+ } catch {
68
+ return reply.status(404).send({ error: `Pipeline '${name}' not found` });
69
+ }
70
+ const parsed = yaml.load(content);
71
+ return reply.send(parsed);
72
+ });
73
+
74
+ // PUT /api/pipelines/:name — create or update a pipeline (YAML or JSON body)
75
+ fastify.put<{ Params: { name: string }; Body: unknown }>(
76
+ '/pipelines/:name',
77
+ {
78
+ schema: {
79
+ tags: ['pipelines'],
80
+ summary: 'Create or update a pipeline (YAML text or JSON body)',
81
+ params: {
82
+ type: 'object',
83
+ properties: { name: { type: 'string' } },
84
+ required: ['name'],
85
+ },
86
+ body: { oneOf: [{ type: 'object', additionalProperties: true }, { type: 'string' }] },
87
+ response: {
88
+ 200: {
89
+ type: 'object',
90
+ properties: { name: { type: 'string' } },
91
+ },
92
+ 400: errorSchema,
93
+ },
94
+ },
95
+ },
96
+ async (request, reply) => {
97
+ const { name } = request.params;
98
+ const contentType = request.headers['content-type'] ?? '';
99
+
100
+ let yamlContent: string;
101
+ if (contentType.includes('application/json')) {
102
+ yamlContent = yaml.dump(request.body);
103
+ } else {
104
+ yamlContent = request.body as string;
105
+ }
106
+
107
+ try {
108
+ yaml.load(yamlContent);
109
+ } catch (err) {
110
+ const message = err instanceof Error ? err.message : 'Invalid YAML';
111
+ return reply.status(400).send({ error: message });
112
+ }
113
+
114
+ await writeFile(pipelinePath(configsDir, name), yamlContent, 'utf-8');
115
+ return reply.send({ name });
116
+ }
117
+ );
118
+
119
+ // DELETE /api/pipelines/:name — delete a pipeline
120
+ fastify.delete<{ Params: { name: string } }>('/pipelines/:name', {
121
+ schema: {
122
+ tags: ['pipelines'],
123
+ summary: 'Delete a pipeline',
124
+ params: {
125
+ type: 'object',
126
+ properties: { name: { type: 'string' } },
127
+ required: ['name'],
128
+ },
129
+ response: {
130
+ 200: {
131
+ type: 'object',
132
+ properties: { deleted: { type: 'string' } },
133
+ },
134
+ 404: errorSchema,
135
+ },
136
+ },
137
+ }, async (request, reply) => {
138
+ const { name } = request.params;
139
+ try {
140
+ await unlink(pipelinePath(configsDir, name));
141
+ } catch {
142
+ return reply.status(404).send({ error: `Pipeline '${name}' not found` });
143
+ }
144
+ return reply.send({ deleted: name });
145
+ });
146
+ }
@@ -0,0 +1,237 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import * as yaml from 'js-yaml';
4
+ import { createHash } from 'node:crypto';
5
+ import type { FastifyInstance } from 'fastify';
6
+ import type { ServerDeps } from '../server.js';
7
+
8
+ function projectId(configsDir: string): string {
9
+ return createHash('sha256').update(configsDir).digest('hex').slice(0, 12);
10
+ }
11
+
12
+ async function listResources(dir: string, suffix: string): Promise<string[]> {
13
+ try {
14
+ const entries = await readdir(dir);
15
+ return entries
16
+ .filter(f => f.endsWith(suffix))
17
+ .map(f => f.slice(0, -suffix.length));
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
22
+
23
+ export async function projectsRoutes(
24
+ fastify: FastifyInstance,
25
+ options: { deps: ServerDeps }
26
+ ): Promise<void> {
27
+ const { configsDir, projectName, studioVersion, maskedConfig } = options.deps;
28
+ const id = projectId(configsDir);
29
+
30
+ const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
31
+
32
+ // GET /api/projects — returns the single project this API serves
33
+ fastify.get('/projects', {
34
+ schema: {
35
+ tags: ['projects'],
36
+ summary: 'List projects served by this API instance',
37
+ response: {
38
+ 200: {
39
+ type: 'object',
40
+ properties: {
41
+ projects: {
42
+ type: 'array',
43
+ items: {
44
+ type: 'object',
45
+ properties: {
46
+ id: { type: 'string' },
47
+ name: { type: 'string' },
48
+ pipelines_dir: { type: 'string' },
49
+ },
50
+ },
51
+ },
52
+ },
53
+ },
54
+ },
55
+ },
56
+ }, async (_request, reply) => {
57
+ return reply.send({
58
+ projects: [
59
+ {
60
+ id,
61
+ name: projectName,
62
+ pipelines_dir: join(configsDir, 'pipelines'),
63
+ },
64
+ ],
65
+ });
66
+ });
67
+
68
+ // GET /api/projects/:id/pipelines
69
+ fastify.get<{ Params: { id: string } }>('/projects/:id/pipelines', {
70
+ schema: {
71
+ tags: ['projects'],
72
+ summary: 'List pipelines for a project',
73
+ params: {
74
+ type: 'object',
75
+ properties: { id: { type: 'string' } },
76
+ required: ['id'],
77
+ },
78
+ response: {
79
+ 200: {
80
+ type: 'object',
81
+ properties: {
82
+ pipelines: { type: 'array', items: { type: 'string' } },
83
+ },
84
+ },
85
+ 404: errorSchema,
86
+ },
87
+ },
88
+ }, async (request, reply) => {
89
+ if (request.params.id !== id) {
90
+ return reply.status(404).send({ error: 'Project not found' });
91
+ }
92
+
93
+ const pipelinesDir = join(configsDir, 'pipelines');
94
+ let entries: string[];
95
+ try {
96
+ entries = await readdir(pipelinesDir);
97
+ } catch {
98
+ return reply.send({ pipelines: [] });
99
+ }
100
+
101
+ const pipelines = entries
102
+ .filter(f => f.endsWith('.pipeline.yaml'))
103
+ .map(f => f.replace('.pipeline.yaml', ''));
104
+
105
+ return reply.send({ pipelines });
106
+ });
107
+ // GET /api/projects/:id/inputs
108
+ fastify.get<{ Params: { id: string } }>('/projects/:id/inputs', {
109
+ schema: {
110
+ tags: ['projects'],
111
+ summary: 'List inputs for a project',
112
+ params: {
113
+ type: 'object',
114
+ properties: { id: { type: 'string' } },
115
+ required: ['id'],
116
+ },
117
+ response: {
118
+ 200: {
119
+ type: 'object',
120
+ properties: {
121
+ inputs: { type: 'array', items: { type: 'string' } },
122
+ },
123
+ },
124
+ 404: errorSchema,
125
+ },
126
+ },
127
+ }, async (request, reply) => {
128
+ if (request.params.id !== id) {
129
+ return reply.status(404).send({ error: 'Project not found' });
130
+ }
131
+
132
+ const inputsDir = join(configsDir, 'inputs');
133
+ let entries: string[];
134
+ try {
135
+ entries = await readdir(inputsDir);
136
+ } catch {
137
+ return reply.send({ inputs: [] });
138
+ }
139
+
140
+ const inputs = entries
141
+ .filter(f => f.endsWith('.input.yaml'))
142
+ .map(f => f.replace('.input.yaml', ''));
143
+
144
+ return reply.send({ inputs });
145
+ });
146
+
147
+
148
+ // GET /api/projects/:id/inputs/:name — read an input file (YAML parsed to JSON)
149
+ fastify.get<{ Params: { id: string; name: string } }>('/projects/:id/inputs/:name', {
150
+ schema: {
151
+ tags: ['projects'],
152
+ summary: 'Get an input file by name',
153
+ params: {
154
+ type: 'object',
155
+ properties: {
156
+ id: { type: 'string' },
157
+ name: { type: 'string' },
158
+ },
159
+ required: ['id', 'name'],
160
+ },
161
+ response: {
162
+ 200: { type: 'object', additionalProperties: true },
163
+ 404: errorSchema,
164
+ },
165
+ },
166
+ }, async (request, reply) => {
167
+ if (request.params.id !== id) {
168
+ return reply.status(404).send({ error: 'Project not found' });
169
+ }
170
+
171
+ const filePath = join(configsDir, 'inputs', `${request.params.name}.input.yaml`);
172
+ let content: string;
173
+ try {
174
+ content = await readFile(filePath, 'utf-8');
175
+ } catch {
176
+ return reply.status(404).send({ error: 'Input not found' });
177
+ }
178
+ return reply.send(yaml.load(content));
179
+ });
180
+
181
+ // GET /api/project — full introspection of the current Studio project
182
+ fastify.get('/project', {
183
+ schema: {
184
+ tags: ['projects'],
185
+ summary: 'Introspect the current Studio project',
186
+ response: {
187
+ 200: {
188
+ type: 'object',
189
+ properties: {
190
+ studio_version: { type: 'string' },
191
+ studio_dir: { type: 'string' },
192
+ config: {
193
+ type: 'object',
194
+ properties: {
195
+ defaults: {
196
+ type: 'object',
197
+ properties: {
198
+ provider: { type: 'string' },
199
+ model: { type: 'string' },
200
+ },
201
+ },
202
+ providers: { type: 'array', items: { type: 'string' } },
203
+ },
204
+ },
205
+ pipelines: { type: 'array', items: { type: 'string' } },
206
+ contracts: { type: 'array', items: { type: 'string' } },
207
+ agents: { type: 'array', items: { type: 'string' } },
208
+ tools: { type: 'array', items: { type: 'string' } },
209
+ skills: { type: 'array', items: { type: 'string' } },
210
+ inputs: { type: 'array', items: { type: 'string' } },
211
+ },
212
+ },
213
+ },
214
+ },
215
+ }, async (_request, reply) => {
216
+ const [pipelines, contracts, agents, tools, skills, inputs] = await Promise.all([
217
+ listResources(join(configsDir, 'pipelines'), '.pipeline.yaml'),
218
+ listResources(join(configsDir, 'contracts'), '.contract.yaml'),
219
+ listResources(join(configsDir, 'agents'), '.agent.yaml'),
220
+ listResources(join(configsDir, 'tools'), '.tool.yaml'),
221
+ listResources(join(configsDir, 'skills'), '.skill.md'),
222
+ listResources(join(configsDir, 'inputs'), '.input.yaml'),
223
+ ]);
224
+
225
+ return reply.send({
226
+ studio_version: studioVersion,
227
+ studio_dir: configsDir,
228
+ config: maskedConfig,
229
+ pipelines,
230
+ contracts,
231
+ agents,
232
+ tools,
233
+ skills,
234
+ inputs,
235
+ });
236
+ });
237
+ }