@semalt-ai/code 1.8.5 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude/settings.local.json +6 -1
  2. package/.github/workflows/ci.yml +69 -0
  3. package/CLAUDE.md +1584 -26
  4. package/README.md +147 -3
  5. package/examples/embed.js +74 -0
  6. package/index.js +251 -10
  7. package/lib/agent.js +711 -104
  8. package/lib/api.js +213 -49
  9. package/lib/args.js +74 -2
  10. package/lib/audit.js +23 -1
  11. package/lib/background.js +584 -0
  12. package/lib/checkpoints.js +757 -0
  13. package/lib/commands/auth.js +94 -0
  14. package/lib/commands/chat-session.js +306 -0
  15. package/lib/commands/chat-slash.js +399 -0
  16. package/lib/commands/chat-turn.js +446 -0
  17. package/lib/commands/chat.js +403 -0
  18. package/lib/commands/custom.js +157 -0
  19. package/lib/commands/history-utils.js +66 -0
  20. package/lib/commands/index.js +268 -0
  21. package/lib/commands/mcp.js +113 -0
  22. package/lib/commands/oneshot.js +193 -0
  23. package/lib/commands/registry.js +269 -0
  24. package/lib/commands/tasks.js +89 -0
  25. package/lib/compact.js +87 -0
  26. package/lib/config.js +333 -11
  27. package/lib/constants.js +372 -3
  28. package/lib/deny.js +199 -0
  29. package/lib/doctor.js +160 -0
  30. package/lib/headless.js +167 -0
  31. package/lib/hooks.js +286 -0
  32. package/lib/images.js +264 -0
  33. package/lib/internals.js +49 -0
  34. package/lib/mcp/boundary.js +131 -0
  35. package/lib/mcp/client.js +270 -0
  36. package/lib/mcp/oauth.js +134 -0
  37. package/lib/memory.js +209 -0
  38. package/lib/metrics.js +37 -2
  39. package/lib/payload.js +54 -0
  40. package/lib/permission-rules.js +401 -0
  41. package/lib/permissions.js +100 -10
  42. package/lib/pricing.js +67 -0
  43. package/lib/proc.js +62 -0
  44. package/lib/prompts.js +84 -5
  45. package/lib/sandbox.js +568 -0
  46. package/lib/sdk.js +328 -0
  47. package/lib/secrets.js +211 -0
  48. package/lib/skills.js +223 -0
  49. package/lib/subagents.js +516 -0
  50. package/lib/tool_registry.js +2558 -0
  51. package/lib/tool_specs.js +222 -2
  52. package/lib/tools.js +272 -1020
  53. package/lib/ui/format.js +22 -1
  54. package/lib/ui/input-field.js +16 -7
  55. package/lib/ui/status-bar.js +79 -11
  56. package/lib/ui/theme.js +1 -0
  57. package/lib/ui/web-activity.js +218 -0
  58. package/lib/verify.js +229 -0
  59. package/lib/web-extract.js +213 -0
  60. package/lib/web-summarize.js +68 -0
  61. package/package.json +19 -4
  62. package/scripts/lint.js +57 -0
  63. package/test/agent-loop.test.js +389 -0
  64. package/test/background.test.js +414 -0
  65. package/test/chat.test.js +114 -0
  66. package/test/checkpoints-agent.test.js +181 -0
  67. package/test/checkpoints.test.js +650 -0
  68. package/test/command-registry.test.js +160 -0
  69. package/test/compact.test.js +116 -0
  70. package/test/completion-lazy.test.js +52 -0
  71. package/test/config-merge.test.js +324 -0
  72. package/test/config-quarantine.test.js +128 -0
  73. package/test/config-write-guard-allow-anywhere.test.js +56 -0
  74. package/test/config-write-guard-skip.test.js +46 -0
  75. package/test/config-write-guard.test.js +153 -0
  76. package/test/context-split.test.js +215 -0
  77. package/test/cost-doctor.test.js +142 -0
  78. package/test/custom-commands-chat.test.js +106 -0
  79. package/test/custom-commands.test.js +230 -0
  80. package/test/deny-windows.test.js +120 -0
  81. package/test/deny.test.js +83 -0
  82. package/test/download-allow-anywhere.test.js +66 -0
  83. package/test/download-confine.test.js +153 -0
  84. package/test/executors.test.js +362 -0
  85. package/test/extract-tool-calls.test.js +315 -0
  86. package/test/fetch-url-validation.test.js +219 -0
  87. package/test/fixtures/tool-calls.js +57 -0
  88. package/test/fixtures/web-page.js +91 -0
  89. package/test/git-tools.test.js +384 -0
  90. package/test/grep-glob-serialize.test.js +242 -0
  91. package/test/grep-glob.test.js +268 -0
  92. package/test/harness/README.md +57 -0
  93. package/test/harness/chat-harness.js +142 -0
  94. package/test/harness/memwarn-headless-child.js +65 -0
  95. package/test/harness/mock-llm.js +120 -0
  96. package/test/harness/mock-mcp-server.js +142 -0
  97. package/test/harness/sse-server.js +69 -0
  98. package/test/headless.test.js +203 -0
  99. package/test/history-utils.test.js +88 -0
  100. package/test/hooks-agent.test.js +238 -0
  101. package/test/hooks-verify-sandbox.test.js +232 -0
  102. package/test/hooks.test.js +216 -0
  103. package/test/http-get-user-agent.test.js +142 -0
  104. package/test/images-api.test.js +208 -0
  105. package/test/images.test.js +238 -0
  106. package/test/max-iterations.test.js +216 -0
  107. package/test/mcp-boundary.test.js +57 -0
  108. package/test/mcp-client.test.js +267 -0
  109. package/test/mcp-oauth.test.js +86 -0
  110. package/test/memory-truncation-warning.test.js +222 -0
  111. package/test/memory.test.js +198 -0
  112. package/test/native-dispatch.test.js +356 -0
  113. package/test/output-chokepoint.test.js +188 -0
  114. package/test/path-guards.test.js +134 -0
  115. package/test/payload.test.js +99 -0
  116. package/test/permission-rules-agent.test.js +210 -0
  117. package/test/permission-rules.test.js +297 -0
  118. package/test/permissions.test.js +163 -0
  119. package/test/plan-mode.test.js +167 -0
  120. package/test/read-paginate.test.js +275 -0
  121. package/test/readonly-tools.test.js +177 -0
  122. package/test/result-cap.test.js +233 -0
  123. package/test/sandbox-agent.test.js +147 -0
  124. package/test/sandbox-integration.test.js +216 -0
  125. package/test/sandbox.test.js +408 -0
  126. package/test/sdk.test.js +234 -0
  127. package/test/shell-output-cap.test.js +181 -0
  128. package/test/skills-chat.test.js +110 -0
  129. package/test/skills.test.js +295 -0
  130. package/test/smoke.test.js +68 -0
  131. package/test/status-bar-pause.test.js +164 -0
  132. package/test/stream-parser.test.js +147 -0
  133. package/test/subagents-agent.test.js +178 -0
  134. package/test/subagents.test.js +222 -0
  135. package/test/tool-registry.test.js +85 -0
  136. package/test/trim-budget.test.js +101 -0
  137. package/test/verify-agent.test.js +317 -0
  138. package/test/verify.test.js +141 -0
  139. package/test/web-activity-ordering.test.js +194 -0
  140. package/test/web-activity.test.js +207 -0
  141. package/test/web-data-extraction-guidance.test.js +71 -0
  142. package/test/web-extract.test.js +185 -0
  143. package/test/web-fetch-agent.test.js +291 -0
  144. package/test/web-fetch-mode.test.js +193 -0
  145. package/test/web-search.test.js +380 -0
  146. package/lib/commands.js +0 -1438
