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.
@@ -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
- mcp: null, // wired by the session (only the "add" action prompts)
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
- const key = TOOL_TO_PERMISSION[toolName] !== undefined ? TOOL_TO_PERMISSION[toolName] : toolName;
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': return String(inp.command ?? inp.args ?? '');
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);
@@ -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,