@flowdular/sdk 0.1.0 → 0.2.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 (175) 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 +2 -2
  28. package/packages/database-pglite/src/driver.ts +21 -3
  29. package/packages/database-testing/package.json +1 -1
  30. package/packages/dev-console/package.json +1 -1
  31. package/packages/harness/package.json +1 -1
  32. package/packages/kernel/package.json +1 -1
  33. package/packages/server/package.json +1 -1
  34. package/packages/server/src/application-routes.ts +80 -8
  35. package/packages/server/src/index.ts +4 -1
  36. package/packages/server/src/web-html.ts +9 -5
  37. package/packages/server/src/web.ts +59 -7
  38. package/packages/ui/package.json +1 -1
  39. package/packages/coding-agent/package.json +0 -44
  40. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  41. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  42. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  43. package/packages/coding-agent/src/index.ts +0 -56
  44. package/packages/coding-agent/src/registry.ts +0 -119
  45. package/packages/coding-agent/src/roles/contract.ts +0 -60
  46. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  47. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  48. package/packages/coding-agent/src/roles/registry.ts +0 -156
  49. package/packages/coding-agent/src/roles/skills.ts +0 -72
  50. package/packages/coding-agent/src/roles/sync.ts +0 -88
  51. package/packages/coding-agent/src/types.ts +0 -102
  52. package/packages/coding-agent/src/workspace.ts +0 -275
  53. package/packages/sandbox/README.md +0 -691
  54. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  55. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  56. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  57. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  84. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  85. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  86. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  87. package/packages/sandbox/dist/client/favicon.svg +0 -1
  88. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  89. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  90. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  91. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  92. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  93. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  94. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  95. package/packages/sandbox/dist/server/entry.js +0 -91671
  96. package/packages/sandbox/dist/server/favicon.svg +0 -1
  97. package/packages/sandbox/dist/server/index.html +0 -161
  98. package/packages/sandbox/index.html +0 -159
  99. package/packages/sandbox/octane.config.ts +0 -36
  100. package/packages/sandbox/package.json +0 -83
  101. package/packages/sandbox/public/favicon.svg +0 -1
  102. package/packages/sandbox/public/og.png +0 -0
  103. package/packages/sandbox/src/App.tsrx +0 -924
  104. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  105. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  106. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  107. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  108. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  109. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  110. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  111. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  112. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  113. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  114. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  115. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  116. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  117. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  118. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  119. package/packages/sandbox/src/client/api.ts +0 -797
  120. package/packages/sandbox/src/client/attachments.ts +0 -73
  121. package/packages/sandbox/src/client/dashboard.ts +0 -46
  122. package/packages/sandbox/src/client/github-settings.ts +0 -48
  123. package/packages/sandbox/src/client/highlight.ts +0 -105
  124. package/packages/sandbox/src/client/i18n.ts +0 -32
  125. package/packages/sandbox/src/client/locales/en.json +0 -466
  126. package/packages/sandbox/src/client/locales/pl.json +0 -466
  127. package/packages/sandbox/src/client/session-labels.ts +0 -77
  128. package/packages/sandbox/src/client/state.ts +0 -108
  129. package/packages/sandbox/src/index.ts +0 -10
  130. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  131. package/packages/sandbox/src/preview/load-module.ts +0 -16
  132. package/packages/sandbox/src/server/attachments.ts +0 -308
  133. package/packages/sandbox/src/server/auto-review.ts +0 -181
  134. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  135. package/packages/sandbox/src/server/config.ts +0 -460
  136. package/packages/sandbox/src/server/dashboard.ts +0 -245
  137. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  138. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  139. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  140. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  141. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  142. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  143. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  144. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  145. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  146. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  147. package/packages/sandbox/src/server/dependencies.ts +0 -88
  148. package/packages/sandbox/src/server/diff.ts +0 -230
  149. package/packages/sandbox/src/server/gates.ts +0 -388
  150. package/packages/sandbox/src/server/index.ts +0 -139
  151. package/packages/sandbox/src/server/path-guard.ts +0 -282
  152. package/packages/sandbox/src/server/planning.ts +0 -749
  153. package/packages/sandbox/src/server/platform-client.ts +0 -189
  154. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  155. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  156. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  157. package/packages/sandbox/src/server/preview-database.ts +0 -109
  158. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  159. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  160. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  161. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  162. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  163. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  164. package/packages/sandbox/src/server/reference.ts +0 -251
  165. package/packages/sandbox/src/server/routes.ts +0 -2258
  166. package/packages/sandbox/src/server/runtime.ts +0 -188
  167. package/packages/sandbox/src/server/session-owner.ts +0 -23
  168. package/packages/sandbox/src/server/sessions.ts +0 -849
  169. package/packages/sandbox/src/server/spec.ts +0 -471
  170. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  171. package/packages/sandbox/src/server/turns.ts +0 -993
  172. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  173. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  174. package/packages/sandbox/src/styles.css +0 -1573
  175. package/packages/sandbox/vite.config.ts +0 -133
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "vite": "8.2.2",
16
- "vitest": "4.1.10"
16
+ "vitest": "4.1.11"
17
17
  },
18
18
  "license": "MIT",
19
19
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "devDependencies": {
23
23
  "@types/node": "24.13.3",
24
24
  "typescript": "5.9.3",
25
- "vitest": "4.1.10"
25
+ "vitest": "4.1.11"
26
26
  },
27
27
  "license": "MIT",
28
28
  "repository": {
@@ -19,7 +19,7 @@
19
19
  "devDependencies": {
20
20
  "@types/node": "24.13.3",
21
21
  "typescript": "5.9.3",
22
- "vitest": "4.1.10"
22
+ "vitest": "4.1.11"
23
23
  },
24
24
  "license": "MIT",
25
25
  "repository": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "devDependencies": {
19
19
  "typescript": "5.9.3",
20
- "vitest": "4.1.10",
20
+ "vitest": "4.1.11",
21
21
  "@types/node": "24.13.3"
22
22
  },
23
23
  "license": "MIT",
@@ -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
- }