sapper-iq 1.4.6 → 1.4.7

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 (2) hide show
  1. package/package.json +1 -1
  2. package/sapper.mjs +77 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "AI-powered development assistant that executes commands and builds projects",
5
5
  "main": "sapper.mjs",
6
6
  "bin": {
package/sapper.mjs CHANGED
@@ -1171,6 +1171,7 @@ const DEFAULT_CONFIG = Object.freeze({
1171
1171
  tools: ['read', 'read_chunk', 'list', 'search', 'grep', 'find', 'regex', 'head', 'tail', 'cat', 'pwd', 'changes', 'fetch_web', 'recall_memory', 'search_memory_notes', 'read_memory_notes'], // Read-only subset by default — no write / patch / shell / open / mkdir / rmdir.
1172
1172
  saveTranscripts: true, // Persist every consultation to disk for audit.
1173
1173
  transcriptDir: '.sapper/consultations', // Where transcripts are written.
1174
+ verbose: true, // Print full request, each tool call/result, and final answer to the terminal during the consultation.
1174
1175
  systemPrompt: '', // Override the built-in consultant system prompt (empty = use default).
1175
1176
  }),
1176
1177
  prompt: Object.freeze({
@@ -1591,6 +1592,7 @@ function normalizeConsultantConfig(consultantConfig = {}) {
1591
1592
  transcriptDir: typeof consultantConfig.transcriptDir === 'string' && consultantConfig.transcriptDir.trim()
1592
1593
  ? consultantConfig.transcriptDir.trim()
1593
1594
  : D.transcriptDir,
1595
+ verbose: normalizeBoolean(consultantConfig.verbose, D.verbose),
1594
1596
  systemPrompt: typeof consultantConfig.systemPrompt === 'string' ? consultantConfig.systemPrompt : D.systemPrompt,
1595
1597
  };
1596
1598
  }
@@ -5901,6 +5903,34 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5901
5903
  'Consulting expert', 'magenta'
5902
5904
  ));
5903
5905
 
5906
+ const verbose = cfg.verbose !== false;
5907
+ const dim = chalk.gray;
5908
+ const accent = chalk.hex('#b7b9ff'); // magenta tone
5909
+ const log = (msg) => { if (verbose) console.log(msg); };
5910
+ const logBlock = (label, body, max = 600) => {
5911
+ if (!verbose) return;
5912
+ const text = String(body ?? '').trim();
5913
+ if (!text) { console.log(`${accent('[consult]')} ${dim(label + ': (empty)')}`); return; }
5914
+ const truncated = text.length > max ? text.slice(0, max) + dim(`\n... (${text.length - max} more chars)`) : text;
5915
+ console.log(`${accent('[consult]')} ${chalk.white(label)}:\n${dim(' ' + truncated.split('\\n').join('\n '))}`);
5916
+ };
5917
+
5918
+ // Show what we are about to send to the consultant
5919
+ log(`${accent('[consult]')} ${chalk.white('request')} ${dim('to')} ${chalk.white(cfg.model)}`);
5920
+ logBlock('goal', goal);
5921
+ logBlock('question', question);
5922
+ logBlock('summary', summaryText, 800);
5923
+ if (attempts) logBlock('attempts', attempts);
5924
+ if (hints) logBlock('hints', hints);
5925
+ if (attachments.length > 0) {
5926
+ log(`${accent('[consult]')} ${chalk.white('files')} ${dim(`(${attachments.length}, ${formatBytes(totalBytes)})`)}:`);
5927
+ for (const a of attachments) {
5928
+ log(` ${a.error ? chalk.red('!') : chalk.green('+')} ${chalk.white(a.path)}${a.error ? dim(` -- ${a.error}`) : ''}`);
5929
+ }
5930
+ } else {
5931
+ log(`${accent('[consult]')} ${dim('files: none attached \u2014 consultant must rely on its own tools')}`);
5932
+ }
5933
+
5904
5934
  const consultantSpinner = ora(chalk.magenta(`Consultant (${cfg.model.split(':')[0]}) is thinking...`)).start();
5905
5935
  let finalAnswer = '';
5906
5936
  let rounds = 0;
