@studio-foundation/api 0.3.0-beta.1 → 0.3.0-beta.6

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 (62) hide show
  1. package/package.json +7 -4
  2. package/ARCHITECTURE.md +0 -52
  3. package/src/.gitkeep +0 -0
  4. package/src/api.ts +0 -8
  5. package/src/bootstrap.ts +0 -259
  6. package/src/event-bus.ts +0 -64
  7. package/src/index.ts +0 -58
  8. package/src/integration-runtime.ts +0 -180
  9. package/src/integration-store.ts +0 -125
  10. package/src/integrations/linear/failure-handler.ts +0 -93
  11. package/src/integrations/linear/webhook-handler.ts +0 -156
  12. package/src/integrations/registry.ts +0 -12
  13. package/src/integrations/types.ts +0 -37
  14. package/src/launcher.ts +0 -214
  15. package/src/logger.ts +0 -50
  16. package/src/plans.ts +0 -43
  17. package/src/routes/agents.ts +0 -131
  18. package/src/routes/config.ts +0 -154
  19. package/src/routes/contracts.ts +0 -205
  20. package/src/routes/pipelines.ts +0 -146
  21. package/src/routes/projects.ts +0 -237
  22. package/src/routes/runs.ts +0 -468
  23. package/src/routes/skills.ts +0 -136
  24. package/src/routes/tools.ts +0 -222
  25. package/src/routes/users.ts +0 -169
  26. package/src/routes/validate.ts +0 -196
  27. package/src/routes/webhooks.ts +0 -120
  28. package/src/server.ts +0 -155
  29. package/src/spawners/http-api-spawner.ts +0 -96
  30. package/src/user-store-pg.ts +0 -138
  31. package/src/user-store.ts +0 -125
  32. package/src/utils/repo-resolver.ts +0 -3
  33. package/src/webhook-dispatcher.ts +0 -142
  34. package/src/webhook-store.ts +0 -141
  35. package/tests/agents.test.ts +0 -164
  36. package/tests/cancel.test.ts +0 -120
  37. package/tests/config.test.ts +0 -196
  38. package/tests/contracts.test.ts +0 -302
  39. package/tests/event-bus.test.ts +0 -53
  40. package/tests/http-api-spawner.test.ts +0 -158
  41. package/tests/integration-runtime.test.ts +0 -199
  42. package/tests/integration-store.test.ts +0 -66
  43. package/tests/integrations/linear/failure-handler.test.ts +0 -113
  44. package/tests/integrations/linear/webhook-handler.test.ts +0 -191
  45. package/tests/launcher.test.ts +0 -380
  46. package/tests/linear-webhook.test.ts +0 -390
  47. package/tests/pipelines.test.ts +0 -166
  48. package/tests/projects.test.ts +0 -283
  49. package/tests/runs.test.ts +0 -379
  50. package/tests/server.test.ts +0 -208
  51. package/tests/skills.test.ts +0 -149
  52. package/tests/sse.test.ts +0 -129
  53. package/tests/tools.test.ts +0 -233
  54. package/tests/user-store.test.ts +0 -93
  55. package/tests/users.test.ts +0 -189
  56. package/tests/utils/repo-resolver.test.ts +0 -105
  57. package/tests/validate.test.ts +0 -233
  58. package/tests/webhook-dispatcher.test.ts +0 -214
  59. package/tests/webhook-store.test.ts +0 -98
  60. package/tests/webhooks.test.ts +0 -176
  61. package/tsconfig.json +0 -24
  62. package/vitest.config.ts +0 -7
@@ -1,136 +0,0 @@
1
- import { readdir, readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import type { FastifyInstance } from 'fastify';
4
- import type { ServerDeps } from '../server.js';
5
-
6
- export async function skillsRoutes(
7
- fastify: FastifyInstance,
8
- options: { deps: ServerDeps }
9
- ): Promise<void> {
10
- const skillsDir = join(options.deps.configsDir, 'skills');
11
-
12
- const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
13
-
14
- // GET /api/skills
15
- fastify.get('/skills', {
16
- schema: {
17
- tags: ['skills'],
18
- summary: 'List all skill names',
19
- response: {
20
- 200: {
21
- type: 'object',
22
- properties: {
23
- skills: { type: 'array', items: { type: 'string' } },
24
- },
25
- },
26
- },
27
- },
28
- }, async (_request, reply) => {
29
- let entries: string[];
30
- try {
31
- entries = await readdir(skillsDir);
32
- } catch {
33
- return reply.send({ skills: [] });
34
- }
35
- const skills = entries
36
- .filter(f => f.endsWith('.skill.md'))
37
- .map(f => f.slice(0, -'.skill.md'.length));
38
- return reply.send({ skills });
39
- });
40
-
41
- // GET /api/skills/:name
42
- fastify.get<{ Params: { name: string } }>('/skills/:name', {
43
- schema: {
44
- tags: ['skills'],
45
- summary: 'Get a skill by name',
46
- params: {
47
- type: 'object',
48
- properties: { name: { type: 'string' } },
49
- required: ['name'],
50
- },
51
- response: {
52
- 200: {
53
- type: 'object',
54
- properties: {
55
- name: { type: 'string' },
56
- content: { type: 'string' },
57
- },
58
- },
59
- 404: errorSchema,
60
- },
61
- },
62
- }, async (request, reply) => {
63
- const filePath = join(skillsDir, `${request.params.name}.skill.md`);
64
- let content: string;
65
- try {
66
- content = await readFile(filePath, 'utf-8');
67
- } catch {
68
- return reply.status(404).send({ error: 'Skill not found' });
69
- }
70
- return reply.send({ name: request.params.name, content });
71
- });
72
-
73
- // PUT /api/skills/:name
74
- fastify.put<{
75
- Params: { name: string };
76
- Body: Record<string, unknown>;
77
- }>('/skills/:name', {
78
- schema: {
79
- tags: ['skills'],
80
- summary: 'Create or update a skill',
81
- params: {
82
- type: 'object',
83
- properties: { name: { type: 'string' } },
84
- required: ['name'],
85
- },
86
- body: { type: 'object' },
87
- response: {
88
- 200: {
89
- type: 'object',
90
- properties: {
91
- name: { type: 'string' },
92
- content: { type: 'string' },
93
- },
94
- },
95
- 400: errorSchema,
96
- },
97
- },
98
- }, async (request, reply) => {
99
- const { content } = request.body;
100
-
101
- if (typeof content !== 'string') {
102
- return reply.status(400).send({ error: "Skill must have a 'content' field (string)" });
103
- }
104
-
105
- await mkdir(skillsDir, { recursive: true });
106
- const filePath = join(skillsDir, `${request.params.name}.skill.md`);
107
- await writeFile(filePath, content, 'utf-8');
108
-
109
- return reply.send({ name: request.params.name, content });
110
- });
111
-
112
- // DELETE /api/skills/:name
113
- fastify.delete<{ Params: { name: string } }>('/skills/:name', {
114
- schema: {
115
- tags: ['skills'],
116
- summary: 'Delete a skill',
117
- params: {
118
- type: 'object',
119
- properties: { name: { type: 'string' } },
120
- required: ['name'],
121
- },
122
- response: {
123
- 204: { type: 'null', description: 'No content' },
124
- 404: errorSchema,
125
- },
126
- },
127
- }, async (request, reply) => {
128
- const filePath = join(skillsDir, `${request.params.name}.skill.md`);
129
- try {
130
- await unlink(filePath);
131
- } catch {
132
- return reply.status(404).send({ error: 'Skill not found' });
133
- }
134
- return reply.status(204).send();
135
- });
136
- }
@@ -1,222 +0,0 @@
1
- import { readdir, readFile, writeFile, unlink, mkdir, access } 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
- import {
7
- BUILTIN_TOOL_NAMES,
8
- listAvailableToolTemplates,
9
- getBundledToolTemplate,
10
- } from '@studio-foundation/runner';
11
-
12
- function toolPath(configsDir: string, name: string): string {
13
- return join(configsDir, 'tools', `${name}.tool.yaml`);
14
- }
15
-
16
- async function isCustomTool(configsDir: string, name: string): Promise<boolean> {
17
- try {
18
- await access(toolPath(configsDir, name));
19
- return true;
20
- } catch {
21
- return false;
22
- }
23
- }
24
-
25
- export async function toolsRoutes(
26
- fastify: FastifyInstance,
27
- options: { deps: ServerDeps }
28
- ): Promise<void> {
29
- const { configsDir } = options.deps;
30
- const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
31
-
32
- // GET /api/tools — list builtins + custom
33
- fastify.get('/tools', {
34
- schema: {
35
- tags: ['tools'],
36
- summary: 'List all available tools (builtins + custom)',
37
- response: {
38
- 200: {
39
- type: 'object',
40
- properties: {
41
- tools: {
42
- type: 'array',
43
- items: {
44
- type: 'object',
45
- properties: {
46
- name: { type: 'string' },
47
- description: { type: 'string' },
48
- is_builtin: { type: 'boolean' },
49
- },
50
- },
51
- },
52
- },
53
- },
54
- },
55
- },
56
- }, async (_request, reply) => {
57
- const toolsList: { name: string; description: string; is_builtin: boolean }[] = [];
58
-
59
- // Builtins from bundled templates (skip if overridden by a custom file)
60
- const available = await listAvailableToolTemplates();
61
- for (const t of available) {
62
- const overridden = await isCustomTool(configsDir, t.name);
63
- if (!overridden) {
64
- toolsList.push({ name: t.name, description: t.description, is_builtin: true });
65
- }
66
- }
67
-
68
- // Custom tools from .studio/tools/
69
- const toolsDir = join(configsDir, 'tools');
70
- let entries: string[] = [];
71
- try {
72
- entries = await readdir(toolsDir);
73
- } catch {
74
- // dir doesn't exist — no custom tools
75
- }
76
- for (const file of entries.filter(f => f.endsWith('.tool.yaml'))) {
77
- const name = file.replace('.tool.yaml', '');
78
- const content = await readFile(join(toolsDir, file), 'utf-8');
79
- const def = yaml.load(content) as { description?: string };
80
- const isBuiltin = BUILTIN_TOOL_NAMES.has(name);
81
- toolsList.push({ name, description: def.description ?? '', is_builtin: isBuiltin });
82
- }
83
-
84
- return reply.send({ tools: toolsList });
85
- });
86
-
87
- // GET /api/tools/:name — read a tool definition
88
- fastify.get<{ Params: { name: string } }>('/tools/:name', {
89
- schema: {
90
- tags: ['tools'],
91
- summary: 'Get a tool definition by name',
92
- params: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
93
- response: {
94
- 200: { type: 'object', additionalProperties: true },
95
- 404: errorSchema,
96
- },
97
- },
98
- }, async (request, reply) => {
99
- const { name } = request.params;
100
-
101
- // Custom tool takes priority
102
- if (await isCustomTool(configsDir, name)) {
103
- const content = await readFile(toolPath(configsDir, name), 'utf-8');
104
- const parsed = yaml.load(content) as Record<string, unknown>;
105
- return reply.send({ ...parsed, is_builtin: BUILTIN_TOOL_NAMES.has(name) });
106
- }
107
-
108
- // Fall back to bundled builtin template
109
- const template = await getBundledToolTemplate(name);
110
- if (template) {
111
- const parsed = yaml.load(template) as Record<string, unknown>;
112
- return reply.send({ ...parsed, is_builtin: true });
113
- }
114
-
115
- return reply.status(404).send({ error: `Tool '${name}' not found` });
116
- });
117
-
118
- // PUT /api/tools/:name — create or update a custom tool
119
- fastify.put<{ Params: { name: string }; Body: unknown }>(
120
- '/tools/:name',
121
- {
122
- schema: {
123
- tags: ['tools'],
124
- summary: 'Create or update a custom tool (YAML text or JSON body)',
125
- params: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
126
- body: { oneOf: [{ type: 'object', additionalProperties: true }, { type: 'string' }] },
127
- response: {
128
- 200: { type: 'object', properties: { name: { type: 'string' } } },
129
- 400: errorSchema,
130
- },
131
- },
132
- },
133
- async (request, reply) => {
134
- const { name } = request.params;
135
- const contentType = request.headers['content-type'] ?? '';
136
-
137
- let yamlContent: string;
138
- if (contentType.includes('application/json')) {
139
- yamlContent = yaml.dump(request.body);
140
- } else {
141
- yamlContent = request.body as string;
142
- }
143
-
144
- try {
145
- yaml.load(yamlContent);
146
- } catch (err) {
147
- const message = err instanceof Error ? err.message : 'Invalid YAML';
148
- return reply.status(400).send({ error: message });
149
- }
150
-
151
- await mkdir(join(configsDir, 'tools'), { recursive: true });
152
- await writeFile(toolPath(configsDir, name), yamlContent, 'utf-8');
153
- return reply.send({ name });
154
- }
155
- );
156
-
157
- // DELETE /api/tools/:name — delete a custom tool (403 for builtins not overridden)
158
- fastify.delete<{ Params: { name: string } }>('/tools/:name', {
159
- schema: {
160
- tags: ['tools'],
161
- summary: 'Delete a custom tool',
162
- params: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
163
- response: {
164
- 200: { type: 'object', properties: { deleted: { type: 'string' } } },
165
- 403: errorSchema,
166
- 404: errorSchema,
167
- },
168
- },
169
- }, async (request, reply) => {
170
- const { name } = request.params;
171
-
172
- // 403 if builtin with no custom override file
173
- if (BUILTIN_TOOL_NAMES.has(name) && !(await isCustomTool(configsDir, name))) {
174
- return reply.status(403).send({ error: `Cannot delete builtin tool '${name}'` });
175
- }
176
-
177
- try {
178
- await unlink(toolPath(configsDir, name));
179
- } catch {
180
- return reply.status(404).send({ error: `Tool '${name}' not found` });
181
- }
182
- return reply.send({ deleted: name });
183
- });
184
-
185
- // POST /api/tools/install — install from the bundled registry
186
- fastify.post<{ Body: { name: string } }>('/tools/install', {
187
- schema: {
188
- tags: ['tools'],
189
- summary: 'Install a tool from the bundled registry',
190
- body: {
191
- type: 'object',
192
- properties: { name: { type: 'string' } },
193
- required: ['name'],
194
- },
195
- response: {
196
- 200: { type: 'object', properties: { installed: { type: 'string' } } },
197
- 404: errorSchema,
198
- 409: errorSchema,
199
- },
200
- },
201
- }, async (request, reply) => {
202
- const { name } = request.body;
203
-
204
- const template = await getBundledToolTemplate(name);
205
- if (!template) {
206
- const available = await listAvailableToolTemplates();
207
- return reply.status(404).send({
208
- error: `Tool '${name}' not found in registry. Available: ${available.map(t => t.name).join(', ')}`,
209
- });
210
- }
211
-
212
- const destPath = toolPath(configsDir, name);
213
- const alreadyInstalled = await access(destPath).then(() => true).catch(() => false);
214
- if (alreadyInstalled) {
215
- return reply.status(409).send({ error: `Tool '${name}' is already installed` });
216
- }
217
-
218
- await mkdir(join(configsDir, 'tools'), { recursive: true });
219
- await writeFile(destPath, template, 'utf-8');
220
- return reply.send({ installed: name });
221
- });
222
- }
@@ -1,169 +0,0 @@
1
- // api/src/routes/users.ts
2
- import { randomUUID, randomBytes } from 'node:crypto';
3
- import type { FastifyInstance } from 'fastify';
4
- import type { ServerDeps } from '../server.js';
5
-
6
- const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
7
-
8
- const userSchema = {
9
- type: 'object',
10
- properties: {
11
- id: { type: 'string' },
12
- email: { type: 'string' },
13
- plan: { type: 'string' },
14
- created_at: { type: 'string' },
15
- },
16
- };
17
-
18
- const userWithKeySchema = {
19
- ...userSchema,
20
- properties: { ...userSchema.properties, api_key: { type: 'string' } },
21
- };
22
-
23
- const todayUsageSchema = {
24
- type: 'object',
25
- properties: {
26
- runs_count: { type: 'number' },
27
- tokens_used: { type: 'number' },
28
- },
29
- };
30
-
31
- const userWithUsageSchema = {
32
- type: 'object',
33
- properties: {
34
- ...userSchema.properties,
35
- today_usage: todayUsageSchema,
36
- },
37
- };
38
-
39
- export async function usersRoutes(
40
- fastify: FastifyInstance,
41
- options: { deps: ServerDeps },
42
- ): Promise<void> {
43
- const { userStore } = options.deps;
44
- if (!userStore) return; // no-op if userStore not configured
45
-
46
- // POST /api/users — create a user
47
- fastify.post<{ Body: { email: string; plan?: string } }>('/users', {
48
- schema: {
49
- tags: ['users'],
50
- summary: 'Create a new user',
51
- body: {
52
- type: 'object',
53
- required: ['email'],
54
- properties: {
55
- email: { type: 'string', format: 'email' },
56
- plan: { type: 'string', default: 'free' },
57
- },
58
- },
59
- response: {
60
- 201: userWithKeySchema,
61
- 400: errorSchema,
62
- 409: errorSchema,
63
- },
64
- },
65
- }, async (request, reply) => {
66
- const { email, plan = 'free' } = request.body;
67
-
68
- const apiKey = randomBytes(32).toString('hex');
69
- const user = {
70
- id: randomUUID(),
71
- email,
72
- plan,
73
- api_key: apiKey,
74
- created_at: new Date().toISOString(),
75
- };
76
-
77
- try {
78
- await userStore.saveUser(user);
79
- } catch (err: unknown) {
80
- const msg = err instanceof Error ? err.message : String(err);
81
- if (msg.includes('UNIQUE constraint failed: users.email')) {
82
- return reply.status(409).send({ error: 'Email already in use' });
83
- }
84
- throw err;
85
- }
86
- return reply.status(201).send(user);
87
- });
88
-
89
- // GET /api/users — list users (no api_key exposed)
90
- fastify.get('/users', {
91
- schema: {
92
- tags: ['users'],
93
- summary: 'List all users',
94
- response: {
95
- 200: { type: 'array', items: userSchema },
96
- },
97
- },
98
- }, async (_request, reply) => {
99
- const users = await userStore.listUsers();
100
- return reply.send(users.map(({ api_key: _k, ...u }) => u));
101
- });
102
-
103
- // GET /api/users/me — current user + today usage
104
- fastify.get('/users/me', {
105
- schema: {
106
- tags: ['users'],
107
- summary: 'Get current authenticated user',
108
- response: {
109
- 200: userWithUsageSchema,
110
- 401: errorSchema,
111
- },
112
- },
113
- }, async (request, reply) => {
114
- if (!request.user) return reply.status(401).send({ error: 'Unauthorized' });
115
-
116
- const today = new Date().toISOString().slice(0, 10);
117
- const usage = await userStore.getDailyUsage(request.user.id, today);
118
- const { api_key: _k, ...userWithoutKey } = request.user;
119
-
120
- return reply.send({
121
- ...userWithoutKey,
122
- today_usage: { runs_count: usage.runs_count, tokens_used: usage.tokens_used },
123
- });
124
- });
125
-
126
- // GET /api/users/:id — user detail + today usage
127
- fastify.get<{ Params: { id: string } }>('/users/:id', {
128
- schema: {
129
- tags: ['users'],
130
- summary: 'Get a user by ID',
131
- params: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
132
- response: {
133
- 200: userWithUsageSchema,
134
- 404: errorSchema,
135
- },
136
- },
137
- }, async (request, reply) => {
138
- const user = await userStore.getUserById(request.params.id);
139
- if (!user) return reply.status(404).send({ error: 'User not found' });
140
-
141
- const today = new Date().toISOString().slice(0, 10);
142
- const usage = await userStore.getDailyUsage(user.id, today);
143
- const { api_key: _k, ...userWithoutKey } = user;
144
-
145
- return reply.send({
146
- ...userWithoutKey,
147
- today_usage: { runs_count: usage.runs_count, tokens_used: usage.tokens_used },
148
- });
149
- });
150
-
151
- // DELETE /api/users/:id
152
- fastify.delete<{ Params: { id: string } }>('/users/:id', {
153
- schema: {
154
- tags: ['users'],
155
- summary: 'Delete a user',
156
- params: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
157
- response: {
158
- 204: { type: 'null', description: 'No content' },
159
- 404: errorSchema,
160
- },
161
- },
162
- }, async (request, reply) => {
163
- const user = await userStore.getUserById(request.params.id);
164
- if (!user) return reply.status(404).send({ error: 'User not found' });
165
-
166
- await userStore.deleteUser(request.params.id);
167
- return reply.status(204).send();
168
- });
169
- }