cawdex 1.35.69 → 1.35.74

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 (45) hide show
  1. package/README.md +188 -188
  2. package/dist/command-palette.js +5 -4
  3. package/dist/command-palette.js.map +1 -1
  4. package/dist/config.d.ts +1 -1
  5. package/dist/config.js +20 -13
  6. package/dist/config.js.map +1 -1
  7. package/dist/curator.d.ts +2 -2
  8. package/dist/curator.js +2 -2
  9. package/dist/fixed-footer.d.ts +29 -0
  10. package/dist/fixed-footer.js +383 -0
  11. package/dist/fixed-footer.js.map +1 -0
  12. package/dist/hooks.d.ts +1 -1
  13. package/dist/hooks.js +7 -7
  14. package/dist/hooks.js.map +1 -1
  15. package/dist/index.js +352 -57
  16. package/dist/index.js.map +1 -1
  17. package/dist/instant-answer.d.ts +6 -1
  18. package/dist/instant-answer.js +32 -1
  19. package/dist/instant-answer.js.map +1 -1
  20. package/dist/live-queue.d.ts +0 -32
  21. package/dist/live-queue.js +15 -0
  22. package/dist/live-queue.js.map +1 -1
  23. package/dist/modes.d.ts +2 -1
  24. package/dist/modes.js +368 -361
  25. package/dist/modes.js.map +1 -1
  26. package/dist/query.d.ts +1 -0
  27. package/dist/query.js +163 -44
  28. package/dist/query.js.map +1 -1
  29. package/dist/swarm.d.ts +8 -3
  30. package/dist/swarm.js +39 -14
  31. package/dist/swarm.js.map +1 -1
  32. package/dist/system-prompt.js +14 -14
  33. package/dist/system-prompt.js.map +1 -1
  34. package/dist/theme.d.ts +4 -0
  35. package/dist/theme.js +22 -0
  36. package/dist/theme.js.map +1 -1
  37. package/dist/tools/skill.d.ts +2 -2
  38. package/dist/tools/skill.js +2 -2
  39. package/dist/types.d.ts +15 -0
  40. package/dist/types.js.map +1 -1
  41. package/dist/updater.d.ts +16 -0
  42. package/dist/updater.js +157 -0
  43. package/dist/updater.js.map +1 -0
  44. package/dist/walkthrough.js +7 -7
  45. package/package.json +1 -2
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import { initHooksDir, runHooks, listHooks, saveHooksConfig, clearQuarantinedHoo
27
27
  import { printUsageSummary, setBudget } from './cost-tracker.js';
28
28
  import { getCompactionStats, OPENROUTER_FREE_ROUTER_SAFE_CONTEXT_WINDOW_TOKENS, OPENROUTER_UNKNOWN_FREE_MODEL_CONTEXT_WINDOW_TOKENS, } from './compaction.js';
29
29
  import { extractPatterns, printInstinctStatus, pruneExpired, listInstincts, exportInstincts, importInstincts } from './learning.js';
30
- import { MODES, listModes } from './modes.js';
30
+ import { MODES, listModes, normalizeModeName } from './modes.js';
31
31
  import { printModelOptions, switchModel, classifyComplexity, routeModel } from './model-router.js';
32
32
  import { buildCommitPrompt, buildPRPrompt, printDiff, printLog } from './git-workflow.js';
33
33
  import { buildReviewPrompt, buildTDDPrompt, buildSecurityReviewPrompt, runAudit, printAuditReport, buildPlanPrompt, buildE2EPrompt, buildBuildFixPrompt, buildEvalPrompt, buildBenchmarkPrompt, splitBenchmarkArgs, } from './evaluation.js';
@@ -79,7 +79,7 @@ import { status as sandboxStatus } from './sandbox.js';
79
79
  // API key rotation pool (/keys)
80
80
  import { listStatus as keyPoolStatus, setPool as syncKeyPool } from './key-rotation.js';
81
81
  // Agentic swarm — fan-out concurrent agents on the same task (/swarm)
82
- import { buildSwarmAgentTask, buildSwarmPlan, decodeSwarmSentinel, encodeSwarmSentinel, formatSwarmResults, parseSwarmCommandArgs, resolveAgents, runSwarm, } from './swarm.js';
82
+ import { buildSwarmAgentTask, buildSwarmPlan, clampSwarmMaxAgents, decodeSwarmSentinel, encodeSwarmSentinel, formatSwarmResults, parseSwarmCommandArgs, resolveAgents, runSwarm, } from './swarm.js';
83
83
  // Voice / accessibility — built-in dictation (Whisper) + readout (ElevenLabs)
84
84
  import { printVoiceStatus, isVoiceEnabled, getTtsConfig, getSttConfig, getAccessibilityConfig, speak, dictateOnce, } from './voice.js';
85
85
  import { isFfmpegAvailable, audioCue, startRecording, probeMic, micProbeMessage } from './audio.js';
@@ -88,6 +88,8 @@ import { COMMAND_CATALOG, allSlashCommandNames, completeSlashCommandNames, resol
88
88
  import { inlineSuggest, resolveInlineSuggestQuestionInput } from './inline-suggest.js';
89
89
  import { normalizeTypeaheadDraftForPrompt } from './prompt-buffer.js';
90
90
  import { maybeInstantAnswer } from './instant-answer.js';
91
+ import { getCurrentVersion, startStartupUpdateCheck } from './updater.js';
92
+ import { activateFooter, askWithFooterPrompt, buildFooterSnapshot, deactivateFooter, isFooterActive, shouldUseFixedFooter, updateFooter, } from './fixed-footer.js';
91
93
  /**
92
94
  * Unified prompt resolver — prefers the bundled ECC prompt for a given
93
95
  * intent and falls back to the built-in builder when ECC isn't installed.
@@ -127,8 +129,8 @@ function clipSetupValue(value, max = 120) {
127
129
  const single = value.replace(/\s+/g, ' ').trim();
128
130
  return single.length <= max ? single : single.slice(0, max - 1) + '…';
129
131
  }
130
- async function runSwarmSetupWizard(rl, plan) {
131
- if (!stdin.isTTY || process.env.CAWDEX_SWARM_WIZARD === '0')
132
+ async function runSwarmSetupWizard(rl, plan, config) {
133
+ if (!stdin.isTTY || process.env.CAWDEX_SWARM_WIZARD === '0' || config.swarm?.setupWizard === false)
132
134
  return plan;
133
135
  const next = {
134
136
  ...plan,
@@ -189,6 +191,115 @@ function printUnknownSlashCommand(cmd) {
189
191
  console.log(d(' Did you mean: ') + suggestions.map((entry) => c(entry.command)).join(d(', ')));
190
192
  console.log(d(' Try: ') + c(`/help ${suggestions[0].command}`) + d(' for exact usage.'));
191
193
  }
194
+ async function promptSecretLine(rl, prompt, existingValue = '') {
195
+ const suffix = existingValue ? ' [stored; Enter keeps it]' : '';
196
+ const label = `${prompt}${suffix}: `;
197
+ if (!stdin.isTTY || !stdout.isTTY) {
198
+ const answer = await rl.question(chalk.yellow(label));
199
+ return answer.trim() || existingValue;
200
+ }
201
+ return new Promise((resolve) => {
202
+ let value = '';
203
+ const wasRaw = stdin.isRaw;
204
+ const keypressListeners = stdin.listeners('keypress').slice();
205
+ const detached = keypressListeners.filter((listener) => !listener.__cawdexHotkey__);
206
+ function render() {
207
+ const shown = value ? '*'.repeat(Math.min(value.length, 16)) : '';
208
+ stdout.write(`\r\x1b[2K${chalk.yellow(label)}${chalk.dim(shown)}`);
209
+ }
210
+ function cleanup() {
211
+ stdin.removeListener('data', onData);
212
+ for (const listener of detached)
213
+ stdin.on('keypress', listener);
214
+ try {
215
+ stdin.setRawMode(wasRaw);
216
+ }
217
+ catch { /* noop */ }
218
+ stdout.write('\n');
219
+ }
220
+ function done(nextValue) {
221
+ cleanup();
222
+ resolve(nextValue.trim() || existingValue);
223
+ }
224
+ function onData(buf) {
225
+ if (buf.length === 1 && buf[0] === 0x03) {
226
+ cleanup();
227
+ process.exit(130);
228
+ }
229
+ if (buf.length === 1 && buf[0] === 0x1B) {
230
+ done(existingValue);
231
+ return;
232
+ }
233
+ if (buf.length === 1 && (buf[0] === 0x0D || buf[0] === 0x0A)) {
234
+ done(value);
235
+ return;
236
+ }
237
+ if (buf.length === 1 && (buf[0] === 0x7F || buf[0] === 0x08)) {
238
+ value = value.slice(0, -1);
239
+ render();
240
+ return;
241
+ }
242
+ const text = buf.toString('utf8').replace(/[^\x20-\x7E]/g, '');
243
+ if (text) {
244
+ value += text;
245
+ render();
246
+ }
247
+ }
248
+ for (const listener of detached)
249
+ stdin.removeListener('keypress', listener);
250
+ try {
251
+ stdin.setRawMode(true);
252
+ }
253
+ catch { /* noop */ }
254
+ stdin.resume();
255
+ stdin.on('data', onData);
256
+ render();
257
+ });
258
+ }
259
+ async function runAppearanceWizard(rl, config) {
260
+ const themeChoices = [
261
+ { label: 'full', detail: 'banner and full context on launch', value: 'full' },
262
+ { label: 'compact', detail: 'same banner, tighter future surfaces', value: 'compact' },
263
+ { label: 'minimal', detail: 'small launch header', value: 'minimal' },
264
+ ];
265
+ const currentTheme = config.theme || 'full';
266
+ const themeDefault = Math.max(0, themeChoices.findIndex((choice) => choice.value === currentTheme));
267
+ const selectedTheme = await selectConfigChoice(rl, 'Display theme', themeChoices, {
268
+ defaultIndex: themeDefault,
269
+ fallbackPrompt: ` Display theme [${themeDefault + 1}]: `,
270
+ parseFallback: (answer, defaultIndex) => {
271
+ const raw = (answer || String(defaultIndex + 1)).trim().toLowerCase();
272
+ const byName = themeChoices.find((choice) => choice.value === raw);
273
+ if (byName)
274
+ return byName.value;
275
+ return themeChoices[Number.parseInt(raw, 10) - 1]?.value;
276
+ },
277
+ });
278
+ const paletteChoices = listPalettes().map((meta) => ({
279
+ label: meta.id,
280
+ detail: meta.description,
281
+ value: meta.id,
282
+ }));
283
+ const currentPalette = getPaletteId();
284
+ const paletteDefault = Math.max(0, paletteChoices.findIndex((choice) => choice.value === currentPalette));
285
+ const selectedPalette = await selectConfigChoice(rl, 'Color palette', paletteChoices, {
286
+ defaultIndex: paletteDefault,
287
+ fallbackPrompt: ` Color palette [${paletteDefault + 1}]: `,
288
+ parseFallback: (answer, defaultIndex) => {
289
+ const raw = (answer || String(defaultIndex + 1)).trim().toLowerCase();
290
+ const resolved = resolvePaletteId(raw);
291
+ if (resolved)
292
+ return resolved;
293
+ return paletteChoices[Number.parseInt(raw, 10) - 1]?.value;
294
+ },
295
+ });
296
+ config.theme = selectedTheme;
297
+ setPalette(selectedPalette);
298
+ config.palette = selectedPalette;
299
+ saveConfig(config);
300
+ console.log(theme.brandBold(` ${sym.mark} Appearance: ${selectedTheme} / ${selectedPalette}`));
301
+ console.log(theme.dim(' Brand · ') + theme.brand('brand') + theme.dim(' · ') + theme.success('success') + theme.dim(' · ') + theme.warning('warning') + theme.dim(' · ') + theme.error('error') + theme.dim(' · ') + theme.command('command'));
302
+ }
192
303
  async function setupWizard(rl, currentConfig) {
193
304
  console.log(chalk.bold.cyan(`\n ${BRAND_NAME} — First-time Setup\n`));
194
305
  const providerKeys = Object.keys(PROVIDERS);
@@ -220,8 +331,9 @@ async function setupWizard(rl, currentConfig) {
220
331
  }
221
332
  let apiKey = '';
222
333
  let openaiAuth;
334
+ const sameProvider = normalizeProvider(currentConfig?.provider ?? '') === normalizeProvider(provider.name);
223
335
  if (provider.requiresKey) {
224
- apiKey = await rl.question(chalk.yellow(` API Key for ${provider.name}: `));
336
+ apiKey = await promptSecretLine(rl, ` API Key for ${provider.name}`, sameProvider ? currentConfig?.apiKey ?? '' : '');
225
337
  }
226
338
  if (providerKey === 'openai-codex') {
227
339
  openaiAuth = {
@@ -241,7 +353,6 @@ async function setupWizard(rl, currentConfig) {
241
353
  console.log(chalk.dim(` After setup, run /openai-login or run "codex login". ${BRAND_NAME} will read ~/.codex/auth.json.`));
242
354
  }
243
355
  }
244
- const sameProvider = normalizeProvider(currentConfig?.provider ?? '') === normalizeProvider(provider.name);
245
356
  let model = sameProvider && currentConfig?.model ? currentConfig.model : provider.defaultModel;
246
357
  const modelInput = await rl.question(chalk.yellow(` Model [${model}]: `));
247
358
  if (modelInput.trim())
@@ -274,6 +385,46 @@ async function setupWizard(rl, currentConfig) {
274
385
  console.log(chalk.dim(' Free variants usually end with :free. Switch later with /openrouter-free or /model <id>.'));
275
386
  console.log('');
276
387
  }
388
+ let fallbackModel;
389
+ if (providerKey === 'openrouter') {
390
+ const currentFallback = sameProvider ? currentConfig?.fallbackModel : undefined;
391
+ const fallbackDefaultIndex = currentFallback === PROVIDERS.openrouter.defaultModel
392
+ ? 1
393
+ : currentFallback
394
+ ? 2
395
+ : 0;
396
+ console.log(chalk.white(' OpenRouter fallback'));
397
+ console.log(chalk.dim(' Fallback retries can hide a bad primary model by silently switching to another model.'));
398
+ console.log(chalk.dim(' Choose explicitly; you can change this later with /fallback.'));
399
+ const fallbackChoice = await selectConfigChoice(rl, 'Fallback behavior', [
400
+ { label: 'Disable fallback', detail: 'show the primary model failure; never switch to free automatically', value: 'off' },
401
+ { label: 'Use openrouter/free', detail: 'retry once with the free router on timeout, empty, or cryptic errors', value: 'free' },
402
+ { label: 'Custom fallback model', detail: 'enter a specific OpenRouter model id', value: 'custom' },
403
+ ], {
404
+ defaultIndex: fallbackDefaultIndex,
405
+ fallbackPrompt: ` Fallback behavior [${fallbackDefaultIndex + 1}]: `,
406
+ parseFallback: (answer, defaultIndex) => {
407
+ const raw = (answer || String(defaultIndex + 1)).trim().toLowerCase();
408
+ if (['off', 'none', 'disable', 'disabled', 'no', 'n', '1'].includes(raw))
409
+ return 'off';
410
+ if (['free', 'openrouter/free', 'yes', 'y', '2'].includes(raw))
411
+ return 'free';
412
+ if (['custom', 'model', '3'].includes(raw))
413
+ return 'custom';
414
+ return undefined;
415
+ },
416
+ });
417
+ if (fallbackChoice === 'free')
418
+ fallbackModel = PROVIDERS.openrouter.defaultModel;
419
+ if (fallbackChoice === 'custom') {
420
+ const customFallback = currentFallback && currentFallback !== PROVIDERS.openrouter.defaultModel
421
+ ? currentFallback
422
+ : '';
423
+ const answer = await rl.question(chalk.yellow(` Fallback model${customFallback ? ` [${customFallback}]` : ''}: `));
424
+ fallbackModel = answer.trim() || customFallback || undefined;
425
+ }
426
+ console.log('');
427
+ }
277
428
  const permissionChoices = [
278
429
  { label: 'ask', detail: 'prompt before writes/commands (safest)', value: 'ask' },
279
430
  { label: 'auto', detail: 'auto-approve reads, ask for destructive', value: 'auto' },
@@ -293,21 +444,17 @@ async function setupWizard(rl, currentConfig) {
293
444
  return byNumber?.value;
294
445
  },
295
446
  });
296
- // ── MemPalace memory setup ──────────────────────────────
297
- // Featured capability, opt-out at setup time. Explain briefly so the
298
- // user can make an informed choice — most users want this on.
299
- console.log(chalk.white('\n MemPalace persistent memory'));
300
- console.log(chalk.dim(' Lets the agent remember your preferences, codebase landmarks, and lessons across sessions.'));
301
- console.log(chalk.dim(' Two stores: global (~/.cawdex/memory) for cross-project facts, project (.cawdex/memory'));
302
- console.log(chalk.dim(' in each repo) for codebase-specific knowledge. Searchable via /memory or by the agent itself.'));
303
- console.log(chalk.dim(' Zero external dependencies; storage is local JSON files. Can be toggled anytime via /memory disable.'));
304
- const memoryDefaultIndex = currentConfig?.memory?.enabled === false ? 1 : 0;
305
- const memoryEnabled = await selectConfigChoice(rl, 'Enable MemPalace memory', [
306
- { label: 'Enable', detail: 'local global + project memory stores', value: true },
307
- { label: 'Disable', detail: 'can be re-enabled later with /memory enable', value: false },
447
+ // /config stays focused on the main live controls: model/fallback,
448
+ // permissions, and swarm. Other subsystems keep their own commands.
449
+ console.log(chalk.white('\n Swarm settings'));
450
+ console.log(chalk.dim(' Defaults for /swarm <task>. Per-task setup still comes from the task text.'));
451
+ const swarmWizardDefaultIndex = currentConfig?.swarm?.setupWizard === false ? 1 : 0;
452
+ const swarmSetupWizard = await selectConfigChoice(rl, 'Swarm setup wizard', [
453
+ { label: 'Enable', detail: 'ask goal/budget/stack/assets/quality before natural /swarm tasks', value: true },
454
+ { label: 'Disable', detail: 'infer setup fields without asking', value: false },
308
455
  ], {
309
- defaultIndex: memoryDefaultIndex,
310
- fallbackPrompt: ` Enable MemPalace memory? [${memoryDefaultIndex === 0 ? 'Y/n' : 'y/N'}]: `,
456
+ defaultIndex: swarmWizardDefaultIndex,
457
+ fallbackPrompt: ` Swarm setup wizard? [${swarmWizardDefaultIndex === 0 ? 'Y/n' : 'y/N'}]: `,
311
458
  parseFallback: (answer, defaultIndex) => {
312
459
  const raw = answer.trim().toLowerCase();
313
460
  if (!raw)
@@ -324,31 +471,51 @@ async function setupWizard(rl, currentConfig) {
324
471
  return undefined;
325
472
  },
326
473
  });
474
+ const currentMaxAgents = clampSwarmMaxAgents(currentConfig?.swarm?.maxAgents);
475
+ const maxAgentsAnswer = await rl.question(chalk.yellow(` Max swarm agents [${currentMaxAgents}]: `));
476
+ const maxAgents = maxAgentsAnswer.trim()
477
+ ? clampSwarmMaxAgents(Number.parseInt(maxAgentsAnswer.trim(), 10))
478
+ : currentMaxAgents;
479
+ const askOptionalSwarmDefault = async (label, current) => {
480
+ const answer = await rl.question(chalk.yellow(` ${label}${current ? ` [${current}]` : ''}: `));
481
+ return answer.trim() || current || undefined;
482
+ };
483
+ const defaultBudget = await askOptionalSwarmDefault('Default budget/time', currentConfig?.swarm?.defaultBudget);
484
+ const defaultTarget = await askOptionalSwarmDefault('Default target platform/stack', currentConfig?.swarm?.defaultTarget);
485
+ const defaultAssets = await askOptionalSwarmDefault('Default starting assets/resources', currentConfig?.swarm?.defaultAssets);
486
+ const defaultQuality = await askOptionalSwarmDefault('Default quality bar/release target', currentConfig?.swarm?.defaultQuality);
327
487
  const config = {
488
+ ...(currentConfig ?? {}),
328
489
  apiKey,
329
490
  baseURL,
330
491
  model,
331
- fallbackModel: providerKey === 'openrouter' ? PROVIDERS.openrouter.defaultModel : undefined,
332
492
  provider: provider.name,
333
- ...(openaiAuth ? { openaiAuth } : {}),
334
- maxTokens: 8192,
335
- temperature: 0.3,
493
+ maxTokens: currentConfig?.maxTokens ?? 8192,
494
+ temperature: currentConfig?.temperature ?? 0.3,
336
495
  permissionMode: permMode,
337
- memory: {
338
- enabled: memoryEnabled,
339
- globalScope: true,
340
- projectScope: true,
496
+ swarm: {
497
+ setupWizard: swarmSetupWizard,
498
+ maxAgents,
499
+ ...(defaultBudget ? { defaultBudget } : {}),
500
+ ...(defaultTarget ? { defaultTarget } : {}),
501
+ ...(defaultAssets ? { defaultAssets } : {}),
502
+ ...(defaultQuality ? { defaultQuality } : {}),
341
503
  },
342
504
  };
505
+ if (fallbackModel)
506
+ config.fallbackModel = fallbackModel;
507
+ else
508
+ delete config.fallbackModel;
509
+ if (openaiAuth)
510
+ config.openaiAuth = openaiAuth;
511
+ else
512
+ delete config.openaiAuth;
343
513
  applyModelSelection(config, model);
344
514
  saveConfig(config);
345
515
  console.log(chalk.green(`\n Config saved to ${getConfigDir()}/config.json`));
346
- if (memoryEnabled) {
347
- console.log(chalk.dim(` MemPalace: ENABLED — 7 memory_* tools available to the agent. Storage created on first write.`));
348
- }
349
- else {
350
- console.log(chalk.dim(` MemPalace: disabled. Re-enable anytime with /memory enable.`));
351
- }
516
+ console.log(chalk.dim(` Configured: /model, /fallback, /perm, and /swarm. Memory stays under /memory.`));
517
+ console.log();
518
+ return config;
352
519
  console.log();
353
520
  return config;
354
521
  }
@@ -494,6 +661,9 @@ function printResumedHistory(messages, sessionName) {
494
661
  * and modeTag, so this is effectively a no-op there.
495
662
  */
496
663
  async function askWithDecoratedPrompt(rl, sessionTag, modeTag, promptGlyph, prefill = '') {
664
+ if (isFooterActive()) {
665
+ return askWithFooterPrompt(rl, prefill);
666
+ }
497
667
  const decorative = sessionTag + modeTag;
498
668
  if (decorative.length > 0) {
499
669
  process.stdout.write(decorative);
@@ -691,7 +861,10 @@ function parseSlashCommand(input) {
691
861
  // { handled: true } — local command, output printed to stdout
692
862
  // { handled: false, injectPrompt } — LLM-driven, prompt ready to send
693
863
  export function handleSlashCommand(input, config, messages, session, mode) {
694
- const { cmd, args } = parseSlashCommand(input);
864
+ const parsed = parseSlashCommand(input);
865
+ const resolved = resolveCommandEntry(parsed.cmd);
866
+ const cmd = resolved?.entry.command ?? parsed.cmd;
867
+ const args = parsed.args;
695
868
  switch (cmd) {
696
869
  // ── Help ──────────────────────────────────────────
697
870
  case '/help': {
@@ -715,6 +888,7 @@ export function handleSlashCommand(input, config, messages, session, mode) {
715
888
  console.log(d(' ') + c('/theme [mode]') + d(' — toggle display mode (full/compact/minimal)'));
716
889
  console.log(d(' ') + c('/palette [id]') + d(' — switch color palette; run /palettes to list'));
717
890
  console.log(d(' ') + c('/palettes') + d(' — list available color palettes with preview'));
891
+ console.log(d(' ') + c('/footer [sub]') + d(' — customize the fixed bottom footer and startup prompt'));
718
892
  console.log(d(' ') + c('/clear') + d(' — clear conversation'));
719
893
  console.log(d(' ') + c('/back [n]') + d(' — rewind to before the nth most-recent user turn (no arg lists turns)'));
720
894
  console.log(d(' ') + c('/fork [name]') + d(' — branch current conversation; previous session reachable via /resume (alias: /branch)'));
@@ -742,9 +916,9 @@ export function handleSlashCommand(input, config, messages, session, mode) {
742
916
  console.log(d(' ') + c('/keys [add|rm]') + d(' — multi-key rotation pool (e.g. several OpenRouter accounts)'));
743
917
  console.log(d(' ') + c('/route') + d(' — auto-route model based on next message'));
744
918
  console.log(h('\n ── Modes ──'));
745
- console.log(d(' ') + c('/mode [name]') + d(' — switch mode (dev/review/tdd/research/plan/debug/architect/hermes/design)'));
919
+ console.log(d(' ') + c('/mode [name]') + d(' — switch mode (dev/review/tdd/research/plan/debug/architect/sentience/design)'));
746
920
  console.log(d(' ') + c('/modes') + d(' — list all modes (read-only; use /mode <name> to switch)'));
747
- console.log(d(' ') + c('/hermes') + d(' alias for /mode hermes (self-improving learning loop)'));
921
+ console.log(d(' ') + c('/sentience') + d(' — self-improving learning loop (alias: /hermes)'));
748
922
  console.log(d(' ') + c('/design [task]') + d(' — switch to design mode (Stitch-powered UI generation); optional task to start immediately'));
749
923
  console.log(h('\n ── Session ──'));
750
924
  console.log(d(' ') + c('/sessions') + d(' — list saved sessions'));
@@ -890,10 +1064,90 @@ export function handleSlashCommand(input, config, messages, session, mode) {
890
1064
  }
891
1065
  }
892
1066
  else {
893
- const current = config.theme || 'full';
894
- console.log(chalk.dim(` Current theme: ${current}`));
1067
+ return { handled: true, injectPrompt: '__PICK_APPEARANCE__' };
895
1068
  }
896
1069
  return { handled: true };
1070
+ case '/footer': {
1071
+ const input = args.trim();
1072
+ const parts = input.split(/\s+/).filter(Boolean);
1073
+ const sub = (parts[0] || '').toLowerCase();
1074
+ config.footer = config.footer ?? { enabled: true, openingPrompt: '/model' };
1075
+ const refreshFooter = () => {
1076
+ const snapshot = buildFooterSnapshot(config, mode.current, session, process.cwd());
1077
+ if (shouldUseFixedFooter(config))
1078
+ activateFooter(snapshot);
1079
+ else
1080
+ deactivateFooter();
1081
+ updateFooter(snapshot);
1082
+ };
1083
+ const printStatus = () => {
1084
+ console.log(theme.header('\n Footer'));
1085
+ console.log(theme.dim(` Enabled: ${config.footer?.enabled === false ? 'off' : 'on'}`));
1086
+ console.log(theme.dim(` Version: ${getCurrentVersion()}`));
1087
+ console.log(theme.dim(` Opening prompt: ${config.footer?.openingPrompt ?? '/model'}`));
1088
+ console.log(theme.dim(` Status row: ${config.footer?.template || '(default)'}`));
1089
+ console.log(theme.dim(` Template keys: {provider}, {model}, {permissions}, {session}, {mode}, {cwd}, {workspace}, {sandbox}, {cost}, {budget}, {activity}, {version}`));
1090
+ console.log(theme.dim('\n Commands:'));
1091
+ console.log(theme.dim(' /footer on | off'));
1092
+ console.log(theme.dim(' /footer text Provider {provider} | Model {model} | v{version}'));
1093
+ console.log(theme.dim(' /footer opening /model (or: /footer opening off)'));
1094
+ console.log(theme.dim(' /footer clear (clear custom status row)'));
1095
+ console.log(theme.dim(' /footer reset (default footer + /model opening prompt)'));
1096
+ };
1097
+ if (!sub) {
1098
+ printStatus();
1099
+ return { handled: true };
1100
+ }
1101
+ if (sub === 'on') {
1102
+ config.footer.enabled = true;
1103
+ saveConfig(config);
1104
+ refreshFooter();
1105
+ console.log(chalk.green(' Footer: on'));
1106
+ return { handled: true };
1107
+ }
1108
+ if (sub === 'off') {
1109
+ config.footer.enabled = false;
1110
+ saveConfig(config);
1111
+ refreshFooter();
1112
+ console.log(chalk.green(' Footer: off'));
1113
+ return { handled: true };
1114
+ }
1115
+ if (sub === 'reset') {
1116
+ config.footer = { enabled: true, openingPrompt: '/model' };
1117
+ saveConfig(config);
1118
+ refreshFooter();
1119
+ console.log(chalk.green(' Footer reset to defaults.'));
1120
+ return { handled: true };
1121
+ }
1122
+ if (sub === 'clear') {
1123
+ delete config.footer.template;
1124
+ saveConfig(config);
1125
+ refreshFooter();
1126
+ console.log(chalk.green(' Footer status row: default'));
1127
+ return { handled: true };
1128
+ }
1129
+ if (sub === 'opening') {
1130
+ const value = input.slice(parts[0].length).trim();
1131
+ config.footer.openingPrompt = /^(off|none|disable)$/i.test(value) ? '' : (value || '/model');
1132
+ saveConfig(config);
1133
+ refreshFooter();
1134
+ console.log(chalk.green(` Footer opening prompt: ${config.footer.openingPrompt || '(off)'}`));
1135
+ return { handled: true };
1136
+ }
1137
+ const template = sub === 'text'
1138
+ ? input.slice(parts[0].length).trim()
1139
+ : input;
1140
+ if (!template) {
1141
+ printStatus();
1142
+ return { handled: true };
1143
+ }
1144
+ config.footer.template = template;
1145
+ saveConfig(config);
1146
+ refreshFooter();
1147
+ console.log(chalk.green(' Footer status row updated.'));
1148
+ console.log(chalk.dim(` ${template}`));
1149
+ return { handled: true };
1150
+ }
897
1151
  // ── Clear ─────────────────────────────────────────
898
1152
  case '/clear': {
899
1153
  // Also reset the global state that's keyed to the conversation
@@ -1174,7 +1428,7 @@ export function handleSlashCommand(input, config, messages, session, mode) {
1174
1428
  if (!target) {
1175
1429
  const cur = config.fallbackModel ?? '(not set)';
1176
1430
  console.log(chalk.dim(` Current fallback model: ${cur}`));
1177
- console.log(chalk.dim(` Used once per chain when the primary returns "unknown" provider errors.`));
1431
+ console.log(chalk.dim(` Used once per chain when the primary times out, returns empty text, or hits an unknown provider error.`));
1178
1432
  console.log(chalk.dim(` Set with: /fallback <model-id> · disable with: /fallback off`));
1179
1433
  return { handled: true };
1180
1434
  }
@@ -1187,18 +1441,19 @@ export function handleSlashCommand(input, config, messages, session, mode) {
1187
1441
  config.fallbackModel = target;
1188
1442
  saveConfig(config);
1189
1443
  console.log(chalk.green(` Fallback model: ${target}`));
1190
- console.log(chalk.dim(` Will be tried automatically once per chain if ${config.model} returns a cryptic provider error.`));
1444
+ console.log(chalk.dim(` Will be tried automatically once per chain if ${config.model} times out, returns empty text, or hits an unknown provider error.`));
1191
1445
  return { handled: true };
1192
1446
  }
1193
1447
  case '/openrouter-free': {
1194
1448
  config.provider = PROVIDERS.openrouter.name;
1195
1449
  config.baseURL = PROVIDERS.openrouter.baseURL;
1196
1450
  applyModelSelection(config, PROVIDERS.openrouter.defaultModel);
1197
- config.fallbackModel = PROVIDERS.openrouter.defaultModel;
1451
+ config.fallbackModel = undefined;
1198
1452
  saveConfig(config);
1199
1453
  resetClient();
1200
1454
  console.log(chalk.green(' OpenRouter free-tier mode enabled.'));
1201
1455
  console.log(chalk.dim(` Model: ${config.model}`));
1456
+ console.log(chalk.dim(' Fallback: disabled. Enable explicitly with /fallback <model-id>.'));
1202
1457
  console.log(chalk.dim(' The free router picks a currently available zero-cost model that supports the request shape.'));
1203
1458
  return { handled: true };
1204
1459
  }
@@ -1280,8 +1535,8 @@ export function handleSlashCommand(input, config, messages, session, mode) {
1280
1535
  }
1281
1536
  // ── Mode ──────────────────────────────────────────
1282
1537
  case '/mode':
1283
- if (args && MODES[args]) {
1284
- mode.current = args;
1538
+ if (args && normalizeModeName(args)) {
1539
+ mode.current = normalizeModeName(args);
1285
1540
  const m = MODES[mode.current];
1286
1541
  console.log(chalk.green(` Mode: ${m.label} — ${m.description}`));
1287
1542
  // Soft hint when switching mode with conversation history present.
@@ -1310,10 +1565,11 @@ export function handleSlashCommand(input, config, messages, session, mode) {
1310
1565
  console.log(chalk.dim(` Current: ${mode.current} (${MODES[mode.current].description})`));
1311
1566
  }
1312
1567
  return { handled: true };
1313
- // ── Hermes shorthand (inspired by nousresearch/hermes-agent) ──
1568
+ // ── Sentience shorthand (/hermes remains a compatibility alias) ──
1569
+ case '/sentience':
1314
1570
  case '/hermes': {
1315
- mode.current = 'hermes';
1316
- const m = MODES.hermes;
1571
+ mode.current = 'sentience';
1572
+ const m = MODES.sentience;
1317
1573
  console.log(chalk.cyan(` Mode: ${m.label}`));
1318
1574
  console.log(chalk.dim(` ${m.description}`));
1319
1575
  console.log(chalk.dim(` Recall → user-model → parallelize → distill → persist → schedule.`));
@@ -1442,9 +1698,10 @@ export function handleSlashCommand(input, config, messages, session, mode) {
1442
1698
  // mode.current is a { current: Mode } box (shared closure
1443
1699
  // ref) so mutation propagates to the hotkey listener +
1444
1700
  // prompt rendering automatically.
1445
- if (loaded.mode && loaded.mode !== mode.current) {
1446
- changes.push(`mode ${mode.current} → ${loaded.mode}`);
1447
- mode.current = loaded.mode;
1701
+ const loadedMode = loaded.mode ? normalizeModeName(loaded.mode) : null;
1702
+ if (loadedMode && loadedMode !== mode.current) {
1703
+ changes.push(`mode ${mode.current} -> ${loadedMode}`);
1704
+ mode.current = loadedMode;
1448
1705
  }
1449
1706
  // Adopt the resumed session's identity so subsequent
1450
1707
  // autosaves write to its file, not the current session's.
@@ -2802,9 +3059,7 @@ export function handleSlashCommand(input, config, messages, session, mode) {
2802
3059
  case '/palette': {
2803
3060
  const target = args.trim().toLowerCase();
2804
3061
  if (!target) {
2805
- console.log(chalk.dim(` Current palette: ${getPaletteId()}`));
2806
- console.log(chalk.dim(` Run /palettes to see all options.`));
2807
- return { handled: true };
3062
+ return { handled: true, injectPrompt: '__PICK_APPEARANCE__' };
2808
3063
  }
2809
3064
  const resolved = resolvePaletteId(target);
2810
3065
  if (!resolved) {
@@ -3316,6 +3571,16 @@ async function main() {
3316
3571
  permissionMode: config.permissionMode,
3317
3572
  });
3318
3573
  const messages = [];
3574
+ const syncFooter = () => {
3575
+ const snapshot = buildFooterSnapshot(config, mode.current, session, process.cwd());
3576
+ if (!nonInteractive && shouldUseFixedFooter(config)) {
3577
+ activateFooter(snapshot);
3578
+ }
3579
+ else {
3580
+ deactivateFooter();
3581
+ }
3582
+ updateFooter(snapshot);
3583
+ };
3319
3584
  // Session start hook + memory persistence
3320
3585
  await runHooks({ event: 'SessionStart', sessionId: session.id, cwd: process.cwd(), permissionMode: config.permissionMode });
3321
3586
  const memoryContext = onSessionStart(session.id, process.cwd());
@@ -3361,6 +3626,17 @@ async function main() {
3361
3626
  console.log(theme.dim(` Free-tier-safe switch: /openrouter-free`));
3362
3627
  console.log('');
3363
3628
  }
3629
+ syncFooter();
3630
+ startStartupUpdateCheck({
3631
+ onUpdateStarted: (currentVersion, latestVersion) => {
3632
+ deactivateFooter();
3633
+ console.log(theme.info(` Update available: cawdex ${currentVersion} -> ${latestVersion}. Downloading in the background; restart Cawdex to use it.`));
3634
+ syncFooter();
3635
+ },
3636
+ onError: (error) => {
3637
+ dbgEmit('debug', 'update.check.failed', { error: error.slice(0, 200) });
3638
+ },
3639
+ });
3364
3640
  let autoRoute = false;
3365
3641
  // ── F-key hotkey listener ────────────────────────────────
3366
3642
  // Voice / accessibility hotkeys — all on the F-row.
@@ -4202,6 +4478,7 @@ async function main() {
4202
4478
  });
4203
4479
  messages.push({ role: 'user', content: resolvedPrompt.prompt });
4204
4480
  try {
4481
+ await saveWithSnapshot();
4205
4482
  await runQuery({
4206
4483
  config,
4207
4484
  messages,
@@ -4235,10 +4512,12 @@ async function main() {
4235
4512
  process.exit(1);
4236
4513
  }
4237
4514
  }
4515
+ let firstInteractivePrompt = true;
4238
4516
  // Main REPL loop
4239
4517
  while (true) {
4240
4518
  let input;
4241
4519
  try {
4520
+ syncFooter();
4242
4521
  // Screen-reader-aware prompt construction:
4243
4522
  // - The Unicode prompt glyph (❯) gets re-substituted by the
4244
4523
  // symbol→word filter on EVERY readline redraw (one per keystroke),
@@ -4260,7 +4539,9 @@ async function main() {
4260
4539
  // repaints the prompt line on every render — it needs both the
4261
4540
  // styled string (to write with color) and the visible-char
4262
4541
  // length (to position the cursor at end-of-filter).
4263
- const promptStyled = sessionTag + modeTag + theme.prompt(promptGlyph);
4542
+ const promptStyled = isFooterActive()
4543
+ ? theme.prompt('> ')
4544
+ : sessionTag + modeTag + theme.prompt(promptGlyph);
4264
4545
  globalThis
4265
4546
  .__cawdexPromptStyled = promptStyled;
4266
4547
  globalThis
@@ -4276,7 +4557,11 @@ async function main() {
4276
4557
  g.__cawdexQueuedInput = undefined;
4277
4558
  g.__cawdexSlashPrefillInput = undefined;
4278
4559
  const restoredDraft = normalizeTypeaheadDraftForPrompt(queued);
4279
- const prefill = slashPrefill || restoredDraft;
4560
+ const openingPrompt = firstInteractivePrompt && isFooterActive()
4561
+ ? (config.footer?.openingPrompt ?? '/model')
4562
+ : '';
4563
+ firstInteractivePrompt = false;
4564
+ const prefill = slashPrefill || restoredDraft || openingPrompt;
4280
4565
  input = await askWithDecoratedPrompt(rl, sessionTag, modeTag, promptGlyph, prefill);
4281
4566
  }
4282
4567
  catch {
@@ -4342,9 +4627,11 @@ async function main() {
4342
4627
  messages.push(...result.newMessages);
4343
4628
  }
4344
4629
  if (trimmed.startsWith('/config') && !result?.shouldExit) {
4630
+ deactivateFooter();
4345
4631
  config = await setupWizard(rl, config);
4346
4632
  resetClient();
4347
4633
  printThemedBanner(config.provider, config.model, mode.current, config.permissionMode, session.id, ALL_TOOLS.map((t) => t.name));
4634
+ syncFooter();
4348
4635
  continue;
4349
4636
  }
4350
4637
  if (trimmed === '/route') {
@@ -4402,6 +4689,10 @@ async function main() {
4402
4689
  }
4403
4690
  continue;
4404
4691
  }
4692
+ else if (result.injectPrompt === '__PICK_APPEARANCE__') {
4693
+ await runAppearanceWizard(rl, config);
4694
+ continue;
4695
+ }
4405
4696
  else if (result.injectPrompt === '__RESUME_PICK__') {
4406
4697
  const sessions = listSessions();
4407
4698
  if (sessions.length === 0) {
@@ -4466,9 +4757,9 @@ async function main() {
4466
4757
  console.log(chalk.yellow(' /swarm: could not parse swarm payload.'));
4467
4758
  continue;
4468
4759
  }
4469
- let plan = buildSwarmPlan(payload);
4760
+ let plan = buildSwarmPlan(payload, config.swarm);
4470
4761
  if (payload.mode === 'auto') {
4471
- plan = await runSwarmSetupWizard(rl, plan);
4762
+ plan = await runSwarmSetupWizard(rl, plan, config);
4472
4763
  }
4473
4764
  try {
4474
4765
  const agents = resolveAgents(plan.agents);
@@ -4496,6 +4787,7 @@ async function main() {
4496
4787
  else {
4497
4788
  messages.push({ role: 'user', content: result.injectPrompt });
4498
4789
  }
4790
+ await saveWithSnapshot();
4499
4791
  await runQuery({ config, messages, cwd: process.cwd(), rl, sessionId: session.id, mode: mode.current });
4500
4792
  await saveWithSnapshot();
4501
4793
  continue;
@@ -4524,6 +4816,7 @@ async function main() {
4524
4816
  }
4525
4817
  // Add user message and run query
4526
4818
  messages.push({ role: 'user', content: trimmed });
4819
+ await saveWithSnapshot();
4527
4820
  await runQuery({
4528
4821
  config,
4529
4822
  messages,
@@ -4545,12 +4838,14 @@ async function main() {
4545
4838
  await runHooks({ event: 'SessionStop', sessionId: session.id, cwd: process.cwd(), permissionMode: config.permissionMode });
4546
4839
  // Final save
4547
4840
  await saveWithSnapshot();
4841
+ deactivateFooter();
4548
4842
  console.log(chalk.dim(`\nSession saved: ${session.id}`));
4549
4843
  console.log(chalk.dim('Goodbye!\n'));
4550
4844
  rl.close();
4551
4845
  process.exit(0);
4552
4846
  }
4553
4847
  main().catch((err) => {
4848
+ deactivateFooter();
4554
4849
  console.error(chalk.red(`Fatal: ${err.message || err}`));
4555
4850
  process.exit(1);
4556
4851
  });