package/lib/skills.js ADDED
@@ -0,0 +1,223 @@
1
+ 'use strict';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Skills — reusable methodology with progressive disclosure (Task 3.5)
5
+ // ---------------------------------------------------------------------------
6
+ //
7
+ // A skill is a folder containing a SKILL.md (frontmatter `name`/`description` +
8
+ // a Markdown body) and, optionally, assets/scripts alongside it. Skills package
9
+ // reusable methodology the agent can pull in on demand.
10
+ //
11
+ // PROGRESSIVE DISCLOSURE is the whole point of the feature: only each skill's
12
+ // name + description is ever injected into the system prompt. The body — the
13
+ // actual instructions, which can be large — is loaded into context ONLY when
14
+ // the skill is invoked (the agent choosing it, or the user running its
15
+ // `/<skill>` slash command). `discoverSkills` therefore returns metadata only
16
+ // (no body); `loadSkillBody` reads the body on demand. Keeping the body out of
17
+ // the always-resident structures is what keeps the prompt from bloating.
18
+ //
19
+ // Discovery mirrors project memory (Task 2.3) and custom commands (Task 3.1):
20
+ // 1. global ~/.semalt-ai/skills/<skill>/SKILL.md
21
+ // 2. project <nearest>/.semalt/skills/<skill>/SKILL.md (upward from cwd,
22
+ // bounded by the repo root — the .git holder)
23
+ // On a name collision project wins over global. The total metadata size is
24
+ // bounded; oversized metadata is truncated with a visible notice. With no
25
+ // skills present the system-prompt block is '' so the prompt is unchanged.
26
+
27
+ const fs = require('fs');
28
+ const os = require('os');
29
+ const path = require('path');
30
+
31
+ // Skills metadata is name + one-line description per skill, so it stays small.
32
+ // 16 KB comfortably holds dozens of skills yet keeps the prompt lean.
33
+ const DEFAULT_SKILLS_MAX_BYTES = 16 * 1024;
34
+
35
+ const SKILL_FILE = 'SKILL.md';
36
+
37
+ // Walk up from startDir for the nearest `.semalt/skills` directory, bounded by
38
+ // the repo root (the directory holding `.git` is the last one checked). Mirrors
39
+ // findProjectCommandsDir (Task 3.1). Returns the directory path or null.
40
+ function findProjectSkillsDir(startDir) {
41
+ let dir = path.resolve(startDir);
42
+ while (true) {
43
+ const candidate = path.join(dir, '.semalt', 'skills');
44
+ try { if (fs.statSync(candidate).isDirectory()) return candidate; } catch {}
45
+ let atRepoRoot = false;
46
+ try { atRepoRoot = fs.existsSync(path.join(dir, '.git')); } catch {}
47
+ if (atRepoRoot) break;
48
+ const parent = path.dirname(dir);
49
+ if (parent === dir) break; // filesystem root
50
+ dir = parent;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ // Parse optional `---`-delimited frontmatter at the top of a SKILL.md. Only
56
+ // `name` and `description` are recognized; unknown keys are ignored. Returns
57
+ // { meta, body }. With no frontmatter the whole text is the body. Pure.
58
+ function parseSkillFrontmatter(text) {
59
+ const meta = { name: '', description: '' };
60
+ if (typeof text !== 'string') return { meta, body: '' };
61
+ const src = text.replace(/^/, '').replace(/\r\n/g, '\n');
62
+ const m = /^---\n([\s\S]*?)\n---[ \t]*\n?/.exec(src);
63
+ if (!m) return { meta, body: src };
64
+ const body = src.slice(m[0].length);
65
+ for (const rawLine of m[1].split('\n')) {
66
+ const line = rawLine.trim();
67
+ if (!line || line.startsWith('#')) continue;
68
+ const idx = line.indexOf(':');
69
+ if (idx < 0) continue;
70
+ const key = line.slice(0, idx).trim().toLowerCase();
71
+ const val = line.slice(idx + 1).trim().replace(/^['"]|['"]$/g, '');
72
+ if (key === 'name') meta.name = val;
73
+ else if (key === 'description') meta.description = val;
74
+ }
75
+ return { meta, body };
76
+ }
77
+
78
+ // Turn a skill-folder basename into a slash-command-safe slug: lowercase, spaces
79
+ // and underscores → hyphens, drop anything else. Used as the invocation token.
80
+ function _slugify(s) {
81
+ return String(s || '')
82
+ .trim()
83
+ .toLowerCase()
84
+ .replace(/[\s_]+/g, '-')
85
+ .replace(/[^a-z0-9-]/g, '')
86
+ .replace(/-+/g, '-')
87
+ .replace(/^-|-$/g, '');
88
+ }
89
+
90
+ // Read the metadata for every `<skill>/SKILL.md` under `dir` into a spec list,
91
+ // sorted by folder name for deterministic order. The body is intentionally NOT
92
+ // returned (progressive disclosure) — only name/description + the path needed to
93
+ // load the body later. Unreadable / SKILL.md-less folders are skipped.
94
+ function loadSkillsFromDir(dir, source) {
95
+ const out = [];
96
+ let entries;
97
+ try { entries = fs.readdirSync(dir); } catch { return out; }
98
+ for (const entry of entries.slice().sort()) {
99
+ const skillDir = path.join(dir, entry);
100
+ const skillPath = path.join(skillDir, SKILL_FILE);
101
+ let raw;
102
+ try {
103
+ if (!fs.statSync(skillDir).isDirectory()) continue;
104
+ if (!fs.statSync(skillPath).isFile()) continue;
105
+ raw = fs.readFileSync(skillPath, 'utf8');
106
+ } catch { continue; }
107
+ const slug = _slugify(entry);
108
+ if (!slug) continue;
109
+ const { meta } = parseSkillFrontmatter(raw); // body discarded here on purpose
110
+ out.push({
111
+ name: '/' + slug,
112
+ slug,
113
+ displayName: meta.name || slug,
114
+ description: meta.description || '',
115
+ source,
116
+ dir: skillDir,
117
+ skillPath,
118
+ });
119
+ }
120
+ return out;
121
+ }
122
+
123
+ // Discover skills for a (home, cwd). Global skills load first, then the nearest
124
+ // project skills; on a name (slug) collision project wins. Returns an ordered,
125
+ // de-duplicated METADATA spec list (no bodies). home/cwd injectable for tests.
126
+ function discoverSkills(opts = {}) {
127
+ const home = opts.home || os.homedir();
128
+ const cwd = opts.cwd || process.cwd();
129
+ const global = loadSkillsFromDir(path.join(home, '.semalt-ai', 'skills'), 'global');
130
+ const projectDir = findProjectSkillsDir(cwd);
131
+ const project = projectDir ? loadSkillsFromDir(projectDir, 'project') : [];
132
+ const byName = new Map();
133
+ for (const s of project) if (!byName.has(s.name)) byName.set(s.name, s);
134
+ for (const s of global) if (!byName.has(s.name)) byName.set(s.name, s);
135
+ return Array.from(byName.values());
136
+ }
137
+
138
+ // Load a skill's BODY on demand — the progressive-disclosure read. Takes a spec
139
+ // (or anything carrying `skillPath`) and returns the Markdown body (frontmatter
140
+ // stripped). Returns '' when the file is unreadable. This is the only place a
141
+ // skill body is read into memory; it is never part of discovery/metadata.
142
+ function loadSkillBody(skill) {
143
+ if (!skill || !skill.skillPath) return '';
144
+ let raw;
145
+ try { raw = fs.readFileSync(skill.skillPath, 'utf8'); } catch { return ''; }
146
+ return parseSkillFrontmatter(raw).body;
147
+ }
148
+
149
+ // Build the system-prompt SKILLS section from METADATA only (name +
150
+ // description). Returns { block, truncated }. Empty list → '' (prompt
151
+ // unchanged). Oversized metadata is truncated with a visible notice.
152
+ function _buildBlock(skills, maxBytes) {
153
+ if (!skills.length) return { block: '', truncated: false };
154
+ const lines = skills.map((s) => {
155
+ const label = s.displayName && s.displayName !== s.slug ? ` (${s.displayName})` : '';
156
+ const desc = s.description ? ` — ${s.description}` : '';
157
+ return `- ${s.name}${label}${desc}`;
158
+ });
159
+ let body = lines.join('\n');
160
+ let truncated = false;
161
+ if (Buffer.byteLength(body, 'utf8') > maxBytes) {
162
+ body = body.slice(0, maxBytes);
163
+ truncated = true;
164
+ }
165
+ let block = '\n\n<<<SKILLS>>>\n'
166
+ + 'The following skills are available — reusable methodology you can pull in '
167
+ + 'on demand. Only each skill\'s name and description is shown here (progressive '
168
+ + 'disclosure); the full instructions are NOT loaded yet. When a task matches a '
169
+ + 'skill, load its full instructions by invoking it as the slash command shown '
170
+ + '(e.g. tell the user to run it, or run it yourself if available). This is '
171
+ + 'trusted project context, not untrusted external content.\n\n'
172
+ + body;
173
+ if (truncated) {
174
+ block += `\n\n[skills metadata truncated to ${maxBytes} bytes — some skills omitted.]`;
175
+ }
176
+ block += '\n<<<END_SKILLS>>>';
177
+ return { block, truncated };
178
+ }
179
+
180
+ // Load skills for the current (or supplied) cwd/home. Returns:
181
+ // { block, skills, truncated }
182
+ // `block` is the metadata-only system-prompt section ('' when none exist, so the
183
+ // prompt is byte-for-byte unchanged), and `skills` is the metadata list (no
184
+ // bodies) used for registration and the /skills view.
185
+ function loadSkills(opts = {}) {
186
+ const maxBytes = opts.maxBytes || DEFAULT_SKILLS_MAX_BYTES;
187
+ const skills = discoverSkills(opts);
188
+ const { block, truncated } = _buildBlock(skills, maxBytes);
189
+ return { block, skills, truncated };
190
+ }
191
+
192
+ // Human-readable status lines for the /skills command: which skills are loaded,
193
+ // their source, and a reminder that bodies load only on invocation.
194
+ function skillsStatusLines(result) {
195
+ const lines = [];
196
+ const skills = (result && result.skills) || [];
197
+ if (!skills.length) {
198
+ lines.push('No skills found.');
199
+ lines.push('Add a skill at ~/.semalt-ai/skills/<name>/SKILL.md (global) or .semalt/skills/<name>/SKILL.md (project).');
200
+ return lines;
201
+ }
202
+ lines.push(`Loaded ${skills.length} skill(s) (metadata only — bodies load on invocation):`);
203
+ for (const s of skills) {
204
+ const desc = s.description ? ` — ${s.description}` : '';
205
+ lines.push(` • ${s.name} [${s.source}]${desc}`);
206
+ }
207
+ if (result && result.truncated) {
208
+ lines.push('⚠ Skills metadata was truncated (too many/too large). Trim skill descriptions.');
209
+ }
210
+ return lines;
211
+ }
212
+
213
+ module.exports = {
214
+ DEFAULT_SKILLS_MAX_BYTES,
215
+ SKILL_FILE,
216
+ findProjectSkillsDir,
217
+ parseSkillFrontmatter,
218
+ loadSkillsFromDir,
219
+ discoverSkills,
220
+ loadSkillBody,
221
+ loadSkills,
222
+ skillsStatusLines,
223
+ };