@flowdular/sdk 0.1.0 → 0.2.0

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 (174) hide show
  1. package/.ai/skills/test-hardening/SKILL.md +1 -1
  2. package/README.md +69 -12
  3. package/assets/flowdular-banner.png +0 -0
  4. package/modules/agents/package.json +1 -1
  5. package/modules/auth/package.json +1 -1
  6. package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
  7. package/modules/auth/src/server/endpoints.ts +3 -1
  8. package/modules/auth/src/server/runtime.ts +4 -0
  9. package/modules/automations/package.json +1 -1
  10. package/modules/automations-workflows-integration/package.json +1 -1
  11. package/modules/profile/package.json +1 -1
  12. package/modules/sandbox/package.json +1 -1
  13. package/modules/system/package.json +1 -1
  14. package/modules/users/package.json +1 -1
  15. package/modules/workflows/package.json +1 -1
  16. package/package.json +5 -13
  17. package/packages/ai-provider/package.json +1 -1
  18. package/packages/cli-protocol/package.json +1 -1
  19. package/packages/client/package.json +1 -1
  20. package/packages/client/src/routing.ts +39 -2
  21. package/packages/client/src/shell/BrandMark.tsrx +5 -1
  22. package/packages/client/src/shell/navigation.ts +3 -1
  23. package/packages/client/src/state.ts +6 -2
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/contracts/schemas/project.schema.json +16 -42
  26. package/packages/database/package.json +1 -1
  27. package/packages/database-pglite/package.json +1 -1
  28. package/packages/database-testing/package.json +1 -1
  29. package/packages/dev-console/package.json +1 -1
  30. package/packages/harness/package.json +1 -1
  31. package/packages/kernel/package.json +1 -1
  32. package/packages/server/package.json +1 -1
  33. package/packages/server/src/application-routes.ts +80 -8
  34. package/packages/server/src/index.ts +4 -1
  35. package/packages/server/src/web-html.ts +9 -5
  36. package/packages/server/src/web.ts +59 -7
  37. package/packages/ui/package.json +1 -1
  38. package/packages/coding-agent/package.json +0 -44
  39. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  40. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  41. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  42. package/packages/coding-agent/src/index.ts +0 -56
  43. package/packages/coding-agent/src/registry.ts +0 -119
  44. package/packages/coding-agent/src/roles/contract.ts +0 -60
  45. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  46. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  47. package/packages/coding-agent/src/roles/registry.ts +0 -156
  48. package/packages/coding-agent/src/roles/skills.ts +0 -72
  49. package/packages/coding-agent/src/roles/sync.ts +0 -88
  50. package/packages/coding-agent/src/types.ts +0 -102
  51. package/packages/coding-agent/src/workspace.ts +0 -275
  52. package/packages/sandbox/README.md +0 -691
  53. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  54. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  55. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  56. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  57. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  84. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  85. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  86. package/packages/sandbox/dist/client/favicon.svg +0 -1
  87. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  88. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  89. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  90. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  91. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  92. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  93. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  94. package/packages/sandbox/dist/server/entry.js +0 -91671
  95. package/packages/sandbox/dist/server/favicon.svg +0 -1
  96. package/packages/sandbox/dist/server/index.html +0 -161
  97. package/packages/sandbox/index.html +0 -159
  98. package/packages/sandbox/octane.config.ts +0 -36
  99. package/packages/sandbox/package.json +0 -83
  100. package/packages/sandbox/public/favicon.svg +0 -1
  101. package/packages/sandbox/public/og.png +0 -0
  102. package/packages/sandbox/src/App.tsrx +0 -924
  103. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  104. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  105. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  106. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  107. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  108. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  109. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  110. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  111. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  112. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  113. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  114. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  115. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  116. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  117. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  118. package/packages/sandbox/src/client/api.ts +0 -797
  119. package/packages/sandbox/src/client/attachments.ts +0 -73
  120. package/packages/sandbox/src/client/dashboard.ts +0 -46
  121. package/packages/sandbox/src/client/github-settings.ts +0 -48
  122. package/packages/sandbox/src/client/highlight.ts +0 -105
  123. package/packages/sandbox/src/client/i18n.ts +0 -32
  124. package/packages/sandbox/src/client/locales/en.json +0 -466
  125. package/packages/sandbox/src/client/locales/pl.json +0 -466
  126. package/packages/sandbox/src/client/session-labels.ts +0 -77
  127. package/packages/sandbox/src/client/state.ts +0 -108
  128. package/packages/sandbox/src/index.ts +0 -10
  129. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  130. package/packages/sandbox/src/preview/load-module.ts +0 -16
  131. package/packages/sandbox/src/server/attachments.ts +0 -308
  132. package/packages/sandbox/src/server/auto-review.ts +0 -181
  133. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  134. package/packages/sandbox/src/server/config.ts +0 -460
  135. package/packages/sandbox/src/server/dashboard.ts +0 -245
  136. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  137. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  138. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  139. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  140. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  141. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  142. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  143. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  144. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  145. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  146. package/packages/sandbox/src/server/dependencies.ts +0 -88
  147. package/packages/sandbox/src/server/diff.ts +0 -230
  148. package/packages/sandbox/src/server/gates.ts +0 -388
  149. package/packages/sandbox/src/server/index.ts +0 -139
  150. package/packages/sandbox/src/server/path-guard.ts +0 -282
  151. package/packages/sandbox/src/server/planning.ts +0 -749
  152. package/packages/sandbox/src/server/platform-client.ts +0 -189
  153. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  154. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  155. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  156. package/packages/sandbox/src/server/preview-database.ts +0 -109
  157. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  158. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  159. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  160. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  161. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  162. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  163. package/packages/sandbox/src/server/reference.ts +0 -251
  164. package/packages/sandbox/src/server/routes.ts +0 -2258
  165. package/packages/sandbox/src/server/runtime.ts +0 -188
  166. package/packages/sandbox/src/server/session-owner.ts +0 -23
  167. package/packages/sandbox/src/server/sessions.ts +0 -849
  168. package/packages/sandbox/src/server/spec.ts +0 -471
  169. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  170. package/packages/sandbox/src/server/turns.ts +0 -993
  171. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  172. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  173. package/packages/sandbox/src/styles.css +0 -1573
  174. package/packages/sandbox/vite.config.ts +0 -133
