create-cogitator-app 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  // src/index.ts
4
4
  import * as p3 from "@clack/prompts";
5
- import { readFileSync } from "fs";
5
+ import { readFileSync as readFileSync2 } from "fs";
6
+ import path5 from "path";
6
7
  import pc2 from "picocolors";
7
8
 
8
9
  // src/utils/logger.ts
@@ -34,74 +35,170 @@ function banner(version) {
34
35
 
35
36
  // src/prompts.ts
36
37
  import * as p from "@clack/prompts";
37
- import path from "path";
38
+ import path3 from "path";
38
39
 
39
40
  // src/utils/providers.ts
40
41
  var defaultModels = {
41
- ollama: "qwen3:8b",
42
+ ollama: "qwen3.5:9b",
42
43
  openai: "gpt-6.1-sol",
43
44
  anthropic: "claude-sonnet-5-5",
44
45
  google: "gemini-3.8-flash"
45
46
  };
46
- function providerConfig(provider) {
47
- switch (provider) {
48
- case "ollama":
49
- return [
50
- ` llm: {`,
51
- ` defaultProvider: 'ollama',`,
52
- ` providers: {`,
53
- ` ollama: { baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434' },`,
54
- ` },`,
55
- ` },`
56
- ].join("\n");
57
- case "openai":
58
- return [
59
- ` llm: {`,
60
- ` defaultProvider: 'openai',`,
61
- ` providers: {`,
62
- ` openai: { apiKey: process.env.OPENAI_API_KEY! },`,
63
- ` },`,
64
- ` },`
65
- ].join("\n");
66
- case "anthropic":
67
- return [
68
- ` llm: {`,
69
- ` defaultProvider: 'anthropic',`,
70
- ` providers: {`,
71
- ` anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! },`,
72
- ` },`,
73
- ` },`
74
- ].join("\n");
75
- case "google":
76
- return [
77
- ` llm: {`,
78
- ` defaultProvider: 'google',`,
79
- ` providers: {`,
80
- ` google: { apiKey: process.env.GOOGLE_API_KEY! },`,
81
- ` },`,
82
- ` },`
83
- ].join("\n");
47
+ function modelFor(options) {
48
+ return options.model?.trim() || defaultModels[options.provider];
49
+ }
50
+ function providerEnvKey(provider) {
51
+ const map = {
52
+ ollama: "OLLAMA_BASE_URL",
53
+ openai: "OPENAI_API_KEY",
54
+ anthropic: "ANTHROPIC_API_KEY",
55
+ google: "GOOGLE_API_KEY"
56
+ };
57
+ return map[provider];
58
+ }
59
+ function apiKeyExpression(envKey, access) {
60
+ return access === "requireEnv" ? `requireEnv('${envKey}')` : `process.env.${envKey} ?? ''`;
61
+ }
62
+ function providerConfig(provider, access = "process-env") {
63
+ if (provider === "ollama") {
64
+ return [
65
+ ` llm: {`,
66
+ ` defaultProvider: 'ollama',`,
67
+ ` providers: {`,
68
+ ` ollama: { baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434' },`,
69
+ ` },`,
70
+ ` },`
71
+ ].join("\n");
84
72
  }
73
+ return [
74
+ ` llm: {`,
75
+ ` defaultProvider: '${provider}',`,
76
+ ` providers: {`,
77
+ ` ${provider}: { apiKey: ${apiKeyExpression(providerEnvKey(provider), access)} },`,
78
+ ` },`,
79
+ ` },`
80
+ ].join("\n");
85
81
  }
86
82
 
87
83
  // src/templates/versions.ts
84
+ import { existsSync, readFileSync } from "fs";
85
+ import path from "path";
86
+ import { fileURLToPath } from "url";
88
87
  var ZOD_VERSION = "^4.6.5";
88
+ var IOREDIS_VERSION = "^6.0.0";
89
+ var OWN_NAME = "create-cogitator-app";
90
+ var SCOPE = "@cogitator-ai/";
91
+ function isRecord(value) {
92
+ return typeof value === "object" && value !== null && !Array.isArray(value);
93
+ }
94
+ function readManifest(file) {
95
+ const parsed = JSON.parse(readFileSync(file, "utf-8"));
96
+ if (!isRecord(parsed)) throw new Error(`${file} is not a package manifest`);
97
+ return parsed;
98
+ }
99
+ function findOwnRoot(from) {
100
+ let dir = from;
101
+ while (true) {
102
+ const manifest = path.join(dir, "package.json");
103
+ if (existsSync(manifest) && readManifest(manifest).name === OWN_NAME) return dir;
104
+ const parent = path.dirname(dir);
105
+ if (parent === dir) throw new Error(`Cannot find the ${OWN_NAME} package.json above ${from}`);
106
+ dir = parent;
107
+ }
108
+ }
109
+ function linkedVersion(packageRoot, name) {
110
+ const manifest = path.join(packageRoot, "node_modules", name, "package.json");
111
+ if (!existsSync(manifest)) {
112
+ throw new Error(`Cannot resolve the version of ${name}: ${manifest} does not exist`);
113
+ }
114
+ const version = readManifest(manifest).version;
115
+ if (typeof version !== "string") throw new Error(`${manifest} has no version`);
116
+ return version;
117
+ }
118
+ function readCogitatorVersions(packageRoot) {
119
+ const devDependencies = readManifest(path.join(packageRoot, "package.json")).devDependencies;
120
+ const versions = {};
121
+ if (!isRecord(devDependencies)) return versions;
122
+ for (const [name, range] of Object.entries(devDependencies)) {
123
+ if (!name.startsWith(SCOPE) || typeof range !== "string") continue;
124
+ versions[name] = range.startsWith("workspace:") ? `^${linkedVersion(packageRoot, name)}` : range;
125
+ }
126
+ return versions;
127
+ }
128
+ var resolved;
129
+ function cogitatorVersion(name) {
130
+ resolved ??= readCogitatorVersions(findOwnRoot(path.dirname(fileURLToPath(import.meta.url))));
131
+ const range = resolved[name];
132
+ if (!range) {
133
+ throw new Error(`${name} is not a devDependency of ${OWN_NAME}, so its version is unknown`);
134
+ }
135
+ return range;
136
+ }
137
+
138
+ // src/templates/shared.ts
139
+ function tsString(value) {
140
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\p{Zl}/gu, "\\u2028").replace(/\p{Zp}/gu, "\\u2029");
141
+ return `'${escaped}'`;
142
+ }
143
+ function needsApiKey(provider) {
144
+ return provider !== "ollama";
145
+ }
146
+ function envHelperFiles(provider) {
147
+ if (!needsApiKey(provider)) return [];
148
+ const content = [
149
+ `export function requireEnv(name: string): string {`,
150
+ ` const value = process.env[name]`,
151
+ ` if (!value) {`,
152
+ ` throw new Error(\`\${name} is not set. Copy .env.example to .env and fill it in.\`)`,
153
+ ` }`,
154
+ ` return value`,
155
+ `}`,
156
+ ``
157
+ ].join("\n");
158
+ return [{ path: "src/env.ts", content }];
159
+ }
160
+ function envHelperImport(provider) {
161
+ return needsApiKey(provider) ? [`import { requireEnv } from './env.js'`] : [];
162
+ }
163
+ var RUN_MAIN = [
164
+ `main().catch((error: unknown) => {`,
165
+ ` console.error(error)`,
166
+ ` process.exitCode = 1`,
167
+ `})`,
168
+ ``
169
+ ];
170
+ function scriptTemplateScripts() {
171
+ return {
172
+ dev: "tsx watch --env-file-if-exists=.env src/index.ts",
173
+ start: "tsx --env-file-if-exists=.env src/index.ts",
174
+ build: "tsc",
175
+ typecheck: "tsc --noEmit"
176
+ };
177
+ }
178
+ function scriptTemplateDevDependencies() {
179
+ return {
180
+ typescript: "^5.8.0",
181
+ tsx: "^4.19.0",
182
+ "@types/node": "^22.0.0"
183
+ };
184
+ }
89
185
 
90
186
  // src/templates/basic.ts
91
187
  var basicTemplate = {
92
188
  files(options) {
93
- const model = defaultModels[options.provider];
189
+ const model = modelFor(options);
94
190
  const indexTs = [
95
191
  `import { Cogitator, Agent } from '@cogitator-ai/core'`,
96
192
  `import { searchTool, summarizeTool } from './tools.js'`,
193
+ ...envHelperImport(options.provider),
97
194
  ``,
98
195
  `const cogitator = new Cogitator({`,
99
- providerConfig(options.provider),
196
+ providerConfig(options.provider, "requireEnv"),
100
197
  `})`,
101
198
  ``,
102
199
  `const agent = new Agent({`,
103
- ` name: '${options.name}-agent',`,
104
- ` model: '${model}',`,
200
+ ` name: ${tsString(`${options.name}-agent`)},`,
201
+ ` model: ${tsString(model)},`,
105
202
  ` instructions: 'You are a helpful AI assistant. Use your tools to help the user.',`,
106
203
  ` tools: [searchTool, summarizeTool],`,
107
204
  ` temperature: 0.7,`,
@@ -115,8 +212,7 @@ var basicTemplate = {
115
212
  ` console.log(result.output)`,
116
213
  `}`,
117
214
  ``,
118
- `main().catch(console.error)`,
119
- ``
215
+ ...RUN_MAIN
120
216
  ].join("\n");
121
217
  const toolsTs = [
122
218
  `import { tool } from '@cogitator-ai/core'`,
@@ -149,42 +245,36 @@ var basicTemplate = {
149
245
  ].join("\n");
150
246
  return [
151
247
  { path: "src/index.ts", content: indexTs },
152
- { path: "src/tools.ts", content: toolsTs }
248
+ { path: "src/tools.ts", content: toolsTs },
249
+ ...envHelperFiles(options.provider)
153
250
  ];
154
251
  },
155
252
  dependencies() {
156
253
  return {
157
- "@cogitator-ai/core": "latest",
254
+ "@cogitator-ai/core": cogitatorVersion("@cogitator-ai/core"),
158
255
  zod: ZOD_VERSION
159
256
  };
160
257
  },
161
258
  devDependencies() {
162
- return {
163
- typescript: "^5.8.0",
164
- tsx: "^4.19.0",
165
- "@types/node": "^22.0.0"
166
- };
259
+ return scriptTemplateDevDependencies();
167
260
  },
168
261
  scripts() {
169
- return {
170
- dev: "tsx watch src/index.ts",
171
- start: "tsx src/index.ts",
172
- build: "tsc",
173
- typecheck: "tsc --noEmit"
174
- };
262
+ return scriptTemplateScripts();
175
263
  }
176
264
  };
177
265
 
178
266
  // src/templates/memory.ts
179
267
  var memoryTemplate = {
268
+ memoryAdapter: "redis",
180
269
  files(options) {
181
- const model = defaultModels[options.provider];
270
+ const model = modelFor(options);
182
271
  const indexTs = [
183
272
  `import { Cogitator, Agent } from '@cogitator-ai/core'`,
184
273
  `import { searchTool, noteTool } from './tools.js'`,
274
+ ...envHelperImport(options.provider),
185
275
  ``,
186
276
  `const cogitator = new Cogitator({`,
187
- providerConfig(options.provider),
277
+ providerConfig(options.provider, "requireEnv"),
188
278
  ` memory: {`,
189
279
  ` adapter: 'redis',`,
190
280
  ` redis: { url: process.env.REDIS_URL || 'redis://localhost:6379' },`,
@@ -192,8 +282,8 @@ var memoryTemplate = {
192
282
  `})`,
193
283
  ``,
194
284
  `const agent = new Agent({`,
195
- ` name: '${options.name}-agent',`,
196
- ` model: '${model}',`,
285
+ ` name: ${tsString(`${options.name}-agent`)},`,
286
+ ` model: ${tsString(model)},`,
197
287
  ` instructions: [`,
198
288
  ` 'You are an AI assistant with persistent memory.',`,
199
289
  ` 'You remember past conversations and user preferences.',`,
@@ -206,25 +296,26 @@ var memoryTemplate = {
206
296
  `async function main() {`,
207
297
  ` const threadId = 'demo-thread'`,
208
298
  ``,
209
- ` const result1 = await cogitator.run(agent, {`,
210
- ` input: 'My name is Alex and I prefer TypeScript.',`,
211
- ` threadId,`,
212
- ` useMemory: true,`,
213
- ` })`,
214
- ` console.log('Response 1:', result1.output)`,
215
- ``,
216
- ` const result2 = await cogitator.run(agent, {`,
217
- ` input: 'What is my name and preferred language?',`,
218
- ` threadId,`,
219
- ` useMemory: true,`,
220
- ` })`,
221
- ` console.log('Response 2:', result2.output)`,
222
- ``,
223
- ` await cogitator.close()`,
299
+ ` try {`,
300
+ ` const result1 = await cogitator.run(agent, {`,
301
+ ` input: 'My name is Alex and I prefer TypeScript.',`,
302
+ ` threadId,`,
303
+ ` useMemory: true,`,
304
+ ` })`,
305
+ ` console.log('Response 1:', result1.output)`,
306
+ ``,
307
+ ` const result2 = await cogitator.run(agent, {`,
308
+ ` input: 'What is my name and preferred language?',`,
309
+ ` threadId,`,
310
+ ` useMemory: true,`,
311
+ ` })`,
312
+ ` console.log('Response 2:', result2.output)`,
313
+ ` } finally {`,
314
+ ` await cogitator.close()`,
315
+ ` }`,
224
316
  `}`,
225
317
  ``,
226
- `main().catch(console.error)`,
227
- ``
318
+ ...RUN_MAIN
228
319
  ].join("\n");
229
320
  const toolsTs = [
230
321
  `import { tool } from '@cogitator-ai/core'`,
@@ -256,45 +347,38 @@ var memoryTemplate = {
256
347
  ].join("\n");
257
348
  return [
258
349
  { path: "src/index.ts", content: indexTs },
259
- { path: "src/tools.ts", content: toolsTs }
350
+ { path: "src/tools.ts", content: toolsTs },
351
+ ...envHelperFiles(options.provider)
260
352
  ];
261
353
  },
262
354
  dependencies() {
263
355
  return {
264
- "@cogitator-ai/core": "latest",
265
- "@cogitator-ai/memory": "latest",
266
- "@cogitator-ai/redis": "latest",
356
+ "@cogitator-ai/core": cogitatorVersion("@cogitator-ai/core"),
357
+ "@cogitator-ai/memory": cogitatorVersion("@cogitator-ai/memory"),
358
+ "@cogitator-ai/redis": cogitatorVersion("@cogitator-ai/redis"),
359
+ ioredis: IOREDIS_VERSION,
267
360
  zod: ZOD_VERSION
268
361
  };
269
362
  },
270
363
  devDependencies() {
271
- return {
272
- typescript: "^5.8.0",
273
- tsx: "^4.19.0",
274
- "@types/node": "^22.0.0"
275
- };
364
+ return scriptTemplateDevDependencies();
276
365
  },
277
366
  scripts() {
278
- return {
279
- dev: "tsx watch src/index.ts",
280
- start: "tsx src/index.ts",
281
- build: "tsc",
282
- typecheck: "tsc --noEmit"
283
- };
367
+ return scriptTemplateScripts();
284
368
  }
285
369
  };
286
370
 
287
371
  // src/templates/swarm.ts
288
372
  var swarmTemplate = {
289
373
  files(options) {
290
- const model = defaultModels[options.provider];
374
+ const model = modelFor(options);
291
375
  const researcherTs = [
292
376
  `import { Agent } from '@cogitator-ai/core'`,
293
377
  `import { searchTool } from '../tools.js'`,
294
378
  ``,
295
379
  `export const researcher = new Agent({`,
296
380
  ` name: 'researcher',`,
297
- ` model: '${model}',`,
381
+ ` model: ${tsString(model)},`,
298
382
  ` instructions: [`,
299
383
  ` 'You are a research specialist.',`,
300
384
  ` 'Use the search tool to find information.',`,
@@ -310,7 +394,7 @@ var swarmTemplate = {
310
394
  ``,
311
395
  `export const writer = new Agent({`,
312
396
  ` name: 'writer',`,
313
- ` model: '${model}',`,
397
+ ` model: ${tsString(model)},`,
314
398
  ` instructions: [`,
315
399
  ` 'You are a skilled content writer.',`,
316
400
  ` 'Take research findings and craft well-structured, engaging content.',`,
@@ -325,7 +409,7 @@ var swarmTemplate = {
325
409
  ``,
326
410
  `export const reviewer = new Agent({`,
327
411
  ` name: 'reviewer',`,
328
- ` model: '${model}',`,
412
+ ` model: ${tsString(model)},`,
329
413
  ` instructions: [`,
330
414
  ` 'You are a meticulous content reviewer.',`,
331
415
  ` 'Check for accuracy, grammar, and overall quality.',`,
@@ -357,13 +441,14 @@ var swarmTemplate = {
357
441
  `import { researcher } from './agents/researcher.js'`,
358
442
  `import { writer } from './agents/writer.js'`,
359
443
  `import { reviewer } from './agents/reviewer.js'`,
444
+ ...envHelperImport(options.provider),
360
445
  ``,
361
446
  `const cogitator = new Cogitator({`,
362
- providerConfig(options.provider),
447
+ providerConfig(options.provider, "requireEnv"),
363
448
  `})`,
364
449
  ``,
365
450
  `const team = new Swarm(cogitator, {`,
366
- ` name: '${options.name}-team',`,
451
+ ` name: ${tsString(`${options.name}-team`)},`,
367
452
  ` strategy: 'hierarchical',`,
368
453
  ` supervisor: reviewer,`,
369
454
  ` workers: [researcher, writer],`,
@@ -380,75 +465,68 @@ var swarmTemplate = {
380
465
  `async function main() {`,
381
466
  ` console.log('Starting swarm...')`,
382
467
  ``,
383
- ` const result = await team.run({`,
384
- ` input: 'Write a comprehensive article about AI agent orchestration.',`,
385
- ` })`,
468
+ ` try {`,
469
+ ` const result = await team.run({`,
470
+ ` input: 'Write a comprehensive article about AI agent orchestration.',`,
471
+ ` })`,
386
472
  ``,
387
- ` console.log('Result:', result.output)`,
388
- ` console.log('Strategy:', team.strategyType)`,
389
- ``,
390
- ` await team.close()`,
473
+ ` console.log('Result:', result.output)`,
474
+ ` console.log('Strategy:', team.strategyType)`,
475
+ ` } finally {`,
476
+ ` await team.close()`,
477
+ ` }`,
391
478
  `}`,
392
479
  ``,
393
- `main().catch(console.error)`,
394
- ``
480
+ ...RUN_MAIN
395
481
  ].join("\n");
396
482
  return [
397
483
  { path: "src/index.ts", content: indexTs },
398
484
  { path: "src/tools.ts", content: toolsTs },
399
485
  { path: "src/agents/researcher.ts", content: researcherTs },
400
486
  { path: "src/agents/writer.ts", content: writerTs },
401
- { path: "src/agents/reviewer.ts", content: reviewerTs }
487
+ { path: "src/agents/reviewer.ts", content: reviewerTs },
488
+ ...envHelperFiles(options.provider)
402
489
  ];
403
490
  },
404
491
  dependencies() {
405
492
  return {
406
- "@cogitator-ai/core": "latest",
407
- "@cogitator-ai/swarms": "latest",
493
+ "@cogitator-ai/core": cogitatorVersion("@cogitator-ai/core"),
494
+ "@cogitator-ai/swarms": cogitatorVersion("@cogitator-ai/swarms"),
408
495
  zod: ZOD_VERSION
409
496
  };
410
497
  },
411
498
  devDependencies() {
412
- return {
413
- typescript: "^5.8.0",
414
- tsx: "^4.19.0",
415
- "@types/node": "^22.0.0"
416
- };
499
+ return scriptTemplateDevDependencies();
417
500
  },
418
501
  scripts() {
419
- return {
420
- dev: "tsx watch src/index.ts",
421
- start: "tsx src/index.ts",
422
- build: "tsc",
423
- typecheck: "tsc --noEmit"
424
- };
502
+ return scriptTemplateScripts();
425
503
  }
426
504
  };
427
505
 
428
506
  // src/templates/workflow.ts
429
507
  var workflowTemplate = {
430
508
  files(options) {
431
- const model = defaultModels[options.provider];
509
+ const model = modelFor(options);
432
510
  const agentsTs = [
433
511
  `import { Agent } from '@cogitator-ai/core'`,
434
512
  ``,
435
513
  `export const analyzer = new Agent({`,
436
514
  ` name: 'analyzer',`,
437
- ` model: '${model}',`,
515
+ ` model: ${tsString(model)},`,
438
516
  ` instructions: 'You analyze input data and extract key information. Be thorough and structured.',`,
439
517
  ` temperature: 0.3,`,
440
518
  `})`,
441
519
  ``,
442
520
  `export const processor = new Agent({`,
443
521
  ` name: 'processor',`,
444
- ` model: '${model}',`,
522
+ ` model: ${tsString(model)},`,
445
523
  ` instructions: 'You process analyzed data and transform it into actionable insights.',`,
446
524
  ` temperature: 0.5,`,
447
525
  `})`,
448
526
  ``,
449
527
  `export const formatter = new Agent({`,
450
528
  ` name: 'formatter',`,
451
- ` model: '${model}',`,
529
+ ` model: ${tsString(model)},`,
452
530
  ` instructions: 'You format processed data into a clean, readable report.',`,
453
531
  ` temperature: 0.7,`,
454
532
  `})`,
@@ -463,9 +541,10 @@ var workflowTemplate = {
463
541
  ` functionNode,`,
464
542
  `} from '@cogitator-ai/workflows'`,
465
543
  `import { analyzer, processor, formatter } from './agents.js'`,
544
+ ...envHelperImport(options.provider),
466
545
  ``,
467
546
  `const cogitator = new Cogitator({`,
468
- providerConfig(options.provider),
547
+ providerConfig(options.provider, "requireEnv"),
469
548
  `})`,
470
549
  ``,
471
550
  `type WorkflowState = {`,
@@ -495,7 +574,7 @@ var workflowTemplate = {
495
574
  ` async (state) => state.report,`,
496
575
  `)`,
497
576
  ``,
498
- `const workflow = new WorkflowBuilder<WorkflowState>('${options.name}-workflow')`,
577
+ `const workflow = new WorkflowBuilder<WorkflowState>(${tsString(`${options.name}-workflow`)})`,
499
578
  ` .initialState({ input: '' })`,
500
579
  ` .entryPoint(analyzeNode.name)`,
501
580
  ` .addNode(analyzeNode.name, analyzeNode.fn)`,
@@ -513,48 +592,45 @@ var workflowTemplate = {
513
592
  ` input: 'Analyze the current state of AI agent frameworks and their adoption in enterprise.',`,
514
593
  ` })`,
515
594
  ``,
516
- ` console.log('Workflow completed!')`,
517
- ` console.log('Result:', JSON.stringify(result, null, 2))`,
595
+ ` if (result.error) throw result.error`,
596
+ ``,
597
+ ` console.log(\`Workflow completed in \${result.duration}ms\`)`,
598
+ ` console.log('Report:', result.state.report)`,
599
+ ` console.log('Nodes:', JSON.stringify(Object.fromEntries(result.nodeResults), null, 2))`,
518
600
  `}`,
519
601
  ``,
520
- `main().catch(console.error)`,
521
- ``
602
+ ...RUN_MAIN
522
603
  ].join("\n");
523
604
  return [
524
605
  { path: "src/index.ts", content: indexTs },
525
- { path: "src/agents.ts", content: agentsTs }
606
+ { path: "src/agents.ts", content: agentsTs },
607
+ ...envHelperFiles(options.provider)
526
608
  ];
527
609
  },
528
610
  dependencies() {
529
611
  return {
530
- "@cogitator-ai/core": "latest",
531
- "@cogitator-ai/workflows": "latest",
612
+ "@cogitator-ai/core": cogitatorVersion("@cogitator-ai/core"),
613
+ "@cogitator-ai/workflows": cogitatorVersion("@cogitator-ai/workflows"),
532
614
  zod: ZOD_VERSION
533
615
  };
534
616
  },
535
617
  devDependencies() {
536
- return {
537
- typescript: "^5.8.0",
538
- tsx: "^4.19.0",
539
- "@types/node": "^22.0.0"
540
- };
618
+ return scriptTemplateDevDependencies();
541
619
  },
542
620
  scripts() {
543
- return {
544
- dev: "tsx watch src/index.ts",
545
- start: "tsx src/index.ts",
546
- build: "tsc",
547
- typecheck: "tsc --noEmit"
548
- };
621
+ return scriptTemplateScripts();
549
622
  }
550
623
  };
551
624
 
552
625
  // src/templates/api-server.ts
553
626
  var BASE_PATH = "/api";
627
+ var PUBLIC_PATHS = ["/health", "/ready", "/docs", "/openapi.json"];
554
628
  var apiServerTemplate = {
629
+ memoryAdapter: "memory",
630
+ secrets: ["API_TOKEN"],
555
631
  healthPath: `${BASE_PATH}/health`,
556
632
  files(options) {
557
- const model = defaultModels[options.provider];
633
+ const model = modelFor(options);
558
634
  const agentsTs = [
559
635
  `import { Agent, tool } from '@cogitator-ai/core'`,
560
636
  `import { z } from 'zod'`,
@@ -572,7 +648,7 @@ var apiServerTemplate = {
572
648
  ``,
573
649
  `export const assistant = new Agent({`,
574
650
  ` name: 'assistant',`,
575
- ` model: '${model}',`,
651
+ ` model: ${tsString(model)},`,
576
652
  ` instructions: 'You are a helpful API assistant. Answer questions clearly and concisely.',`,
577
653
  ` tools: [searchTool],`,
578
654
  ` temperature: 0.7,`,
@@ -580,23 +656,45 @@ var apiServerTemplate = {
580
656
  ``,
581
657
  `export const coder = new Agent({`,
582
658
  ` name: 'coder',`,
583
- ` model: '${model}',`,
659
+ ` model: ${tsString(model)},`,
584
660
  ` instructions: 'You are an expert programmer. Write clean, well-structured code.',`,
585
661
  ` temperature: 0.3,`,
586
662
  `})`,
587
663
  ``
588
664
  ].join("\n");
589
665
  const indexTs = [
590
- `import express from 'express'`,
666
+ `import { timingSafeEqual } from 'node:crypto'`,
667
+ `import express, { type Request } from 'express'`,
591
668
  `import { Cogitator } from '@cogitator-ai/core'`,
592
669
  `import { CogitatorServer } from '@cogitator-ai/express'`,
593
670
  `import { assistant, coder } from './agents.js'`,
671
+ ...envHelperImport(options.provider),
672
+ ``,
673
+ `const production = process.env.NODE_ENV === 'production'`,
674
+ `const port = Number(process.env.PORT ?? 3000)`,
675
+ `const host = process.env.HOST ?? (production ? '0.0.0.0' : '127.0.0.1')`,
676
+ `const apiToken = process.env.API_TOKEN`,
677
+ `const corsOrigin = process.env.CORS_ORIGIN`,
678
+ `const publicPaths = new Set([${PUBLIC_PATHS.map(tsString).join(", ")}])`,
679
+ ``,
680
+ `function hasToken(header: string | undefined): boolean {`,
681
+ ` if (!apiToken || !header) return false`,
682
+ ` const expected = Buffer.from(\`Bearer \${apiToken}\`)`,
683
+ ` const given = Buffer.from(header)`,
684
+ ` return given.length === expected.length && timingSafeEqual(given, expected)`,
685
+ `}`,
686
+ ``,
687
+ `function authenticate(req: Request) {`,
688
+ ` if (!apiToken || publicPaths.has(req.path)) return undefined`,
689
+ ` if (!hasToken(req.get('authorization'))) throw new Error('Unauthorized')`,
690
+ ` return { userId: 'api-token' }`,
691
+ `}`,
594
692
  ``,
595
693
  `const app = express()`,
596
- `const port = process.env.PORT || 3000`,
597
694
  ``,
598
695
  `const cogitator = new Cogitator({`,
599
- providerConfig(options.provider),
696
+ providerConfig(options.provider, "requireEnv"),
697
+ ` memory: { adapter: 'memory' },`,
600
698
  `})`,
601
699
  ``,
602
700
  `const server = new CogitatorServer({`,
@@ -606,62 +704,62 @@ var apiServerTemplate = {
606
704
  ` config: {`,
607
705
  ` basePath: '${BASE_PATH}',`,
608
706
  ` enableSwagger: true,`,
609
- ` cors: { origin: '*' },`,
707
+ ` auth: authenticate,`,
708
+ ` ...(corsOrigin ? { cors: { origin: corsOrigin.split(',').map((o) => o.trim()) } } : {}),`,
610
709
  ` swagger: {`,
611
- ` title: '${options.name} API',`,
710
+ ` title: ${tsString(`${options.name} API`)},`,
612
711
  ` version: '1.0.0',`,
613
712
  ` },`,
614
713
  ` },`,
615
714
  `})`,
616
715
  ``,
617
716
  `async function main() {`,
717
+ ` if (production && !apiToken) {`,
718
+ ` throw new Error('Set API_TOKEN in production: it guards every route except health and docs')`,
719
+ ` }`,
720
+ ``,
618
721
  ` await server.init()`,
619
722
  ``,
620
- ` app.listen(port, () => {`,
621
- ` console.log(\`Server running at http://localhost:\${port}\`)`,
622
- ` console.log(\`Swagger docs at http://localhost:\${port}${BASE_PATH}/docs\`)`,
723
+ ` app.listen(port, host, () => {`,
724
+ ` console.log(\`Server running at http://\${host}:\${port}\`)`,
725
+ ` console.log(\`Swagger docs at http://\${host}:\${port}${BASE_PATH}/docs\`)`,
726
+ ` if (!apiToken) console.log('API_TOKEN is not set, so the API is open to anyone who can reach it')`,
623
727
  ` })`,
624
728
  `}`,
625
729
  ``,
626
- `main().catch(console.error)`,
627
- ``
730
+ ...RUN_MAIN
628
731
  ].join("\n");
629
732
  return [
630
733
  { path: "src/index.ts", content: indexTs },
631
- { path: "src/agents.ts", content: agentsTs }
734
+ { path: "src/agents.ts", content: agentsTs },
735
+ ...envHelperFiles(options.provider)
632
736
  ];
633
737
  },
634
738
  dependencies() {
635
739
  return {
636
- "@cogitator-ai/core": "latest",
637
- "@cogitator-ai/express": "latest",
740
+ "@cogitator-ai/core": cogitatorVersion("@cogitator-ai/core"),
741
+ "@cogitator-ai/express": cogitatorVersion("@cogitator-ai/express"),
638
742
  express: "^5.2.1",
639
743
  zod: ZOD_VERSION
640
744
  };
641
745
  },
642
746
  devDependencies() {
643
747
  return {
644
- typescript: "^5.8.0",
645
- tsx: "^4.19.0",
646
- "@types/node": "^22.0.0",
748
+ ...scriptTemplateDevDependencies(),
647
749
  "@types/express": "^5.0.6"
648
750
  };
649
751
  },
650
752
  scripts() {
651
- return {
652
- dev: "tsx watch src/index.ts",
653
- start: "tsx src/index.ts",
654
- build: "tsc",
655
- typecheck: "tsc --noEmit"
656
- };
753
+ return scriptTemplateScripts();
657
754
  }
658
755
  };
659
756
 
660
757
  // src/templates/nextjs.ts
661
758
  var nextjsTemplate = {
759
+ memoryAdapter: "memory",
662
760
  healthPath: "/api/health",
663
761
  files(options) {
664
- const model = defaultModels[options.provider];
762
+ const model = modelFor(options);
665
763
  const agentTs = [
666
764
  `import { Cogitator, Agent, tool } from '@cogitator-ai/core'`,
667
765
  `import { z } from 'zod'`,
@@ -679,11 +777,12 @@ var nextjsTemplate = {
679
777
  ``,
680
778
  `export const cogitator = new Cogitator({`,
681
779
  providerConfig(options.provider),
780
+ ` memory: { adapter: 'memory' },`,
682
781
  `})`,
683
782
  ``,
684
783
  `export const agent = new Agent({`,
685
- ` name: '${options.name}-chat',`,
686
- ` model: '${model}',`,
784
+ ` name: ${tsString(`${options.name}-chat`)},`,
785
+ ` model: ${tsString(model)},`,
687
786
  ` instructions: 'You are a helpful AI chat assistant. Be conversational and helpful.',`,
688
787
  ` tools: [searchTool],`,
689
788
  ` temperature: 0.7,`,
@@ -710,7 +809,7 @@ var nextjsTemplate = {
710
809
  `import { useState, useRef, useEffect } from 'react'`,
711
810
  ``,
712
811
  `export default function Home() {`,
713
- ` const { messages, input, setInput, send, isLoading } = useCogitatorChat({`,
812
+ ` const { messages, input, setInput, send, stop, isLoading, error } = useCogitatorChat({`,
714
813
  ` api: '/api/chat',`,
715
814
  ` })`,
716
815
  ` const messagesEndRef = useRef<HTMLDivElement>(null)`,
@@ -730,27 +829,29 @@ var nextjsTemplate = {
730
829
  ` <main className="flex min-h-screen flex-col items-center p-4 bg-gray-950 text-white">`,
731
830
  ` <div className="w-full max-w-2xl flex flex-col h-screen">`,
732
831
  ` <header className="py-6 text-center">`,
733
- ` <h1 className="text-2xl font-bold">${options.name}</h1>`,
832
+ ` <h1 className="text-2xl font-bold">{${tsString(options.name)}}</h1>`,
734
833
  ` <p className="text-gray-400 text-sm mt-1">Powered by Cogitator</p>`,
735
834
  ` </header>`,
736
835
  ``,
737
836
  ` <div className="flex-1 overflow-y-auto space-y-4 pb-4">`,
738
- ` {messages.map((msg, i) => (`,
739
- ` <div`,
740
- ` key={i}`,
741
- ` className={\`flex \${msg.role === 'user' ? 'justify-end' : 'justify-start'}\`}`,
742
- ` >`,
837
+ ` {messages`,
838
+ ` .filter((msg) => msg.content)`,
839
+ ` .map((msg) => (`,
743
840
  ` <div`,
744
- ` className={\`max-w-[80%] rounded-2xl px-4 py-2 \${`,
745
- ` msg.role === 'user'`,
746
- ` ? 'bg-blue-600 text-white'`,
747
- ` : 'bg-gray-800 text-gray-100'`,
748
- ` }\`}`,
841
+ ` key={msg.id}`,
842
+ ` className={\`flex \${msg.role === 'user' ? 'justify-end' : 'justify-start'}\`}`,
749
843
  ` >`,
750
- ` {msg.content}`,
844
+ ` <div`,
845
+ ` className={\`max-w-[80%] rounded-2xl px-4 py-2 \${`,
846
+ ` msg.role === 'user'`,
847
+ ` ? 'bg-blue-600 text-white'`,
848
+ ` : 'bg-gray-800 text-gray-100'`,
849
+ ` }\`}`,
850
+ ` >`,
851
+ ` {msg.content}`,
852
+ ` </div>`,
751
853
  ` </div>`,
752
- ` </div>`,
753
- ` ))}`,
854
+ ` ))}`,
754
855
  ` {isLoading && (`,
755
856
  ` <div className="flex justify-start">`,
756
857
  ` <div className="bg-gray-800 rounded-2xl px-4 py-2 text-gray-400">`,
@@ -758,6 +859,11 @@ var nextjsTemplate = {
758
859
  ` </div>`,
759
860
  ` </div>`,
760
861
  ` )}`,
862
+ ` {error && (`,
863
+ ` <div className="rounded-xl border border-red-800 bg-red-950 px-4 py-2 text-sm text-red-200">`,
864
+ ` {error.message}`,
865
+ ` </div>`,
866
+ ` )}`,
761
867
  ` <div ref={messagesEndRef} />`,
762
868
  ` </div>`,
763
869
  ``,
@@ -768,13 +874,23 @@ var nextjsTemplate = {
768
874
  ` placeholder="Type a message..."`,
769
875
  ` className="flex-1 bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"`,
770
876
  ` />`,
771
- ` <button`,
772
- ` type="submit"`,
773
- ` disabled={isLoading || !input.trim()}`,
774
- ` className="bg-blue-600 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl px-6 py-3 font-medium transition-colors"`,
775
- ` >`,
776
- ` Send`,
777
- ` </button>`,
877
+ ` {isLoading ? (`,
878
+ ` <button`,
879
+ ` type="button"`,
880
+ ` onClick={stop}`,
881
+ ` className="bg-gray-700 hover:bg-gray-600 text-white rounded-xl px-6 py-3 font-medium transition-colors"`,
882
+ ` >`,
883
+ ` Stop`,
884
+ ` </button>`,
885
+ ` ) : (`,
886
+ ` <button`,
887
+ ` type="submit"`,
888
+ ` disabled={!input.trim()}`,
889
+ ` className="bg-blue-600 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl px-6 py-3 font-medium transition-colors"`,
890
+ ` >`,
891
+ ` Send`,
892
+ ` </button>`,
893
+ ` )}`,
778
894
  ` </form>`,
779
895
  ` </div>`,
780
896
  ` </main>`,
@@ -787,7 +903,7 @@ var nextjsTemplate = {
787
903
  `import './globals.css'`,
788
904
  ``,
789
905
  `export const metadata: Metadata = {`,
790
- ` title: '${options.name}',`,
906
+ ` title: ${tsString(options.name)},`,
791
907
  ` description: 'AI chat app powered by Cogitator',`,
792
908
  `}`,
793
909
  ``,
@@ -881,8 +997,8 @@ var nextjsTemplate = {
881
997
  },
882
998
  dependencies() {
883
999
  return {
884
- "@cogitator-ai/core": "latest",
885
- "@cogitator-ai/next": "latest",
1000
+ "@cogitator-ai/core": cogitatorVersion("@cogitator-ai/core"),
1001
+ "@cogitator-ai/next": cogitatorVersion("@cogitator-ai/next"),
886
1002
  next: "^15.0.0",
887
1003
  react: "^19.0.0",
888
1004
  "react-dom": "^19.0.0",
@@ -905,7 +1021,7 @@ var nextjsTemplate = {
905
1021
  dev: "next dev",
906
1022
  build: "next build",
907
1023
  start: "next start",
908
- lint: "next lint"
1024
+ typecheck: "tsc --noEmit"
909
1025
  };
910
1026
  }
911
1027
  };
@@ -946,12 +1062,8 @@ var templateChoices = [
946
1062
  // src/utils/package-manager.ts
947
1063
  import { execSync } from "child_process";
948
1064
  function detectPackageManager() {
949
- const ua = process.env.npm_config_user_agent;
950
- if (ua) {
951
- if (ua.startsWith("pnpm")) return "pnpm";
952
- if (ua.startsWith("yarn")) return "yarn";
953
- if (ua.startsWith("bun")) return "bun";
954
- }
1065
+ const name = process.env.npm_config_user_agent?.split("/")[0];
1066
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
955
1067
  return "pnpm";
956
1068
  }
957
1069
  function installDependencies(cwd, pm) {
@@ -962,6 +1074,112 @@ function devCommand(pm) {
962
1074
  return `${pm} dev`;
963
1075
  }
964
1076
 
1077
+ // src/utils/project-name.ts
1078
+ import path2 from "path";
1079
+ var PACKAGE_NAME = /^[a-z0-9][a-z0-9._-]*$/;
1080
+ var MAX_LENGTH = 214;
1081
+ function validateProjectName(name) {
1082
+ const trimmed = name.trim();
1083
+ if (!trimmed) return "Project name is required";
1084
+ if (trimmed.length > MAX_LENGTH) return `Use at most ${MAX_LENGTH} characters`;
1085
+ if (!PACKAGE_NAME.test(trimmed)) {
1086
+ return 'Use lowercase letters, digits, ".", "_" or "-", starting with a letter or digit';
1087
+ }
1088
+ return void 0;
1089
+ }
1090
+ function projectNameFromDirectory(dir, cwd = process.cwd()) {
1091
+ return path2.basename(path2.resolve(cwd, dir.trim()));
1092
+ }
1093
+
1094
+ // src/utils/ollama.ts
1095
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
1096
+ function isRecord2(value) {
1097
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1098
+ }
1099
+ function trimTrailingSlashes(value) {
1100
+ let end = value.length;
1101
+ while (end > 0 && value[end - 1] === "/") end--;
1102
+ return value.slice(0, end);
1103
+ }
1104
+ function resolveOllamaUrl(env = process.env) {
1105
+ const raw = env.OLLAMA_BASE_URL?.trim() || env.OLLAMA_HOST?.trim();
1106
+ if (!raw) return DEFAULT_OLLAMA_URL;
1107
+ const withScheme = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
1108
+ return trimTrailingSlashes(withScheme);
1109
+ }
1110
+ async function listOllamaModels(baseUrl, timeoutMs = 2e3) {
1111
+ try {
1112
+ const res = await fetch(`${baseUrl}/api/tags`, { signal: AbortSignal.timeout(timeoutMs) });
1113
+ if (!res.ok) return void 0;
1114
+ const data = await res.json();
1115
+ if (!isRecord2(data) || !Array.isArray(data.models)) return void 0;
1116
+ return data.models.flatMap(
1117
+ (model) => isRecord2(model) && typeof model.name === "string" ? [{ name: model.name, size: typeof model.size === "number" ? model.size : 0 }] : []
1118
+ );
1119
+ } catch {
1120
+ return void 0;
1121
+ }
1122
+ }
1123
+ function withTag(name) {
1124
+ return name.includes(":") ? name : `${name}:latest`;
1125
+ }
1126
+ function hasOllamaModel(installed, model) {
1127
+ const wanted = withTag(model);
1128
+ return installed.some((m) => withTag(m.name) === wanted);
1129
+ }
1130
+ function describeProgress(event, status) {
1131
+ const { completed, total } = event;
1132
+ if (typeof completed === "number" && typeof total === "number" && total > 0) {
1133
+ return `${status} ${Math.floor(completed / total * 100)}%`;
1134
+ }
1135
+ return status;
1136
+ }
1137
+ async function* ndjson(stream) {
1138
+ const reader = stream.getReader();
1139
+ const decoder = new TextDecoder();
1140
+ let buffer = "";
1141
+ try {
1142
+ while (true) {
1143
+ const { done, value } = await reader.read();
1144
+ if (done) break;
1145
+ buffer += decoder.decode(value, { stream: true });
1146
+ let newline = buffer.indexOf("\n");
1147
+ while (newline >= 0) {
1148
+ const line = buffer.slice(0, newline).trim();
1149
+ buffer = buffer.slice(newline + 1);
1150
+ if (line) yield JSON.parse(line);
1151
+ newline = buffer.indexOf("\n");
1152
+ }
1153
+ }
1154
+ const rest = (buffer + decoder.decode()).trim();
1155
+ if (rest) yield JSON.parse(rest);
1156
+ } finally {
1157
+ reader.releaseLock();
1158
+ }
1159
+ }
1160
+ async function pullOllamaModel(baseUrl, model, onStatus) {
1161
+ const res = await fetch(`${baseUrl}/api/pull`, {
1162
+ method: "POST",
1163
+ headers: { "Content-Type": "application/json" },
1164
+ body: JSON.stringify({ model, stream: true })
1165
+ });
1166
+ if (!res.ok || !res.body) {
1167
+ const detail = await res.text().catch(() => "");
1168
+ throw new Error(`Failed to pull ${model}: HTTP ${res.status}${detail ? ` ${detail}` : ""}`);
1169
+ }
1170
+ let succeeded = false;
1171
+ for await (const event of ndjson(res.body)) {
1172
+ if (!isRecord2(event)) continue;
1173
+ if (typeof event.error === "string") throw new Error(`Failed to pull ${model}: ${event.error}`);
1174
+ if (typeof event.status !== "string") continue;
1175
+ if (event.status === "success") succeeded = true;
1176
+ onStatus(describeProgress(event, event.status));
1177
+ }
1178
+ if (!succeeded) {
1179
+ throw new Error(`Failed to pull ${model}: the download stopped before it finished`);
1180
+ }
1181
+ }
1182
+
965
1183
  // src/prompts.ts
966
1184
  var validTemplates = ["basic", "memory", "swarm", "workflow", "api-server", "nextjs"];
967
1185
  var validProviders = ["ollama", "openai", "anthropic", "google"];
@@ -980,6 +1198,12 @@ function parseArgs(args) {
980
1198
  const val = args[++i];
981
1199
  if (validProviders.includes(val)) parsed.provider = val;
982
1200
  }
1201
+ } else if (arg === "--model") {
1202
+ const val = args[i + 1];
1203
+ if (val !== void 0 && !val.startsWith("-")) {
1204
+ parsed.model = val;
1205
+ i++;
1206
+ }
983
1207
  } else if (arg === "--pm") {
984
1208
  if (i + 1 < args.length) {
985
1209
  const val = args[++i];
@@ -1019,22 +1243,54 @@ function unlessCancelled(answer) {
1019
1243
  }
1020
1244
  return answer;
1021
1245
  }
1246
+ function validateDirectory(value) {
1247
+ if (!value) return void 0;
1248
+ if (!value.trim()) return "Project name is required";
1249
+ return validateProjectName(projectNameFromDirectory(value));
1250
+ }
1251
+ function formatSize(bytes) {
1252
+ return bytes > 0 ? `${(bytes / 1e9).toFixed(1)} GB` : void 0;
1253
+ }
1254
+ async function promptOllamaModel() {
1255
+ const installed = await listOllamaModels(resolveOllamaUrl());
1256
+ if (!installed) return void 0;
1257
+ const recommended = defaultModels.ollama;
1258
+ const others = installed.filter((model) => !hasOllamaModel([model], recommended));
1259
+ return unlessCancelled(
1260
+ await p.select({
1261
+ message: "Which Ollama model?",
1262
+ options: [
1263
+ {
1264
+ value: recommended,
1265
+ label: recommended,
1266
+ hint: hasOllamaModel(installed, recommended) ? "recommended, installed" : "recommended, not installed yet"
1267
+ },
1268
+ ...others.map((model) => ({
1269
+ value: model.name,
1270
+ label: model.name,
1271
+ hint: formatSize(model.size)
1272
+ }))
1273
+ ],
1274
+ initialValue: recommended
1275
+ })
1276
+ );
1277
+ }
1022
1278
  async function collectOptions(args) {
1023
1279
  const interactive = !args.yes;
1024
- const rawName = args.name || (interactive ? unlessCancelled(
1280
+ const directory = (args.name || (interactive ? unlessCancelled(
1025
1281
  await p.text({
1026
1282
  message: "Where should we create your project?",
1027
1283
  placeholder: `./${defaultAnswers.name}`,
1028
1284
  defaultValue: defaultAnswers.name,
1029
- validate: (value) => {
1030
- if (value && !value.trim()) return "Project name is required";
1031
- return void 0;
1032
- }
1285
+ validate: validateDirectory
1033
1286
  })
1034
- ) : defaultAnswers.name);
1035
- const name = rawName.trim();
1036
- const projectName = path.basename(path.normalize(name).replace(/^\.\//, "").replace(/^\.\\/, ""));
1037
- const projectPath = path.resolve(process.cwd(), name);
1287
+ ) : defaultAnswers.name)).trim();
1288
+ const projectPath = path3.resolve(process.cwd(), directory);
1289
+ const projectName = projectNameFromDirectory(directory);
1290
+ const nameError = validateProjectName(projectName);
1291
+ if (nameError) {
1292
+ throw new Error(`Invalid project name "${projectName}": ${nameError}`);
1293
+ }
1038
1294
  const template = args.template ?? (interactive ? unlessCancelled(
1039
1295
  await p.select({
1040
1296
  message: "Which template would you like?",
@@ -1049,7 +1305,7 @@ async function collectOptions(args) {
1049
1305
  {
1050
1306
  value: "ollama",
1051
1307
  label: "Ollama",
1052
- hint: `${defaultModels.ollama} \u2014 local, free, requires Ollama installed`
1308
+ hint: `${defaultModels.ollama}, local, free, requires Ollama installed`
1053
1309
  },
1054
1310
  { value: "openai", label: "OpenAI", hint: defaultModels.openai },
1055
1311
  { value: "anthropic", label: "Anthropic", hint: defaultModels.anthropic },
@@ -1058,6 +1314,7 @@ async function collectOptions(args) {
1058
1314
  initialValue: defaultAnswers.provider
1059
1315
  })
1060
1316
  ) : defaultAnswers.provider);
1317
+ const model = args.model ?? (interactive && provider === "ollama" ? await promptOllamaModel() : void 0);
1061
1318
  const packageManager = args.packageManager ?? (interactive ? unlessCancelled(
1062
1319
  await p.select({
1063
1320
  message: "Package manager?",
@@ -1087,6 +1344,7 @@ async function collectOptions(args) {
1087
1344
  path: projectPath,
1088
1345
  template,
1089
1346
  provider,
1347
+ ...model !== void 0 && { model },
1090
1348
  packageManager,
1091
1349
  docker,
1092
1350
  git,
@@ -1096,7 +1354,7 @@ async function collectOptions(args) {
1096
1354
 
1097
1355
  // src/scaffold.ts
1098
1356
  import fs from "fs";
1099
- import path2 from "path";
1357
+ import path4 from "path";
1100
1358
  import * as p2 from "@clack/prompts";
1101
1359
 
1102
1360
  // src/templates/base/tsconfig.ts
@@ -1127,15 +1385,19 @@ function generateTsconfig() {
1127
1385
  }
1128
1386
 
1129
1387
  // src/templates/base/gitignore.ts
1130
- function generateGitignore() {
1388
+ var NEXT_OUTPUT = [".next/", "out/", "next-env.d.ts"];
1389
+ function generateGitignore(template) {
1131
1390
  return {
1132
1391
  path: ".gitignore",
1133
1392
  content: [
1134
1393
  "node_modules/",
1135
1394
  "dist/",
1395
+ ...template === "nextjs" ? NEXT_OUTPUT : [],
1136
1396
  ".env",
1137
- ".env.local",
1397
+ ".env.*",
1398
+ "!.env.example",
1138
1399
  "*.log",
1400
+ "*.tsbuildinfo",
1139
1401
  ".DS_Store",
1140
1402
  "coverage/",
1141
1403
  ".turbo/",
@@ -1153,16 +1415,16 @@ function generateEnvExample(provider, template) {
1153
1415
  lines.push("# OLLAMA_BASE_URL=http://localhost:11434");
1154
1416
  break;
1155
1417
  case "openai":
1156
- lines.push("# OpenAI");
1157
- lines.push("OPENAI_API_KEY=sk-...");
1418
+ lines.push("# OpenAI, https://platform.openai.com/api-keys");
1419
+ lines.push("OPENAI_API_KEY=");
1158
1420
  break;
1159
1421
  case "anthropic":
1160
- lines.push("# Anthropic");
1161
- lines.push("ANTHROPIC_API_KEY=sk-ant-...");
1422
+ lines.push("# Anthropic, https://console.anthropic.com/settings/keys");
1423
+ lines.push("ANTHROPIC_API_KEY=");
1162
1424
  break;
1163
1425
  case "google":
1164
- lines.push("# Google AI");
1165
- lines.push("GOOGLE_API_KEY=...");
1426
+ lines.push("# Google AI, https://aistudio.google.com/apikey");
1427
+ lines.push("GOOGLE_API_KEY=");
1166
1428
  break;
1167
1429
  }
1168
1430
  if (template === "memory") {
@@ -1170,25 +1432,54 @@ function generateEnvExample(provider, template) {
1170
1432
  lines.push("# Redis");
1171
1433
  lines.push("REDIS_URL=redis://localhost:6379");
1172
1434
  }
1435
+ if (template === "api-server") {
1436
+ lines.push("");
1437
+ lines.push("# Bearer token that every API route except health and docs requires.");
1438
+ lines.push("# Optional in development, where the server listens on 127.0.0.1 only,");
1439
+ lines.push("# required in production (NODE_ENV=production), where it listens on 0.0.0.0.");
1440
+ lines.push("API_TOKEN=");
1441
+ lines.push("");
1442
+ lines.push("# Comma-separated browser origins allowed to call the API, CORS is off without it");
1443
+ lines.push("# CORS_ORIGIN=http://localhost:5173");
1444
+ lines.push("");
1445
+ lines.push("# HOST=127.0.0.1");
1446
+ lines.push("# PORT=3000");
1447
+ }
1173
1448
  lines.push("");
1174
1449
  return { path: ".env.example", content: lines.join("\n") };
1175
1450
  }
1176
1451
 
1177
1452
  // src/templates/base/docker-compose.ts
1178
- function generateDockerCompose(provider) {
1453
+ function ollamaServices(model) {
1454
+ return [
1455
+ " ollama:",
1456
+ " image: ollama/ollama:latest",
1457
+ " ports:",
1458
+ ' - "11434:11434"',
1459
+ " volumes:",
1460
+ " - ollama_data:/root/.ollama",
1461
+ " healthcheck:",
1462
+ ' test: ["CMD", "ollama", "list"]',
1463
+ " interval: 5s",
1464
+ " timeout: 5s",
1465
+ " retries: 20",
1466
+ "",
1467
+ " ollama-pull:",
1468
+ " image: ollama/ollama:latest",
1469
+ " depends_on:",
1470
+ " ollama:",
1471
+ " condition: service_healthy",
1472
+ " environment:",
1473
+ " OLLAMA_HOST: http://ollama:11434",
1474
+ ` entrypoint: ["ollama", "pull", ${JSON.stringify(model)}]`,
1475
+ ' restart: "no"',
1476
+ ""
1477
+ ];
1478
+ }
1479
+ function generateDockerCompose(provider, model = defaultModels[provider]) {
1179
1480
  const services = [];
1180
1481
  if (provider === "ollama") {
1181
- services.push(
1182
- ...[
1183
- " ollama:",
1184
- " image: ollama/ollama:latest",
1185
- " ports:",
1186
- ' - "11434:11434"',
1187
- " volumes:",
1188
- " - ollama_data:/root/.ollama",
1189
- ""
1190
- ]
1191
- );
1482
+ services.push(...ollamaServices(model));
1192
1483
  }
1193
1484
  services.push(
1194
1485
  ...[
@@ -1219,12 +1510,15 @@ function generateDockerCompose(provider) {
1219
1510
  }
1220
1511
 
1221
1512
  // src/templates/base/cogitator-yml.ts
1222
- function generateCogitatorYml(provider, template) {
1223
- const lines = [
1224
- "llm:",
1225
- ` defaultProvider: ${provider}`,
1226
- ` defaultModel: ${defaultModels[provider]}`
1227
- ];
1513
+ var HEADER = [
1514
+ "# Read by `cogitator run` and `cogitator deploy`: the provider and model, the",
1515
+ "# memory to provision and the deploy settings. The code in src/ configures",
1516
+ "# Cogitator itself, so change the provider, model or memory in both places.",
1517
+ ""
1518
+ ];
1519
+ function generateCogitatorYml(provider, template, model = defaultModels[provider]) {
1520
+ const generator = template ? getTemplate(template) : void 0;
1521
+ const lines = [...HEADER, "llm:", ` defaultProvider: ${provider}`, ` defaultModel: ${model}`];
1228
1522
  if (provider === "ollama") {
1229
1523
  lines.push(
1230
1524
  " providers:",
@@ -1232,7 +1526,7 @@ function generateCogitatorYml(provider, template) {
1232
1526
  " baseUrl: ${OLLAMA_BASE_URL:-http://localhost:11434}"
1233
1527
  );
1234
1528
  }
1235
- if (template === "memory") {
1529
+ if (generator?.memoryAdapter === "redis") {
1236
1530
  lines.push(
1237
1531
  "",
1238
1532
  "memory:",
@@ -1240,15 +1534,56 @@ function generateCogitatorYml(provider, template) {
1240
1534
  " redis:",
1241
1535
  " url: ${REDIS_URL:-redis://localhost:6379}"
1242
1536
  );
1537
+ } else if (generator?.memoryAdapter === "memory") {
1538
+ lines.push("", "memory:", " adapter: memory");
1539
+ }
1540
+ const deploy = [];
1541
+ if (generator?.healthPath) {
1542
+ deploy.push(" health:", ` path: ${generator.healthPath}`);
1543
+ }
1544
+ if (generator?.secrets?.length) {
1545
+ const secrets = [
1546
+ ...provider === "ollama" ? [] : [providerEnvKey(provider)],
1547
+ ...generator.secrets
1548
+ ];
1549
+ deploy.push(" secrets:", ...secrets.map((name) => ` - ${name}`));
1243
1550
  }
1244
- const healthPath = template ? getTemplate(template).healthPath : void 0;
1245
- if (healthPath) {
1246
- lines.push("", "deploy:", " health:", ` path: ${healthPath}`);
1551
+ if (deploy.length > 0) {
1552
+ lines.push("", "deploy:", ...deploy);
1247
1553
  }
1248
1554
  lines.push("");
1249
1555
  return { path: "cogitator.yml", content: lines.join("\n") };
1250
1556
  }
1251
1557
 
1558
+ // src/next-steps.ts
1559
+ function nextSteps(options, state) {
1560
+ const steps = [];
1561
+ if (state.directory) {
1562
+ const dir = /^[\w./-]+$/.test(state.directory) ? state.directory : JSON.stringify(state.directory);
1563
+ steps.push({ command: `cd ${dir}` });
1564
+ }
1565
+ if (!state.installed) {
1566
+ steps.push({ command: `${options.packageManager} install` });
1567
+ }
1568
+ if (options.provider !== "ollama") {
1569
+ steps.push({
1570
+ command: "cp .env.example .env",
1571
+ note: `then set ${providerEnvKey(options.provider)} in .env`
1572
+ });
1573
+ }
1574
+ if (options.provider === "ollama" && !state.modelReady) {
1575
+ steps.push({
1576
+ command: `ollama pull ${modelFor(options)}`,
1577
+ note: options.docker ? "download the model the agents use, or let `docker compose up -d` pull it into the Ollama container" : "download the model the agents use"
1578
+ });
1579
+ }
1580
+ if (options.template === "memory" && options.docker) {
1581
+ steps.push({ command: "docker compose up -d redis", note: "the memory lives in Redis" });
1582
+ }
1583
+ steps.push({ command: devCommand(options.packageManager) });
1584
+ return steps;
1585
+ }
1586
+
1252
1587
  // src/utils/links.ts
1253
1588
  var DOCS_URL = "https://cogitator.app/docs";
1254
1589
  var REPO_URL = "https://github.com/cogitator-ai/Cogitator-AI";
@@ -1263,24 +1598,33 @@ var templateNames = {
1263
1598
  nextjs: "Next.js Chat App"
1264
1599
  };
1265
1600
  function generateReadme(options) {
1266
- const dev = devCommand(options.packageManager);
1267
1601
  const templateName = templateNames[options.template] || options.template;
1602
+ const steps = nextSteps(options, { installed: false, modelReady: false });
1268
1603
  const content = [
1269
1604
  `# ${options.name}`,
1270
1605
  "",
1271
- `> Created with [create-cogitator-app](${REPO_URL}) \u2014 ${templateName} template`,
1606
+ `> Created with [create-cogitator-app](${REPO_URL}) - ${templateName} template`,
1272
1607
  "",
1273
1608
  "## Getting Started",
1274
1609
  "",
1275
1610
  "```bash",
1276
- `# Install dependencies`,
1277
- `${options.packageManager} install`,
1278
- "",
1279
- `# Run the project`,
1280
- dev,
1611
+ ...steps.flatMap((step, i) => [
1612
+ ...i > 0 ? [""] : [],
1613
+ ...step.note ? [`# ${step.note}`] : [],
1614
+ step.command
1615
+ ]),
1281
1616
  "```",
1282
1617
  "",
1283
- ...options.docker ? ["## Docker Services", "", "```bash", "docker compose up -d", "```", ""] : [],
1618
+ ...options.docker ? [
1619
+ "## Docker Services",
1620
+ "",
1621
+ "```bash",
1622
+ "docker compose up -d",
1623
+ "```",
1624
+ "",
1625
+ options.provider === "ollama" ? "Starts Redis, Postgres and Ollama, and pulls the model into the Ollama container." : "Starts Redis and Postgres.",
1626
+ ""
1627
+ ] : [],
1284
1628
  "## Learn More",
1285
1629
  "",
1286
1630
  `- [Cogitator Documentation](${DOCS_URL})`,
@@ -1337,9 +1681,10 @@ function initGitRepo(cwd) {
1337
1681
  }
1338
1682
 
1339
1683
  // src/scaffold.ts
1684
+ var NODE_ENGINES = ">=22.12.0";
1340
1685
  function writeFile(basePath, file) {
1341
- const fullPath = path2.join(basePath, file.path);
1342
- fs.mkdirSync(path2.dirname(fullPath), { recursive: true });
1686
+ const fullPath = path4.join(basePath, file.path);
1687
+ fs.mkdirSync(path4.dirname(fullPath), { recursive: true });
1343
1688
  fs.writeFileSync(fullPath, file.content, "utf-8");
1344
1689
  }
1345
1690
  function buildPackageJson(options) {
@@ -1350,6 +1695,7 @@ function buildPackageJson(options) {
1350
1695
  version: "0.1.0",
1351
1696
  private: true,
1352
1697
  ...isNextjs ? {} : { type: "module" },
1698
+ engines: { node: NODE_ENGINES },
1353
1699
  scripts: template.scripts(),
1354
1700
  dependencies: template.dependencies(),
1355
1701
  devDependencies: template.devDependencies()
@@ -1361,9 +1707,9 @@ function collectFiles(options) {
1361
1707
  const files = [];
1362
1708
  files.push({ path: "package.json", content: buildPackageJson(options) });
1363
1709
  files.push(...template.files(options));
1364
- files.push(generateGitignore());
1710
+ files.push(generateGitignore(options.template));
1365
1711
  files.push(generateEnvExample(options.provider, options.template));
1366
- files.push(generateCogitatorYml(options.provider, options.template));
1712
+ files.push(generateCogitatorYml(options.provider, options.template, modelFor(options)));
1367
1713
  files.push(generateReadme(options));
1368
1714
  if (options.template !== "nextjs") {
1369
1715
  files.push(generateTsconfig());
@@ -1372,22 +1718,26 @@ function collectFiles(options) {
1372
1718
  files.push(generatePnpmWorkspace());
1373
1719
  }
1374
1720
  if (options.docker) {
1375
- files.push(generateDockerCompose(options.provider));
1721
+ files.push(generateDockerCompose(options.provider, modelFor(options)));
1376
1722
  }
1377
1723
  return files;
1378
1724
  }
1379
- function runStep(spinner2, messages, step) {
1380
- spinner2.start(messages.start);
1725
+ function runStep(spinner3, messages, step) {
1726
+ spinner3.start(messages.start);
1381
1727
  try {
1382
1728
  step();
1383
- spinner2.stop(messages.done);
1729
+ spinner3.stop(messages.done);
1384
1730
  return { status: "done" };
1385
1731
  } catch (error) {
1386
- spinner2.stop(messages.failed);
1732
+ spinner3.stop(messages.failed);
1387
1733
  return { status: "failed", error: error instanceof Error ? error : new Error(String(error)) };
1388
1734
  }
1389
1735
  }
1390
1736
  async function scaffold(options) {
1737
+ const nameError = validateProjectName(options.name);
1738
+ if (nameError) {
1739
+ throw new Error(`Invalid project name "${options.name}": ${nameError}`);
1740
+ }
1391
1741
  if (fs.existsSync(options.path) && fs.readdirSync(options.path).length > 0) {
1392
1742
  throw new Error(`Directory "${options.path}" already exists and is not empty`);
1393
1743
  }
@@ -1421,7 +1771,35 @@ async function scaffold(options) {
1421
1771
  }
1422
1772
 
1423
1773
  // src/index.ts
1424
- var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
1774
+ var pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
1775
+ async function prepareOllamaModel(options, interactive) {
1776
+ const model = modelFor(options);
1777
+ const baseUrl = resolveOllamaUrl();
1778
+ const installed = await listOllamaModels(baseUrl);
1779
+ if (!installed) {
1780
+ p3.log.warn(
1781
+ `Ollama is not running at ${baseUrl}. Start it and pull ${model} before the first run.`
1782
+ );
1783
+ return false;
1784
+ }
1785
+ if (hasOllamaModel(installed, model)) return true;
1786
+ if (!interactive) return false;
1787
+ const pull = await p3.confirm({
1788
+ message: `${model} is not installed in Ollama. Pull it now?`,
1789
+ initialValue: true
1790
+ });
1791
+ if (p3.isCancel(pull) || !pull) return false;
1792
+ const s = p3.spinner();
1793
+ s.start(`Pulling ${model}`);
1794
+ try {
1795
+ await pullOllamaModel(baseUrl, model, (status) => s.message(`Pulling ${model}: ${status}`));
1796
+ s.stop(`Pulled ${model}`);
1797
+ return true;
1798
+ } catch (error) {
1799
+ s.stop(error instanceof Error ? error.message : `Failed to pull ${model}`);
1800
+ return false;
1801
+ }
1802
+ }
1425
1803
  async function main() {
1426
1804
  banner(pkg.version);
1427
1805
  p3.intro(pc2.cyan("Let's build something with AI agents"));
@@ -1434,15 +1812,19 @@ async function main() {
1434
1812
  if (result.git.status === "failed") {
1435
1813
  p3.log.warn(`git init failed: ${result.git.error.message}`);
1436
1814
  }
1437
- const dev = devCommand(options.packageManager);
1438
- const install = result.install.status === "done" ? [] : [` ${pc2.cyan(`${options.packageManager} install`)}`];
1815
+ const modelReady = options.provider === "ollama" ? await prepareOllamaModel(options, !args.yes) : true;
1816
+ const steps = nextSteps(options, {
1817
+ installed: result.install.status === "done",
1818
+ modelReady,
1819
+ directory: path5.relative(process.cwd(), options.path) || void 0
1820
+ });
1439
1821
  p3.outro(
1440
1822
  [
1441
1823
  pc2.green("Done! ") + "Next steps:",
1442
1824
  "",
1443
- ` ${pc2.cyan("cd")} ${options.name}`,
1444
- ...install,
1445
- ` ${pc2.cyan(dev)}`,
1825
+ ...steps.map(
1826
+ (step) => ` ${pc2.cyan(step.command)}${step.note ? pc2.dim(` # ${step.note}`) : ""}`
1827
+ ),
1446
1828
  "",
1447
1829
  pc2.dim(`Docs: ${DOCS_URL}`)
1448
1830
  ].join("\n")