@@ -5908,6 +5938,7 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5908
5938
  while (true) {
5909
5939
  if (Date.now() > deadline) {
5910
5940
  consultantSpinner.stop();
5941
+ log(`${chalk.red('[consult]')} timeout hit at ${Math.round((Date.now() - startedAt) / 1000)}s`);
5911
5942
  return `Consultant aborted: hit timeout of ${Math.round(cfg.timeoutMs / 1000)}s before finishing.\n\nPartial answer:\n${finalAnswer || '(none produced yet)'}`;
5912
5943
  }
5913
5944
 
@@ -5923,12 +5954,13 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5923
5954
  if (cfg.thinking === 'on') chatOpts.think = true;
5924
5955
  if (useTools && consultantNativeTools.length) chatOpts.tools = consultantNativeTools;
5925
5956
 
5957
+ const roundStart = Date.now();
5926
5958
  let resp;
5927
5959
  try {
5928
5960
  resp = await ollama.chat(chatOpts);
5929
5961
  } catch (err) {
5930
- const msg = err?.message || String(err);
5931
- if (/does not support thinking/i.test(msg) && chatOpts.think) {
5962
+ const errMsg = err?.message || String(err);
5963
+ if (/does not support thinking/i.test(errMsg) && chatOpts.think) {
5932
5964
  delete chatOpts.think;
5933
5965
  resp = await ollama.chat(chatOpts);
5934
5966
  } else {
@@ -5938,7 +5970,16 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5938
5970
 
5939
5971
  const msg = resp?.message || {};
5940
5972
  const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
5941
- finalAnswer = msg.content || finalAnswer;
5973
+ const replyContent = msg.content || '';
5974
+ const replyThinking = msg.thinking || '';
5975
+ finalAnswer = replyContent || finalAnswer;
5976
+
5977
+ consultantSpinner.stop();
5978
+ if (replyThinking) logBlock(`round ${rounds} thinking (${Math.round((Date.now() - roundStart) / 1000)}s)`, replyThinking, 400);
5979
+ if (replyContent) logBlock(`round ${rounds} content`, replyContent, 1200);
5980
+ if (toolCalls.length > 0) {
5981
+ log(`${accent('[consult]')} ${chalk.white(`round ${rounds} \u2192 ${toolCalls.length} tool call${toolCalls.length === 1 ? '' : 's'}`)}`);
5982
+ }
5942
5983
 
5943
5984
  if (toolCalls.length === 0 || rounds >= cfg.toolRoundLimit) {
5944
5985
  break;
@@ -5947,31 +5988,49 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5947
5988
  // Push assistant message with tool_calls
5948
5989
  consultMessages.push({ role: 'assistant', content: msg.content || '', tool_calls: toolCalls });
5949
5990
  rounds++;
5950
- consultantSpinner.text = chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`);
5991
+ consultantSpinner.start(chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`));
5951
5992
 
5952
5993
  for (const tc of toolCalls) {
5953
5994
  const fn = tc.function || {};
5954
5995
  const args = fn.arguments || {};
5955
5996
  const mapped = normalizeToolName(fn.name || '');
5997
+ const argPreview = (() => {
5998
+ try {
5999
+ const keys = Object.keys(args || {});
6000
+ if (keys.length === 0) return '';
6001
+ return keys.map(k => `${k}=${ellipsis(String(args[k] ?? ''), 60)}`).join(', ');
6002
+ } catch { return ''; }
6003
+ })();
6004
+ consultantSpinner.stop();
6005
+ log(` ${accent('\u2192')} ${chalk.white(fn.name)}${argPreview ? dim('(' + argPreview + ')') : ''}`);
5956
6006
  if (!allowedSet.has(mapped)) {
6007
+ log(` ${chalk.red('blocked')} ${dim('not in allowed tools')}`);
5957
6008
  consultMessages.push({ role: 'tool', tool_name: fn.name, content: `Tool ${fn.name} is not allowed for the consultant (read-only restriction).` });
6009
+ consultantSpinner.start(chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`));
5958
6010
  continue;
5959
6011
  }
5960
6012
  let toolResult;
6013
+ const tStart = Date.now();
5961
6014
  try {
5962
6015
  toolResult = await runConsultantToolCall(fn.name, args);
5963
6016
  } catch (err) {
5964
6017
  toolResult = `Error: ${err.message}`;
5965
6018
  }
6019
+ const tMs = Date.now() - tStart;
6020
+ const resultStr = String(toolResult ?? '');
6021
+ const isErr = /^error/i.test(resultStr.trim());
6022
+ log(` ${isErr ? chalk.red('err') : chalk.green('ok')} ${dim(`${tMs}ms, ${resultStr.length} chars`)}: ${dim(ellipsis(resultStr.replace(/\s+/g, ' '), 200))}`);
5966
6023
  consultMessages.push({
5967
6024
  role: 'tool',
5968
6025
  tool_name: fn.name,
5969
- content: truncateToolText(String(toolResult ?? ''), 16000),
6026
+ content: truncateToolText(resultStr, 16000),
5970
6027
  });
6028
+ consultantSpinner.start(chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`));
5971
6029
  }
5972
6030
  }
5973
6031
  } catch (err) {
5974
6032
  consultantSpinner.stop();
6033
+ log(`${chalk.red('[consult]')} error during consultation: ${err.message}`);
5975
6034
  return `Error during consultation: ${err.message}`;
5976
6035
  }
5977
6036
  consultantSpinner.stop();
@@ -5979,6 +6038,11 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5979
6038
  const elapsed = Math.round((Date.now() - startedAt) / 1000);
5980
6039
  const answer = (finalAnswer || '').trim() || '(consultant returned no content)';
5981
6040
 
6041
+ // Final summary box
6042
+ log('');
6043
+ log(`${accent('[consult]')} ${chalk.white('final answer')} ${dim(`(${elapsed}s, ${rounds} tool round${rounds === 1 ? '' : 's'})`)}:`);
6044
+ logBlock('answer', answer, 2000);
6045
+
5982
6046
  // Save transcript for audit
5983
6047
  const transcriptBody = [
5984
6048
  `# Consultation Transcript`,
@@ -8283,13 +8347,18 @@ async function runSapper() {
8283
8347
  }
8284
8348
  if (subcommand === 'transcripts' || subcommand === 'transcript') {
8285
8349
  if (['on', 'true', 'yes'].includes(value.toLowerCase())) { updateConsultant({ saveTranscripts: true }); console.log(chalk.green('Consultant transcripts: on')); continue; }
8286
- if (['off', 'false', 'no'].includes(value.toLowerCase())) { updateConsultant({ saveTranscripts: false }); console.log(chalk.yellow('Consultant transcripts: off')); continue; }
8287
- console.log(chalk.yellow('Usage: /consult transcripts on|off'));
8350
+ if (['off', 'false', 'no'].includes(value.toLowerCase())) { updateConsultant({ saveTranscripts: false }); console.log(chalk.yellow('Consultant transcripts: off')); continue; } console.log(chalk.yellow('Usage: /consult transcripts on|off'));
8351
+ continue;
8352
+ }
8353
+ if (subcommand === 'verbose' || subcommand === 'log' || subcommand === 'logging') {
8354
+ if (['on', 'true', 'yes', '1'].includes(value.toLowerCase())) { updateConsultant({ verbose: true }); console.log(chalk.green('Consultant verbose logging: on')); continue; }
8355
+ if (['off', 'false', 'no', '0'].includes(value.toLowerCase())) { updateConsultant({ verbose: false }); console.log(chalk.yellow('Consultant verbose logging: off')); continue; }
8356
+ console.log(chalk.yellow('Usage: /consult verbose on|off'));
8288
8357
  continue;
8289
8358
  }
8290
8359
 
8291
8360
  console.log(chalk.yellow(`Unknown consult option: ${subcommand}`));
8292
- console.log(chalk.gray(' Usage: /consult | /consult model <name> | /consult on|off | /consult tools <a,b,c> | /consult minwords <N> | /consult rounds <N> | /consult timeout <secs> | /consult thinking <auto|on|off> | /consult temp <0..2> | /consult transcripts on|off | /consult reset'));
8361
+ console.log(chalk.gray(' Usage: /consult | /consult model <name> | /consult on|off | /consult tools <a,b,c> | /consult minwords <N> | /consult rounds <N> | /consult timeout <secs> | /consult thinking <auto|on|off> | /consult temp <0..2> | /consult transcripts on|off | /consult verbose on|off | /consult reset'));
8293
8362
  continue;
8294
8363
  }
8295
8364