@semalt-ai/code 1.8.3 → 1.8.5

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/lib/config.js CHANGED
@@ -20,6 +20,7 @@ function _maybeWarnApiKeyAny(cfg) {
20
20
  }
21
21
  if (_LOCAL_HOSTS.has(host)) return;
22
22
  _apiKeyAnyWarned = true;
23
+ // audit: allowed — pre-UI startup warning, fires once before TUI initialises.
23
24
  process.stderr.write(
24
25
  "⚠ api_key='any' against non-local endpoint — requests will likely fail " +
25
26
  "with 401. Run 'semalt-code config set api_key <key>' to set a real key.\n"
@@ -110,6 +111,18 @@ function configSet(key, value) {
110
111
  return cfg;
111
112
  }
112
113
 
114
+ // Resolves whether the active profile uses native function calling.
115
+ // Defaults to true if no profile match is found (matches normalizeConfig
116
+ // default and the agent.js lookup fallback).
117
+ function isNativeToolsActive(model) {
118
+ const cfg = loadConfig();
119
+ if (!Array.isArray(cfg.models)) return true;
120
+ const profile = cfg.models.find(
121
+ (p) => p && p.api_base === cfg.api_base && p.model === model
122
+ );
123
+ return !(profile && profile.native_tools === false);
124
+ }
125
+
113
126
  const REDACTED_KEYS = new Set(['api_key', 'auth_token']);
114
127
 
115
128
  function configShow(systemPromptOverride = null) {
@@ -131,6 +144,7 @@ function configShow(systemPromptOverride = null) {
131
144
  module.exports = {
132
145
  configSet,
133
146
  configShow,
147
+ isNativeToolsActive,
134
148
  loadConfig,
135
149
  normalizeConfig,
136
150
  saveConfig,
package/lib/constants.js CHANGED
@@ -12,7 +12,7 @@ const DEFAULT_CONFIG = {
12
12
  api_key: 'any',
13
13
  dashboard_url: 'https://cli.semalt.ai',
14
14
  auth_token: '',
15
- default_model: 'default',
15
+ default_model: '',
16
16
  dashboard_model_id: null,
17
17
  temperature: 0.7,
18
18
  request_timeout_ms: DEFAULT_API_TIMEOUT_MS,
package/lib/debug.js ADDED
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ // Two mutually-exclusive debug modes, configured once at startup from the
4
+ // CLI flags (--debug or --debug-file <path>).
5
+ //
6
+ // off — no debug output anywhere.
7
+ // simple — visible inline. Basic per-iteration info routed through
8
+ // writer.scrollback so the TUI keeps working (no SSE dumps,
9
+ // no per-chunk noise).
10
+ // file — every debug call (basic AND extended) is written to a file.
11
+ // Nothing debug-related goes to stdout. The TUI stays clean.
12
+ //
13
+ // Two log functions with a clear semantic split:
14
+ //
15
+ // log(line) — "always-on" debug. Visible in simple mode (scrollback)
16
+ // and file mode (file). Silent in off mode.
17
+ // logExtended(line) — extended traces (raw SSE, request bodies, delta
18
+ // accumulators). Visible only in file mode.
19
+ //
20
+ // File-mode lines are formatted as `[ISO-timestamp] <line>\n` so they're
21
+ // greppable and tail-friendly.
22
+
23
+ const fs = require('fs');
24
+
25
+ let mode = 'off';
26
+ let fileStream = null;
27
+
28
+ function init({ debug, debugFile } = {}) {
29
+ if (debug && debugFile) {
30
+ // Belt-and-braces: cli.js (args parser) errors out before this is ever
31
+ // reached. Throw rather than silently coerce so any internal misuse is
32
+ // surfaced loudly.
33
+ throw new Error('debug and debugFile are mutually exclusive');
34
+ }
35
+ if (debugFile) {
36
+ mode = 'file';
37
+ fileStream = fs.createWriteStream(debugFile, { flags: 'a' });
38
+ const ts = new Date().toISOString();
39
+ try {
40
+ fileStream.write(`\n[${ts}] [session] semalt-code debug session start pid=${process.pid}\n`);
41
+ } catch {}
42
+ } else if (debug) {
43
+ mode = 'simple';
44
+ } else {
45
+ mode = 'off';
46
+ }
47
+ }
48
+
49
+ function isActive() { return mode !== 'off'; }
50
+ function isSimple() { return mode === 'simple'; }
51
+ function isFile() { return mode === 'file'; }
52
+ function getMode() { return mode; }
53
+
54
+ function _writeFile(line) {
55
+ if (!fileStream) return;
56
+ const ts = new Date().toISOString();
57
+ try { fileStream.write(`[${ts}] ${line}\n`); } catch {}
58
+ }
59
+
60
+ // "Always-on" debug — visible in simple mode (scrollback) and file mode (file).
61
+ // Silent in off mode. Multi-line input gets one timestamp per line in file mode
62
+ // so each line stays greppable.
63
+ function log(line) {
64
+ if (mode === 'off') return;
65
+ const s = String(line);
66
+ if (mode === 'simple') {
67
+ // Lazy-require to avoid a require cycle: writer pulls in this module
68
+ // for its own drift diagnostic.
69
+ const writer = require('./ui/writer');
70
+ writer.scrollback(s);
71
+ } else {
72
+ for (const l of s.split('\n')) _writeFile(l);
73
+ }
74
+ }
75
+
76
+ // Extended-only debug — visible in file mode only. Used for high-volume
77
+ // per-chunk traces (raw SSE, request body dumps, accumulator state) that
78
+ // would shred the TUI if printed inline.
79
+ function logExtended(line) {
80
+ if (mode !== 'file') return;
81
+ const s = String(line);
82
+ for (const l of s.split('\n')) _writeFile(l);
83
+ }
84
+
85
+ function close() {
86
+ if (fileStream) {
87
+ try {
88
+ const ts = new Date().toISOString();
89
+ fileStream.write(`[${ts}] [session] end pid=${process.pid}\n`);
90
+ fileStream.end();
91
+ } catch {}
92
+ fileStream = null;
93
+ }
94
+ mode = 'off';
95
+ }
96
+
97
+ module.exports = {
98
+ init,
99
+ isActive,
100
+ isSimple,
101
+ isFile,
102
+ getMode,
103
+ log,
104
+ logExtended,
105
+ close,
106
+ };
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const writer = require('./ui/writer');
4
+ const messages = require('./ui/messages');
5
+
3
6
  const TIER_FS = ['read_file', 'write_file', 'append_file', 'delete_file', 'list_dir', 'make_dir', 'move_file', 'copy_file', 'file_stat', 'search_files', 'store_memory', 'recall_memory'];
4
7
  const TIER_EXEC = ['exec'];
5
8
  const TIER_NET = ['http_get', 'download'];
@@ -117,7 +120,7 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
117
120
  if (uiCallbacks) {
118
121
  uiCallbacks.onAddMessage({ role: 'system', content: `✓ Auto-approved: ${description}` });
119
122
  } else {
120
- console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approved: ${description}${RST}`);
123
+ messages.sysSuccess(`Auto-approved: ${description}`);
121
124
  }
122
125
  }
123
126
 
@@ -133,7 +136,7 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
133
136
  }
134
137
 
135
138
  if (!process.stdout.isTTY || !process.stdin.isTTY) {
136
- process.stdout.write(` [non-TTY] Auto-approving: ${description}\n`);
139
+ writer.scrollback(` [non-TTY] Auto-approving: ${description}`);
137
140
  return true;
138
141
  }
139
142
 
@@ -155,13 +158,11 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
155
158
  return true;
156
159
  }
157
160
 
158
- // Fallback: legacy TTY interactive select (used outside of chat UI)
161
+ // Fallback: TTY interactive select (used outside of chat UI)
159
162
  const alwaysLabel = tag ? `Yes, always for <${tag}>` : 'Yes, always';
160
163
  const choices = ['Yes', alwaysLabel, 'No'];
161
164
 
162
- console.log();
163
- console.log(` ${FG_YELLOW}${BOLD}⚠ Permission required${RST}`);
164
- console.log(` ${FG_GRAY}${actionType}: ${description}${RST}`);
165
+ writer.scrollback(`\n ${FG_YELLOW}${BOLD}⚠ Permission required${RST}\n ${FG_GRAY}${actionType}: ${description}${RST}`);
165
166
 
166
167
  const selectedIndex = await interactiveSelect(
167
168
  choices,
@@ -174,13 +175,13 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
174
175
  );
175
176
 
176
177
  if (selectedIndex === null || selectedIndex === 2) {
177
- console.log(` ${FG_RED}✗${RST} ${FG_DARK}Denied${RST}`);
178
+ writer.scrollback(` ${FG_RED}✗${RST} ${FG_DARK}Denied${RST}`);
178
179
  return false;
179
180
  }
180
181
 
181
182
  if (selectedIndex === 1 && tag) {
182
183
  state.sessionApprovedTags.add(tag);
183
- console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approve enabled for <${tag}> this session${RST}`);
184
+ writer.scrollback(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approve enabled for <${tag}> this session${RST}`);
184
185
  }
185
186
 
186
187
  return true;
package/lib/proc.js ADDED
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ const dbg = require('./debug');
4
+
5
+ // Platform-aware subprocess spawn + tree-kill helpers.
6
+ //
7
+ // Why this module exists: when a child is started with `shell: true`, the
8
+ // PID Node hands back is the shell wrapper (`sh -c "..."` on POSIX, `cmd.exe
9
+ // /c "..."` on Windows). Calling `child.kill()` kills the wrapper, but its
10
+ // descendants (the actual `find`, `grep`, `bash` pipeline) become orphans
11
+ // and keep running. To abort cleanly we have to kill the whole process tree.
12
+ //
13
+ // Constraint from the project: no other file imports `process.kill` or
14
+ // `child.kill` directly — those calls live here. `tools.js` (and any future
15
+ // caller) only knows about `spawnWithGroup` and `killTreeEscalating`.
16
+
17
+ const isWindows = process.platform === 'win32';
18
+
19
+ // Wrap `child_process.spawn` so the resulting child is addressable as a
20
+ // process group. POSIX: `detached: true` makes the child a process-group
21
+ // leader, so `process.kill(-pid, sig)` reaches all descendants. Windows:
22
+ // taskkill /T walks the PID hierarchy itself, so `detached` is unnecessary
23
+ // and actively harmful — it would spawn the child in a new console window.
24
+ function spawnWithGroup(spawn, command, args, opts = {}) {
25
+ const finalOpts = { ...opts };
26
+ if (!isWindows) finalOpts.detached = true;
27
+ return spawn(command, args, finalOpts);
28
+ }
29
+
30
+ function killTree(child, signal) {
31
+ if (!child || child.killed || child.exitCode !== null || child.pid == null) return;
32
+ if (isWindows) {
33
+ // taskkill /T = traverse children, /F = force. windowsHide prevents the
34
+ // brief CMD window flash. Fire and forget — taskkill exits on its own
35
+ // and we don't care about its result code (the child's `exit` event is
36
+ // the authoritative signal).
37
+ const { spawn } = require('child_process');
38
+ try {
39
+ const args = ['/PID', String(child.pid), '/T'];
40
+ if (signal === 'SIGKILL') args.push('/F');
41
+ const tk = spawn('taskkill', args, { windowsHide: true, stdio: 'ignore' });
42
+ tk.on('error', () => {});
43
+ tk.unref();
44
+ } catch {
45
+ // taskkill failed to launch (PID already gone, or taskkill missing on
46
+ // a stripped-down Windows image). The child's exit event will still
47
+ // fire if the process is gone; nothing else to do here.
48
+ }
49
+ } else {
50
+ try {
51
+ // Negative PID = whole process group. Requires detached:true at spawn.
52
+ process.kill(-child.pid, signal || 'SIGTERM');
53
+ } catch (err) {
54
+ // ESRCH = process group already gone. Anything else is unexpected but
55
+ // not fatal — surface only when debug is active for triage.
56
+ if (err.code !== 'ESRCH') {
57
+ dbg.log(`[killTree] kill failed: ${err.code} ${err.message}`);
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ // Send SIGTERM (or taskkill graceful), wait 2s, escalate to SIGKILL (or
64
+ // taskkill /F) if the tree didn't exit. Hard-coded 2s grace per the abort
65
+ // requirements — long enough for well-behaved children to clean up, short
66
+ // enough that a stuck `trap "" TERM` process doesn't tie up the agent.
67
+ function killTreeEscalating(child) {
68
+ killTree(child, 'SIGTERM');
69
+ const escalation = setTimeout(() => {
70
+ if (child.exitCode === null && !child.killed) killTree(child, 'SIGKILL');
71
+ }, 2000);
72
+ // Don't keep the event loop alive solely for the escalation timer; if the
73
+ // process exits naturally first, the `once('exit')` listener clears it.
74
+ escalation.unref();
75
+ child.once('exit', () => clearTimeout(escalation));
76
+ }
77
+
78
+ // Future Windows-enablement notes:
79
+ // - Job objects (CreateJobObject API via a native binding) give stronger
80
+ // tree-kill guarantees than taskkill, especially for grandchild
81
+ // processes that detach themselves. Consider migrating if taskkill
82
+ // proves unreliable for nested children.
83
+ // - Windows has no SIGTERM/SIGKILL distinction at the OS level for
84
+ // spawned processes. taskkill (without /F) attempts WM_CLOSE-style
85
+ // graceful close; /F is a hard terminate. The 2s escalation here maps
86
+ // to "graceful taskkill, then forceful taskkill" — same shape as POSIX.
87
+ // - shell: true on Windows uses cmd.exe by default. Cross-platform
88
+ // command translation (find, grep, etc.) is the tool layer's problem,
89
+ // not this module's.
90
+
91
+ module.exports = {
92
+ spawnWithGroup,
93
+ killTree,
94
+ killTreeEscalating,
95
+ isWindows,
96
+ };
package/lib/prompts.js CHANGED
@@ -9,6 +9,7 @@ const WRAPPER_NAMES = new Set([
9
9
  'parameter',
10
10
  'tool_call',
11
11
  'function_call',
12
+ 'function',
12
13
  ]);
13
14
 
14
15
  // For each tool tag: required attributes and a one-line purpose.
@@ -32,7 +33,7 @@ const TOOL_TAG_SPECS = {
32
33
  edit_file: { attrs: ['path', 'line'], purpose: 'Replace a single line in a file (inline content = new line).' },
33
34
  search_files: { attrs: ['pattern?', 'dir?'], purpose: 'Find files by glob pattern.' },
34
35
  search_in_file: { attrs: ['path'], purpose: 'Regex search inside a file (inline content = pattern).' },
35
- replace_in_file: { attrs: ['path', 'search', 'replace'], purpose: 'Regex replace inside a file.' },
36
+ replace_in_file: { attrs: ['path', 'search', 'replace'], purpose: 'Regex replace inside a file; inline content is interpreted as regex flags (e.g. g, i, gi).' },
36
37
  get_env: { attrs: [], purpose: 'Read an env var (inline content = name).' },
37
38
  set_env: { attrs: ['name', 'value'], purpose: 'Set an env var for this process.' },
38
39
  download: { attrs: [], purpose: 'HTTP download to the CWD (inline content = URL).' },
@@ -80,8 +81,8 @@ ${TAG_INVENTORY}
80
81
  ## Reasoning vs planning — IMPORTANT:
81
82
 
82
83
  - Your internal chain-of-thought reasoning uses your native \`<think>...</think>\` block. Use it normally for deliberation. Do NOT treat \`<think>\` as a user-facing tool and do NOT try to emit \`<think>\` as an action — it is reserved for your own reasoning and is handled by the runtime.
83
- - When you need to explicitly record a short plan that the agent framework can see (for logging or hand-off between steps), use \`<plan>...</plan>\` instead. \`<plan>\` is a tool tag; \`<think>\` is not.
84
- - Never emit \`<think>\` as an action. The valid action tags are the ones listed above.
84
+ - When you need to explicitly record a short plan that the agent framework can see (for logging or hand-off between steps), use \`<plan>...</plan>\` instead. Both \`<think>\` and \`<plan>\` are display-only tags handled by the runtime — never emit either as an action.
85
+ - The valid action tags are the ones listed above.
85
86
 
86
87
  ## STRICT RULES — follow exactly:
87
88
 
@@ -94,12 +95,9 @@ ${TAG_INVENTORY}
94
95
  7. Be concise. Provide working solutions. Use markdown for code blocks in explanations.
95
96
  8. Current working directory: __CWD__
96
97
 
97
- Response contract (strict):
98
- Every response must end with exactly one of:
99
- (a) a tool call, using one of the XML tags listed above; OR
100
- (b) <final_answer>...</final_answer> containing your answer to the user.
101
- A response containing neither is invalid and will be rejected.
102
- Do not describe actions in prose outside these two forms. Either call the tool, or wrap your final reply in <final_answer>.`;
98
+ Response contract:
99
+ - If the task requires an action, emit the appropriate tool tag(s) — do not narrate intended actions in prose without the tag.
100
+ - If the task is complete or the user's question can be answered directly without running a tool, reply in plain prose. No special wrapper is needed.`;
103
101
 
104
102
  const NATIVE_SYSTEM_PROMPT_TEMPLATE = `You are Semalt.AI, an expert AI coding assistant running in the user's terminal. Use the provided tools to execute shell commands and file operations; do not just print instructions. Each call is approved by the user before execution, and the result is returned to you for the next step.
105
103
 
@@ -107,7 +105,7 @@ Use \`<think>...</think>\` for internal reasoning (runtime-handled; never emit a
107
105
 
108
106
  Be concise. Use markdown for code blocks in explanations. Current working directory: __CWD__
109
107
 
110
- Response contract: every response must end with either (a) one or more tool calls, or (b) <final_answer>...</final_answer> containing your answer to the user. Prose outside those two forms is invalid.`;
108
+ Response contract: if the task requires an action, emit one or more tool calls do not narrate intended actions in prose without the tool call. Otherwise, answer in plain prose; no special wrapper is needed.`;
111
109
 
112
110
  function getSystemPrompt(nativeTools = false) {
113
111
  const template = nativeTools ? NATIVE_SYSTEM_PROMPT_TEMPLATE : SYSTEM_PROMPT_TEMPLATE;
package/lib/tool_specs.js CHANGED
@@ -75,9 +75,10 @@ const TOOL_SPECS = {
75
75
  content: {
76
76
  type: 'string',
77
77
  description: 'Full UTF-8 text to write as the new file contents',
78
+ default: '',
78
79
  },
79
80
  },
80
- required: ['path', 'content'],
81
+ required: ['path'],
81
82
  },
82
83
  },
83
84
 
@@ -93,9 +94,10 @@ const TOOL_SPECS = {
93
94
  content: {
94
95
  type: 'string',
95
96
  description: 'Full UTF-8 text to write as the initial file contents',
97
+ default: '',
96
98
  },
97
99
  },
98
- required: ['path', 'content'],
100
+ required: ['path'],
99
101
  },
100
102
  },
101
103
 
@@ -111,9 +113,10 @@ const TOOL_SPECS = {
111
113
  content: {
112
114
  type: 'string',
113
115
  description: 'UTF-8 text to append to the end of the file',
116
+ default: '',
114
117
  },
115
118
  },
116
- required: ['path', 'content'],
119
+ required: ['path'],
117
120
  },
118
121
  },
119
122
 
@@ -241,9 +244,10 @@ const TOOL_SPECS = {
241
244
  content: {
242
245
  type: 'string',
243
246
  description: 'New text for the target line; trailing newline is added automatically when the file is rejoined',
247
+ default: '',
244
248
  },
245
249
  },
246
- required: ['path', 'line', 'content'],
250
+ required: ['path', 'line'],
247
251
  },
248
252
  },
249
253
 
@@ -255,6 +259,7 @@ const TOOL_SPECS = {
255
259
  pattern: {
256
260
  type: 'string',
257
261
  description: 'Glob pattern such as "*.ts" or "src/**/*.js"; matches against the basename when the pattern contains no slash, otherwise against the relative path',
262
+ default: '*',
258
263
  },
259
264
  dir: {
260
265
  type: 'string',
@@ -262,7 +267,7 @@ const TOOL_SPECS = {
262
267
  default: '.',
263
268
  },
264
269
  },
265
- required: ['pattern'],
270
+ required: [],
266
271
  },
267
272
  },
268
273
 
@@ -300,6 +305,7 @@ const TOOL_SPECS = {
300
305
  replace: {
301
306
  type: 'string',
302
307
  description: 'Replacement string; supports the standard $1, $2, $& back-references',
308
+ default: '',
303
309
  },
304
310
  flags: {
305
311
  type: 'string',
@@ -307,7 +313,7 @@ const TOOL_SPECS = {
307
313
  default: '',
308
314
  },
309
315
  },
310
- required: ['path', 'search', 'replace'],
316
+ required: ['path', 'search'],
311
317
  },
312
318
  },
313
319
 
@@ -370,9 +376,10 @@ const TOOL_SPECS = {
370
376
  content: {
371
377
  type: 'string',
372
378
  description: 'Base64-encoded payload to decode and write as the file contents',
379
+ default: '',
373
380
  },
374
381
  },
375
- required: ['path', 'content'],
382
+ required: ['path'],
376
383
  },
377
384
  },
378
385