@cogitator-ai/cli 0.3.16 → 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.
Files changed (73) hide show
  1. package/README.md +159 -510
  2. package/dist/commands/assistant.d.ts +13 -0
  3. package/dist/commands/assistant.d.ts.map +1 -1
  4. package/dist/commands/assistant.js +64 -58
  5. package/dist/commands/assistant.js.map +1 -1
  6. package/dist/commands/build.d.ts +2 -0
  7. package/dist/commands/build.d.ts.map +1 -1
  8. package/dist/commands/build.js +114 -59
  9. package/dist/commands/build.js.map +1 -1
  10. package/dist/commands/daemon.d.ts.map +1 -1
  11. package/dist/commands/daemon.js +223 -262
  12. package/dist/commands/daemon.js.map +1 -1
  13. package/dist/commands/deploy.d.ts.map +1 -1
  14. package/dist/commands/deploy.js +80 -42
  15. package/dist/commands/deploy.js.map +1 -1
  16. package/dist/commands/init.d.ts +44 -0
  17. package/dist/commands/init.d.ts.map +1 -1
  18. package/dist/commands/init.js +334 -265
  19. package/dist/commands/init.js.map +1 -1
  20. package/dist/commands/logs.d.ts +1 -0
  21. package/dist/commands/logs.d.ts.map +1 -1
  22. package/dist/commands/logs.js +8 -4
  23. package/dist/commands/logs.js.map +1 -1
  24. package/dist/commands/models.d.ts +1 -1
  25. package/dist/commands/models.d.ts.map +1 -1
  26. package/dist/commands/models.js +83 -93
  27. package/dist/commands/models.js.map +1 -1
  28. package/dist/commands/run.d.ts +4 -1
  29. package/dist/commands/run.d.ts.map +1 -1
  30. package/dist/commands/run.js +151 -128
  31. package/dist/commands/run.js.map +1 -1
  32. package/dist/commands/skill.d.ts +17 -0
  33. package/dist/commands/skill.d.ts.map +1 -1
  34. package/dist/commands/skill.js +203 -81
  35. package/dist/commands/skill.js.map +1 -1
  36. package/dist/commands/status.d.ts.map +1 -1
  37. package/dist/commands/status.js +20 -24
  38. package/dist/commands/status.js.map +1 -1
  39. package/dist/commands/up.d.ts +4 -0
  40. package/dist/commands/up.d.ts.map +1 -1
  41. package/dist/commands/up.js +182 -130
  42. package/dist/commands/up.js.map +1 -1
  43. package/dist/commands/wizard.d.ts +6 -0
  44. package/dist/commands/wizard.d.ts.map +1 -1
  45. package/dist/commands/wizard.js +301 -267
  46. package/dist/commands/wizard.js.map +1 -1
  47. package/dist/index.js +8 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/utils/daemon.d.ts +37 -0
  50. package/dist/utils/daemon.d.ts.map +1 -0
  51. package/dist/utils/daemon.js +194 -0
  52. package/dist/utils/daemon.js.map +1 -0
  53. package/dist/utils/docker.d.ts +9 -0
  54. package/dist/utils/docker.d.ts.map +1 -1
  55. package/dist/utils/docker.js +67 -12
  56. package/dist/utils/docker.js.map +1 -1
  57. package/dist/utils/env.d.ts +7 -0
  58. package/dist/utils/env.d.ts.map +1 -0
  59. package/dist/utils/env.js +41 -0
  60. package/dist/utils/env.js.map +1 -0
  61. package/dist/utils/module-loader.d.ts +5 -0
  62. package/dist/utils/module-loader.d.ts.map +1 -0
  63. package/dist/utils/module-loader.js +52 -0
  64. package/dist/utils/module-loader.js.map +1 -0
  65. package/dist/utils/ollama.d.ts +20 -0
  66. package/dist/utils/ollama.d.ts.map +1 -0
  67. package/dist/utils/ollama.js +111 -0
  68. package/dist/utils/ollama.js.map +1 -0
  69. package/dist/utils/provider-models.d.ts +13 -0
  70. package/dist/utils/provider-models.d.ts.map +1 -0
  71. package/dist/utils/provider-models.js +78 -0
  72. package/dist/utils/provider-models.js.map +1 -0
  73. package/package.json +8 -8
@@ -1,296 +1,154 @@
1
1
  import { Command } from 'commander';
2
- import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
3
- import { join, resolve } from 'node:path';
4
- import { execSync } from 'node:child_process';
2
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { execFileSync } from 'node:child_process';
5
6
  import * as p from '@clack/prompts';
6
7
  import chalk from 'chalk';
7
8
  import { printBanner } from '../utils/logger.js';
8
- const PROVIDER_MODELS = {
9
- anthropic: [
10
- { label: 'Claude Sonnet 4 (recommended)', value: 'anthropic/claude-sonnet-4-20250514' },
11
- { label: 'Claude Opus 4', value: 'anthropic/claude-opus-4-20250514' },
12
- { label: 'Claude Haiku 3.5', value: 'anthropic/claude-3-5-haiku-20241022' },
13
- ],
14
- openai: [
15
- { label: 'GPT-4o (recommended)', value: 'openai/gpt-4o' },
16
- { label: 'GPT-4o mini', value: 'openai/gpt-4o-mini' },
17
- { label: 'o3-mini', value: 'openai/o3-mini' },
18
- ],
19
- google: [
20
- { label: 'Gemini 2.5 Flash (recommended)', value: 'google/gemini-2.5-flash' },
21
- { label: 'Gemini 2.5 Pro', value: 'google/gemini-2.5-pro' },
22
- ],
23
- ollama: [
24
- { label: 'Llama 3.1 8B (recommended)', value: 'ollama/llama3.1:8b' },
25
- { label: 'Gemma 3 4B', value: 'ollama/gemma3:4b' },
26
- { label: 'Mistral 7B', value: 'ollama/mistral:7b' },
27
- ],
28
- };
29
- const API_KEY_NAMES = {
30
- anthropic: 'ANTHROPIC_API_KEY',
31
- openai: 'OPENAI_API_KEY',
32
- google: 'GOOGLE_API_KEY',
33
- };
34
- export const initCommand = new Command('init')
35
- .description('Create a new Cogitator AI assistant project')
36
- .argument('[name]', 'Project name')
37
- .option('--no-install', 'Skip dependency installation')
38
- .action(async (nameArg, options) => {
39
- printBanner();
40
- p.intro(chalk.bgCyan(chalk.black(' cogitator init ')));
41
- const answers = await collectAnswers(nameArg);
42
- if (!answers)
43
- return;
44
- const projectPath = resolve(process.cwd(), answers.projectName);
45
- if (existsSync(projectPath)) {
46
- p.cancel(`Directory "${answers.projectName}" already exists`);
47
- process.exit(1);
48
- }
49
- await p.tasks([
50
- {
51
- title: 'Creating project structure',
52
- task: () => {
53
- createProjectFiles(projectPath, answers);
54
- return 'Project files created';
55
- },
56
- },
57
- {
58
- title: 'Installing dependencies',
59
- enabled: options.install,
60
- task: () => {
61
- try {
62
- execSync('pnpm install', { cwd: projectPath, stdio: 'pipe' });
63
- return 'Dependencies installed';
64
- }
65
- catch {
66
- return 'Install failed — run pnpm install manually';
67
- }
68
- },
69
- },
70
- ]);
71
- const channelNames = answers.channels.join(', ') || 'webchat';
72
- p.note([`cd ${answers.projectName}`, !options.install ? 'pnpm install' : '', 'cogitator up']
73
- .filter(Boolean)
74
- .join('\n'), 'Next steps');
75
- p.outro(`${chalk.green('Your assistant is ready!')} Channels: ${chalk.cyan(channelNames)}`);
76
- });
77
- async function collectAnswers(nameArg) {
78
- const projectName = nameArg ??
79
- (await p.text({
80
- message: 'Project name',
81
- placeholder: 'my-assistant',
82
- validate: (v) => (!v.trim() ? 'Name is required' : undefined),
83
- }));
84
- if (p.isCancel(projectName)) {
85
- p.cancel('Setup cancelled');
86
- process.exit(0);
9
+ import { formatEnvLine } from '../utils/env.js';
10
+ import { DEFAULT_OLLAMA_URL, resolveOllamaUrl } from '../utils/ollama.js';
11
+ import { API_KEY_ENV, fetchOllamaModelOptions, fetchProviderModels, isSetupProvider, } from '../utils/provider-models.js';
12
+ export const DEFAULT_POSTGRES_URL = 'postgresql://cogitator:cogitator@localhost:5432/cogitator';
13
+ const PROJECT_NAME = /^[a-z0-9][a-z0-9._-]*$/;
14
+ const COGITATOR_PACKAGES = ['@cogitator-ai/core', '@cogitator-ai/channels', '@cogitator-ai/memory'];
15
+ export function validateProjectName(name) {
16
+ const trimmed = name.trim();
17
+ if (!trimmed)
18
+ return 'Name is required';
19
+ if (trimmed.length > 214)
20
+ return 'Name must be 214 characters or fewer';
21
+ if (!PROJECT_NAME.test(trimmed)) {
22
+ return 'Use lowercase letters, digits, ".", "_" or "-" (must start with a letter or digit)';
87
23
  }
88
- const provider = (await p.select({
89
- message: 'Which LLM provider?',
90
- options: [
91
- { value: 'anthropic', label: 'Anthropic (Claude)', hint: 'recommended' },
92
- { value: 'openai', label: 'OpenAI (GPT-4o)' },
93
- { value: 'google', label: 'Google (Gemini)' },
94
- { value: 'ollama', label: 'Ollama (local, free)', hint: 'no API key needed' },
95
- ],
96
- }));
97
- if (p.isCancel(provider)) {
98
- p.cancel('Setup cancelled');
99
- process.exit(0);
100
- }
101
- let apiKey = '';
102
- if (provider !== 'ollama') {
103
- const keyResult = await p.password({
104
- message: `${API_KEY_NAMES[provider]}:`,
105
- validate: (v) => (!v.trim() ? 'API key is required' : undefined),
106
- });
107
- if (p.isCancel(keyResult)) {
108
- p.cancel('Setup cancelled');
109
- process.exit(0);
110
- }
111
- apiKey = keyResult;
112
- }
113
- const model = (await p.select({
114
- message: 'Default model',
115
- options: PROVIDER_MODELS[provider],
116
- }));
117
- if (p.isCancel(model)) {
118
- p.cancel('Setup cancelled');
119
- process.exit(0);
120
- }
121
- const channels = (await p.multiselect({
122
- message: 'Which channels to connect?',
123
- options: [
124
- { value: 'telegram', label: 'Telegram' },
125
- { value: 'discord', label: 'Discord' },
126
- { value: 'slack', label: 'Slack' },
127
- { value: 'webchat', label: 'WebChat (localhost)', hint: 'no setup needed' },
128
- ],
129
- required: false,
130
- }));
131
- if (p.isCancel(channels)) {
132
- p.cancel('Setup cancelled');
133
- process.exit(0);
134
- }
135
- let telegramToken;
136
- let discordToken;
137
- let slackToken;
138
- let slackSigningSecret;
139
- if (channels.includes('telegram')) {
140
- const token = await p.password({
141
- message: 'Telegram bot token (from @BotFather):',
142
- validate: (v) => (!v.trim() ? 'Token is required' : undefined),
143
- });
144
- if (p.isCancel(token)) {
145
- p.cancel('Setup cancelled');
146
- process.exit(0);
24
+ return undefined;
25
+ }
26
+ export function resolveDependencyVersions(cliPackageJson = join(dirname(fileURLToPath(import.meta.url)), '../../package.json')) {
27
+ const versions = {};
28
+ let deps = {};
29
+ try {
30
+ const parsed = JSON.parse(readFileSync(cliPackageJson, 'utf-8'));
31
+ if (typeof parsed === 'object' && parsed !== null && 'dependencies' in parsed) {
32
+ const value = parsed.dependencies;
33
+ if (typeof value === 'object' && value !== null)
34
+ deps = Object.fromEntries(Object.entries(value));
147
35
  }
148
- telegramToken = token;
149
36
  }
150
- if (channels.includes('discord')) {
151
- const token = await p.password({
152
- message: 'Discord bot token:',
153
- validate: (v) => (!v.trim() ? 'Token is required' : undefined),
154
- });
155
- if (p.isCancel(token)) {
156
- p.cancel('Setup cancelled');
157
- process.exit(0);
158
- }
159
- discordToken = token;
37
+ catch {
38
+ deps = {};
160
39
  }
161
- if (channels.includes('slack')) {
162
- const token = await p.password({
163
- message: 'Slack bot token (xoxb-...):',
164
- validate: (v) => (!v.trim() ? 'Token is required' : undefined),
165
- });
166
- if (p.isCancel(token)) {
167
- p.cancel('Setup cancelled');
168
- process.exit(0);
169
- }
170
- slackToken = token;
171
- const secret = await p.password({
172
- message: 'Slack signing secret:',
173
- validate: (v) => (!v.trim() ? 'Signing secret is required' : undefined),
174
- });
175
- if (p.isCancel(secret)) {
176
- p.cancel('Setup cancelled');
177
- process.exit(0);
178
- }
179
- slackSigningSecret = secret;
40
+ for (const name of COGITATOR_PACKAGES) {
41
+ const range = deps[name];
42
+ versions[name] =
43
+ typeof range === 'string' && /^\^?~?\d/.test(range)
44
+ ? `^${range.replace(/^[\^~]/, '')}`
45
+ : 'latest';
180
46
  }
181
- const memory = (await p.select({
182
- message: 'Memory adapter',
183
- options: [
184
- { value: 'memory', label: 'In-memory (no persistence)', hint: 'simple, for testing' },
185
- { value: 'sqlite', label: 'SQLite (recommended)', hint: 'zero config, file-based' },
186
- { value: 'postgres', label: 'PostgreSQL', hint: 'production-grade' },
187
- ],
188
- }));
189
- if (p.isCancel(memory)) {
190
- p.cancel('Setup cancelled');
191
- process.exit(0);
192
- }
193
- return {
194
- provider,
195
- apiKey,
196
- model,
197
- channels: channels.length > 0 ? channels : ['webchat'],
198
- telegramToken,
199
- discordToken,
200
- slackToken,
201
- slackSigningSecret,
202
- memory,
203
- projectName: projectName,
204
- };
47
+ return versions;
205
48
  }
206
- function createProjectFiles(projectPath, answers) {
207
- mkdirSync(join(projectPath, 'src'), { recursive: true });
49
+ export function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
50
+ const name = userAgent?.split('/')[0];
51
+ if (name === 'npm' || name === 'yarn' || name === 'bun' || name === 'pnpm')
52
+ return name;
53
+ return 'pnpm';
54
+ }
55
+ function runScript(pm, script) {
56
+ return pm === 'npm' ? `npm run ${script}` : `${pm} ${script}`;
57
+ }
58
+ export function buildPackageJson(answers, options) {
208
59
  const deps = {
209
- '@cogitator-ai/core': '^0.1.0',
210
- '@cogitator-ai/channels': '^0.1.0',
211
- '@cogitator-ai/memory': '^0.1.0',
212
- zod: '^3.22.4',
60
+ ...options.dependencyVersions,
61
+ zod: '^4.0.0',
213
62
  };
214
63
  if (answers.channels.includes('telegram'))
215
64
  deps.grammy = '^1.20.0';
216
65
  if (answers.channels.includes('discord'))
217
66
  deps['discord.js'] = '^14.0.0';
218
67
  if (answers.channels.includes('slack'))
219
- deps['@slack/bolt'] = '^3.0.0';
68
+ deps['@slack/bolt'] = '^4.0.0';
220
69
  if (answers.channels.includes('webchat'))
221
- deps.ws = '^8.0.0';
222
- writeFileSync(join(projectPath, 'package.json'), JSON.stringify({
70
+ deps.ws = '^8.18.0';
71
+ if (answers.memory === 'sqlite')
72
+ deps['better-sqlite3'] = '^11.6.0';
73
+ if (answers.memory === 'postgres')
74
+ deps.pg = '^8.18.0';
75
+ const sortedDeps = Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b)));
76
+ return (JSON.stringify({
223
77
  name: answers.projectName,
224
78
  version: '0.1.0',
79
+ private: true,
225
80
  type: 'module',
81
+ engines: { node: '>=20.12.0' },
226
82
  scripts: {
227
83
  dev: 'tsx watch src/agent.ts',
228
84
  start: 'tsx src/agent.ts',
229
85
  build: 'tsc',
230
86
  },
231
- dependencies: deps,
87
+ dependencies: sortedDeps,
232
88
  devDependencies: {
233
- '@types/node': '^20.10.0',
234
- tsx: '^4.7.0',
89
+ '@types/node': '^22.0.0',
90
+ tsx: '^4.21.0',
235
91
  typescript: '^5.3.0',
236
92
  },
237
- }, null, 2));
238
- writeFileSync(join(projectPath, 'tsconfig.json'), JSON.stringify({
239
- compilerOptions: {
240
- target: 'ES2022',
241
- module: 'NodeNext',
242
- moduleResolution: 'NodeNext',
243
- strict: true,
244
- esModuleInterop: true,
245
- skipLibCheck: true,
246
- outDir: './dist',
247
- rootDir: './src',
248
- },
249
- include: ['src/**/*'],
250
- }, null, 2));
251
- writeEnvFile(projectPath, answers);
252
- writeConfigFile(projectPath, answers);
253
- writeAgentFile(projectPath, answers);
254
- writeFileSync(join(projectPath, '.gitignore'), `node_modules/
255
- dist/
256
- .env
257
- *.log
258
- sessions.db
259
- `);
93
+ }, null, 2) + '\n');
260
94
  }
261
- function writeEnvFile(projectPath, answers) {
95
+ export function buildEnvFile(answers) {
262
96
  const lines = [];
263
- if (answers.apiKey && API_KEY_NAMES[answers.provider]) {
264
- lines.push(`${API_KEY_NAMES[answers.provider]}=${answers.apiKey}`);
97
+ if (answers.provider !== 'ollama' && answers.apiKey) {
98
+ lines.push(formatEnvLine(API_KEY_ENV[answers.provider], answers.apiKey));
265
99
  }
266
100
  if (answers.telegramToken)
267
- lines.push(`TELEGRAM_BOT_TOKEN=${answers.telegramToken}`);
101
+ lines.push(formatEnvLine('TELEGRAM_BOT_TOKEN', answers.telegramToken));
268
102
  if (answers.discordToken)
269
- lines.push(`DISCORD_BOT_TOKEN=${answers.discordToken}`);
103
+ lines.push(formatEnvLine('DISCORD_BOT_TOKEN', answers.discordToken));
270
104
  if (answers.slackToken)
271
- lines.push(`SLACK_BOT_TOKEN=${answers.slackToken}`);
272
- if (answers.slackSigningSecret)
273
- lines.push(`SLACK_SIGNING_SECRET=${answers.slackSigningSecret}`);
274
- if (lines.length > 0) {
275
- writeFileSync(join(projectPath, '.env'), lines.join('\n') + '\n');
105
+ lines.push(formatEnvLine('SLACK_BOT_TOKEN', answers.slackToken));
106
+ if (answers.slackSigningSecret) {
107
+ lines.push(formatEnvLine('SLACK_SIGNING_SECRET', answers.slackSigningSecret));
108
+ }
109
+ if (answers.memory === 'postgres') {
110
+ lines.push(formatEnvLine('DATABASE_URL', answers.databaseUrl ?? DEFAULT_POSTGRES_URL));
111
+ }
112
+ return lines.length > 0 ? lines.join('\n') + '\n' : null;
113
+ }
114
+ function memorySetup(memory) {
115
+ switch (memory) {
116
+ case 'sqlite':
117
+ return {
118
+ importName: 'SQLiteAdapter',
119
+ code: `const memory = new SQLiteAdapter({ provider: 'sqlite', path: './data/memory.db' });`,
120
+ };
121
+ case 'postgres':
122
+ return {
123
+ importName: 'PostgresAdapter',
124
+ code: `const memory = new PostgresAdapter({
125
+ provider: 'postgres',
126
+ connectionString: requireEnv('DATABASE_URL'),
127
+ });`,
128
+ };
129
+ case 'memory':
130
+ return {
131
+ importName: 'InMemoryAdapter',
132
+ code: `const memory = new InMemoryAdapter({ provider: 'memory' });`,
133
+ };
276
134
  }
277
135
  }
278
- function writeConfigFile(projectPath, answers) {
136
+ export function buildGatewayFile(answers) {
279
137
  const channelImports = [];
280
138
  const channelSetup = [];
281
139
  for (const ch of answers.channels) {
282
140
  switch (ch) {
283
141
  case 'telegram':
284
142
  channelImports.push('telegramChannel');
285
- channelSetup.push(` telegramChannel({ token: process.env.TELEGRAM_BOT_TOKEN! }),`);
143
+ channelSetup.push(` telegramChannel({ token: requireEnv('TELEGRAM_BOT_TOKEN') }),`);
286
144
  break;
287
145
  case 'discord':
288
146
  channelImports.push('discordChannel');
289
- channelSetup.push(` discordChannel({ token: process.env.DISCORD_BOT_TOKEN!, mentionOnly: true }),`);
147
+ channelSetup.push(` discordChannel({ token: requireEnv('DISCORD_BOT_TOKEN'), mentionOnly: true }),`);
290
148
  break;
291
149
  case 'slack':
292
150
  channelImports.push('slackChannel');
293
- channelSetup.push(` slackChannel({\n token: process.env.SLACK_BOT_TOKEN!,\n signingSecret: process.env.SLACK_SIGNING_SECRET!,\n }),`);
151
+ channelSetup.push(` slackChannel({\n token: requireEnv('SLACK_BOT_TOKEN'),\n signingSecret: requireEnv('SLACK_SIGNING_SECRET'),\n }),`);
294
152
  break;
295
153
  case 'webchat':
296
154
  channelImports.push('webchatChannel');
@@ -299,11 +157,23 @@ function writeConfigFile(projectPath, answers) {
299
157
  }
300
158
  }
301
159
  const providerConfig = answers.provider === 'ollama'
302
- ? ` ollama: { baseUrl: 'http://localhost:11434' },`
303
- : ` ${answers.provider}: { apiKey: process.env.${API_KEY_NAMES[answers.provider]} },`;
304
- const content = `import { Cogitator, Agent } from '@cogitator-ai/core';
160
+ ? ` ollama: { baseUrl: process.env.OLLAMA_URL ?? '${DEFAULT_OLLAMA_URL}' },`
161
+ : ` ${answers.provider}: { apiKey: requireEnv('${API_KEY_ENV[answers.provider]}') },`;
162
+ const memory = memorySetup(answers.memory);
163
+ return `import { existsSync } from 'node:fs';
164
+ import { fileURLToPath } from 'node:url';
165
+ import { Cogitator, Agent } from '@cogitator-ai/core';
305
166
  import { Gateway, ${channelImports.join(', ')} } from '@cogitator-ai/channels';
306
- import { InMemoryAdapter } from '@cogitator-ai/memory';
167
+ import { ${memory.importName} } from '@cogitator-ai/memory';
168
+
169
+ const envFile = fileURLToPath(new URL('../.env', import.meta.url));
170
+ if (existsSync(envFile)) process.loadEnvFile(envFile);
171
+
172
+ function requireEnv(name: string): string {
173
+ const value = process.env[name];
174
+ if (!value) throw new Error(\`Missing environment variable \${name} (see .env)\`);
175
+ return value;
176
+ }
307
177
 
308
178
  const agent = new Agent({
309
179
  name: 'assistant',
@@ -314,13 +184,19 @@ Be concise and friendly. Use tools when available.\`,
314
184
 
315
185
  const cogitator = new Cogitator({
316
186
  llm: {
187
+ defaultProvider: '${answers.provider}',
317
188
  providers: {
318
189
  ${providerConfig}
319
190
  },
320
191
  },
321
192
  });
322
193
 
323
- const memory = new InMemoryAdapter();
194
+ ${memory.code}
195
+
196
+ const connected = await memory.connect();
197
+ if (!connected.success) {
198
+ throw new Error(\`Failed to connect memory: \${connected.error}\`);
199
+ }
324
200
 
325
201
  export const gateway = new Gateway({
326
202
  agent,
@@ -338,26 +214,219 @@ ${channelSetup.join('\n')}
338
214
  },
339
215
  });
340
216
  `;
341
- writeFileSync(join(projectPath, 'src', 'gateway.ts'), content);
342
217
  }
343
- function writeAgentFile(projectPath, answers) {
344
- const content = `import { gateway } from './gateway.js';
218
+ export function buildAgentFile(answers) {
219
+ const webchatLine = answers.channels.includes('webchat')
220
+ ? ` console.log('WebChat: ws://localhost:18789/ws');\n`
221
+ : '';
222
+ return `import { gateway } from './gateway.js';
345
223
 
346
- async function main() {
347
- await gateway.start();
224
+ let stopping = false;
348
225
 
349
- console.log('Assistant is running!');
350
- console.log('Connected channels:', gateway.stats.connectedChannels.join(', '));
351
- ${answers.channels.includes('webchat') ? ` console.log('WebChat: ws://localhost:18789/ws');\n` : ''}
352
- process.on('SIGINT', async () => {
353
- console.log('\\nShutting down...');
354
- await gateway.stop();
355
- process.exit(0);
356
- });
226
+ async function shutdown(signal: string) {
227
+ if (stopping) return;
228
+ stopping = true;
229
+ console.log(\`\\nReceived \${signal}, shutting down...\`);
230
+ await gateway.stop();
231
+ process.exit(0);
357
232
  }
358
233
 
359
- main().catch(console.error);
360
- `;
361
- writeFileSync(join(projectPath, 'src', 'agent.ts'), content);
234
+ process.once('SIGINT', () => void shutdown('SIGINT'));
235
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
236
+
237
+ await gateway.start();
238
+
239
+ console.log('Assistant is running!');
240
+ console.log('Connected channels:', gateway.stats.connectedChannels.join(', '));
241
+ ${webchatLine}`;
242
+ }
243
+ export function buildGitignore(answers) {
244
+ const lines = ['node_modules/', 'dist/', '.env', '*.log', '.cogitator/'];
245
+ if (answers.memory === 'sqlite')
246
+ lines.push('data/');
247
+ return lines.join('\n') + '\n';
248
+ }
249
+ export const TSCONFIG = {
250
+ compilerOptions: {
251
+ target: 'ES2022',
252
+ module: 'NodeNext',
253
+ moduleResolution: 'NodeNext',
254
+ strict: true,
255
+ esModuleInterop: true,
256
+ skipLibCheck: true,
257
+ outDir: './dist',
258
+ rootDir: './src',
259
+ },
260
+ include: ['src/**/*'],
261
+ };
262
+ export function buildProjectFiles(answers, options) {
263
+ const files = {
264
+ 'package.json': buildPackageJson(answers, options),
265
+ 'tsconfig.json': JSON.stringify(TSCONFIG, null, 2) + '\n',
266
+ 'src/gateway.ts': buildGatewayFile(answers),
267
+ 'src/agent.ts': buildAgentFile(answers),
268
+ '.gitignore': buildGitignore(answers),
269
+ };
270
+ const env = buildEnvFile(answers);
271
+ if (env)
272
+ files['.env'] = env;
273
+ return files;
362
274
  }
275
+ export function writeProjectFiles(projectPath, files) {
276
+ for (const [relativePath, content] of Object.entries(files)) {
277
+ const fullPath = join(projectPath, relativePath);
278
+ mkdirSync(dirname(fullPath), { recursive: true });
279
+ writeFileSync(fullPath, content, relativePath === '.env' ? { mode: 0o600 } : undefined);
280
+ }
281
+ }
282
+ function cancelled() {
283
+ p.cancel('Setup cancelled');
284
+ process.exit(0);
285
+ }
286
+ function answer(result) {
287
+ if (p.isCancel(result))
288
+ cancelled();
289
+ return result;
290
+ }
291
+ async function askSecret(message, emptyMessage) {
292
+ return answer(await p.password({ message, validate: (v) => (!v.trim() ? emptyMessage : undefined) })).trim();
293
+ }
294
+ async function collectAnswers(nameArg) {
295
+ const projectName = nameArg ??
296
+ answer(await p.text({
297
+ message: 'Project name',
298
+ placeholder: 'my-assistant',
299
+ validate: validateProjectName,
300
+ })).trim();
301
+ const providerChoice = answer(await p.select({
302
+ message: 'Which LLM provider?',
303
+ options: [
304
+ { value: 'anthropic', label: 'Anthropic (Claude)', hint: 'recommended' },
305
+ { value: 'openai', label: 'OpenAI (GPT)' },
306
+ { value: 'google', label: 'Google (Gemini)' },
307
+ { value: 'ollama', label: 'Ollama (local, free)', hint: 'no API key needed' },
308
+ ],
309
+ }));
310
+ if (!isSetupProvider(providerChoice))
311
+ cancelled();
312
+ const provider = providerChoice;
313
+ const apiKey = provider === 'ollama'
314
+ ? ''
315
+ : await askSecret(`${API_KEY_ENV[provider]}:`, 'API key is required');
316
+ const spinner = p.spinner();
317
+ spinner.start('Fetching available models...');
318
+ const modelOptions = provider === 'ollama'
319
+ ? await fetchOllamaModelOptions(resolveOllamaUrl(), process.env.OLLAMA_API_KEY)
320
+ : await fetchProviderModels(provider);
321
+ spinner.stop(`Found ${modelOptions.length} models`);
322
+ const model = answer(await p.select({ message: 'Default model', options: modelOptions }));
323
+ const channelChoice = answer(await p.multiselect({
324
+ message: 'Which channels to connect?',
325
+ options: [
326
+ { value: 'telegram', label: 'Telegram' },
327
+ { value: 'discord', label: 'Discord' },
328
+ { value: 'slack', label: 'Slack' },
329
+ { value: 'webchat', label: 'WebChat (localhost)', hint: 'no setup needed' },
330
+ ],
331
+ required: false,
332
+ }));
333
+ const channels = channelChoice.filter((c) => c === 'telegram' || c === 'discord' || c === 'slack' || c === 'webchat');
334
+ const telegramToken = channels.includes('telegram')
335
+ ? await askSecret('Telegram bot token (from @BotFather):', 'Token is required')
336
+ : undefined;
337
+ const discordToken = channels.includes('discord')
338
+ ? await askSecret('Discord bot token:', 'Token is required')
339
+ : undefined;
340
+ const slackToken = channels.includes('slack')
341
+ ? await askSecret('Slack bot token (xoxb-...):', 'Token is required')
342
+ : undefined;
343
+ const slackSigningSecret = channels.includes('slack')
344
+ ? await askSecret('Slack signing secret:', 'Signing secret is required')
345
+ : undefined;
346
+ const memoryChoice = answer(await p.select({
347
+ message: 'Memory adapter',
348
+ options: [
349
+ { value: 'sqlite', label: 'SQLite (recommended)', hint: 'zero config, file-based' },
350
+ { value: 'memory', label: 'In-memory', hint: 'no persistence, for testing' },
351
+ { value: 'postgres', label: 'PostgreSQL', hint: 'production-grade' },
352
+ ],
353
+ }));
354
+ const memory = memoryChoice === 'memory' || memoryChoice === 'postgres' ? memoryChoice : 'sqlite';
355
+ const databaseUrl = memory === 'postgres'
356
+ ? answer(await p.text({
357
+ message: 'PostgreSQL connection string',
358
+ initialValue: DEFAULT_POSTGRES_URL,
359
+ validate: (v) => !/^postgres(ql)?:\/\//.test(v.trim()) ? 'Expected postgres://...' : undefined,
360
+ })).trim()
361
+ : undefined;
362
+ return {
363
+ projectName,
364
+ provider,
365
+ apiKey,
366
+ model,
367
+ channels: channels.length > 0 ? channels : ['webchat'],
368
+ telegramToken,
369
+ discordToken,
370
+ slackToken,
371
+ slackSigningSecret,
372
+ memory,
373
+ databaseUrl,
374
+ };
375
+ }
376
+ export const initCommand = new Command('init')
377
+ .description('Create a new Cogitator AI assistant project')
378
+ .argument('[name]', 'Project name')
379
+ .option('--no-install', 'Skip dependency installation')
380
+ .action(async (nameArg, options) => {
381
+ printBanner();
382
+ p.intro(chalk.bgCyan(chalk.black(' cogitator init ')));
383
+ if (nameArg !== undefined) {
384
+ const error = validateProjectName(nameArg);
385
+ if (error) {
386
+ p.cancel(`Invalid project name "${nameArg}": ${error}`);
387
+ process.exit(1);
388
+ }
389
+ }
390
+ const answers = await collectAnswers(nameArg?.trim());
391
+ const projectPath = resolve(process.cwd(), answers.projectName);
392
+ if (existsSync(projectPath)) {
393
+ p.cancel(`Directory "${answers.projectName}" already exists`);
394
+ process.exit(1);
395
+ }
396
+ const pm = detectPackageManager();
397
+ const files = buildProjectFiles(answers, { dependencyVersions: resolveDependencyVersions() });
398
+ let installed = false;
399
+ await p.tasks([
400
+ {
401
+ title: 'Creating project structure',
402
+ task: () => {
403
+ writeProjectFiles(projectPath, files);
404
+ return 'Project files created';
405
+ },
406
+ },
407
+ {
408
+ title: `Installing dependencies with ${pm}`,
409
+ enabled: options.install,
410
+ task: () => {
411
+ try {
412
+ execFileSync(pm, ['install'], { cwd: projectPath, stdio: 'pipe' });
413
+ installed = true;
414
+ return 'Dependencies installed';
415
+ }
416
+ catch {
417
+ return `Install failed — run "${pm} install" manually`;
418
+ }
419
+ },
420
+ },
421
+ ]);
422
+ p.note([
423
+ `cd ${answers.projectName}`,
424
+ installed ? '' : `${pm} install`,
425
+ `${runScript(pm, 'dev')} # start with hot reload`,
426
+ `cogitator assistant # or run with the live dashboard`,
427
+ ]
428
+ .filter(Boolean)
429
+ .join('\n'), 'Next steps');
430
+ p.outro(`${chalk.green('Your assistant is ready!')} Channels: ${chalk.cyan(answers.channels.join(', '))}`);
431
+ });
363
432
  //# sourceMappingURL=init.js.map