@positronic/template-new-project 0.0.2

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.
@@ -0,0 +1,425 @@
1
+ # Tips for AI Agents
2
+
3
+ This document contains helpful tips and patterns for AI agents working with Positronic projects.
4
+
5
+ ## TypeScript Compilation
6
+
7
+ Run `npm run typecheck` frequently as you make changes to ensure your TypeScript code compiles correctly. This will catch type errors early and help maintain code quality.
8
+
9
+ ## Running the Development Server
10
+
11
+ When you need to run a development server, use the `--log-file` option to capture server output. **Important**: Always place the server log file in the `/tmp` directory so it gets cleaned up automatically by the operating system.
12
+
13
+ ### 1. Start the server with logging
14
+
15
+ **Default mode (recommended for most cases):**
16
+ ```bash
17
+ px server -d
18
+ ```
19
+
20
+ This starts the server on the default port (3000) with logs written to `.positronic-server.log`.
21
+
22
+ **Custom port mode (when you need a specific port):**
23
+
24
+ First, generate a random port between 30000 and 50000:
25
+ ```bash
26
+ echo $((30000 + RANDOM % 20000))
27
+ ```
28
+
29
+ **Remember this port number** and use it for all subsequent commands. For example, if the port is 38291:
30
+
31
+ ```bash
32
+ px server --port 38291 --log-file /tmp/server-38291.log -d
33
+ ```
34
+
35
+ Note: When using `--port` with `-d`, you MUST also specify `--log-file`.
36
+
37
+ The `-d` flag runs the server in detached/background mode. The server will output its process ID (PID) which you can use to stop it later.
38
+
39
+ ### 2. Run commands using your server
40
+
41
+ **If using default port (3000):**
42
+ ```bash
43
+ # No need to set POSITRONIC_PORT
44
+ px brain list
45
+ px brain run my-brain
46
+ ```
47
+
48
+ **If using custom port:**
49
+ ```bash
50
+ # Set the port environment variable for subsequent commands (using your remembered port)
51
+ export POSITRONIC_PORT=38291
52
+
53
+ # Now all px commands will use your server
54
+ px brain list
55
+ px brain run my-brain
56
+ ```
57
+
58
+ ### 3. Check server logs when needed
59
+
60
+ **Default server:**
61
+ ```bash
62
+ # View the entire log file
63
+ cat .positronic-server.log
64
+
65
+ # View the last 50 lines of the log file
66
+ tail -n 50 .positronic-server.log
67
+ ```
68
+
69
+ **Custom port server:**
70
+ ```bash
71
+ # View the entire log file (using your remembered port)
72
+ cat /tmp/server-38291.log
73
+
74
+ # View the last 50 lines of the log file
75
+ tail -n 50 /tmp/server-38291.log
76
+ ```
77
+
78
+ ### 4. Stop the server when done
79
+
80
+ **Using the built-in kill option (recommended for default server):**
81
+ ```bash
82
+ # Kill default server
83
+ px server -k
84
+ ```
85
+
86
+ **Manual methods:**
87
+ ```bash
88
+ # Default server
89
+ kill $(cat .positronic-server.pid)
90
+
91
+ # Custom port server (PID file includes port number)
92
+ kill $(cat .positronic-server-38291.pid)
93
+
94
+ # Or find and kill the server process by port
95
+ kill $(lsof -ti:38291)
96
+ ```
97
+
98
+ ### Important Notes
99
+ - The `-d` flag runs the server in detached/background mode (similar to Docker's -d)
100
+ - Default server: PID stored in `.positronic-server.pid`, logs in `.positronic-server.log`
101
+ - Custom port servers: PID stored in `.positronic-server-{port}.pid`
102
+ - When using `--port` with `-d`, you MUST also specify `--log-file`
103
+ - Log files are always appended to (never overwritten)
104
+ - The server will error if another server is already running on the same port
105
+ - Always clean up by killing the server process when done
106
+ - The log file contains timestamped entries with [INFO], [ERROR], and [WARN] prefixes
107
+
108
+ ## Brain DSL Type Inference
109
+
110
+ The Brain DSL has very strong type inference capabilities. **Important**: You should NOT explicitly specify types on the state object as it flows through steps. The types are automatically inferred from the previous step.
111
+
112
+ ```typescript
113
+ // ❌ DON'T DO THIS - unnecessary type annotations
114
+ brain('example')
115
+ .step('init', ({ state }: { state: {} }) => ({
116
+ count: 0,
117
+ name: 'test'
118
+ }))
119
+ .step('process', ({ state }: { state: { count: number; name: string } }) => ({
120
+ ...state,
121
+ processed: true
122
+ }))
123
+
124
+ // ✅ DO THIS - let TypeScript infer the types
125
+ brain('example')
126
+ .step('init', ({ state }) => ({
127
+ count: 0,
128
+ name: 'test'
129
+ }))
130
+ .step('process', ({ state }) => ({
131
+ ...state, // TypeScript knows state has count: number and name: string
132
+ processed: true
133
+ }))
134
+ ```
135
+
136
+ The type inference flows through the entire chain, making the code cleaner and more maintainable.
137
+
138
+ ## Error Handling in Brains
139
+
140
+ **Important**: Do NOT catch errors in brain steps unless error handling is specifically part of the brain's workflow logic. The brain runner handles all errors automatically.
141
+
142
+ ```typescript
143
+ // ❌ DON'T DO THIS - unnecessary error catching
144
+ brain('example')
145
+ .step('fetch data', async ({ state }) => {
146
+ try {
147
+ const data = await fetchSomeData();
148
+ return { ...state, data };
149
+ } catch (error) {
150
+ console.error('Error:', error);
151
+ return { ...state, error: error.message };
152
+ }
153
+ })
154
+
155
+ // ✅ DO THIS - let errors propagate
156
+ brain('example')
157
+ .step('fetch data', async ({ state }) => {
158
+ const data = await fetchSomeData(); // If this throws, the runner handles it
159
+ return { ...state, data };
160
+ })
161
+
162
+ // ✅ ONLY catch errors when it's part of the workflow logic
163
+ brain('validation-example')
164
+ .step('validate input', async ({ state }) => {
165
+ try {
166
+ const result = await validateData(state.input);
167
+ return { ...state, valid: true, result };
168
+ } catch (validationError) {
169
+ // Only if the next step needs to know about validation failures
170
+ return { ...state, valid: false, validationError: validationError.message };
171
+ }
172
+ })
173
+ .step('process based on validation', ({ state }) => {
174
+ if (!state.valid) {
175
+ // Handle validation failure as part of the workflow
176
+ return { ...state, status: 'validation-failed' };
177
+ }
178
+ // Continue with valid data
179
+ return { ...state, status: 'processing' };
180
+ })
181
+ ```
182
+
183
+ Most generated brains should not have try-catch blocks. Only use them when the error state is meaningful to subsequent steps in the workflow.
184
+
185
+ ## Service Organization
186
+
187
+ When implementing services for the project brain, consider creating a `services/` directory at the root of your project to keep service implementations organized and reusable:
188
+
189
+ ```
190
+ services/
191
+ ├── gmail.js # Gmail API integration
192
+ ├── slack.js # Slack notifications
193
+ ├── database.js # Database client
194
+ └── analytics.js # Analytics tracking
195
+ ```
196
+
197
+ Then in your `brain.ts` (at the project root):
198
+
199
+ ```typescript
200
+ import gmail from './services/gmail.js';
201
+ import slack from './services/slack.js';
202
+ import database from './services/database.js';
203
+ import analytics from './services/analytics.js';
204
+
205
+ export function brain<
206
+ TOptions extends object = object,
207
+ TState extends object = object
208
+ >(
209
+ brainConfig: string | { title: string; description?: string }
210
+ ) {
211
+ return coreBrain(brainConfig)
212
+ .withServices({
213
+ gmail,
214
+ slack,
215
+ database,
216
+ analytics
217
+ });
218
+ }
219
+ ```
220
+
221
+ This keeps your service implementations separate from your brain logic and makes them easier to test and maintain.
222
+
223
+ ## Important: ESM Module Imports
224
+
225
+ This project uses ES modules (ESM). **Always include the `.js` extension in your imports**, even when importing TypeScript files:
226
+
227
+ ```typescript
228
+ // ✅ CORRECT - Include .js extension
229
+ import { brain } from '../brain.js'; // From a file in brains/ directory
230
+ import { analyzeData } from '../utils/analyzer.js';
231
+ import gmail from '../services/gmail.js';
232
+
233
+ // ❌ WRONG - Missing .js extension
234
+ import { brain } from '../brain';
235
+ import { analyzeData } from '../utils/analyzer';
236
+ import gmail from '../services/gmail';
237
+ ```
238
+
239
+ This applies to all imports in:
240
+ - Brain files
241
+ - Service files
242
+ - Test files
243
+ - Any other TypeScript/JavaScript files
244
+
245
+ The `.js` extension is required for ESM compatibility, even though the source files are `.ts`.
246
+
247
+ ## Creating New Brains - Test-Driven Development
248
+
249
+ **IMPORTANT**: When asked to generate or create a new brain, you should ALWAYS follow this test-driven development approach. This ensures the brain works correctly and helps catch issues early.
250
+
251
+ ### 1. Write a Failing Test First
252
+
253
+ Start by following the brain testing guide (`/docs/brain-testing-guide.md`) and write a failing test that describes the expected behavior of the brain.
254
+
255
+ ```typescript
256
+ // tests/my-new-brain.test.ts
257
+ import { describe, it, expect } from '@jest/globals';
258
+ import { createMockClient, runBrainTest } from '@positronic/core/testing';
259
+ import myNewBrain from '../brains/my-new-brain';
260
+
261
+ describe('MyNewBrain', () => {
262
+ it('should process data and return expected result', async () => {
263
+ const mockClient = createMockClient();
264
+
265
+ // Mock any AI responses if the brain uses prompts
266
+ mockClient.mockResponses(
267
+ { processedData: 'expected output' }
268
+ );
269
+
270
+ const result = await runBrainTest(myNewBrain, {
271
+ client: mockClient,
272
+ initialState: { input: 'test data' }
273
+ });
274
+
275
+ expect(result.completed).toBe(true);
276
+ expect(result.finalState.output).toBe('expected output');
277
+ });
278
+ });
279
+ ```
280
+
281
+ ### 2. Review Documentation
282
+
283
+ Before implementing the brain:
284
+ - Re-read the **Brain DSL guide** (`/docs/brain-dsl-guide.md`) to understand the DSL patterns
285
+ - Re-read this **Tips for Agents** document if you haven't already
286
+ - Pay special attention to type inference and error handling guidelines
287
+
288
+ ### 3. Start the Development Server
289
+
290
+ Before implementing, start the development server in detached mode so you can actually run and test your brain:
291
+
292
+ ```bash
293
+ # For most cases, just use the default:
294
+ px server -d
295
+
296
+ # Verify the server is running
297
+ px brain list
298
+ ```
299
+
300
+ If you need a custom port (e.g., when running multiple servers):
301
+ ```bash
302
+ # 1. Generate a random port
303
+ PORT=$(echo $((30000 + RANDOM % 20000)))
304
+ echo "Using port: $PORT"
305
+
306
+ # 2. Start the server in detached mode (--log-file is required with --port)
307
+ px server --port $PORT --log-file /tmp/server-$PORT.log -d
308
+
309
+ # 3. Set environment variable for all subsequent px commands
310
+ export POSITRONIC_PORT=$PORT
311
+
312
+ # 4. Verify the server is running
313
+ px brain list
314
+ ```
315
+
316
+ ### 4. Implement Incrementally
317
+
318
+ Build the brain one step at a time, testing as you go. **Actually run the brain after each change** to see if it works:
319
+
320
+ ```bash
321
+ # 1. Create the brain with just the first step
322
+ # Write minimal implementation in brains/my-new-brain.ts
323
+
324
+ # 2. Run the brain to test the first step
325
+ px brain run my-new-brain
326
+
327
+ # 3. Check the server log to see execution details
328
+ # For default server:
329
+ tail -f .positronic-server.log
330
+ # For custom port server:
331
+ # tail -f /tmp/server-$PORT.log
332
+
333
+ # 4. Run the test to see if it's getting closer to passing
334
+ npm test tests/my-new-brain.test.ts
335
+
336
+ # 5. Add the next step, run again, check logs
337
+ # Repeat until the test passes
338
+
339
+ # 6. When done, stop the server
340
+ px server -k # (for default server) or: kill $(cat .positronic-server.pid)
341
+ ```
342
+
343
+ ### 5. Example Workflow
344
+
345
+ Here's a complete example of creating a brain that processes user feedback:
346
+
347
+ ```typescript
348
+ // Step 1: Write the test first
349
+ describe('FeedbackProcessor', () => {
350
+ it('should analyze feedback and generate response', async () => {
351
+ const mockClient = createMockClient();
352
+ mockClient.mockResponses(
353
+ { sentiment: 'positive', score: 0.8 },
354
+ { response: 'Thank you for your feedback!' }
355
+ );
356
+
357
+ const result = await runBrainTest(feedbackBrain, {
358
+ client: mockClient,
359
+ initialState: { feedback: 'Great product!' }
360
+ });
361
+
362
+ expect(result.completed).toBe(true);
363
+ expect(result.finalState.sentiment).toBe('positive');
364
+ expect(result.finalState.response).toBeTruthy();
365
+ });
366
+ });
367
+
368
+ // Step 2: Create minimal brain implementation
369
+ import { brain } from '@positronic/core';
370
+ import { z } from 'zod';
371
+
372
+ const feedbackBrain = brain('feedback-processor')
373
+ .step('Initialize', ({ state }) => ({
374
+ ...state,
375
+ timestamp: Date.now()
376
+ }));
377
+
378
+ export default feedbackBrain;
379
+
380
+ // Step 3: Run and check logs, see it doesn't analyze yet
381
+ // Step 4: Add sentiment analysis step
382
+ .prompt('Analyze sentiment', {
383
+ template: ({ feedback }) =>
384
+ <%= "`Analyze the sentiment of this feedback: \"${feedback}\"`" %>,
385
+ outputSchema: {
386
+ schema: z.object({
387
+ sentiment: z.enum(['positive', 'neutral', 'negative']),
388
+ score: z.number().min(0).max(1)
389
+ }),
390
+ name: 'sentimentAnalysis' as const
391
+ }
392
+ })
393
+
394
+ // Step 5: Run again, check logs, test still fails (no response)
395
+ // Step 6: Add response generation
396
+ .prompt('Generate response', {
397
+ template: ({ sentimentAnalysis, feedback }) =>
398
+ <%= "`Generate a brief response to this ${sentimentAnalysis.sentiment} feedback: \"${feedback}\"`" %>,
399
+ outputSchema: {
400
+ schema: z.object({
401
+ response: z.string()
402
+ }),
403
+ name: 'responseData' as const
404
+ }
405
+ })
406
+ .step('Format output', ({ state }) => ({
407
+ ...state,
408
+ sentiment: state.sentimentAnalysis.sentiment,
409
+ response: state.responseData.response
410
+ }));
411
+
412
+ // Step 7: Run test - it should pass now!
413
+ ```
414
+
415
+ ### 6. Important Reminders
416
+
417
+ - Always start with a test that describes what the brain should do
418
+ - Start the development server in detached mode (`-d`) before implementing
419
+ - **Actually run the brain** after each change to verify it works
420
+ - Build incrementally - one step at a time
421
+ - Use the server logs to debug and understand execution
422
+ - Let TypeScript infer types - don't add explicit type annotations
423
+ - Don't catch errors unless it's part of the workflow logic
424
+ - Run `npm run typecheck` frequently to catch type errors early
425
+ - Stop the server when done: `px server -k` (default server) or `kill $(cat .positronic-server.pid)`
@@ -0,0 +1,24 @@
1
+ export default {
2
+ preset: 'ts-jest/presets/default-esm',
3
+ testEnvironment: 'node',
4
+ extensionsToTreatAsEsm: ['.ts'],
5
+ moduleNameMapper: {
6
+ '^(\\.{1,2}/.*)\\.js$': '$1',
7
+ },
8
+ transform: {
9
+ '^.+\\.tsx?$': [
10
+ 'ts-jest',
11
+ {
12
+ useESM: true,
13
+ tsconfig: {
14
+ moduleResolution: 'NodeNext',
15
+ isolatedModules: true,
16
+ },
17
+ },
18
+ ],
19
+ },
20
+ transformIgnorePatterns: [],
21
+ testMatch: ['<rootDir>/tests/**/*.test.ts'],
22
+ moduleFileExtensions: ['ts', 'js', 'json'],
23
+ collectCoverageFrom: ['brains/**/*.ts'],
24
+ };
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "<%= name %>",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "private": true,
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "NODE_OPTIONS='--experimental-vm-modules' jest",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "dependencies": {
15
+ "zod": "^3.24.1",
16
+ "@positronic/client-vercel": "<%= positronicClientVercelVersion %>",
17
+ "@ai-sdk/openai": "^1.3.22",
18
+ "@positronic/core": "<%= positronicCoreVersion %>"<% if (backend === 'cloudflare') { %>,
19
+ "@positronic/cloudflare": "<%= positronicCloudflareVersion %>"<% } %>
20
+ },
21
+ "devDependencies": {<% if (backend === 'cloudflare') { %>
22
+ "wrangler": "^4.0.0",<% } %>
23
+ "typescript": "^5.0.0",
24
+ "jest": "^30.0.4",
25
+ "@jest/globals": "^30.0.4",
26
+ "ts-jest": "^29.2.6",
27
+ "@types/jest": "^30.0.0"
28
+ }
29
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "projectName": "<%= name %>",
3
+ "backend": {
4
+ "type": "<%= backend %>",
5
+ "package": "<%= backendPackage %>"
6
+ },
7
+ "version": "1"
8
+ }
File without changes
@@ -0,0 +1,9 @@
1
+ import { BrainRunner } from '@positronic/core';
2
+ import { VercelClient } from '@positronic/client-vercel';
3
+ import { openai } from '@ai-sdk/openai';
4
+
5
+ export const runner = new BrainRunner({
6
+ adapters: [],
7
+ client: new VercelClient(openai('gpt-4o-mini')),
8
+ resources: {},
9
+ });
@@ -0,0 +1,20 @@
1
+ import { createMockClient, runBrainTest } from '@positronic/core/testing';
2
+ import exampleBrain from '../brains/example.js';
3
+
4
+ describe('example brain', () => {
5
+ it('should complete successfully with welcome messages', async () => {
6
+ // Arrange
7
+ const mockClient = createMockClient();
8
+
9
+ // Act
10
+ const result = await runBrainTest(exampleBrain, { client: mockClient });
11
+
12
+ // Assert
13
+ expect(result.completed).toBe(true);
14
+ expect(result.error).toBeNull();
15
+ expect(result.finalState).toMatchObject({
16
+ message: 'Welcome to Positronic!',
17
+ finalMessage: 'Welcome to Positronic! Your project is set up.'
18
+ });
19
+ });
20
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "lib": ["ESNext", "WebWorker"],
6
+ "strict": true,
7
+ "moduleResolution": "nodenext",
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "outDir": "./dist",
12
+ "rootDir": "./"
13
+ },
14
+ "include": ["brains/**/*.ts", "resources.d.ts"],
15
+ "exclude": ["node_modules", ".positronic"]
16
+ }