@positronic/template-new-project 0.0.62 → 0.0.64
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/index.js +4 -4
- package/package.json +1 -1
- package/template/CLAUDE.md +13 -3
- package/template/_gitignore +1 -0
- package/template/docs/brain-dsl-guide.md +97 -19
- package/template/docs/tips-for-agents.md +8 -6
package/index.js
CHANGED
|
@@ -53,10 +53,10 @@ module.exports = {
|
|
|
53
53
|
],
|
|
54
54
|
setup: async ctx => {
|
|
55
55
|
const devRootPath = process.env.POSITRONIC_LOCAL_PATH;
|
|
56
|
-
let coreVersion = '^0.0.
|
|
57
|
-
let cloudflareVersion = '^0.0.
|
|
58
|
-
let clientVercelVersion = '^0.0.
|
|
59
|
-
let genUIComponentsVersion = '^0.0.
|
|
56
|
+
let coreVersion = '^0.0.64';
|
|
57
|
+
let cloudflareVersion = '^0.0.64';
|
|
58
|
+
let clientVercelVersion = '^0.0.64';
|
|
59
|
+
let genUIComponentsVersion = '^0.0.64';
|
|
60
60
|
|
|
61
61
|
// Map backend selection to package names
|
|
62
62
|
const backendPackageMap = {
|
package/package.json
CHANGED
package/template/CLAUDE.md
CHANGED
|
@@ -101,7 +101,7 @@ export default approvalWebhook;
|
|
|
101
101
|
|
|
102
102
|
### Using Webhooks in Brains
|
|
103
103
|
|
|
104
|
-
Import the webhook and use
|
|
104
|
+
Import the webhook and use `.wait()` to pause execution:
|
|
105
105
|
|
|
106
106
|
```typescript
|
|
107
107
|
import { brain } from '../brain.js';
|
|
@@ -109,9 +109,9 @@ import approvalWebhook from '../webhooks/approval.js';
|
|
|
109
109
|
|
|
110
110
|
export default brain('approval-workflow')
|
|
111
111
|
.step('Request approval', ({ state }) => ({
|
|
112
|
-
|
|
113
|
-
waitFor: [approvalWebhook(state.requestId)], // pause and wait
|
|
112
|
+
...state, status: 'pending',
|
|
114
113
|
}))
|
|
114
|
+
.wait('Wait for approval', ({ state }) => approvalWebhook(state.requestId))
|
|
115
115
|
.step('Process approval', ({ state, response }) => ({
|
|
116
116
|
...state,
|
|
117
117
|
status: response.approved ? 'approved' : 'rejected',
|
|
@@ -119,6 +119,16 @@ export default brain('approval-workflow')
|
|
|
119
119
|
}));
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
### CSRF Tokens for Pages with Forms
|
|
123
|
+
|
|
124
|
+
If your brain generates a custom HTML page with a form that submits to a webhook, you must include a CSRF token. Without a token, the server will reject the submission.
|
|
125
|
+
|
|
126
|
+
1. Generate a token with `generateFormToken()` from `@positronic/core`
|
|
127
|
+
2. Add `<input type="hidden" name="__positronic_token" value="${token}">` to the form
|
|
128
|
+
3. Pass the token when creating the webhook registration: `myWebhook(identifier, token)`
|
|
129
|
+
|
|
130
|
+
The `.ui()` step handles this automatically. See `/docs/brain-dsl-guide.md` for full examples.
|
|
131
|
+
|
|
122
132
|
### How Auto-Discovery Works
|
|
123
133
|
|
|
124
134
|
- Place webhook files in `/webhooks` directory
|
package/template/_gitignore
CHANGED
|
@@ -202,7 +202,7 @@ Each step receives these parameters:
|
|
|
202
202
|
- `client` - AI client for generating structured objects
|
|
203
203
|
- `resources` - Loaded resources (files, documents, etc.)
|
|
204
204
|
- `options` - Runtime options passed to the brain
|
|
205
|
-
- `response` - Webhook response data (available after `
|
|
205
|
+
- `response` - Webhook response data (available after `.wait()` completes)
|
|
206
206
|
- `page` - Generated page object (available after `.ui()` step)
|
|
207
207
|
- `pages` - Pages service for HTML page management
|
|
208
208
|
- `env` - Runtime environment containing `origin` (base URL) and `secrets` (typed secrets object)
|
|
@@ -867,8 +867,6 @@ brain('Batch Processor')
|
|
|
867
867
|
}, {
|
|
868
868
|
over: (state) => state.items, // Array to iterate over
|
|
869
869
|
concurrency: 10, // Parallel requests (default: 10)
|
|
870
|
-
stagger: 100, // Delay between requests in ms
|
|
871
|
-
maxRetries: 3,
|
|
872
870
|
error: (item, error) => ({ summary: 'Failed to summarize' }) // Fallback on error
|
|
873
871
|
})
|
|
874
872
|
.step('Process Results', ({ state }) => ({
|
|
@@ -884,9 +882,7 @@ brain('Batch Processor')
|
|
|
884
882
|
### Batch Options
|
|
885
883
|
|
|
886
884
|
- `over: (state) => T[]` - Function returning the array to iterate over
|
|
887
|
-
- `concurrency: number` - Maximum parallel
|
|
888
|
-
- `stagger: number` - Milliseconds to wait between starting requests
|
|
889
|
-
- `maxRetries: number` - Maximum number of retries for failed requests (passed to the AI client SDK)
|
|
885
|
+
- `concurrency: number` - Maximum number of items processed in parallel (default: 10)
|
|
890
886
|
- `error: (item, error) => Response` - Fallback function when a request fails
|
|
891
887
|
|
|
892
888
|
### Result Format
|
|
@@ -1103,9 +1099,90 @@ The created page object contains:
|
|
|
1103
1099
|
- `url: string` - Public URL to access the page
|
|
1104
1100
|
- `webhook: WebhookConfig` - Webhook configuration for handling form submissions
|
|
1105
1101
|
|
|
1102
|
+
### Custom Pages with Forms (CSRF Token)
|
|
1103
|
+
|
|
1104
|
+
When building custom HTML pages with forms, you must include a CSRF token to prevent unauthorized submissions. The `.ui()` step handles this automatically, but custom pages require manual setup. This applies whether you submit to the built-in `ui-form` endpoint or to a custom webhook.
|
|
1105
|
+
|
|
1106
|
+
#### Using a Custom Webhook
|
|
1107
|
+
|
|
1108
|
+
If your page submits to a custom webhook (e.g., `/webhooks/archive`), pass the token as the second argument when creating the webhook registration:
|
|
1109
|
+
|
|
1110
|
+
```typescript
|
|
1111
|
+
import { generateFormToken } from '@positronic/core';
|
|
1112
|
+
import archiveWebhook from '../webhooks/archive.js';
|
|
1113
|
+
|
|
1114
|
+
brain('Archive Workflow')
|
|
1115
|
+
.step('Create Page', async ({ state, pages, env }) => {
|
|
1116
|
+
const formToken = generateFormToken();
|
|
1117
|
+
|
|
1118
|
+
const html = `<html>
|
|
1119
|
+
<body>
|
|
1120
|
+
<form method="POST" action="<%= '${env.origin}' %>/webhooks/archive">
|
|
1121
|
+
<input type="hidden" name="__positronic_token" value="<%= '${formToken}' %>">
|
|
1122
|
+
<input type="text" name="name" placeholder="Your name">
|
|
1123
|
+
<button type="submit">Submit</button>
|
|
1124
|
+
</form>
|
|
1125
|
+
</body>
|
|
1126
|
+
</html>`;
|
|
1127
|
+
|
|
1128
|
+
await pages.create('my-page', html);
|
|
1129
|
+
return { ...state, formToken };
|
|
1130
|
+
})
|
|
1131
|
+
.wait('Wait for submission', ({ state }) => archiveWebhook(state.sessionId, state.formToken))
|
|
1132
|
+
.step('Process', ({ state, response }) => ({
|
|
1133
|
+
...state,
|
|
1134
|
+
name: response.name,
|
|
1135
|
+
}));
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
#### Using the System `ui-form` Endpoint
|
|
1139
|
+
|
|
1140
|
+
If your page submits to the built-in `ui-form` endpoint, include the token in the webhook registration object:
|
|
1141
|
+
|
|
1142
|
+
```typescript
|
|
1143
|
+
import { generateFormToken } from '@positronic/core';
|
|
1144
|
+
|
|
1145
|
+
brain('Custom Form')
|
|
1146
|
+
.step('Create Form Page', async ({ state, pages, env }) => {
|
|
1147
|
+
const formToken = generateFormToken();
|
|
1148
|
+
const webhookIdentifier = `custom-form-<%= '${Date.now()}' %>`;
|
|
1149
|
+
const formAction = `<%= '${env.origin}' %>/webhooks/system/ui-form?identifier=<%= '${encodeURIComponent(webhookIdentifier)}' %>`;
|
|
1150
|
+
|
|
1151
|
+
const page = await pages.create('my-form', `<html>
|
|
1152
|
+
<body>
|
|
1153
|
+
<form method="POST" action="<%= '${formAction}' %>">
|
|
1154
|
+
<input type="hidden" name="__positronic_token" value="<%= '${formToken}' %>">
|
|
1155
|
+
<input type="text" name="name" placeholder="Your name">
|
|
1156
|
+
<button type="submit">Submit</button>
|
|
1157
|
+
</form>
|
|
1158
|
+
</body>
|
|
1159
|
+
</html>`);
|
|
1160
|
+
|
|
1161
|
+
return {
|
|
1162
|
+
...state,
|
|
1163
|
+
pageUrl: page.url,
|
|
1164
|
+
webhook: { slug: 'ui-form', identifier: webhookIdentifier, token: formToken },
|
|
1165
|
+
};
|
|
1166
|
+
})
|
|
1167
|
+
.wait('Wait for form', ({ state }) => state.webhook)
|
|
1168
|
+
.step('Process', ({ state, response }) => ({
|
|
1169
|
+
...state,
|
|
1170
|
+
name: response.name,
|
|
1171
|
+
}));
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
#### Summary
|
|
1175
|
+
|
|
1176
|
+
The three required pieces for any custom page with a form:
|
|
1177
|
+
1. Call `generateFormToken()` to get a token
|
|
1178
|
+
2. Add `<input type="hidden" name="__positronic_token" value="...">` to your form
|
|
1179
|
+
3. Include the `token` in your webhook registration — either as the second argument to a custom webhook function (e.g., `myWebhook(identifier, token)`) or in the registration object for `ui-form`
|
|
1180
|
+
|
|
1181
|
+
Without a token, the server will reject the form submission.
|
|
1182
|
+
|
|
1106
1183
|
## UI Steps
|
|
1107
1184
|
|
|
1108
|
-
UI steps allow brains to generate dynamic user interfaces using AI. The `.ui()` step generates a page and provides a `page` object to the next step. You then notify users and use `
|
|
1185
|
+
UI steps allow brains to generate dynamic user interfaces using AI. The `.ui()` step generates a page and provides a `page` object to the next step. You then notify users and use `.wait()` to pause until the form is submitted.
|
|
1109
1186
|
|
|
1110
1187
|
### Basic UI Step
|
|
1111
1188
|
|
|
@@ -1128,14 +1205,13 @@ brain('Feedback Collector')
|
|
|
1128
1205
|
comments: z.string(),
|
|
1129
1206
|
}),
|
|
1130
1207
|
})
|
|
1131
|
-
// Notify user
|
|
1132
|
-
.step('Notify
|
|
1208
|
+
// Notify user
|
|
1209
|
+
.step('Notify', async ({ state, page, slack }) => {
|
|
1133
1210
|
await slack.post('#feedback', `Please fill out: <%= '${page.url}' %>`);
|
|
1134
|
-
return
|
|
1135
|
-
state,
|
|
1136
|
-
waitFor: [page.webhook],
|
|
1137
|
-
};
|
|
1211
|
+
return state;
|
|
1138
1212
|
})
|
|
1213
|
+
// Wait for form submission
|
|
1214
|
+
.wait('Wait for submission', ({ page }) => page.webhook)
|
|
1139
1215
|
// Process the form data (comes through response, not page)
|
|
1140
1216
|
.step('Process Feedback', ({ state, response }) => ({
|
|
1141
1217
|
...state,
|
|
@@ -1151,8 +1227,8 @@ brain('Feedback Collector')
|
|
|
1151
1227
|
2. **AI Generation**: The AI creates a component tree based on the prompt
|
|
1152
1228
|
3. **Page Object**: Next step receives `page` with `url` and `webhook`
|
|
1153
1229
|
4. **Notification**: You notify users however you want (Slack, email, etc.)
|
|
1154
|
-
5. **Wait**: Use
|
|
1155
|
-
6. **Form Data**: Step after `
|
|
1230
|
+
5. **Wait**: Use `.wait('title', ({ page }) => page.webhook)` to pause until form submission
|
|
1231
|
+
6. **Form Data**: Step after `.wait()` receives form data via `response`
|
|
1156
1232
|
|
|
1157
1233
|
### The `page` Object
|
|
1158
1234
|
|
|
@@ -1225,10 +1301,11 @@ brain('User Onboarding')
|
|
|
1225
1301
|
dob: z.string(),
|
|
1226
1302
|
}),
|
|
1227
1303
|
})
|
|
1228
|
-
.step('
|
|
1304
|
+
.step('Notify Personal', async ({ state, page, notify }) => {
|
|
1229
1305
|
await notify(`Step 1: <%= '${page.url}' %>`);
|
|
1230
|
-
return
|
|
1306
|
+
return state;
|
|
1231
1307
|
})
|
|
1308
|
+
.wait('Wait for Personal', ({ page }) => page.webhook)
|
|
1232
1309
|
.step('Save Personal', ({ state, response }) => ({
|
|
1233
1310
|
...state,
|
|
1234
1311
|
userData: { ...state.userData, ...response },
|
|
@@ -1247,10 +1324,11 @@ brain('User Onboarding')
|
|
|
1247
1324
|
contactMethod: z.enum(['email', 'phone', 'sms']),
|
|
1248
1325
|
}),
|
|
1249
1326
|
})
|
|
1250
|
-
.step('
|
|
1327
|
+
.step('Notify Preferences', async ({ state, page, notify }) => {
|
|
1251
1328
|
await notify(`Step 2: <%= '${page.url}' %>`);
|
|
1252
|
-
return
|
|
1329
|
+
return state;
|
|
1253
1330
|
})
|
|
1331
|
+
.wait('Wait for Preferences', ({ page }) => page.webhook)
|
|
1254
1332
|
.step('Complete', ({ state, response }) => ({
|
|
1255
1333
|
...state,
|
|
1256
1334
|
userData: { ...state.userData, preferences: response },
|
|
@@ -207,8 +207,8 @@ Most generated brains should not have try-catch blocks. Only use them when the e
|
|
|
207
207
|
When you need to collect user input, use the `.ui()` method. The pattern is:
|
|
208
208
|
1. `.ui()` generates the page
|
|
209
209
|
2. Next step gets `page.url` and `page.webhook`
|
|
210
|
-
3. Notify users
|
|
211
|
-
4. Step after `
|
|
210
|
+
3. Notify users, then use `.wait()` with `page.webhook`
|
|
211
|
+
4. Step after `.wait()` gets form data in `response`
|
|
212
212
|
|
|
213
213
|
```typescript
|
|
214
214
|
import { z } from 'zod';
|
|
@@ -231,11 +231,13 @@ brain('feedback-collector')
|
|
|
231
231
|
comments: z.string(),
|
|
232
232
|
}),
|
|
233
233
|
})
|
|
234
|
-
// Notify
|
|
234
|
+
// Notify users
|
|
235
235
|
.step('Notify', async ({ state, page, slack }) => {
|
|
236
236
|
await slack.post('#feedback', `Fill out: <%= '${page.url}' %>`);
|
|
237
|
-
return
|
|
237
|
+
return state;
|
|
238
238
|
})
|
|
239
|
+
// Wait for form submission
|
|
240
|
+
.wait('Wait for submission', ({ page }) => page.webhook)
|
|
239
241
|
// Form data comes through response (not page)
|
|
240
242
|
.step('Process', ({ state, response }) => ({
|
|
241
243
|
...state,
|
|
@@ -246,8 +248,8 @@ brain('feedback-collector')
|
|
|
246
248
|
|
|
247
249
|
Key points:
|
|
248
250
|
- `page.url` - where to send users
|
|
249
|
-
- `page.webhook` - use with `
|
|
250
|
-
- `response` - form data arrives here (in step after `
|
|
251
|
+
- `page.webhook` - use with `.wait()` to pause for submission
|
|
252
|
+
- `response` - form data arrives here (in step after `.wait()`)
|
|
251
253
|
- You control how users are notified (Slack, email, etc.)
|
|
252
254
|
|
|
253
255
|
See `/docs/brain-dsl-guide.md` for more UI step examples.
|