@positronic/template-new-project 0.0.63 → 0.0.65
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 -1
- package/template/_gitignore +1 -0
- package/template/docs/brain-dsl-guide.md +85 -7
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.65';
|
|
57
|
+
let cloudflareVersion = '^0.0.65';
|
|
58
|
+
let clientVercelVersion = '^0.0.65';
|
|
59
|
+
let genUIComponentsVersion = '^0.0.65';
|
|
60
60
|
|
|
61
61
|
// Map backend selection to package names
|
|
62
62
|
const backendPackageMap = {
|
package/package.json
CHANGED
package/template/CLAUDE.md
CHANGED
|
@@ -111,7 +111,7 @@ export default brain('approval-workflow')
|
|
|
111
111
|
.step('Request approval', ({ state }) => ({
|
|
112
112
|
...state, status: 'pending',
|
|
113
113
|
}))
|
|
114
|
-
.wait('Wait for approval', ({ state }) => approvalWebhook(state.requestId))
|
|
114
|
+
.wait('Wait for approval', ({ state }) => approvalWebhook(state.requestId), { timeout: '24h' })
|
|
115
115
|
.step('Process approval', ({ state, response }) => ({
|
|
116
116
|
...state,
|
|
117
117
|
status: response.approved ? 'approved' : 'rejected',
|
|
@@ -119,6 +119,18 @@ export default brain('approval-workflow')
|
|
|
119
119
|
}));
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
The optional `timeout` parameter accepts durations like `'30m'`, `'1h'`, `'24h'`, `'7d'`, or a number in milliseconds. If the timeout elapses without a webhook response, the brain is cancelled. Without a timeout, the brain waits indefinitely.
|
|
123
|
+
|
|
124
|
+
### CSRF Tokens for Pages with Forms
|
|
125
|
+
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
1. Generate a token with `generateFormToken()` from `@positronic/core`
|
|
129
|
+
2. Add `<input type="hidden" name="__positronic_token" value="${token}">` to the form
|
|
130
|
+
3. Pass the token when creating the webhook registration: `myWebhook(identifier, token)`
|
|
131
|
+
|
|
132
|
+
The `.ui()` step handles this automatically. See `/docs/brain-dsl-guide.md` for full examples.
|
|
133
|
+
|
|
122
134
|
### How Auto-Discovery Works
|
|
123
135
|
|
|
124
136
|
- Place webhook files in `/webhooks` directory
|
package/template/_gitignore
CHANGED
|
@@ -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
|
|
@@ -1003,6 +999,7 @@ Key points about tool `waitFor`:
|
|
|
1003
999
|
- You can wait for multiple webhooks (first response wins): `{ waitFor: [webhook1(...), webhook2(...)] }`
|
|
1004
1000
|
- The `execute` function receives a `context` parameter with access to `state`, `options`, `env`, etc.
|
|
1005
1001
|
- Use this pattern for approvals, external API callbacks, or any human-in-the-loop workflow
|
|
1002
|
+
- The built-in `waitForWebhook` tool defaults to a 1-hour timeout. Agents can customize via the `timeout` parameter (e.g., "30m", "24h", "7d"). If the timeout elapses, the brain is cancelled.
|
|
1006
1003
|
|
|
1007
1004
|
### Agent Output Schema
|
|
1008
1005
|
|
|
@@ -1103,6 +1100,87 @@ The created page object contains:
|
|
|
1103
1100
|
- `url: string` - Public URL to access the page
|
|
1104
1101
|
- `webhook: WebhookConfig` - Webhook configuration for handling form submissions
|
|
1105
1102
|
|
|
1103
|
+
### Custom Pages with Forms (CSRF Token)
|
|
1104
|
+
|
|
1105
|
+
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.
|
|
1106
|
+
|
|
1107
|
+
#### Using a Custom Webhook
|
|
1108
|
+
|
|
1109
|
+
If your page submits to a custom webhook (e.g., `/webhooks/archive`), pass the token as the second argument when creating the webhook registration:
|
|
1110
|
+
|
|
1111
|
+
```typescript
|
|
1112
|
+
import { generateFormToken } from '@positronic/core';
|
|
1113
|
+
import archiveWebhook from '../webhooks/archive.js';
|
|
1114
|
+
|
|
1115
|
+
brain('Archive Workflow')
|
|
1116
|
+
.step('Create Page', async ({ state, pages, env }) => {
|
|
1117
|
+
const formToken = generateFormToken();
|
|
1118
|
+
|
|
1119
|
+
const html = `<html>
|
|
1120
|
+
<body>
|
|
1121
|
+
<form method="POST" action="<%= '${env.origin}' %>/webhooks/archive">
|
|
1122
|
+
<input type="hidden" name="__positronic_token" value="<%= '${formToken}' %>">
|
|
1123
|
+
<input type="text" name="name" placeholder="Your name">
|
|
1124
|
+
<button type="submit">Submit</button>
|
|
1125
|
+
</form>
|
|
1126
|
+
</body>
|
|
1127
|
+
</html>`;
|
|
1128
|
+
|
|
1129
|
+
await pages.create('my-page', html);
|
|
1130
|
+
return { ...state, formToken };
|
|
1131
|
+
})
|
|
1132
|
+
.wait('Wait for submission', ({ state }) => archiveWebhook(state.sessionId, state.formToken), { timeout: '24h' })
|
|
1133
|
+
.step('Process', ({ state, response }) => ({
|
|
1134
|
+
...state,
|
|
1135
|
+
name: response.name,
|
|
1136
|
+
}));
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
#### Using the System `ui-form` Endpoint
|
|
1140
|
+
|
|
1141
|
+
If your page submits to the built-in `ui-form` endpoint, include the token in the webhook registration object:
|
|
1142
|
+
|
|
1143
|
+
```typescript
|
|
1144
|
+
import { generateFormToken } from '@positronic/core';
|
|
1145
|
+
|
|
1146
|
+
brain('Custom Form')
|
|
1147
|
+
.step('Create Form Page', async ({ state, pages, env }) => {
|
|
1148
|
+
const formToken = generateFormToken();
|
|
1149
|
+
const webhookIdentifier = `custom-form-<%= '${Date.now()}' %>`;
|
|
1150
|
+
const formAction = `<%= '${env.origin}' %>/webhooks/system/ui-form?identifier=<%= '${encodeURIComponent(webhookIdentifier)}' %>`;
|
|
1151
|
+
|
|
1152
|
+
const page = await pages.create('my-form', `<html>
|
|
1153
|
+
<body>
|
|
1154
|
+
<form method="POST" action="<%= '${formAction}' %>">
|
|
1155
|
+
<input type="hidden" name="__positronic_token" value="<%= '${formToken}' %>">
|
|
1156
|
+
<input type="text" name="name" placeholder="Your name">
|
|
1157
|
+
<button type="submit">Submit</button>
|
|
1158
|
+
</form>
|
|
1159
|
+
</body>
|
|
1160
|
+
</html>`);
|
|
1161
|
+
|
|
1162
|
+
return {
|
|
1163
|
+
...state,
|
|
1164
|
+
pageUrl: page.url,
|
|
1165
|
+
webhook: { slug: 'ui-form', identifier: webhookIdentifier, token: formToken },
|
|
1166
|
+
};
|
|
1167
|
+
})
|
|
1168
|
+
.wait('Wait for form', ({ state }) => state.webhook)
|
|
1169
|
+
.step('Process', ({ state, response }) => ({
|
|
1170
|
+
...state,
|
|
1171
|
+
name: response.name,
|
|
1172
|
+
}));
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
#### Summary
|
|
1176
|
+
|
|
1177
|
+
The three required pieces for any custom page with a form:
|
|
1178
|
+
1. Call `generateFormToken()` to get a token
|
|
1179
|
+
2. Add `<input type="hidden" name="__positronic_token" value="...">` to your form
|
|
1180
|
+
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`
|
|
1181
|
+
|
|
1182
|
+
Without a token, the server will reject the form submission.
|
|
1183
|
+
|
|
1106
1184
|
## UI Steps
|
|
1107
1185
|
|
|
1108
1186
|
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.
|
|
@@ -1133,8 +1211,8 @@ brain('Feedback Collector')
|
|
|
1133
1211
|
await slack.post('#feedback', `Please fill out: <%= '${page.url}' %>`);
|
|
1134
1212
|
return state;
|
|
1135
1213
|
})
|
|
1136
|
-
// Wait for form submission
|
|
1137
|
-
.wait('Wait for submission', ({ page }) => page.webhook)
|
|
1214
|
+
// Wait for form submission (timeout after 24 hours, brain is cancelled if no response)
|
|
1215
|
+
.wait('Wait for submission', ({ page }) => page.webhook, { timeout: '24h' })
|
|
1138
1216
|
// Process the form data (comes through response, not page)
|
|
1139
1217
|
.step('Process Feedback', ({ state, response }) => ({
|
|
1140
1218
|
...state,
|