delimit-cli 4.1.38 → 4.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -167,6 +167,9 @@ npx delimit-cli recall # Show recent memories
167
167
  npx delimit-cli recall --tag deploy --all # Filter by tag, show all
168
168
  npx delimit-cli recall --export # Export as markdown
169
169
  npx delimit-cli forget abc123 # Delete a memory by ID
170
+ npx delimit-cli models # Configure deliberation API keys (BYOK wizard)
171
+ npx delimit-cli models --status # Show current model config
172
+ npx delimit-cli status # Compact dashboard of your Delimit setup
170
173
  npx delimit-cli doctor # Check setup health
171
174
  npx delimit-cli uninstall --dry-run # Preview removal
172
175
  ```
@@ -270,6 +273,22 @@ rules:
270
273
 
271
274
  ---
272
275
 
276
+ ## FAQ
277
+
278
+ **How does this compare to Obsidian Mind?**
279
+
280
+ Obsidian Mind is a great Obsidian vault template for Claude Code users who want persistent memory via markdown files. Delimit takes a different approach: it's an MCP server that works across Claude Code, Codex, Gemini CLI, and Cursor. Your memory, ledger, and governance travel with you when you switch models. Delimit also adds API governance (27-type breaking change detection), CI gates, git hooks, and policy enforcement that Obsidian Mind doesn't cover. Use Obsidian Mind if you're all-in on Claude + Obsidian. Use Delimit if you switch between models or need governance.
281
+
282
+ **Does this work without Claude Code?**
283
+
284
+ Yes. Delimit works with Claude Code, Codex (OpenAI), Gemini CLI (Google), and Cursor. The `remember`/`recall` commands work standalone with zero config. The MCP server integrates with any client that supports the Model Context Protocol.
285
+
286
+ **Is this free?**
287
+
288
+ The free tier includes API governance, persistent memory, zero-spec extraction, project scanning, and 3 multi-model deliberations. Pro ($10/mo) adds unlimited deliberation, security audit, test verification, deploy pipeline, and agent orchestration.
289
+
290
+ ---
291
+
273
292
  ## Links
274
293
 
275
294
  - [delimit.ai](https://delimit.ai) -- homepage
@@ -128,6 +128,20 @@ if (process.env.DELIMIT_DEBUG_CONTINUITY === '1') {
128
128
  console.log('');
129
129
  }
130
130
 
131
+ // Helper to format a timestamp as relative time (e.g. "2h ago", "3d ago")
132
+ function _relativeTime(ts) {
133
+ const diff = Date.now() - ts;
134
+ const mins = Math.floor(diff / 60000);
135
+ if (mins < 1) return 'just now';
136
+ if (mins < 60) return mins + 'm ago';
137
+ const hrs = Math.floor(mins / 60);
138
+ if (hrs < 24) return hrs + 'h ago';
139
+ const days = Math.floor(hrs / 24);
140
+ if (days < 30) return days + 'd ago';
141
+ const months = Math.floor(days / 30);
142
+ return months + 'mo ago';
143
+ }
144
+
131
145
  // Helper to check if agent is running
132
146
  async function checkAgent() {
133
147
  try {
@@ -311,99 +325,193 @@ program
311
325
  // Status command
312
326
  program
313
327
  .command('status')
314
- .description('Show governance status')
328
+ .description('Show a compact dashboard of your Delimit setup')
315
329
 
316
330
  .option('--verbose', 'Show detailed status')
317
331
  .action(async (options) => {
318
- const agentRunning = await checkAgent();
319
-
320
- console.log(chalk.blue.bold('\nDelimit Governance Status\n'));
321
- console.log('Agent:', agentRunning ? chalk.green('✓ Running') : chalk.red('✗ Not running'));
332
+ const homedir = os.homedir();
333
+ const delimitHome = path.join(homedir, '.delimit');
334
+ const target = process.cwd();
335
+
336
+ console.log(chalk.bold('\n Delimit Status\n'));
337
+
338
+ // --- Memory stats ---
339
+ const memoryDir = path.join(delimitHome, 'memory');
340
+ let memTotal = 0;
341
+ let memRecent = 0;
342
+ let recentMemories = [];
343
+ const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
344
+ try {
345
+ const memFiles = fs.readdirSync(memoryDir).filter(f => f.startsWith('mem-') && f.endsWith('.json'));
346
+ memTotal = memFiles.length;
347
+ for (const f of memFiles) {
348
+ try {
349
+ const data = JSON.parse(fs.readFileSync(path.join(memoryDir, f), 'utf-8'));
350
+ const ts = new Date(data.created_at || data.timestamp || data.created || 0).getTime();
351
+ if (ts > oneWeekAgo) memRecent++;
352
+ recentMemories.push({ text: data.text || data.content || '', tags: data.tags || [], ts });
353
+ } catch {}
354
+ }
355
+ recentMemories.sort((a, b) => b.ts - a.ts);
356
+ recentMemories = recentMemories.slice(0, 3);
357
+ } catch {}
358
+ console.log(` Memory: ${chalk.white.bold(memTotal)} memories${memRecent > 0 ? ` (${memRecent} this week)` : ''}`);
322
359
 
323
- if (options.verbose) {
324
- console.log('\n' + chalk.bold('Continuity Context:'));
325
- console.log(formatContinuityReport(continuityContext).split('\n').slice(1).map(line => ' ' + line.trimStart()).join('\n'));
326
- }
327
-
328
- if (agentRunning) {
329
- const { data } = await axios.get(`${AGENT_URL}/status`);
330
-
331
- // Mode information
332
- console.log('\n' + chalk.bold('Mode Configuration:'));
333
- console.log(` Current Mode: ${chalk.bold(data.sessionMode)}`);
334
- if (data.defaultMode) {
335
- console.log(` Default Mode: ${data.defaultMode}`);
360
+ // --- Governance / Policy ---
361
+ const policyPath = path.join(target, '.delimit', 'policies.yml');
362
+ let policyLabel = chalk.gray('none');
363
+ let hasPolicy = false;
364
+ if (fs.existsSync(policyPath)) {
365
+ hasPolicy = true;
366
+ try {
367
+ const policyContent = yaml.load(fs.readFileSync(policyPath, 'utf-8'));
368
+ const preset = policyContent?.preset || policyContent?.name || 'custom';
369
+ policyLabel = chalk.green(preset + ' policy');
370
+ } catch {
371
+ policyLabel = chalk.green('custom policy');
336
372
  }
337
- if (data.effectiveMode && data.effectiveMode !== data.sessionMode) {
338
- console.log(` Effective Mode: ${chalk.yellow(data.effectiveMode)} (escalated)`);
373
+ }
374
+ // Count tracked specs
375
+ const specPatterns = ['openapi.yaml', 'openapi.yml', 'openapi.json', 'swagger.yaml', 'swagger.yml', 'swagger.json'];
376
+ let specCount = 0;
377
+ const _countSpecs = (dir, depth) => {
378
+ if (depth > 3) return;
379
+ try {
380
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
381
+ if (['node_modules', '.next', 'venv', '.git'].includes(entry.name)) continue;
382
+ const full = path.join(dir, entry.name);
383
+ if (entry.isFile() && specPatterns.includes(entry.name.toLowerCase())) {
384
+ specCount++;
385
+ } else if (entry.isDirectory()) {
386
+ _countSpecs(full, depth + 1);
387
+ }
388
+ }
389
+ } catch {}
390
+ };
391
+ _countSpecs(target, 0);
392
+ const specLabel = specCount > 0 ? `${specCount} spec${specCount > 1 ? 's' : ''} tracked` : chalk.gray('no specs');
393
+ console.log(` Governance: ${policyLabel}${hasPolicy ? ' | ' : ' | '}${specLabel}`);
394
+
395
+ // --- Git hooks ---
396
+ const preCommitPath = path.join(target, '.git', 'hooks', 'pre-commit');
397
+ let hasGitHooks = false;
398
+ try {
399
+ const hookContent = fs.readFileSync(preCommitPath, 'utf-8');
400
+ hasGitHooks = hookContent.includes('delimit');
401
+ } catch {}
402
+ console.log(` Git Hooks: ${hasGitHooks ? chalk.green('pre-commit installed') : chalk.gray('not installed')}`);
403
+
404
+ // --- CI ---
405
+ const workflowPath = path.join(target, '.github', 'workflows', 'api-governance.yml');
406
+ const hasCI = fs.existsSync(workflowPath);
407
+ console.log(` CI: ${hasCI ? chalk.green('GitHub Action active') : chalk.gray('not configured')}`);
408
+
409
+ // --- MCP ---
410
+ const mcpConfigPath = path.join(homedir, '.mcp.json');
411
+ let hasMcp = false;
412
+ let toolCount = 0;
413
+ try {
414
+ const mcpContent = fs.readFileSync(mcpConfigPath, 'utf-8');
415
+ hasMcp = mcpContent.includes('delimit');
416
+ } catch {}
417
+ if (hasMcp) {
418
+ // Count tools from server.py if available
419
+ const serverPyPaths = [
420
+ path.join(delimitHome, 'server', 'ai', 'server.py'),
421
+ path.join(delimitHome, 'server', 'server.py'),
422
+ ];
423
+ for (const sp of serverPyPaths) {
424
+ try {
425
+ const serverContent = fs.readFileSync(sp, 'utf-8');
426
+ const toolMatches = serverContent.match(/@mcp\.tool/g);
427
+ if (toolMatches) {
428
+ toolCount = toolMatches.length;
429
+ break;
430
+ }
431
+ } catch {}
339
432
  }
340
-
341
- // Policies
342
- console.log('\n' + chalk.bold('Policies:'));
343
- if (data.policiesLoaded.length > 0) {
344
- data.policiesLoaded.forEach(policy => {
345
- console.log(` • ${policy}`);
346
- });
347
- if (data.totalRules) {
348
- console.log(` Total Rules: ${data.totalRules}`);
433
+ console.log(` MCP: ${chalk.green('connected')}${toolCount > 0 ? ` (${toolCount} tools)` : ''}`);
434
+ } else {
435
+ console.log(` MCP: ${chalk.gray('not configured')}`);
436
+ }
437
+
438
+ // --- Models ---
439
+ const modelsPath = path.join(delimitHome, 'models.json');
440
+ let modelsLabel = chalk.gray('none configured');
441
+ try {
442
+ const modelsData = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
443
+ const modelNames = [];
444
+ for (const [key, val] of Object.entries(modelsData)) {
445
+ if (val && typeof val === 'object' && (val.api_key || val.enabled !== false)) {
446
+ modelNames.push(key.charAt(0).toUpperCase() + key.slice(1));
349
447
  }
350
- } else {
351
- console.log(' No policies loaded');
352
448
  }
353
-
354
- // Recent activity
355
- console.log('\n' + chalk.bold('Activity:'));
356
- console.log(` Audit Log Entries: ${data.auditLogSize}`);
357
- if (data.lastDecision) {
358
- const timeSince = Date.now() - new Date(data.lastDecision.timestamp);
359
- const minutes = Math.floor(timeSince / 60000);
360
- console.log(` Last Decision: ${minutes} minutes ago (${data.lastDecision.action})`);
449
+ if (modelNames.length > 0) {
450
+ modelsLabel = chalk.white(modelNames.join(' + ')) + chalk.gray(' (BYOK)');
361
451
  }
362
- console.log(` Uptime: ${Math.floor(data.uptime / 60)} minutes`);
363
-
364
- // Verbose mode shows recent decisions
365
- if (options.verbose && data.recentDecisions) {
366
- console.log('\n' + chalk.bold('Recent Decisions:'));
367
- data.recentDecisions.forEach(decision => {
368
- const color = decision.action === 'block' ? chalk.red :
369
- decision.action === 'prompt' ? chalk.yellow :
370
- chalk.green;
371
- console.log(` ${decision.timestamp} | ${color(decision.mode)} | ${decision.rule || 'no rule'}`);
372
- });
452
+ } catch {}
453
+ console.log(` Models: ${modelsLabel}`);
454
+
455
+ // --- License ---
456
+ const licensePath = path.join(delimitHome, 'license.json');
457
+ let licenseLabel = chalk.gray('Free');
458
+ try {
459
+ const licenseData = JSON.parse(fs.readFileSync(licensePath, 'utf-8'));
460
+ const tier = licenseData.tier || licenseData.plan || 'Free';
461
+ const active = licenseData.status === 'active' || licenseData.valid === true;
462
+ if (tier.toLowerCase() !== 'free') {
463
+ licenseLabel = active ? chalk.green(`${tier} (active)`) : chalk.yellow(`${tier} (${licenseData.status || 'unknown'})`);
464
+ }
465
+ } catch {}
466
+ console.log(` License: ${licenseLabel}`);
467
+
468
+ // --- Recent memories ---
469
+ if (recentMemories.length > 0) {
470
+ console.log(chalk.bold('\n Recent memories:'));
471
+ for (const mem of recentMemories) {
472
+ const ago = _relativeTime(mem.ts);
473
+ const tagStr = mem.tags.length > 0 ? ' ' + chalk.gray(mem.tags.map(t => '#' + t).join(' ')) : '';
474
+ const text = mem.text.length > 55 ? mem.text.slice(0, 55) + '...' : mem.text;
475
+ console.log(` ${chalk.gray('[' + ago + ']')} ${text}${tagStr}`);
373
476
  }
374
477
  }
375
-
376
- // System integration
377
- console.log('\n' + chalk.bold('System Integration:'));
378
-
379
- // Git hooks
478
+
479
+ // --- Last session ---
480
+ const sessionsDir = path.join(delimitHome, 'sessions');
380
481
  try {
381
- const hooksPath = execSync('git config --global core.hooksPath').toString().trim();
382
- const hooksActive = hooksPath.includes('.delimit');
383
- console.log(` Git Hooks: ${hooksActive ? chalk.green('✓ Active') : chalk.yellow('⚠ Not configured')}`);
384
- } catch (e) {
385
- console.log(` Git Hooks: ${chalk.red('✗ Not configured')}`);
386
- }
387
-
388
- // PATH
389
- if (process.env.PATH.includes('.delimit/shims')) {
390
- console.log(` AI Tool Interception: ${chalk.green(' Active')}`);
391
- } else {
392
- console.log(` AI Tool Interception: ${chalk.gray('Not active')}`);
393
- }
394
-
395
- // Policy files
396
- const policyFiles = [];
397
- if (fs.existsSync('delimit.yml')) {
398
- policyFiles.push('project');
399
- }
400
- if (fs.existsSync(path.join(process.env.HOME, '.config', 'delimit', 'delimit.yml'))) {
401
- policyFiles.push('user');
402
- }
403
- console.log(` Policy Files: ${policyFiles.length > 0 ? policyFiles.join(', ') : chalk.gray('none')}`);
404
-
482
+ const sessFiles = fs.readdirSync(sessionsDir)
483
+ .filter(f => f.startsWith('session_') && f.endsWith('.json'))
484
+ .sort()
485
+ .reverse();
486
+ if (sessFiles.length > 0) {
487
+ const latest = JSON.parse(fs.readFileSync(path.join(sessionsDir, sessFiles[0]), 'utf-8'));
488
+ const summary = latest.summary || latest.description || latest.title || null;
489
+ if (summary) {
490
+ const truncated = summary.length > 60 ? summary.slice(0, 60) + '...' : summary;
491
+ console.log(chalk.bold('\n Last session: ') + truncated);
492
+ }
493
+ }
494
+ } catch {}
495
+
496
+ // --- Governance readiness ---
497
+ const hasSpecs = specCount > 0;
498
+ const checks = [
499
+ { name: 'API spec', done: hasSpecs },
500
+ { name: 'Policy', done: hasPolicy },
501
+ { name: 'CI gate', done: hasCI },
502
+ { name: 'Git hooks', done: hasGitHooks },
503
+ { name: 'MCP', done: hasMcp },
504
+ ];
505
+ const score = checks.filter(c => c.done).length;
506
+
507
+ console.log(chalk.bold(`\n Governance readiness: ${score}/${checks.length}`));
508
+ console.log(' ' + checks.map(c => c.done ? chalk.green('\u25cf') + ' ' + c.name : chalk.gray('\u25cb') + ' ' + chalk.gray(c.name)).join(' '));
509
+ console.log('');
510
+
405
511
  if (options.verbose) {
406
- console.log('\n' + chalk.gray('Run "delimit doctor" for detailed diagnostics'));
512
+ console.log(chalk.bold(' Continuity Context:'));
513
+ console.log(formatContinuityReport(continuityContext).split('\n').slice(1).map(line => ' ' + line.trimStart()).join('\n'));
514
+ console.log('');
407
515
  }
408
516
  });
409
517
 
@@ -4159,6 +4267,267 @@ if result.get('summary'):
4159
4267
  }
4160
4268
  });
4161
4269
 
4270
+ // ---------------------------------------------------------------------------
4271
+ // Models command: BYOK deliberation key management wizard
4272
+ // ---------------------------------------------------------------------------
4273
+
4274
+ const MODELS_CONFIG_PATH = path.join(os.homedir(), '.delimit', 'models.json');
4275
+ const DELIBERATION_USAGE_PATH = path.join(os.homedir(), '.delimit', 'deliberation_usage.json');
4276
+
4277
+ const DEFAULT_MODELS = {
4278
+ grok: { enabled: false, api_key: '', model: 'grok-4-0709', name: 'Grok 4' },
4279
+ gemini: { enabled: false, api_key: '', model: 'gemini-2.5-pro', name: 'Gemini Pro' },
4280
+ openai: { enabled: false, api_key: '', model: 'gpt-4o', name: 'Codex (GPT-4o)' },
4281
+ };
4282
+
4283
+ const MODEL_PROVIDERS = {
4284
+ grok: { label: 'Grok (xAI)', prefix: 'xai-', endpoint: 'https://api.x.ai/v1/chat/completions', defaultModel: 'grok-4-0709', defaultName: 'Grok 4', variants: ['grok-4-0709', 'grok-3', 'grok-3-mini'] },
4285
+ gemini: { label: 'Gemini (Google)', prefix: 'AIza', endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent', defaultModel: 'gemini-2.5-pro', defaultName: 'Gemini Pro', variants: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash'] },
4286
+ openai: { label: 'Codex/GPT-4o (OpenAI)', prefix: 'sk-', endpoint: 'https://api.openai.com/v1/chat/completions', defaultModel: 'gpt-4o', defaultName: 'Codex (GPT-4o)', variants: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o3-mini'] },
4287
+ };
4288
+
4289
+ function loadModelsConfig() {
4290
+ try {
4291
+ if (fs.existsSync(MODELS_CONFIG_PATH)) {
4292
+ return JSON.parse(fs.readFileSync(MODELS_CONFIG_PATH, 'utf-8'));
4293
+ }
4294
+ } catch {}
4295
+ return {};
4296
+ }
4297
+
4298
+ function saveModelsConfig(config) {
4299
+ const dir = path.dirname(MODELS_CONFIG_PATH);
4300
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4301
+ fs.writeFileSync(MODELS_CONFIG_PATH, JSON.stringify(config, null, 2));
4302
+ }
4303
+
4304
+ function loadDeliberationUsage() {
4305
+ try {
4306
+ if (fs.existsSync(DELIBERATION_USAGE_PATH)) {
4307
+ return JSON.parse(fs.readFileSync(DELIBERATION_USAGE_PATH, 'utf-8'));
4308
+ }
4309
+ } catch {}
4310
+ return { used: 0, limit: 3 };
4311
+ }
4312
+
4313
+ function getModelStatus(config, key) {
4314
+ const entry = config[key];
4315
+ if (entry && entry.enabled && entry.api_key) {
4316
+ return { configured: true, model: entry.model || DEFAULT_MODELS[key].model };
4317
+ }
4318
+ return { configured: false, model: null };
4319
+ }
4320
+
4321
+ function printModelStatus(config) {
4322
+ const usage = loadDeliberationUsage();
4323
+ const remaining = Math.max(0, (usage.limit || 3) - (usage.used || 0));
4324
+ let configuredCount = 0;
4325
+
4326
+ console.log(chalk.bold.blue('\n Delimit Models -- Deliberation Config\n'));
4327
+ console.log(chalk.bold(' Current models:'));
4328
+
4329
+ for (const [key, defaults] of Object.entries(DEFAULT_MODELS)) {
4330
+ const status = getModelStatus(config, key);
4331
+ if (status.configured) {
4332
+ configuredCount++;
4333
+ console.log(` ${chalk.green('*')} ${defaults.name.split(' ')[0].padEnd(10)} -- configured (${status.model})`);
4334
+ } else {
4335
+ const extra = key === 'openai' ? '' : '';
4336
+ console.log(` ${chalk.gray('o')} ${defaults.name.split(' ')[0].padEnd(10)} -- ${chalk.gray('not configured')}${extra}`);
4337
+ }
4338
+ }
4339
+ console.log(` ${chalk.gray('o')} ${'Claude'.padEnd(10)} -- ${chalk.gray('not configured (uses your Claude Code subscription)')}`);
4340
+
4341
+ console.log('');
4342
+ console.log(` ${remaining} free deliberation${remaining === 1 ? '' : 's'} remaining (of ${usage.limit || 3}).`);
4343
+ if (configuredCount > 0) {
4344
+ console.log(` Mode: ${chalk.green('BYOK')} (${configuredCount} model${configuredCount === 1 ? '' : 's'})`);
4345
+ } else {
4346
+ console.log(' Add API keys for unlimited deliberation with your own models.');
4347
+ }
4348
+ console.log('');
4349
+
4350
+ return { configuredCount, remaining };
4351
+ }
4352
+
4353
+ async function testModelKey(providerKey, apiKey, model) {
4354
+ const provider = MODEL_PROVIDERS[providerKey];
4355
+ const prompt = 'What is 2+2? Reply with just the number.';
4356
+
4357
+ try {
4358
+ if (providerKey === 'gemini') {
4359
+ const url = provider.endpoint.replace('{model}', model) + `?key=${apiKey}`;
4360
+ const resp = await axios.post(url, {
4361
+ contents: [{ parts: [{ text: prompt }] }],
4362
+ }, { timeout: 15000, headers: { 'Content-Type': 'application/json' } });
4363
+ const text = resp.data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
4364
+ return { ok: true, response: text.trim() };
4365
+ } else {
4366
+ const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` };
4367
+ const body = { model, messages: [{ role: 'user', content: prompt }], max_tokens: 10 };
4368
+ const resp = await axios.post(provider.endpoint, body, { timeout: 15000, headers });
4369
+ const text = resp.data?.choices?.[0]?.message?.content || '';
4370
+ return { ok: true, response: text.trim() };
4371
+ }
4372
+ } catch (err) {
4373
+ const status = err.response?.status;
4374
+ const msg = err.response?.data?.error?.message || err.message || 'Unknown error';
4375
+ return { ok: false, error: `${status ? `HTTP ${status}: ` : ''}${msg}` };
4376
+ }
4377
+ }
4378
+
4379
+ program
4380
+ .command('models')
4381
+ .description('Configure deliberation model API keys (BYOK)')
4382
+ .option('--status', 'Show current model configuration (non-interactive)')
4383
+ .action(async (options) => {
4384
+ const config = loadModelsConfig();
4385
+
4386
+ // --status: non-interactive output
4387
+ if (options.status) {
4388
+ let configuredCount = 0;
4389
+ console.log('');
4390
+ for (const [key, defaults] of Object.entries(DEFAULT_MODELS)) {
4391
+ const status = getModelStatus(config, key);
4392
+ const label = defaults.name.split(' ')[0] + ':';
4393
+ if (status.configured) {
4394
+ configuredCount++;
4395
+ console.log(` ${label.padEnd(10)} configured (${status.model})`);
4396
+ } else {
4397
+ console.log(` ${label.padEnd(10)} ${chalk.gray('not configured')}`);
4398
+ }
4399
+ }
4400
+ console.log(` ${'Mode:'.padEnd(10)} ${configuredCount > 0 ? `BYOK (${configuredCount} model${configuredCount === 1 ? '' : 's'})` : 'free tier'}`);
4401
+ console.log('');
4402
+ return;
4403
+ }
4404
+
4405
+ // Interactive wizard
4406
+ printModelStatus(config);
4407
+
4408
+ let running = true;
4409
+ while (running) {
4410
+ const choices = [
4411
+ { name: 'Add Grok (xAI)', value: 'add_grok' },
4412
+ { name: 'Add Gemini (Google)', value: 'add_gemini' },
4413
+ { name: 'Add Codex/GPT-4o (OpenAI)', value: 'add_openai' },
4414
+ new inquirer.Separator(),
4415
+ { name: 'Remove a model', value: 'remove' },
4416
+ { name: 'Test deliberation', value: 'test' },
4417
+ { name: 'Exit', value: 'exit' },
4418
+ ];
4419
+
4420
+ const { action } = await inquirer.prompt([{
4421
+ type: 'list',
4422
+ name: 'action',
4423
+ message: 'Configure a model:',
4424
+ choices,
4425
+ }]);
4426
+
4427
+ if (action === 'exit') {
4428
+ running = false;
4429
+ break;
4430
+ }
4431
+
4432
+ if (action.startsWith('add_')) {
4433
+ const providerKey = action.replace('add_', '');
4434
+ const provider = MODEL_PROVIDERS[providerKey];
4435
+ const existing = config[providerKey];
4436
+
4437
+ // Warn if already configured
4438
+ if (existing && existing.enabled && existing.api_key) {
4439
+ const { overwrite } = await inquirer.prompt([{
4440
+ type: 'confirm',
4441
+ name: 'overwrite',
4442
+ message: `${provider.label} is already configured. Overwrite?`,
4443
+ default: false,
4444
+ }]);
4445
+ if (!overwrite) continue;
4446
+ }
4447
+
4448
+ // Prompt for API key
4449
+ const { apiKey } = await inquirer.prompt([{
4450
+ type: 'password',
4451
+ name: 'apiKey',
4452
+ message: `Enter your ${provider.label} API key:`,
4453
+ mask: '*',
4454
+ validate: (input) => {
4455
+ if (!input || input.trim().length === 0) return 'API key cannot be empty.';
4456
+ if (!input.startsWith(provider.prefix)) {
4457
+ return `Key should start with "${provider.prefix}". Got: "${input.slice(0, 6)}..."`;
4458
+ }
4459
+ return true;
4460
+ },
4461
+ }]);
4462
+
4463
+ // Optionally choose model variant
4464
+ const { modelChoice } = await inquirer.prompt([{
4465
+ type: 'list',
4466
+ name: 'modelChoice',
4467
+ message: 'Select model:',
4468
+ choices: provider.variants.map(v => ({ name: v === provider.defaultModel ? `${v} (default)` : v, value: v })),
4469
+ default: provider.defaultModel,
4470
+ }]);
4471
+
4472
+ config[providerKey] = {
4473
+ enabled: true,
4474
+ api_key: apiKey.trim(),
4475
+ model: modelChoice,
4476
+ name: provider.defaultName,
4477
+ };
4478
+ saveModelsConfig(config);
4479
+ console.log(chalk.green(`\n ${provider.label} configured with model ${modelChoice}.\n`));
4480
+ }
4481
+
4482
+ if (action === 'remove') {
4483
+ const configuredModels = Object.entries(config)
4484
+ .filter(([, v]) => v && v.enabled && v.api_key)
4485
+ .map(([k]) => ({ name: `${DEFAULT_MODELS[k]?.name || k} (${config[k].model})`, value: k }));
4486
+
4487
+ if (configuredModels.length === 0) {
4488
+ console.log(chalk.yellow('\n No models configured to remove.\n'));
4489
+ continue;
4490
+ }
4491
+
4492
+ const { toRemove } = await inquirer.prompt([{
4493
+ type: 'list',
4494
+ name: 'toRemove',
4495
+ message: 'Select model to remove:',
4496
+ choices: configuredModels,
4497
+ }]);
4498
+
4499
+ config[toRemove] = { enabled: false, api_key: '', model: DEFAULT_MODELS[toRemove].model, name: DEFAULT_MODELS[toRemove].name };
4500
+ saveModelsConfig(config);
4501
+ console.log(chalk.green(`\n ${DEFAULT_MODELS[toRemove].name} removed.\n`));
4502
+ }
4503
+
4504
+ if (action === 'test') {
4505
+ const configuredModels = Object.entries(config)
4506
+ .filter(([, v]) => v && v.enabled && v.api_key);
4507
+
4508
+ if (configuredModels.length === 0) {
4509
+ console.log(chalk.yellow('\n No models configured. Add a model first.\n'));
4510
+ continue;
4511
+ }
4512
+
4513
+ console.log(chalk.blue('\n Testing deliberation models...\n'));
4514
+ console.log(chalk.gray(' Prompt: "What is 2+2?"\n'));
4515
+
4516
+ for (const [key, entry] of configuredModels) {
4517
+ const label = (entry.name || key).padEnd(18);
4518
+ process.stdout.write(` ${label} `);
4519
+ const result = await testModelKey(key, entry.api_key, entry.model);
4520
+ if (result.ok) {
4521
+ console.log(chalk.green(`pass`) + chalk.gray(` -- "${result.response}"`));
4522
+ } else {
4523
+ console.log(chalk.red(`fail`) + chalk.gray(` -- ${result.error}`));
4524
+ }
4525
+ }
4526
+ console.log('');
4527
+ }
4528
+ }
4529
+ });
4530
+
4162
4531
  // Version subcommand alias (users type 'delimit version' not 'delimit -V')
