@cogitator-ai/cli 0.3.16 → 0.5.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 +160 -511
  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 +115 -60
  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 +51 -0
  17. package/dist/commands/init.d.ts.map +1 -1
  18. package/dist/commands/init.js +349 -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 +156 -131
  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 +307 -271
  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 +81 -0
  72. package/dist/utils/provider-models.js.map +1 -0
  73. package/package.json +21 -19
@@ -4,96 +4,84 @@ import { resolve } from 'node:path';
4
4
  import * as p from '@clack/prompts';
5
5
  import chalk from 'chalk';
6
6
  import { stringify, parse as parseYaml } from 'yaml';
7
- import { ModelRegistry } from '@cogitator-ai/models';
7
+ import { AssistantConfigSchema } from '@cogitator-ai/channels';
8
8
  import { printBanner } from '../utils/logger.js';
9
- const FALLBACK_MODELS = {
10
- anthropic: [
11
- { label: 'Claude Sonnet 4', value: 'anthropic/claude-sonnet-4-20250514' },
12
- { label: 'Claude Opus 4', value: 'anthropic/claude-opus-4-20250514' },
13
- { label: 'Claude Haiku 3.5', value: 'anthropic/claude-3-5-haiku-20241022' },
14
- ],
15
- openai: [
16
- { label: 'GPT-4o', value: 'openai/gpt-4o' },
17
- { label: 'GPT-4o mini', value: 'openai/gpt-4o-mini' },
18
- { label: 'o3-mini', value: 'openai/o3-mini' },
19
- ],
20
- google: [
21
- { label: 'Gemini 2.5 Flash', value: 'google/gemini-2.5-flash' },
22
- { label: 'Gemini 2.5 Pro', value: 'google/gemini-2.5-pro' },
23
- ],
24
- ollama: [
25
- { label: 'Llama 3.1 8B', value: 'ollama/llama3.1:8b' },
26
- { label: 'Gemma 3 4B', value: 'ollama/gemma3:4b' },
27
- { label: 'Mistral 7B', value: 'ollama/mistral:7b' },
28
- ],
9
+ import { mergeEnvContent, parseDotenv } from '../utils/env.js';
10
+ import { DEFAULT_OLLAMA_URL, resolveOllamaUrl } from '../utils/ollama.js';
11
+ import { API_KEY_ENV, fetchOllamaModelOptions, fetchProviderModels, isSetupProvider, withProviderPrefix, } from '../utils/provider-models.js';
12
+ export const CHANNEL_TOKEN_ENV = {
13
+ telegram: 'TG_TOKEN',
14
+ discord: 'DISCORD_TOKEN',
15
+ slack: 'SLACK_BOT_TOKEN',
29
16
  };
30
- async function fetchModels(provider) {
31
- try {
32
- const registry = new ModelRegistry({ fallbackToBuiltin: true });
33
- await registry.initialize();
34
- let models = registry.listModels({
35
- provider,
36
- supportsTools: true,
37
- excludeDeprecated: true,
38
- });
39
- if (provider === 'google') {
40
- models = models.filter((m) => m.id.includes('gemini'));
17
+ const CHANNELS = ['telegram', 'discord', 'slack'];
18
+ const DEFAULT_MEMORY_PATH = '~/.cogitator/memory.db';
19
+ const NOTE_STYLE = { format: (line) => chalk.dim(line) };
20
+ export function splitCommandLine(command) {
21
+ const parts = [];
22
+ let current = '';
23
+ let quote = null;
24
+ let hasToken = false;
25
+ for (let i = 0; i < command.length; i++) {
26
+ const ch = command[i];
27
+ if (quote) {
28
+ if (ch === quote) {
29
+ quote = null;
30
+ }
31
+ else if (ch === '\\' && quote === '"' && i + 1 < command.length) {
32
+ current += command[++i];
33
+ }
34
+ else {
35
+ current += ch;
36
+ }
37
+ continue;
38
+ }
39
+ if (ch === '"' || ch === "'") {
40
+ quote = ch;
41
+ hasToken = true;
42
+ }
43
+ else if (ch === '\\' && i + 1 < command.length) {
44
+ current += command[++i];
45
+ hasToken = true;
46
+ }
47
+ else if (/\s/.test(ch)) {
48
+ if (hasToken)
49
+ parts.push(current);
50
+ current = '';
51
+ hasToken = false;
52
+ }
53
+ else {
54
+ current += ch;
55
+ hasToken = true;
41
56
  }
42
- if (models.length === 0)
43
- return FALLBACK_MODELS[provider] ?? [];
44
- return models.map((m) => ({
45
- label: m.displayName,
46
- value: m.id,
47
- }));
48
- }
49
- catch {
50
- return FALLBACK_MODELS[provider] ?? [];
51
- }
52
- }
53
- async function fetchOllamaModels(baseUrl) {
54
- try {
55
- const res = await fetch(`${baseUrl}/api/tags`);
56
- if (!res.ok)
57
- return FALLBACK_MODELS.ollama;
58
- const data = (await res.json());
59
- if (!data.models?.length)
60
- return FALLBACK_MODELS.ollama;
61
- return data.models.map((m) => ({
62
- label: m.name,
63
- value: `ollama/${m.name}`,
64
- hint: formatBytes(m.size),
65
- }));
66
- }
67
- catch {
68
- return FALLBACK_MODELS.ollama;
69
57
  }
58
+ if (quote)
59
+ throw new Error('Unterminated quote in command');
60
+ if (hasToken)
61
+ parts.push(current);
62
+ return parts;
70
63
  }
71
- function formatBytes(bytes) {
72
- if (bytes < 1024 * 1024 * 1024)
73
- return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
74
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
75
- }
76
- function extractMcpName(args) {
64
+ export function extractMcpName(args) {
65
+ const strip = (name) => name
66
+ .replace(/@[^@/]*$/, '')
67
+ .replace(/^(?:mcp-|server-)+/, '')
68
+ .replace(/(?:-mcp|-server)+$/, '');
77
69
  const pkg = args.find((a) => a.startsWith('@') || (!a.startsWith('-') && a.includes('/')));
78
70
  if (pkg) {
79
- const name = pkg.split('/').pop() ?? pkg;
80
- return name.replace(/^server-/, '').replace(/-server$/, '');
71
+ const name = pkg.startsWith('@') ? (pkg.split('/')[1] ?? pkg) : (pkg.split('/').pop() ?? pkg);
72
+ return strip(name) || 'mcp-server';
81
73
  }
82
74
  const nonFlag = args.find((a) => !a.startsWith('-'));
83
75
  if (nonFlag)
84
- return nonFlag.replace(/^server-/, '').replace(/-server$/, '');
76
+ return strip(nonFlag) || 'mcp-server';
85
77
  return 'mcp-server';
86
78
  }
87
- const API_KEY_NAMES = {
88
- anthropic: 'ANTHROPIC_API_KEY',
89
- openai: 'OPENAI_API_KEY',
90
- google: 'GOOGLE_API_KEY',
91
- };
92
- const CHANNEL_TOKEN_ENV = {
93
- telegram: 'TG_TOKEN',
94
- discord: 'DISCORD_TOKEN',
95
- slack: 'SLACK_BOT_TOKEN',
96
- };
79
+ export function parsePathList(raw) {
80
+ return raw
81
+ .split(',')
82
+ .map((s) => s.trim())
83
+ .filter((s) => s.length > 0);
84
+ }
97
85
  function cancel() {
98
86
  p.cancel('Setup cancelled');
99
87
  process.exit(0);
@@ -103,6 +91,28 @@ function prompt(result) {
103
91
  cancel();
104
92
  return result;
105
93
  }
94
+ function loadExistingConfig(configPath) {
95
+ try {
96
+ const raw = parseYaml(readFileSync(configPath, 'utf-8'));
97
+ const parsed = AssistantConfigSchema.safeParse(raw);
98
+ if (parsed.success)
99
+ return parsed.data;
100
+ p.log.warn('Existing config is not valid; known values will be pre-filled where possible');
101
+ const partial = AssistantConfigSchema.partial().safeParse(raw);
102
+ return partial.success ? partial.data : {};
103
+ }
104
+ catch {
105
+ p.log.warn('Failed to parse existing config, starting fresh');
106
+ return {};
107
+ }
108
+ }
109
+ async function askSecret(opts) {
110
+ const value = prompt(await p.password({
111
+ message: opts.current ? `${opts.message} (leave blank to keep current)` : opts.message,
112
+ validate: (v) => (!opts.current && !v?.trim() ? opts.required : undefined),
113
+ })).trim();
114
+ return value || undefined;
115
+ }
106
116
  export const wizardCommand = new Command('wizard')
107
117
  .description('Interactive assistant setup — generates cogitator.yml + .env')
108
118
  .option('--edit', 'Edit existing cogitator.yml')
@@ -110,88 +120,125 @@ export const wizardCommand = new Command('wizard')
110
120
  printBanner();
111
121
  const configPath = resolve(process.cwd(), 'cogitator.yml');
112
122
  const envPath = resolve(process.cwd(), '.env');
123
+ const existingEnvContent = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
124
+ const existingEnv = parseDotenv(existingEnvContent);
125
+ const editing = options.edit === true && existsSync(configPath);
126
+ if (!options.edit && existsSync(configPath)) {
127
+ const overwrite = prompt(await p.confirm({
128
+ message: 'cogitator.yml already exists. Overwrite it? (use --edit to modify instead)',
129
+ initialValue: false,
130
+ }));
131
+ if (!overwrite)
132
+ cancel();
133
+ }
113
134
  let existing = {};
114
- if (options.edit && existsSync(configPath)) {
135
+ if (editing) {
115
136
  p.intro(chalk.bgCyan(chalk.black(' Cogitator Assistant Setup (editing existing config) ')));
116
- try {
117
- existing = parseYaml(readFileSync(configPath, 'utf-8'));
118
- }
119
- catch {
120
- p.log.warn('Failed to parse existing config, starting fresh');
121
- }
122
- p.note(`Loaded ${chalk.dim(configPath)}`, 'Existing config found');
137
+ existing = loadExistingConfig(configPath);
138
+ p.note(`Loaded ${chalk.dim(configPath)}`, 'Existing config found', NOTE_STYLE);
123
139
  }
124
140
  else {
125
141
  p.intro(chalk.bgCyan(chalk.black(' Cogitator Personal Assistant Setup ')));
126
142
  }
127
- const existingUserName = existing.personality?.match(/assistant for (\w+)/)?.[1];
143
+ const envUpdates = new Map();
144
+ const existingUserName = existing.personality?.match(/assistant for ([^.\n]+)\./)?.[1];
128
145
  const userName = prompt(await p.text({
129
146
  message: "What's your name?",
130
147
  placeholder: 'Alex',
131
148
  ...(existingUserName ? { initialValue: existingUserName } : {}),
132
- validate: (v) => (!v.trim() ? 'Name is required' : undefined),
133
- }));
149
+ validate: (v) => (!v?.trim() ? 'Name is required' : undefined),
150
+ })).trim();
134
151
  const assistantName = prompt(await p.text({
135
152
  message: 'Assistant name',
136
153
  placeholder: 'jarvis',
137
154
  initialValue: existing.name ?? 'jarvis',
138
- validate: (v) => (!v.trim() ? 'Name is required' : undefined),
139
- }));
140
- const provider = prompt(await p.select({
155
+ validate: (v) => (!v?.trim() ? 'Name is required' : undefined),
156
+ })).trim();
157
+ const providerChoice = prompt(await p.select({
141
158
  message: 'LLM Provider',
142
159
  initialValue: existing.llm?.provider ?? 'google',
143
160
  options: [
144
161
  { value: 'google', label: 'Google (Gemini)', hint: 'recommended' },
145
162
  { value: 'anthropic', label: 'Anthropic (Claude)' },
146
- { value: 'openai', label: 'OpenAI (GPT-4o)' },
163
+ { value: 'openai', label: 'OpenAI (GPT)' },
147
164
  { value: 'ollama', label: 'Ollama', hint: 'local or cloud' },
148
165
  ],
149
166
  }));
150
- let ollamaUrl = 'http://localhost:11434';
151
- let apiKey = '';
167
+ if (!isSetupProvider(providerChoice))
168
+ cancel();
169
+ const provider = providerChoice;
170
+ let ollamaUrl = DEFAULT_OLLAMA_URL;
171
+ let ollamaApiKey;
152
172
  if (provider === 'ollama') {
173
+ const currentUrl = resolveOllamaUrl(existingEnv);
153
174
  const ollamaMode = prompt(await p.select({
154
175
  message: 'Ollama deployment',
155
- initialValue: 'local',
176
+ initialValue: currentUrl === DEFAULT_OLLAMA_URL ? 'local' : 'cloud',
156
177
  options: [
157
178
  { value: 'local', label: 'Local', hint: 'running on this machine' },
158
179
  { value: 'cloud', label: 'Cloud / Remote', hint: 'custom URL + optional API key' },
159
180
  ],
160
181
  }));
161
182
  if (ollamaMode === 'cloud') {
162
- ollamaUrl = prompt(await p.text({
183
+ const rawUrl = prompt(await p.text({
163
184
  message: 'Ollama URL',
164
- placeholder: 'https://ollama.example.com',
165
- validate: (v) => (!v.trim() ? 'URL is required' : undefined),
166
- }));
167
- const ollamaApiKey = prompt(await p.text({
168
- message: 'API key (leave blank if not needed)',
169
- placeholder: 'optional',
185
+ placeholder: 'https://ollama.com',
186
+ ...(currentUrl !== DEFAULT_OLLAMA_URL ? { initialValue: currentUrl } : {}),
187
+ validate: (v) => {
188
+ const url = v?.trim();
189
+ if (!url)
190
+ return 'URL is required';
191
+ try {
192
+ new URL(url);
193
+ return undefined;
194
+ }
195
+ catch {
196
+ return 'Enter a valid URL, e.g. https://ollama.com';
197
+ }
198
+ },
170
199
  }));
171
- if (ollamaApiKey.trim()) {
172
- apiKey = ollamaApiKey.trim();
173
- }
200
+ ollamaUrl = resolveOllamaUrl({}, rawUrl);
201
+ envUpdates.set('OLLAMA_URL', ollamaUrl);
202
+ const key = prompt(await p.password({
203
+ message: existingEnv.OLLAMA_API_KEY
204
+ ? 'API key (leave blank to keep current)'
205
+ : 'API key (leave blank if not needed)',
206
+ })).trim();
207
+ if (key)
208
+ envUpdates.set('OLLAMA_API_KEY', key);
209
+ ollamaApiKey = key || existingEnv.OLLAMA_API_KEY;
210
+ }
211
+ else if (existingEnv.OLLAMA_URL) {
212
+ envUpdates.set('OLLAMA_URL', DEFAULT_OLLAMA_URL);
174
213
  }
175
214
  }
176
215
  else {
177
- apiKey = prompt(await p.password({
178
- message: `${API_KEY_NAMES[provider]}:`,
179
- validate: (v) => (!v.trim() ? 'API key is required' : undefined),
180
- }));
216
+ const keyName = API_KEY_ENV[provider];
217
+ const key = await askSecret({
218
+ message: `${keyName}:`,
219
+ current: existingEnv[keyName] ?? process.env[keyName],
220
+ required: 'API key is required',
221
+ });
222
+ if (key)
223
+ envUpdates.set(keyName, key);
181
224
  }
182
225
  const modelSpinner = p.spinner();
183
226
  modelSpinner.start('Fetching available models...');
184
- const modelOptions = provider === 'ollama' ? await fetchOllamaModels(ollamaUrl) : await fetchModels(provider);
185
- modelSpinner.stop(modelOptions.length > 0
186
- ? `Found ${chalk.cyan(modelOptions.length)} models`
187
- : 'Using fallback model list');
188
- const existingModelInList = modelOptions.some((m) => m.value === existing.llm?.model);
227
+ const modelOptions = provider === 'ollama'
228
+ ? await fetchOllamaModelOptions(ollamaUrl, ollamaApiKey)
229
+ : await fetchProviderModels(provider);
230
+ modelSpinner.stop(`Found ${chalk.cyan(modelOptions.length)} models`);
231
+ const existingModel = existing.llm?.model && existing.llm.provider === provider
232
+ ? withProviderPrefix(provider, existing.llm.model)
233
+ : undefined;
189
234
  const model = prompt(await p.select({
190
235
  message: 'Model',
191
- ...(existingModelInList ? { initialValue: existing.llm.model } : {}),
236
+ ...(existingModel && modelOptions.some((m) => m.value === existingModel)
237
+ ? { initialValue: existingModel }
238
+ : {}),
192
239
  options: modelOptions,
193
240
  }));
194
- const existingChannels = Object.keys(existing.channels ?? {}).filter((k) => existing.channels?.[k] !== undefined);
241
+ const existingChannels = CHANNELS.filter((ch) => existing.channels?.[ch] !== undefined);
195
242
  const selectedChannels = prompt(await p.multiselect({
196
243
  message: 'Channels (terminal is always available)',
197
244
  options: [
@@ -201,44 +248,38 @@ export const wizardCommand = new Command('wizard')
201
248
  ],
202
249
  ...(existingChannels.length > 0 ? { initialValues: existingChannels } : {}),
203
250
  required: false,
204
- }));
205
- const envLines = [];
206
- if (provider === 'ollama') {
207
- if (ollamaUrl !== 'http://localhost:11434') {
208
- envLines.push(`OLLAMA_URL=${ollamaUrl}`);
209
- }
210
- if (apiKey) {
211
- envLines.push(`OLLAMA_API_KEY=${apiKey}`);
212
- }
213
- }
214
- else if (apiKey && API_KEY_NAMES[provider]) {
215
- envLines.push(`${API_KEY_NAMES[provider]}=${apiKey}`);
216
- }
251
+ })).filter((c) => c === 'telegram' || c === 'discord' || c === 'slack');
217
252
  const channelsConfig = {};
218
253
  for (const ch of selectedChannels) {
219
- const token = prompt(await p.password({
254
+ const tokenEnv = CHANNEL_TOKEN_ENV[ch];
255
+ const token = await askSecret({
220
256
  message: `${ch} bot token:`,
221
- validate: (v) => (!v.trim() ? 'Token is required' : undefined),
222
- }));
223
- const ownerId = prompt(await p.text({
224
- message: `${ch} owner user ID (for admin commands)`,
257
+ current: existingEnv[tokenEnv],
258
+ required: 'Token is required',
259
+ });
260
+ if (token)
261
+ envUpdates.set(tokenEnv, token);
262
+ const existingOwners = existing.channels?.[ch]?.ownerIds?.join(', ');
263
+ const ownerIdsRaw = prompt(await p.text({
264
+ message: `${ch} owner user ID(s), comma-separated (for admin commands)`,
225
265
  placeholder: 'your user ID',
226
- validate: (v) => (!v.trim() ? 'Owner ID is required' : undefined),
266
+ ...(existingOwners ? { initialValue: existingOwners } : {}),
267
+ validate: (v) => parsePathList(v ?? '').length === 0 ? 'Owner ID is required' : undefined,
227
268
  }));
228
- envLines.push(`${CHANNEL_TOKEN_ENV[ch]}=${token}`);
229
269
  if (ch === 'slack') {
230
- const signingSecret = prompt(await p.password({
270
+ const signingSecret = await askSecret({
231
271
  message: 'Slack signing secret:',
232
- validate: (v) => (!v.trim() ? 'Required' : undefined),
233
- }));
234
- envLines.push(`SLACK_SIGNING_SECRET=${signingSecret}`);
272
+ current: existingEnv.SLACK_SIGNING_SECRET,
273
+ required: 'Required',
274
+ });
275
+ if (signingSecret)
276
+ envUpdates.set('SLACK_SIGNING_SECRET', signingSecret);
235
277
  }
236
- channelsConfig[ch] = { ownerIds: [ownerId] };
278
+ channelsConfig[ch] = { ownerIds: parsePathList(ownerIdsRaw) };
237
279
  }
238
- const existingCaps = Object.keys(existing.capabilities ?? {}).filter((k) => {
239
- const val = existing.capabilities?.[k];
240
- return val === true || (typeof val === 'object' && val !== null);
241
- });
280
+ const existingCaps = Object.entries(existing.capabilities ?? {})
281
+ .filter(([, value]) => value === true || (typeof value === 'object' && value !== null))
282
+ .map(([key]) => key);
242
283
  const selectedCapabilities = prompt(await p.multiselect({
243
284
  message: 'Capabilities',
244
285
  options: [
@@ -261,107 +302,118 @@ export const wizardCommand = new Command('wizard')
261
302
  }));
262
303
  const capabilities = {};
263
304
  for (const cap of selectedCapabilities) {
264
- if (cap === 'fileSystem') {
265
- const existingFsPaths = existing.capabilities?.fileSystem?.paths?.join(', ');
266
- const pathsRaw = prompt(await p.text({
267
- message: 'Paths to allow for file system access',
268
- placeholder: '~/Documents, ~/Projects',
269
- ...(existingFsPaths ? { initialValue: existingFsPaths } : {}),
270
- validate: (v) => (!v.trim() ? 'At least one path is required' : undefined),
271
- }));
272
- capabilities.fileSystem = {
273
- paths: pathsRaw.split(',').map((s) => s.trim()),
274
- };
275
- }
276
- else if (cap === 'browser') {
277
- const browserOpts = prompt(await p.multiselect({
278
- message: 'Browser options',
279
- options: [
280
- { value: 'visible', label: 'Visible window', hint: 'see what the agent does' },
281
- {
282
- value: 'stealth',
283
- label: 'Stealth mode',
284
- hint: 'anti-detection, human-like behavior',
285
- },
286
- ],
287
- required: false,
288
- }));
289
- if (browserOpts.includes('visible') || browserOpts.includes('stealth')) {
290
- capabilities.browser = {
291
- headless: !browserOpts.includes('visible'),
292
- stealth: browserOpts.includes('stealth'),
293
- };
305
+ switch (cap) {
306
+ case 'fileSystem': {
307
+ const existingFsPaths = existing.capabilities?.fileSystem?.paths?.join(', ');
308
+ const pathsRaw = prompt(await p.text({
309
+ message: 'Paths to allow for file system access',
310
+ placeholder: '~/Documents, ~/Projects',
311
+ ...(existingFsPaths ? { initialValue: existingFsPaths } : {}),
312
+ validate: (v) => parsePathList(v ?? '').length === 0 ? 'At least one path is required' : undefined,
313
+ }));
314
+ capabilities.fileSystem = { paths: parsePathList(pathsRaw) };
315
+ break;
294
316
  }
295
- else {
296
- capabilities.browser = true;
317
+ case 'browser': {
318
+ const current = existing.capabilities?.browser;
319
+ const currentOpts = typeof current === 'object'
320
+ ? [...(current.headless ? [] : ['visible']), ...(current.stealth ? ['stealth'] : [])]
321
+ : [];
322
+ const browserOpts = prompt(await p.multiselect({
323
+ message: 'Browser options',
324
+ options: [
325
+ { value: 'visible', label: 'Visible window', hint: 'see what the agent does' },
326
+ {
327
+ value: 'stealth',
328
+ label: 'Stealth mode',
329
+ hint: 'anti-detection, human-like behavior',
330
+ },
331
+ ],
332
+ ...(currentOpts.length > 0 ? { initialValues: currentOpts } : {}),
333
+ required: false,
334
+ }));
335
+ capabilities.browser =
336
+ browserOpts.length > 0
337
+ ? {
338
+ headless: !browserOpts.includes('visible'),
339
+ stealth: browserOpts.includes('stealth'),
340
+ }
341
+ : true;
342
+ break;
297
343
  }
298
- }
299
- else if (cap === 'rag') {
300
- const existingRagPaths = existing.capabilities?.rag?.paths?.join(', ');
301
- const pathsRaw = prompt(await p.text({
302
- message: 'Paths to index for RAG',
303
- placeholder: '~/Documents/notes, ~/wiki',
304
- ...(existingRagPaths ? { initialValue: existingRagPaths } : {}),
305
- validate: (v) => (!v.trim() ? 'At least one path is required' : undefined),
306
- }));
307
- capabilities.rag = {
308
- paths: pathsRaw.split(',').map((s) => s.trim()),
309
- };
310
- }
311
- else if (cap === 'github') {
312
- const ghToken = prompt(await p.password({
313
- message: 'GitHub Personal Access Token (for GitHub API):',
314
- validate: (v) => (!v.trim() ? 'Token is required for GitHub capability' : undefined),
315
- }));
316
- envLines.push(`GITHUB_TOKEN=${ghToken}`);
317
- capabilities.github = true;
318
- }
319
- else {
320
- capabilities[cap] = true;
344
+ case 'rag': {
345
+ const existingRagPaths = existing.capabilities?.rag?.paths?.join(', ');
346
+ const pathsRaw = prompt(await p.text({
347
+ message: 'Paths to index for RAG',
348
+ placeholder: '~/Documents/notes, ~/wiki',
349
+ ...(existingRagPaths ? { initialValue: existingRagPaths } : {}),
350
+ validate: (v) => parsePathList(v ?? '').length === 0 ? 'At least one path is required' : undefined,
351
+ }));
352
+ capabilities.rag = { paths: parsePathList(pathsRaw) };
353
+ break;
354
+ }
355
+ case 'github': {
356
+ const ghToken = await askSecret({
357
+ message: 'GitHub Personal Access Token (for GitHub API):',
358
+ current: existingEnv.GITHUB_TOKEN,
359
+ required: 'Token is required for GitHub capability',
360
+ });
361
+ if (ghToken)
362
+ envUpdates.set('GITHUB_TOKEN', ghToken);
363
+ capabilities.github = true;
364
+ break;
365
+ }
366
+ case 'selfTools':
367
+ capabilities.selfTools = existing.capabilities?.selfTools ?? true;
368
+ break;
369
+ case 'webSearch':
370
+ case 'deviceTools':
371
+ case 'scheduler':
372
+ case 'selfConfig':
373
+ capabilities[cap] = true;
374
+ break;
321
375
  }
322
376
  }
323
- const mcpServers = {};
324
- const existingMcpNames = Object.keys(existing.mcpServers ?? {});
325
- const addMcp = prompt(await p.confirm({
326
- message: 'Add MCP servers?',
327
- initialValue: existingMcpNames.length > 0,
328
- }));
329
- if (addMcp) {
330
- let addMore = true;
331
- while (addMore) {
332
- const fullCommand = prompt(await p.text({
333
- message: 'Full command to start MCP server',
334
- placeholder: 'npx -y @modelcontextprotocol/server-filesystem /home',
335
- validate: (v) => (!v.trim() ? 'Command is required' : undefined),
336
- }));
337
- const parts = fullCommand
338
- .trim()
339
- .split(/\s+/)
340
- .filter((s) => s.length > 0);
341
- const command = parts[0];
342
- const args = parts.slice(1);
343
- const detectedName = extractMcpName(args);
344
- const name = prompt(await p.text({
345
- message: 'Server name',
346
- initialValue: detectedName,
347
- validate: (v) => (!v.trim() ? 'Name is required' : undefined),
348
- }));
349
- mcpServers[name] = { command, args };
350
- p.log.success(`Added MCP server: ${chalk.cyan(name)}`);
351
- addMore = prompt(await p.confirm({
352
- message: 'Add another MCP server?',
353
- initialValue: false,
354
- }));
355
- }
377
+ const mcpServers = { ...(existing.mcpServers ?? {}) };
378
+ const existingMcpNames = Object.keys(mcpServers);
379
+ if (existingMcpNames.length > 0) {
380
+ p.log.info(`Keeping MCP servers: ${existingMcpNames.join(', ')}`);
356
381
  }
357
- const memoryAdapter = prompt(await p.select({
358
- message: 'Memory',
359
- initialValue: existing.memory?.adapter ?? 'sqlite',
360
- options: [
361
- { value: 'sqlite', label: 'SQLite (recommended)', hint: 'zero config, file-based' },
362
- { value: 'postgres', label: 'PostgreSQL', hint: 'production-grade' },
363
- ],
382
+ let addMore = prompt(await p.confirm({
383
+ message: existingMcpNames.length > 0 ? 'Add more MCP servers?' : 'Add MCP servers?',
384
+ initialValue: false,
364
385
  }));
386
+ while (addMore) {
387
+ const fullCommand = prompt(await p.text({
388
+ message: 'Full command to start MCP server',
389
+ placeholder: 'npx -y @modelcontextprotocol/server-filesystem /home',
390
+ validate: (v) => {
391
+ if (!v?.trim())
392
+ return 'Command is required';
393
+ try {
394
+ splitCommandLine(v);
395
+ return undefined;
396
+ }
397
+ catch (error) {
398
+ return error instanceof Error ? error.message : 'Invalid command';
399
+ }
400
+ },
401
+ }));
402
+ const [command, ...args] = splitCommandLine(fullCommand);
403
+ const name = prompt(await p.text({
404
+ message: 'Server name',
405
+ initialValue: extractMcpName(args),
406
+ validate: (v) => (!v?.trim() ? 'Name is required' : undefined),
407
+ })).trim();
408
+ mcpServers[name] = { command, args };
409
+ p.log.success(`Added MCP server: ${chalk.cyan(name)}`);
410
+ addMore = prompt(await p.confirm({ message: 'Add another MCP server?', initialValue: false }));
411
+ }
412
+ const memoryPath = prompt(await p.text({
413
+ message: 'SQLite memory database path',
414
+ initialValue: existing.memory?.path ?? DEFAULT_MEMORY_PATH,
415
+ validate: (v) => (!v?.trim() ? 'Path is required' : undefined),
416
+ })).trim();
365
417
  const defaultPersonality = [
366
418
  `You are ${assistantName}, a personal AI assistant for ${userName}.`,
367
419
  `Be concise, friendly, and proactive. Use tools when they can help.`,
@@ -371,55 +423,39 @@ export const wizardCommand = new Command('wizard')
371
423
  message: 'Assistant personality',
372
424
  placeholder: 'Describe how the assistant should behave...',
373
425
  initialValue: existing.personality ?? defaultPersonality,
426
+ validate: (v) => (!v?.trim() ? 'Personality is required' : undefined),
374
427
  }));
375
428
  const config = {
429
+ ...existing,
376
430
  name: assistantName,
377
431
  personality,
378
- llm: {
379
- provider: provider,
380
- model,
381
- },
432
+ llm: { provider, model },
382
433
  channels: channelsConfig,
383
434
  capabilities,
384
- ...(Object.keys(mcpServers).length > 0 ? { mcpServers } : {}),
435
+ ...(Object.keys(mcpServers).length > 0 ? { mcpServers } : { mcpServers: undefined }),
385
436
  memory: {
386
- adapter: memoryAdapter,
387
- path: memoryAdapter === 'sqlite' ? '~/.cogitator/memory.db' : undefined,
388
437
  autoExtract: true,
389
438
  knowledgeGraph: true,
390
439
  compaction: { threshold: 50 },
440
+ ...existing.memory,
441
+ adapter: 'sqlite',
442
+ path: memoryPath,
391
443
  },
392
- stream: { flushInterval: 600, minChunkSize: 30 },
444
+ stream: existing.stream ?? { flushInterval: 600, minChunkSize: 30 },
393
445
  };
394
- writeFileSync(configPath, stringify(config, { lineWidth: 120 }));
395
- if (envLines.length > 0) {
396
- const existing = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
397
- const merged = mergeEnvLines(existing, envLines);
398
- writeFileSync(envPath, merged);
446
+ const validated = AssistantConfigSchema.parse(config);
447
+ writeFileSync(configPath, stringify(validated, { lineWidth: 120 }));
448
+ if (envUpdates.size > 0) {
449
+ writeFileSync(envPath, mergeEnvContent(existingEnvContent, envUpdates), { mode: 0o600 });
399
450
  }
400
451
  p.note([
401
452
  `${chalk.dim('Config:')} cogitator.yml`,
402
- envLines.length > 0 ? `${chalk.dim('Env:')} .env` : '',
453
+ envUpdates.size > 0 ? `${chalk.dim('Env:')} .env` : '',
403
454
  '',
404
455
  `${chalk.dim('Next:')} cogitator up`,
405
456
  ]
406
457
  .filter(Boolean)
407
- .join('\n'), 'Files written');
458
+ .join('\n'), 'Files written', NOTE_STYLE);
408
459
  p.outro(chalk.green('Setup complete! Run: cogitator up'));
409
460
  });
410
- function mergeEnvLines(existing, newLines) {
411
- const lines = existing.split('\n').filter(Boolean);
412
- const map = new Map();
413
- for (const line of lines) {
414
- const eq = line.indexOf('=');
415
- if (eq > 0)
416
- map.set(line.slice(0, eq), line);
417
- }
418
- for (const line of newLines) {
419
- const eq = line.indexOf('=');
420
- if (eq > 0)
421
- map.set(line.slice(0, eq), line);
422
- }
423
- return [...map.values()].join('\n') + '\n';
424
- }
425
461
  //# sourceMappingURL=wizard.js.map