@@ -1,21 +1,93 @@
1
- import { RenderRoute, ServerRoute, type Route, type RenderRouteEntry } from '@octanejs/app-core';
1
+ import { injectHtmlHead } from './web-html.ts';
2
+ import {
3
+ RenderRoute,
4
+ ServerRoute,
5
+ type Route,
6
+ type RenderRouteEntry,
7
+ } from '@octanejs/app-core';
2
8
 
3
9
  export function validateApplicationPath(path: string): string {
4
- if (!/^\/[a-z][a-z0-9-]*$/.test(path) || ['auth','api','sites','sign-in','sign-up','forgot-password','reset-password','accept-invitation'].includes(path.slice(1))) throw new Error('Invalid application path.');
10
+ if (
11
+ path.length > 64 ||
12
+ !/^\/[a-z][a-z0-9-]*$/.test(path) ||
13
+ [
14
+ 'setup',
15
+ 'health',
16
+ 'ready',
17
+ 'assets',
18
+ 'auth',
19
+ 'api',
20
+ 'sites',
21
+ 'sign-in',
22
+ 'sign-up',
23
+ 'forgot-password',
24
+ 'reset-password',
25
+ 'accept-invitation',
26
+ ].includes(path.slice(1))
27
+ )
28
+ throw new Error('Invalid application path.');
5
29
  return path;
6
30
  }
7
31
 
