loom-agent 1.3.2 → 1.5.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.
- package/CHANGELOG.md +216 -10
- package/README.md +10 -8
- package/package.json +2 -2
- package/src/core/agents.js +14 -1
- package/src/core/cli.js +30 -2
- package/src/core/hooks.js +28 -14
- package/src/core/memory.js +54 -6
- package/src/core/permissions.js +17 -4
- package/src/core/plugin-cmd.js +88 -1
- package/src/core/sandbox.js +311 -75
- package/src/core/session.js +262 -35
- package/src/core/transcript-undo.js +74 -0
- package/src/core/update.js +116 -6
- package/src/mcp/connector-presets.js +105 -0
- package/src/providers/anthropic.js +122 -25
- package/src/providers/google.js +19 -11
- package/src/providers/nvidia.js +45 -33
- package/src/providers/openai-compat.js +59 -27
- package/src/providers/openai.js +42 -18
- package/src/providers/openrouter.js +56 -26
- package/src/tools/index.js +206 -35
- package/src/tui/App.tsx +250 -36
- package/src/tui/components/Modals.tsx +130 -26
- package/src/tui/mcp-presets.ts +4 -65
- package/src/tui/store.ts +6 -1
package/src/core/permissions.js
CHANGED
|
@@ -34,7 +34,7 @@ const DANGER_PATTERNS = [
|
|
|
34
34
|
// user config).
|
|
35
35
|
const PERMISSION_KEYS = [
|
|
36
36
|
'read', 'edit', 'glob', 'grep', 'bash', 'task', 'skill', 'lsp', 'question',
|
|
37
|
-
'webfetch', 'websearch', 'browser',
|
|
37
|
+
'webfetch', 'websearch', 'browser', 'mcp',
|
|
38
38
|
'external_directory', 'doom_loop',
|
|
39
39
|
];
|
|
40
40
|
|
|
@@ -45,6 +45,11 @@ const DEFAULT_PERMISSIONS = {
|
|
|
45
45
|
task: 'ask',
|
|
46
46
|
skill: 'ask',
|
|
47
47
|
question: 'ask',
|
|
48
|
+
// MCP tools can do anything their server exposes (send emails, modify
|
|
49
|
+
// cloud infra, run queries) — prompt by default. Patterns match the full
|
|
50
|
+
// tool name (mcp__<server>__<tool>), so the user can allow one server or
|
|
51
|
+
// tool precisely: {"permission": {"mcp": {"mcp__github__*": "allow"}}}.
|
|
52
|
+
mcp: 'ask',
|
|
48
53
|
doom_loop: 'ask',
|
|
49
54
|
external_directory: 'ask',
|
|
50
55
|
read: {
|
|
@@ -73,6 +78,7 @@ const TOOL_TO_PERMISSION = {
|
|
|
73
78
|
glob: 'glob',
|
|
74
79
|
grep: 'grep',
|
|
75
80
|
bash: 'bash',
|
|
81
|
+
sandbox: 'bash', // same command gate as bash (danger heuristics apply)
|
|
76
82
|
task: 'task',
|
|
77
83
|
skill: 'skill',
|
|
78
84
|
lsp: 'lsp',
|
|
@@ -82,7 +88,9 @@ const TOOL_TO_PERMISSION = {
|
|
|
82
88
|
websearch: 'websearch',
|
|
83
89
|
browser: 'browser',
|
|
84
90
|
todowrite: null, // never prompts
|
|
85
|
-
|
|
91
|
+
todoread: 'read', // read-only todo inspection
|
|
92
|
+
tasks: null, // list/output/kill of the session's own background tasks
|
|
93
|
+
mcp: null, // wired by the session (mcp__* tools + the "add" action prompt)
|
|
86
94
|
};
|
|
87
95
|
|
|
88
96
|
function expandHome(p, home) {
|
|
@@ -213,7 +221,11 @@ class PermissionManager {
|
|
|
213
221
|
* 'allow' | 'ask' | 'deny'.
|
|
214
222
|
*/
|
|
215
223
|
resolve(toolName, arg) {
|
|
216
|
-
|
|
224
|
+
// mcp__<server>__<tool> names resolve through the 'mcp' permission key
|
|
225
|
+
// (patterns match the full tool name).
|
|
226
|
+
const key = (toolName && toolName.indexOf('mcp__') === 0)
|
|
227
|
+
? 'mcp'
|
|
228
|
+
: (TOOL_TO_PERMISSION[toolName] !== undefined ? TOOL_TO_PERMISSION[toolName] : toolName);
|
|
217
229
|
if (!key) return 'allow';
|
|
218
230
|
|
|
219
231
|
// Safety heuristics always run for dangerous shell commands — even when
|
|
@@ -298,7 +310,8 @@ class PermissionManager {
|
|
|
298
310
|
const cwd = cwdOverride || this.cwd || process.cwd();
|
|
299
311
|
const inp = input || {};
|
|
300
312
|
switch (toolName) {
|
|
301
|
-
case 'bash':
|
|
313
|
+
case 'bash':
|
|
314
|
+
case 'sandbox': return String(inp.command ?? inp.args ?? '');
|
|
302
315
|
case 'read': return normPathArg(inp.filePath || inp.path || '', cwd);
|
|
303
316
|
case 'edit':
|
|
304
317
|
case 'write': return normPathArg(inp.filePath || inp.path || '', cwd);
|
package/src/core/plugin-cmd.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { listSkills, installSkill, removeSkill } = require('../skills/skills-manager');
|
|
2
2
|
const { loadServers, addServer, parseMcpAddArgs, removeServer, listServers, toggleServer, seedDefaults } = require('../mcp/mcp-manager');
|
|
3
|
+
const { CONNECTOR_PRESETS } = require('../mcp/connector-presets');
|
|
3
4
|
const sessionStore = require('./session-store');
|
|
4
5
|
|
|
5
6
|
// Quote-aware CLI tokenizer for slash commands, so paths with spaces survive:
|
|
@@ -124,8 +125,9 @@ function mcpHelp() {
|
|
|
124
125
|
'Example (claude-compatible): /mcp add stm32 -- "C:\\stm32-mcp\\.venv\\Scripts\\python.exe" -m stm32_mcp.server',
|
|
125
126
|
'Example with env: /mcp add -e BRAVE_API_KEY=x brave-search -- npx -y @modelcontextprotocol/server-brave-search',
|
|
126
127
|
'From the shell (same syntax): loom add mcp [-e KEY=V] <name> [--] <command> [args...]',
|
|
128
|
+
'Connectors from the shell: loom add connector <preset> [-e KEY=V]... (or the same one-liner form)',
|
|
127
129
|
'The "--" separator is optional; quote any path with spaces. Env keys are usable as $KEY in args.',
|
|
128
|
-
'In the /mcp and /connectors browsers, press A to add with the same one-liner syntax.',
|
|
130
|
+
'In the /mcp and /connectors browsers, press A (or Enter on the "+ Add" row) to add with the same one-liner syntax.',
|
|
129
131
|
].join('\n');
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -172,6 +174,89 @@ function mcpAddLineCmd(line) {
|
|
|
172
174
|
return addServerFromArgv(argv);
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
function connectorUsage() {
|
|
178
|
+
return [
|
|
179
|
+
'Usage: /connectors add <preset> [-e KEY=V]... (presets: ' + CONNECTOR_PRESETS.map(p => p.id).join(', ') + ')',
|
|
180
|
+
' or: /connectors add [-e KEY=V] <name> [--] <command> [args...]',
|
|
181
|
+
'Example: /connectors add supabase -e SUPABASE_ACCESS_TOKEN=xxx',
|
|
182
|
+
].join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// /connectors add + `loom add connector` — preset-aware one-line add. A bare
|
|
186
|
+
// preset name expands to the full one-liner (env from -e flags anywhere, any
|
|
187
|
+
// leftover tokens appended as trailing args, e.g. --project-ref X); anything
|
|
188
|
+
// else falls through to the generic mcp one-liner. A connector IS an MCP
|
|
189
|
+
// server — only the wording differs. A missing required key is an error, not
|
|
190
|
+
// a silently broken server.
|
|
191
|
+
function connectorAddFromArgv(argvIn) {
|
|
192
|
+
let argv = argvIn.slice();
|
|
193
|
+
if (argv[0] === 'add') argv = argv.slice(1);
|
|
194
|
+
else if ((argv[0] === 'connector' || argv[0] === 'connectors') && argv[1] === 'add') argv = argv.slice(2);
|
|
195
|
+
else if (argv[0] === 'connector' || argv[0] === 'connectors') argv = argv.slice(1);
|
|
196
|
+
if (!argv.length) return connectorUsage();
|
|
197
|
+
|
|
198
|
+
// Collect -e/--env KEY=V pairs from the front while looking for a name.
|
|
199
|
+
const env = /** @type {Record<string, string>} */ ({});
|
|
200
|
+
let i = 0;
|
|
201
|
+
while (i + 1 < argv.length && (argv[i] === '-e' || argv[i] === '--env')) {
|
|
202
|
+
const kv = String(argv[i + 1]);
|
|
203
|
+
const eq = kv.indexOf('=');
|
|
204
|
+
if (eq > 0) env[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
205
|
+
i += 2;
|
|
206
|
+
}
|
|
207
|
+
const name = i < argv.length ? String(argv[i]) : '';
|
|
208
|
+
// Explicit "--" means the user wrote a full custom one-liner — never expand.
|
|
209
|
+
const preset = argv.indexOf('--') === -1 ? CONNECTOR_PRESETS.find(p => p.id === name) : undefined;
|
|
210
|
+
if (!preset) {
|
|
211
|
+
const msg = addServerFromArgv(argv);
|
|
212
|
+
return typeof msg === 'string' ? msg.replace('Added MCP server', 'Added connector') : msg;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Preset form: env flags anywhere, leftover tokens appended as extra args.
|
|
216
|
+
const extras = /** @type {string[]} */ ([]);
|
|
217
|
+
for (let j = i + 1; j < argv.length; j++) {
|
|
218
|
+
const a = argv[j];
|
|
219
|
+
if ((a === '-e' || a === '--env') && j + 1 < argv.length) {
|
|
220
|
+
const kv = String(argv[j + 1]);
|
|
221
|
+
const eq = kv.indexOf('=');
|
|
222
|
+
if (eq > 0) env[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
223
|
+
j++;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
extras.push(String(a));
|
|
227
|
+
}
|
|
228
|
+
// Required keys = every prompt the guided TUI flow would ask for.
|
|
229
|
+
const missing = (preset.prompts || []).map(p => p.key).filter(k => !env[k]);
|
|
230
|
+
if (missing.length) {
|
|
231
|
+
const hints = (preset.prompts || [])
|
|
232
|
+
.filter(p => missing.indexOf(p.key) !== -1)
|
|
233
|
+
.map(p => ' -e ' + p.key + '=... ' + p.label);
|
|
234
|
+
const firstKey = (preset.prompts.find(p => p.key === missing[0]) || { key: missing[0] }).key;
|
|
235
|
+
return 'Missing key for ' + preset.label + ':\n' + hints.join('\n') +
|
|
236
|
+
'\nExample: /connectors add ' + preset.id + ' -e ' + firstKey + '=...';
|
|
237
|
+
}
|
|
238
|
+
// Build the claude-style one-liner argv and reuse the shared engine
|
|
239
|
+
// ($KEY resolution, npx wrapping, persistence).
|
|
240
|
+
const out = /** @type {string[]} */ ([]);
|
|
241
|
+
for (const k of Object.keys(env)) out.push('-e', k + '=' + env[k]);
|
|
242
|
+
out.push(preset.id, '--');
|
|
243
|
+
if (preset.command) out.push(preset.command, ...(preset.args || []));
|
|
244
|
+
else out.push('npx', '-y', String(preset.package), ...(preset.args || []));
|
|
245
|
+
for (const x of extras) out.push(x);
|
|
246
|
+
const msg = addServerFromArgv(out);
|
|
247
|
+
return typeof msg === 'string' ? msg.replace('Added MCP server', 'Added connector') : msg;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// argv-array flavor for /connectors add (App.tsx slash args).
|
|
251
|
+
function connectorAddCmd(args) {
|
|
252
|
+
return connectorAddFromArgv(args);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Single-raw-line flavor for `loom add connector` and the one-line add input.
|
|
256
|
+
function connectorAddLineCmd(line) {
|
|
257
|
+
return connectorAddFromArgv(tokenizeCli(String(line || '').trim()));
|
|
258
|
+
}
|
|
259
|
+
|
|
175
260
|
function mcpRemoveCmd(args) {
|
|
176
261
|
const name = args[0];
|
|
177
262
|
if (!name) return 'Usage: /mcp remove <name>';
|
|
@@ -365,6 +450,8 @@ module.exports = {
|
|
|
365
450
|
listMcpText,
|
|
366
451
|
mcpAddCmd,
|
|
367
452
|
mcpAddLineCmd,
|
|
453
|
+
connectorAddCmd,
|
|
454
|
+
connectorAddLineCmd,
|
|
368
455
|
mcpRemoveCmd,
|
|
369
456
|
mcpToggleCmd,
|
|
370
457
|
diffCmd,
|