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