4163
4532
  program
4164
4533
  .command('version')
@@ -4297,18 +4666,80 @@ function extractTags(text) {
4297
4666
  }
4298
4667
 
4299
4668
  function readMemories() {
4300
- if (!fs.existsSync(MEMORY_FILE)) return [];
4301
- const lines = fs.readFileSync(MEMORY_FILE, 'utf-8').split('\n').filter(l => l.trim());
4669
+ if (!fs.existsSync(MEMORY_DIR)) return [];
4302
4670
  const memories = [];
4303
- for (const line of lines) {
4304
- try { memories.push(JSON.parse(line)); } catch {}
4671
+
4672
+ // Read individual .json files (MCP format primary)
4673
+ try {
4674
+ const files = fs.readdirSync(MEMORY_DIR).filter(f => f.endsWith('.json') && f.startsWith('mem-'));
4675
+ for (const f of files) {
4676
+ try {
4677
+ const entry = JSON.parse(fs.readFileSync(path.join(MEMORY_DIR, f), 'utf-8'));
4678
+ // Normalize: MCP uses "content", CLI used "text"
4679
+ if (entry.content && !entry.text) entry.text = entry.content;
4680
+ if (entry.text && !entry.content) entry.content = entry.text;
4681
+ if (entry.created_at && !entry.created) entry.created = entry.created_at;
4682
+ if (entry.created && !entry.created_at) entry.created_at = entry.created;
4683
+ memories.push(entry);
4684
+ } catch {}
4685
+ }
4686
+ } catch {}
4687
+
4688
+ // Also read legacy .jsonl file (CLI format — backwards compat)
4689
+ if (fs.existsSync(MEMORY_FILE)) {
4690
+ const lines = fs.readFileSync(MEMORY_FILE, 'utf-8').split('\n').filter(l => l.trim());
4691
+ for (const line of lines) {
4692
+ try {
4693
+ const entry = JSON.parse(line);
4694
+ // Skip if already loaded from .json file
4695
+ if (!memories.find(m => m.id === entry.id)) {
4696
+ if (entry.text && !entry.content) entry.content = entry.text;
4697
+ if (entry.created && !entry.created_at) entry.created_at = entry.created;
4698
+ memories.push(entry);
4699
+ }
4700
+ } catch {}
4701
+ }
4305
4702
  }
4703
+
4704
+ // Sort by created date, newest first
4705
+ memories.sort((a, b) => new Date(b.created_at || b.created || 0) - new Date(a.created_at || a.created || 0));
4306
4706
  return memories;
4307
4707
  }
4308
4708
 
4309
- function writeMemories(memories) {
4709
+ function writeMemory(entry) {
4710
+ // Write in MCP-compatible format (individual .json files)
4310
4711
  fs.mkdirSync(MEMORY_DIR, { recursive: true });
4311
- fs.writeFileSync(MEMORY_FILE, memories.map(m => JSON.stringify(m)).join('\n') + (memories.length ? '\n' : ''));
4712
+ const memId = 'mem-' + require('crypto').createHash('sha256').update(entry.text.slice(0, 100)).digest('hex').slice(0, 12);
4713
+ const mcpEntry = {
4714
+ id: memId,
4715
+ content: entry.text,
4716
+ tags: entry.tags || [],
4717
+ context: entry.source || 'cli',
4718
+ created_at: entry.created || new Date().toISOString(),
4719
+ };
4720
+ fs.writeFileSync(path.join(MEMORY_DIR, `${memId}.json`), JSON.stringify(mcpEntry, null, 2));
4721
+ return memId;
4722
+ }
4723
+
4724
+ function deleteMemory(id) {
4725
+ // Delete from .json files
4726
+ const jsonFile = path.join(MEMORY_DIR, `${id}.json`);
4727
+ if (fs.existsSync(jsonFile)) {
4728
+ fs.unlinkSync(jsonFile);
4729
+ return true;
4730
+ }
4731
+ // Also check legacy .jsonl
4732
+ if (fs.existsSync(MEMORY_FILE)) {
4733
+ const lines = fs.readFileSync(MEMORY_FILE, 'utf-8').split('\n').filter(l => l.trim());
4734
+ const filtered = lines.filter(l => {
4735
+ try { return JSON.parse(l).id !== id; } catch { return true; }
4736
+ });
4737
+ if (filtered.length < lines.length) {
4738
+ fs.writeFileSync(MEMORY_FILE, filtered.join('\n') + (filtered.length ? '\n' : ''));
4739
+ return true;
4740
+ }
4741
+ }
4742
+ return false;
4312
4743
  }
4313
4744
 
4314
4745
  function relativeTime(isoDate) {
@@ -4349,18 +4780,16 @@ program
4349
4780
  const manualTags = (options.tag || []).map(t => t.toLowerCase());
4350
4781
  const allTags = [...new Set([...autoTags, ...manualTags])];
4351
4782
 
4352
- const memories = readMemories();
4353
4783
  const entry = {
4354
- id: generateShortId(),
4355
4784
  text,
4356
4785
  tags: allTags,
4357
4786
  created: new Date().toISOString(),
4358
4787
  source: 'cli',
4359
4788
  };
4360
- memories.push(entry);
4361
- writeMemories(memories);
4789
+ writeMemory(entry);
4790
+ const total = readMemories().length;
4362
4791
 
4363
- console.log(chalk.green(`\n Remembered.`) + chalk.gray(` (${memories.length} memor${memories.length === 1 ? 'y' : 'ies'} total)\n`));
4792
+ console.log(chalk.green(`\n Remembered.`) + chalk.gray(` (${total} memor${total === 1 ? 'y' : 'ies'} total)\n`));
4364
4793
  });
4365
4794
 
4366
4795
  program
@@ -4375,14 +4804,13 @@ program
4375
4804
 
4376
4805
  // --forget mode
4377
4806
  if (options.forget) {
4378
- const idx = memories.findIndex(m => m.id === options.forget);
4379
- if (idx === -1) {
4807
+ if (deleteMemory(options.forget)) {
4808
+ const remaining = readMemories().length;
4809
+ console.log(chalk.green(`\n Forgotten.`) + chalk.gray(` (${remaining} memor${remaining === 1 ? 'y' : 'ies'} remaining)\n`));
4810
+ } else {
4380
4811
  console.log(chalk.red(`\n No memory found with ID: ${options.forget}\n`));
4381
4812
  process.exit(1);
4382
4813
  }
4383
- memories.splice(idx, 1);
4384
- writeMemories(memories);
4385
- console.log(chalk.green(`\n Forgotten.`) + chalk.gray(` (${memories.length} memor${memories.length === 1 ? 'y' : 'ies'} remaining)\n`));
4386
4814
  return;
4387
4815
  }
4388
4816
 
@@ -4454,15 +4882,13 @@ program
4454
4882
  .command('forget <id>')
4455
4883
  .description('Delete a memory by ID (alias for recall --forget)')
4456
4884
  .action((id) => {
4457
- const memories = readMemories();
4458
- const idx = memories.findIndex(m => m.id === id);
4459
- if (idx === -1) {
4885
+ if (deleteMemory(id)) {
4886
+ const remaining = readMemories().length;
4887
+ console.log(chalk.green(`\n Forgotten.`) + chalk.gray(` (${remaining} memor${remaining === 1 ? 'y' : 'ies'} remaining)\n`));
4888
+ } else {
4460
4889
  console.log(chalk.red(`\n No memory found with ID: ${id}\n`));
4461
4890
  process.exit(1);
4462
4891
  }
4463
- memories.splice(idx, 1);
4464
- writeMemories(memories);
4465
- console.log(chalk.green(`\n Forgotten.`) + chalk.gray(` (${memories.length} memor${memories.length === 1 ? 'y' : 'ies'} remaining)\n`));
4466
4892
  });
4467
4893
 
4468
4894
  const normalizedArgs = normalizeNaturalLanguageArgs(process.argv);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "4.1.38",
4
+ "version": "4.1.41",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "url": "https://github.com/delimit-ai/delimit-mcp-server",
8
8
  "source": "github"
9
9
  },
10
- "version": "4.1.34",
10
+ "version": "4.1.40",
11
11
  "websiteUrl": "https://delimit.ai",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "delimit-cli",
16
- "version": "4.1.34",
16
+ "version": "4.1.40",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }