@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.
- package/.ai/skills/test-hardening/SKILL.md +1 -1
- package/README.md +69 -12
- package/assets/flowdular-banner.png +0 -0
- package/modules/agents/package.json +1 -1
- package/modules/auth/package.json +1 -1
- package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
- package/modules/auth/src/server/endpoints.ts +3 -1
- package/modules/auth/src/server/runtime.ts +4 -0
- package/modules/automations/package.json +1 -1
- package/modules/automations-workflows-integration/package.json +1 -1
- package/modules/profile/package.json +1 -1
- package/modules/sandbox/package.json +1 -1
- package/modules/system/package.json +1 -1
- package/modules/users/package.json +1 -1
- package/modules/workflows/package.json +1 -1
- package/package.json +5 -13
- package/packages/ai-provider/package.json +1 -1
- package/packages/cli-protocol/package.json +1 -1
- package/packages/client/package.json +1 -1
- package/packages/client/src/routing.ts +39 -2
- package/packages/client/src/shell/BrandMark.tsrx +5 -1
- package/packages/client/src/shell/navigation.ts +3 -1
- package/packages/client/src/state.ts +6 -2
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/schemas/project.schema.json +16 -42
- package/packages/database/package.json +1 -1
- package/packages/database-pglite/package.json +1 -1
- package/packages/database-testing/package.json +1 -1
- package/packages/dev-console/package.json +1 -1
- package/packages/harness/package.json +1 -1
- package/packages/kernel/package.json +1 -1
- package/packages/server/package.json +1 -1
- package/packages/server/src/application-routes.ts +80 -8
- package/packages/server/src/index.ts +4 -1
- package/packages/server/src/web-html.ts +9 -5
- package/packages/server/src/web.ts +59 -7
- package/packages/ui/package.json +1 -1
- package/packages/coding-agent/package.json +0 -44
- package/packages/coding-agent/src/drivers/byok.ts +0 -322
- package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
- package/packages/coding-agent/src/drivers/codex.ts +0 -281
- package/packages/coding-agent/src/index.ts +0 -56
- package/packages/coding-agent/src/registry.ts +0 -119
- package/packages/coding-agent/src/roles/contract.ts +0 -60
- package/packages/coding-agent/src/roles/defaults.ts +0 -134
- package/packages/coding-agent/src/roles/handoff.ts +0 -41
- package/packages/coding-agent/src/roles/registry.ts +0 -156
- package/packages/coding-agent/src/roles/skills.ts +0 -72
- package/packages/coding-agent/src/roles/sync.ts +0 -88
- package/packages/coding-agent/src/types.ts +0 -102
- package/packages/coding-agent/src/workspace.ts +0 -275
- package/packages/sandbox/README.md +0 -691
- package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
- package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
- package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
- package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
- package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
- package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
- package/packages/sandbox/dist/client/favicon.svg +0 -1
- package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
- package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
- package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
- package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
- package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
- package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
- package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
- package/packages/sandbox/dist/server/entry.js +0 -91671
- package/packages/sandbox/dist/server/favicon.svg +0 -1
- package/packages/sandbox/dist/server/index.html +0 -161
- package/packages/sandbox/index.html +0 -159
- package/packages/sandbox/octane.config.ts +0 -36
- package/packages/sandbox/package.json +0 -83
- package/packages/sandbox/public/favicon.svg +0 -1
- package/packages/sandbox/public/og.png +0 -0
- package/packages/sandbox/src/App.tsrx +0 -924
- package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
- package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
- package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
- package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
- package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
- package/packages/sandbox/src/client/GateResults.tsrx +0 -60
- package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
- package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
- package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
- package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
- package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
- package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
- package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
- package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
- package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
- package/packages/sandbox/src/client/api.ts +0 -797
- package/packages/sandbox/src/client/attachments.ts +0 -73
- package/packages/sandbox/src/client/dashboard.ts +0 -46
- package/packages/sandbox/src/client/github-settings.ts +0 -48
- package/packages/sandbox/src/client/highlight.ts +0 -105
- package/packages/sandbox/src/client/i18n.ts +0 -32
- package/packages/sandbox/src/client/locales/en.json +0 -466
- package/packages/sandbox/src/client/locales/pl.json +0 -466
- package/packages/sandbox/src/client/session-labels.ts +0 -77
- package/packages/sandbox/src/client/state.ts +0 -108
- package/packages/sandbox/src/index.ts +0 -10
- package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
- package/packages/sandbox/src/preview/load-module.ts +0 -16
- package/packages/sandbox/src/server/attachments.ts +0 -308
- package/packages/sandbox/src/server/auto-review.ts +0 -181
- package/packages/sandbox/src/server/checkpoints.ts +0 -90
- package/packages/sandbox/src/server/config.ts +0 -460
- package/packages/sandbox/src/server/dashboard.ts +0 -245
- package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
- package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
- package/packages/sandbox/src/server/delivery/index.ts +0 -83
- package/packages/sandbox/src/server/delivery/local.ts +0 -151
- package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
- package/packages/sandbox/src/server/delivery/plan.ts +0 -218
- package/packages/sandbox/src/server/delivery/policies.ts +0 -239
- package/packages/sandbox/src/server/delivery/record.ts +0 -41
- package/packages/sandbox/src/server/delivery/steps.ts +0 -464
- package/packages/sandbox/src/server/delivery/types.ts +0 -158
- package/packages/sandbox/src/server/dependencies.ts +0 -88
- package/packages/sandbox/src/server/diff.ts +0 -230
- package/packages/sandbox/src/server/gates.ts +0 -388
- package/packages/sandbox/src/server/index.ts +0 -139
- package/packages/sandbox/src/server/path-guard.ts +0 -282
- package/packages/sandbox/src/server/planning.ts +0 -749
- package/packages/sandbox/src/server/platform-client.ts +0 -189
- package/packages/sandbox/src/server/preview-database-host.ts +0 -340
- package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
- package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
- package/packages/sandbox/src/server/preview-database.ts +0 -109
- package/packages/sandbox/src/server/preview-ipc.ts +0 -15
- package/packages/sandbox/src/server/preview-modules.ts +0 -83
- package/packages/sandbox/src/server/preview-revision.ts +0 -44
- package/packages/sandbox/src/server/preview-runtime.ts +0 -467
- package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
- package/packages/sandbox/src/server/preview-worker.ts +0 -168
- package/packages/sandbox/src/server/reference.ts +0 -251
- package/packages/sandbox/src/server/routes.ts +0 -2258
- package/packages/sandbox/src/server/runtime.ts +0 -188
- package/packages/sandbox/src/server/session-owner.ts +0 -23
- package/packages/sandbox/src/server/sessions.ts +0 -849
- package/packages/sandbox/src/server/spec.ts +0 -471
- package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
- package/packages/sandbox/src/server/turns.ts +0 -993
- package/packages/sandbox/src/server/workspace-install.ts +0 -266
- package/packages/sandbox/src/server/workspace-root.ts +0 -73
- package/packages/sandbox/src/styles.css +0 -1573
- package/packages/sandbox/vite.config.ts +0 -133
|
@@ -1,21 +1,93 @@
|
|
|
1
|
-
import {
|
|
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 (
|
|
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: {
|
|
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',
|
|
12
|
-
'/
|
|
13
|
-
|
|
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(
|
|
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'])
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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:
|
|
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(
|
|
390
|
+
routes.push(fallback(base + '/*unmatched'));
|
|
342
391
|
}
|
|
343
|
-
if (
|
|
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',
|
package/packages/ui/package.json
CHANGED
|
@@ -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
|
-
}
|