8
- export function createApplicationRoutes(options: { readonly path: string; readonly entry: RenderRouteEntry; readonly publicRoot: boolean }): readonly Route[] {
32
+ export function createApplicationRoutes(options: {
33
+ readonly path: string;
34
+ readonly entry: RenderRouteEntry;
35
+ readonly publicRoot: boolean;
36
+ }): readonly Route[] {
9
37
  const base = validateApplicationPath(options.path);
10
38
  const paths = [
11
- '/auth', '/auth/login', '/auth/register', '/auth/forgot-password', '/auth/reset-password', '/auth/accept-invitation', '/auth/mfa',
12
- '/sign-in','/sign-up','/forgot-password','/reset-password','/accept-invitation',
13
- base, base+'/:workspace', base+'/:workspace/:view',
39
+ '/auth',
40
+ '/auth/login',
41
+ '/auth/register',
42
+ '/auth/forgot-password',
43
+ '/auth/reset-password',
44
+ '/auth/accept-invitation',
45
+ '/auth/mfa',
46
+ '/sign-in',
47
+ '/sign-up',
48
+ '/forgot-password',
49
+ '/reset-password',
50
+ '/accept-invitation',
51
+ base,
52
+ base + '/:workspace',
53
+ base + '/:workspace/:view',
14
54
  ...(!options.publicRoot ? ['/', '/:workspace', '/:workspace/:view'] : []),
15
55
  ];
16
- const routes: Route[] = paths.map(path => new RenderRoute({path,entry:options.entry}));
56
+ const routes: Route[] = paths.map(
57
+ (path) =>
58
+ new RenderRoute({
59
+ path,
60
+ entry: options.entry,
61
+ before: [
62
+ async (context, next) => {
63
+ context.state.set('flowdular.application.path', base);
64
+ const response = await next();
65
+ response.headers.set('cache-control', 'no-store');
66
+ return injectHtmlHead(
67
+ response,
68
+ `<script id="flowdular-application-data" type="application/json">${JSON.stringify(base)}</script>`,
69
+ );
70
+ },
71
+ ],
72
+ }),
73
+ );
17
74
  if (base !== '/app') {
18
- for (const path of ['/app','/app/*legacyPath']) routes.push(new ServerRoute({path,methods:['GET','HEAD'],handler: context => new Response(null,{status:308,headers:{location:base+context.url.pathname.slice(4)+context.url.search,'cache-control':'no-store'}})}));
75
+ for (const path of ['/app', '/app/*legacyPath'])
76
+ routes.push(
77
+ new ServerRoute({
78
+ path,
79
+ methods: ['GET', 'HEAD'],
80
+ handler: (context) =>
81
+ new Response(null, {
82
+ status: 308,
83
+ headers: {
84
+ location:
85
+ base + context.url.pathname.slice(4) + context.url.search,
86
+ 'cache-control': 'no-store',
87
+ },
88
+ }),
89
+ }),
90
+ );
19
91
  }
20
92
  return routes;
21
93
  }
@@ -1,5 +1,8 @@
1
1
  export { defineEndpoint } from './endpoint.ts';
2
- export { createApplicationRoutes, validateApplicationPath } from './application-routes.ts';
2
+ export {
3
+ createApplicationRoutes,
4
+ validateApplicationPath,
5
+ } from './application-routes.ts';
3
6
  export { trackResponseBody } from './response-lifetime.ts';
4
7
  export {
5
8
  assertRouteConflicts,
@@ -5,11 +5,6 @@ export function webHtmlResponse(
5
5
  data: unknown,
6
6
  nonce?: string,
7
7
  ): Response {
8
- if (
9
- !response.body ||
10
- !response.headers.get('content-type')?.includes('text/html')
11
- )
12
- return response;
13
8
  const json = JSON.stringify({ url, data })
14
9
  .replace(/</g, '\\u003c')
15
10
  .replace(/\u2028/g, '\\u2028')
@@ -17,6 +12,15 @@ export function webHtmlResponse(
17
12
  const attribute =
18
13
  nonce && /^[a-zA-Z0-9+/=_-]+$/.test(nonce) ? ` nonce="${nonce}"` : '';
19
14
  const insert = `<script id="flowdular-web-data" type="application/json"${attribute}>${json}</script><style${attribute}>.flowdular-splash{display:none!important}html,body,#root{background:initial}</style>`;
15
+ return injectHtmlHead(response, insert);
16
+ }
17
+
18
+ export function injectHtmlHead(response: Response, insert: string): Response {
19
+ if (
20
+ !response.body ||
21
+ !response.headers.get('content-type')?.includes('text/html')
22
+ )
23
+ return response;
20
24
  let buffer = '';
21
25
  let inserted = false;
22
26
  const decoder = new TextDecoder();
@@ -1,3 +1,4 @@
1
+ import { validateApplicationPath } from './application-routes.ts';
1
2
  import {
2
3
  RenderRoute,
3
4
  ServerRoute,
@@ -29,6 +30,10 @@ export type {
29
30
  const DATA_STATE = 'flowdular.web.data';
30
31
  const DATA_ACCEPT = 'application/vnd.flowdular.page+json';
31
32
  const RESERVED = new Set([
33
+ 'setup',
34
+ 'health',
35
+ 'ready',
36
+ 'assets',
32
37
  'app',
33
38
  'api',
34
39
  'auth',
@@ -67,7 +72,7 @@ export function validateWebMounts(input: unknown): readonly WebMount[] {
67
72
  /[\u0000-\u001f]/.test(value.tenantId) ||
68
73
  typeof value.path !== 'string' ||
69
74
  value.path.length > 256 ||
70
- !/^\/[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(value.path) ||
75
+ !/^(?:\/|\/[a-z0-9-]+(?:\/[a-z0-9-]+)*)$/.test(value.path) ||
71
76
  RESERVED.has(value.path.split('/')[1]!) ||
72
77
  (value.enabled !== undefined && typeof value.enabled !== 'boolean')
73
78
  )
@@ -225,11 +230,22 @@ function failure(status: number): Response {
225
230
  export function createModuleWebRoutes(options: {
226
231
  readonly modules: readonly WebModuleComposition[];
227
232
  readonly mounts: readonly WebMount[];
233
+ readonly applicationPath?: string;
228
234
  readonly resolveIdentity: (
229
235
  context: Context,
230
236
  ) => WebIdentity | null | Promise<WebIdentity | null>;
231
237
  }): readonly Route[] {
232
- const mounts = validateWebMounts(options.mounts);
238
+ const mounts = [...validateWebMounts(options.mounts)].sort(
239
+ (a, b) => b.path.length - a.path.length,
240
+ );
241
+ const applicationPath = validateApplicationPath(
242
+ options.applicationPath ?? '/app',
243
+ );
244
+ const reserved = [...RESERVED]
245
+ .map((segment) => '/' + segment)
246
+ .concat(applicationPath);
247
+ const within = (path: string, prefix: string) =>
248
+ path === prefix || path.startsWith(prefix + '/');
233
249
  const surfaces = new Map<string, ModuleWebSurface>();
234
250
  for (const module of options.modules) {
235
251
  for (const surface of module.web ?? []) {
@@ -243,11 +259,32 @@ export function createModuleWebRoutes(options: {
243
259
  }
244
260
  const routes: Route[] = [];
245
261
  for (const site of mounts) {
262
+ if (reserved.some((prefix) => within(site.path, prefix)))
263
+ invalid('mount overlaps application routes.');
264
+ const base = site.path === '/' ? '' : site.path;
246
265
  const surface = surfaces.get(`${site.moduleId}/${site.surfaceId}`);
247
266
  // A removed or disabled module must not fall through to a workspace route.
248
267
  if (site.enabled && surface) {
249
268
  for (const page of surface.pages) {
269
+ if (
270
+ site.path === '/' &&
271
+ (reserved.some((prefix) => within(page.path, prefix)) ||
272
+ mounts.some(
273
+ (other) => other.path !== '/' && within(page.path, other.path),
274
+ ))
275
+ )
276
+ invalid('root page overlaps a reserved address.');
250
277
  const before: Middleware = async (context, next) => {
278
+ if (
279
+ site.path === '/' &&
280
+ (reserved.some((prefix) => within(context.url.pathname, prefix)) ||
281
+ mounts.some(
282
+ (other) =>
283
+ other.path !== '/' &&
284
+ within(context.url.pathname, other.path),
285
+ ))
286
+ )
287
+ return failure(404);
251
288
  if (!['GET', 'HEAD'].includes(context.request.method))
252
289
  return failure(405);
253
290
  try {
@@ -270,8 +307,20 @@ export function createModuleWebRoutes(options: {
270
307
  signal: context.request.signal,
271
308
  });
272
309
  if (result instanceof Response) {
273
- result.headers.set('cache-control', 'no-store');
274
- return result;
310
+ // Redirect/fetch response headers can be immutable.
311
+ const headers = new Headers(result.headers);
312
+ headers.set('cache-control', 'no-store');
313
+ headers.append('vary', 'Accept');
314
+ if (context.request.method === 'HEAD')
315
+ await result.body?.cancel();
316
+ return new Response(
317
+ context.request.method === 'HEAD' ? null : result.body,
318
+ {
319
+ status: result.status,
320
+ statusText: result.statusText,
321
+ headers,
322
+ },
323
+ );
275
324
  }
276
325
  const json = JSON.stringify(result);
277
326
  if (
@@ -318,7 +367,7 @@ export function createModuleWebRoutes(options: {
318
367
  };
319
368
  routes.push(
320
369
  new RenderRoute({
321
- path: site.path + (page.path === '/' ? '' : page.path),
370
+ path: base + (page.path === '/' ? '' : page.path) || '/',
322
371
  entry: page.entry,
323
372
  ...(page.layout ? { layout: page.layout } : {}),
324
373
  before: [before],
@@ -338,9 +387,12 @@ export function createModuleWebRoutes(options: {
338
387
  !surface.pages.some((page) => page.path === '/')
339
388
  )
340
389
  routes.push(fallback(site.path));
341
- routes.push(fallback(site.path + '/*unmatched'));
390
+ routes.push(fallback(base + '/*unmatched'));
342
391
  }
343
- if (!mounts.some((site) => site.path === '/sites')) {
392
+ if (
393
+ mounts.some((site) => site.path.startsWith('/sites/')) &&
394
+ !mounts.some((site) => site.path === '/sites')
395
+ ) {
344
396
  routes.push(
345
397
  new ServerRoute({
346
398
  path: '/sites',
@@ -21,7 +21,7 @@
21
21
  "devDependencies": {
22
22
  "@tsrx/typescript-plugin": "0.3.120",
23
23
  "typescript": "5.9.3",
24
- "vitest": "4.1.10"
24
+ "vitest": "4.1.11"
25
25
  },
26
26
  "license": "MIT",
27
27
  "repository": {
@@ -1,44 +0,0 @@
1
- {
2
- "name": "@flowdular/coding-agent",
3
- "version": "0.1.0",
4
- "type": "module",
5
- "exports": {
6
- ".": "./src/index.ts"
7
- },
8
- "scripts": {
9
- "typecheck": "tsc --noEmit -p tsconfig.json",
10
- "test": "vitest run",
11
- "sync-roles": "node --experimental-transform-types --disable-warning=ExperimentalWarning scripts/sync-roles.ts"
12
- },
13
- "dependencies": {
14
- "@flowdular/ai-provider": "workspace:*",
15
- "ai": "7.0.85",
16
- "yaml": "2.9.0"
17
- },
18
- "devDependencies": {
19
- "@flowdular/database-testing": "workspace:*",
20
- "@types/node": "24.13.3",
21
- "typescript": "5.9.3",
22
- "vitest": "4.1.10"
23
- },
24
- "license": "MIT",
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/Flowdular/flowdular.git",
28
- "directory": "packages/coding-agent"
29
- },
30
- "files": [
31
- "src",
32
- "schemas",
33
- "dist",
34
- "bin",
35
- "public",
36
- "module.json",
37
- "migrations",
38
- "translations",
39
- "spec",
40
- "README.md",
41
- "LICENSE"
42
- ],
43
- "private": true
44
- }
@@ -1,322 +0,0 @@
1
- import {
2
- mkdir,
3
- readFile,
4
- readdir,
5
- rm,
6
- stat,
7
- writeFile,
8
- } from 'node:fs/promises';
9
- import { dirname, join } from 'node:path';
10
- import {
11
- classifyProviderFailure,
12
- normalizeUsage,
13
- probeLanguageModel,
14
- resolveLanguageModel,
15
- type AiProviderConfiguration,
16
- } from '@flowdular/sdk/ai-provider';
17
- import {
18
- jsonSchema,
19
- stepCountIs,
20
- streamText,
21
- tool,
22
- type ModelMessage,
23
- type ToolSet,
24
- } from 'ai';
25
- import {
26
- type CodingAgentAvailability,
27
- type CodingAgentDriver,
28
- type CodingAgentEvent,
29
- type CodingAgentTurnRequest,
30
- type FileChangeKind,
31
- } from '../types.ts';
32
- import {
33
- resolveInsideWorkspace,
34
- resolveReadableInsideWorkspace,
35
- resolveWritableInsideWorkspace,
36
- workspaceRelative,
37
- } from '../workspace.ts';
38
-
39
- export interface ByokDriverOptions {
40
- readonly configuration: AiProviderConfiguration;
41
- readonly maxSteps?: number;
42
- readonly maxOutputTokens?: number;
43
- readonly maxFileBytes?: number;
44
- readonly maxListedFiles?: number;
45
- }
46
-
47
- const DEFAULT_MAX_STEPS = 24;
48
- const DEFAULT_MAX_OUTPUT_TOKENS = 8_192;
49
- const DEFAULT_MAX_FILE_BYTES = 128 * 1024;
50
- const DEFAULT_MAX_LISTED_FILES = 400;
51
- const IGNORED_DIRECTORIES = new Set(['node_modules', '.git', 'dist']);
52
-
53
- interface PendingEvent {
54
- readonly event: CodingAgentEvent;
55
- }
56
-
57
- async function listFiles(
58
- root: string,
59
- directory: string,
60
- limit: number,
61
- found: string[],
62
- ): Promise<void> {
63
- if (found.length >= limit) return;
64
- for (const entry of await readdir(directory, { withFileTypes: true })) {
65
- if (found.length >= limit) return;
66
- if (entry.name.startsWith('.') && entry.name !== '.ai') continue;
67
- if (IGNORED_DIRECTORIES.has(entry.name)) continue;
68
- const path = join(directory, entry.name);
69
- if (entry.isDirectory()) await listFiles(root, path, limit, found);
70
- else found.push(workspaceRelative(root, path));
71
- }
72
- }
73
-
74
- /* The BYOK driver is the only backend a self-hosted sandbox can offer, so its
75
- tools are the whole file surface the model gets: bounded to the session
76
- workspace, size capped, and with no command execution. */
77
- export function createByokDriver(
78
- options: ByokDriverOptions,
79
- ): CodingAgentDriver {
80
- const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
81
- const maxListedFiles = options.maxListedFiles ?? DEFAULT_MAX_LISTED_FILES;
82
-
83
- function workspaceTools(
84
- workspacePath: string,
85
- allowedPaths: readonly string[],
86
- emit: (event: CodingAgentEvent) => void,
87
- ): ToolSet {
88
- const record = (
89
- name: string,
90
- detail: string,
91
- run: () => Promise<string>,
92
- ) => {
93
- emit({ type: 'tool.started', tool: name, detail });
94
- return run().then(
95
- (value) => {
96
- emit({ type: 'tool.completed', tool: name, detail, ok: true });
97
- return value;
98
- },
99
- (error: unknown) => {
100
- emit({ type: 'tool.completed', tool: name, detail, ok: false });
101
- return `Error: ${error instanceof Error ? error.message : String(error)}`;
102
- },
103
- );
104
- };
105
- const changed = async (
106
- path: string,
107
- absolute: string,
108
- ): Promise<FileChangeKind> => {
109
- try {
110
- await stat(absolute);
111
- return 'modified';
112
- } catch {
113
- return 'created';
114
- }
115
- };
116
-
117
- return {
118
- list_files: tool({
119
- description:
120
- 'List the files of the session workspace, relative to its root.',
121
- inputSchema: jsonSchema<{ directory?: string }>({
122
- type: 'object',
123
- properties: {
124
- directory: {
125
- type: 'string',
126
- description: 'Optional subdirectory, relative to the workspace.',
127
- },
128
- },
129
- additionalProperties: false,
130
- }),
131
- execute: (input) =>
132
- record('list_files', input.directory ?? '.', async () => {
133
- const start = resolveInsideWorkspace(
134
- workspacePath,
135
- input.directory ?? '.',
136
- );
137
- const found: string[] = [];
138
- await listFiles(workspacePath, start, maxListedFiles, found);
139
- return found.sort().join('\n') || '(empty)';
140
- }),
141
- }),
142
- read_file: tool({
143
- description: 'Read one file of the session workspace.',
144
- inputSchema: jsonSchema<{ path: string }>({
145
- type: 'object',
146
- properties: { path: { type: 'string' } },
147
- required: ['path'],
148
- additionalProperties: false,
149
- }),
150
- execute: (input) =>
151
- record('read_file', input.path, async () => {
152
- const absolute = await resolveReadableInsideWorkspace(
153
- workspacePath,
154
- input.path,
155
- );
156
- const content = await readFile(absolute, 'utf8');
157
- return content.length > maxFileBytes
158
- ? `${content.slice(0, maxFileBytes)}\n… truncated at ${maxFileBytes} bytes.`
159
- : content;
160
- }),
161
- }),
162
- write_file: tool({
163
- description:
164
- 'Create or replace one file of the session workspace with the complete new content.',
165
- inputSchema: jsonSchema<{ path: string; content: string }>({
166
- type: 'object',
167
- properties: {
168
- path: { type: 'string' },
169
- content: { type: 'string' },
170
- },
171
- required: ['path', 'content'],
172
- additionalProperties: false,
173
- }),
174
- execute: (input) =>
175
- record('write_file', input.path, async () => {
176
- if (input.content.length > maxFileBytes) {
177
- throw new Error(
178
- `File content exceeds ${maxFileBytes} bytes for this workspace.`,
179
- );
180
- }
181
- const absolute = await resolveWritableInsideWorkspace(
182
- workspacePath,
183
- input.path,
184
- allowedPaths,
185
- );
186
- const change = await changed(input.path, absolute);
187
- await mkdir(dirname(absolute), { recursive: true });
188
- await writeFile(absolute, input.content, 'utf8');
189
- emit({
190
- type: 'file.changed',
191
- path: workspaceRelative(workspacePath, absolute),
192
- change,
193
- });
194
- return `Wrote ${workspaceRelative(workspacePath, absolute)}.`;
195
- }),
196
- }),
197
- delete_file: tool({
198
- description: 'Delete one file of the session workspace.',
199
- inputSchema: jsonSchema<{ path: string }>({
200
- type: 'object',
201
- properties: { path: { type: 'string' } },
202
- required: ['path'],
203
- additionalProperties: false,
204
- }),
205
- execute: (input) =>
206
- record('delete_file', input.path, async () => {
207
- const absolute = await resolveWritableInsideWorkspace(
208
- workspacePath,
209
- input.path,
210
- allowedPaths,
211
- );
212
- await rm(absolute, { force: true });
213
- emit({
214
- type: 'file.changed',
215
- path: workspaceRelative(workspacePath, absolute),
216
- change: 'deleted',
217
- });
218
- return `Deleted ${workspaceRelative(workspacePath, absolute)}.`;
219
- }),
220
- }),
221
- };
222
- }
223
-
224
- return {
225
- id: 'byok',
226
- label: 'Bring your own key',
227
- kind: 'byok',
228
- requiresLoopback: false,
229
- description:
230
- 'Vercel AI SDK provider driving the sandbox file tools. The only backend a self-hosted sandbox can offer.',
231
-
232
- async probe(): Promise<CodingAgentAvailability> {
233
- if (!options.configuration.credential.trim()) {
234
- return {
235
- available: false,
236
- detail: 'No provider credential is configured for this sandbox.',
237
- version: null,
238
- };
239
- }
240
- const readiness = await probeLanguageModel(options.configuration);
241
- return {
242
- available: readiness.healthy,
243
- detail: readiness.healthy
244
- ? `${options.configuration.kind} responded in ${readiness.latencyMs} ms.`
245
- : (readiness.errorCode ?? 'The provider probe failed.'),
246
- version: options.configuration.model,
247
- };
248
- },
249
-
250
- async *run(
251
- request: CodingAgentTurnRequest,
252
- ): AsyncIterable<CodingAgentEvent> {
253
- yield {
254
- type: 'turn.started',
255
- driver: 'byok',
256
- role: request.role,
257
- resumeId: request.resumeId ?? null,
258
- };
259
-
260
- const queue: PendingEvent[] = [];
261
- const emit = (event: CodingAgentEvent) => queue.push({ event });
262
- const messages: ModelMessage[] = [
263
- ...(request.history ?? []).map((message) => ({
264
- role: message.role,
265
- content: message.text,
266
- })),
267
- { role: 'user' as const, content: request.prompt },
268
- ];
269
-
270
- try {
271
- const result = streamText({
272
- model: resolveLanguageModel({
273
- ...options.configuration,
274
- ...(request.model ? { model: request.model } : {}),
275
- }),
276
- system: request.systemInstruction,
277
- messages,
278
- maxOutputTokens: options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
279
- maxRetries: 1,
280
- ...(request.signal ? { abortSignal: request.signal } : {}),
281
- stopWhen: stepCountIs(options.maxSteps ?? DEFAULT_MAX_STEPS),
282
- tools: workspaceTools(
283
- request.workspacePath,
284
- /* An omitted role boundary is no write authority. The sandbox always
285
- supplies an explicit allowlist for its selected specialist. */
286
- request.allowedPaths ?? [],
287
- emit,
288
- ),
289
- });
290
-
291
- let text = '';
292
- let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
293
- for await (const part of result.fullStream) {
294
- while (queue.length > 0) yield queue.shift()!.event;
295
- if (part.type === 'text-delta') text += part.text;
296
- if (part.type === 'finish') usage = normalizeUsage(part.totalUsage);
297
- if (part.type === 'error') throw part.error;
298
- }
299
- while (queue.length > 0) yield queue.shift()!.event;
300
- if (text.trim()) yield { type: 'assistant.message', text: text.trim() };
301
- yield {
302
- type: 'turn.completed',
303
- resumeId: request.resumeId ?? null,
304
- usage,
305
- costUsd: null,
306
- finishReason: 'stop',
307
- };
308
- } catch (error) {
309
- while (queue.length > 0) yield queue.shift()!.event;
310
- const failure = classifyProviderFailure(error);
311
- yield { type: 'error', code: failure.code, message: failure.message };
312
- yield {
313
- type: 'turn.completed',
314
- resumeId: request.resumeId ?? null,
315
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
316
- costUsd: null,
317
- finishReason: request.signal?.aborted ? 'aborted' : 'error',
318
- };
319
- }
320
- },
321
- };
322
- }