symbiot 0.35.0 → 0.36.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/agents.mjs CHANGED
@@ -33,6 +33,26 @@ function setHandoffCmd(cmd) {
33
33
  delete cfg.ide; saveConfig(cfg);
34
34
  return { ok: true, cmd: cfg.agentCmd || "" };
35
35
  }
36
+ // Grant a blocked agent what it asked for, instead of hand-editing the command:
37
+ // `tool` (a command like "python3" → Bash(python3:*), or a full "Tool(spec)")
38
+ // joins --allowedTools; `dir` joins --add-dir. Claude only (that's where these
39
+ // flags live); for another agent we say to edit the command directly.
40
+ function grantAgent({ tool, dir } = {}) {
41
+ const cfg = loadConfig(); let cmd = (cfg.agentCmd || CLAUDE_CMD).trim();
42
+ if (!/^\s*claude\b/.test(cmd)) return { error: "Grants apply to the Claude agent command. Pick a Claude preset first, or edit the command directly." };
43
+ if (tool) {
44
+ let t = String(tool).trim();
45
+ if (t && !/^[A-Za-z]+\(/.test(t)) t = `Bash(${t.replace(/[()"]/g, "")}:*)`; // bare word -> Bash(word:*)
46
+ if (t && !cmd.includes(`"${t}"`)) {
47
+ if (/--allowedTools\b/.test(cmd)) cmd = cmd.replace(/(--allowedTools\s+(?:"[^"]*"\s*)+)/, (m) => m.trimEnd() + ` "${t}" `);
48
+ else cmd += ` --allowedTools "${t}"`;
49
+ }
50
+ }
51
+ if (dir) { const d = String(dir).trim(); if (d && !cmd.includes(`--add-dir "${d}"`)) cmd += ` --add-dir "${d}"`; }
52
+ cmd = cmd.replace(/\s+/g, " ").trim();
53
+ cfg.agentCmd = cmd; saveConfig(cfg);
54
+ return { ok: true, cmd };
55
+ }
36
56
  const fillHandoff = (tmpl, repoPath) => tmpl.replace(/\{dir\}/g, shSingle(repoPath)).replace(/\{prompt\}/g, escDq(HANDOFF_PROMPT));
37
57
  // One agent per folder: two identical runs once started on the same repo 6s
38
58
  // apart and raced each other. The registry catches a second click in this
@@ -305,4 +325,4 @@ function agentsList() {
305
325
  });
306
326
  }
307
327
 
308
- export { HANDOFFS, HANDOFF_PROMPT, QUESTIONS_MAX, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, fillHandoff, runHandoff, runningHandoff, writeTasks, releaseHeldTasks, detectHandoffs, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, agentQuestions, answerQuestions, agentsList };
328
+ export { HANDOFFS, HANDOFF_PROMPT, QUESTIONS_MAX, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, fillHandoff, runHandoff, runningHandoff, writeTasks, releaseHeldTasks, detectHandoffs, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, agentQuestions, answerQuestions, agentsList };
package/index.mjs CHANGED
@@ -30,7 +30,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
30
30
  import { EMBEDDED_UI } from "./ui.mjs";
31
31
  import { detectMailSources, mailActivity } from "./mail.mjs";
32
32
  import { CONFIG_PATH, loadConfig, saveConfig, loadTasks, saveTasks, sh, hasCmd, repoState } from "./core.mjs";
33
- import { HANDOFF_PROMPT, QUESTIONS_MAX, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, fillHandoff, runHandoff, writeTasks, releaseHeldTasks, detectHandoffs, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, parseQuestions, agentQuestions, answerQuestions, agentsList } from "./agents.mjs";
33
+ import { HANDOFF_PROMPT, QUESTIONS_MAX, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, fillHandoff, runHandoff, writeTasks, releaseHeldTasks, detectHandoffs, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, parseQuestions, agentQuestions, answerQuestions, agentsList } from "./agents.mjs";
34
34
  import { gitDefaultBranch, loadDeploys, driftRepo } from "./drift.mjs";
35
35
 
36
36
  const HERE = fileURLToPath(new URL(".", import.meta.url));
@@ -1484,6 +1484,7 @@ async function cmdApp() {
1484
1484
  if (u.pathname === "/api/scanroots/remove" && req.method === "POST") { const b = await readBody(req); return json(res, removeScanRoot(String(b.path || ""))); }
1485
1485
  if (u.pathname === "/api/agentcfg") { const d = detectHandoffs(); return json(res, { cmd: handoffCmd(), agents: d.agents, editors: d.editors }); }
1486
1486
  if (u.pathname === "/api/agentcmd" && req.method === "POST") { const b = await readBody(req); return json(res, setHandoffCmd(b.cmd)); }
1487
+ if (u.pathname === "/api/agent/grant" && req.method === "POST") { const b = await readBody(req); return json(res, grantAgent({ tool: b.tool, dir: b.dir })); }
1487
1488
  if (u.pathname === "/api/open" && req.method === "POST") { const b = await readBody(req); const e = runHandoff(String(b.path || "")); return json(res, { opened: !!e && !e.busy, busy: !!(e && e.busy), id: e ? e.id : "" }); }
1488
1489
  if (u.pathname === "/api/setup-local" && req.method === "POST") {
1489
1490
  const b = await readBody(req);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
5
5
  "type": "module",
6
6
  "bin": {
package/ui.mjs CHANGED
@@ -214,6 +214,12 @@ label.check input{width:auto}
214
214
  <input id="agentcmd" type="text" placeholder="e.g. aider --message &quot;{prompt}&quot; --yes · code {dir} · leave blank to just write the file">
215
215
  <div class="note muted">Runs in each repo after tasks are written. Use <b>{dir}</b> = repo path, <b>{prompt}</b> = the task instruction. Works with any agent or editor &mdash; it's your command.</div>
216
216
  <div id="agentpresets" style="margin-top:8px"></div>
217
+ <div id="grantbox" style="margin-top:10px">
218
+ <label style="font-size:12px;color:var(--faint)">When an agent asks to run something or read a folder, grant it here instead of editing the command:</label>
219
+ <div class="row" style="margin-top:6px"><input id="granttool" placeholder="allow a command, e.g. python3 or pytest" style="flex:1"><button class="ghost" id="granttoolbtn">Allow command</button></div>
220
+ <div class="row" style="margin-top:6px"><input id="grantdir" placeholder="allow a folder, e.g. /home/you/GoSolr" style="flex:1"><button class="ghost" id="grantdirbtn">Allow folder</button></div>
221
+ <div class="note" id="grantnote"></div>
222
+ </div>
217
223
  </div>
218
224
  <div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
219
225
  <label>Email &mdash; add what you sent to Week and Standup <span class="muted">(experimental)</span></label>
@@ -500,6 +506,7 @@ if(chips.length>na)h+="<div class='muted' style='font-size:12px;margin-top:8px'>
500
506
  box.innerHTML=h;
501
507
  box.querySelectorAll('.preset').forEach(function(btn){btn.addEventListener('click',function(){document.getElementById('agentcmd').value=chips[+btn.getAttribute('data-i')].tmpl;saveAgent();});});});}
502
508
  function saveAgent(){api('/api/agentcmd',{cmd:document.getElementById('agentcmd').value});}
509
+ function grant(kind){var i=document.getElementById(kind==='tool'?'granttool':'grantdir');var v=(i.value||'').trim();if(!v)return;var body={};body[kind]=v;api('/api/agent/grant',body).then(function(r){var n=document.getElementById('grantnote');if(!r||r.error){n.className='note err';n.textContent=(r&&r.error)||'could not grant';return;}i.value='';n.className='note ok';n.textContent=(kind==='tool'?'Command allowed':'Folder allowed')+' — the agent can use it on its next run.';if(r.cmd)document.getElementById('agentcmd').value=r.cmd;});}
503
510
  function loadScanRoots(){api('/api/scanroots').then(function(d){var box=document.getElementById('scanroots');var roots=d.effective||[];
504
511
  box.innerHTML=roots.map(function(r){var custom=(d.roots||[]).indexOf(r)>=0;return "<div class='task' data-p='"+esc(r)+"'><span class='t' style='font-family:ui-monospace,monospace;font-size:12px'>"+esc(r)+"</span>"+(r===d.home?"<span class='rp'>home</span>":"")+(custom?"<button class='rm rmroot' title='remove'>&times;</button>":"")+"</div>";}).join("");
505
512
  box.querySelectorAll('.rmroot').forEach(function(btn){btn.addEventListener('click',function(){api('/api/scanroots/remove',{path:btn.closest('.task').getAttribute('data-p')}).then(function(){loadScanRoots();mapLoaded=false;driftLoaded=false;});});});});}
@@ -565,6 +572,10 @@ document.getElementById('agentsrefresh').addEventListener('click',loadAgents);
565
572
  document.getElementById('addtask').addEventListener('click',addTaskUI);
566
573
  document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
567
574
  document.getElementById('agentcmd').addEventListener('change',saveAgent);
575
+ document.getElementById('granttoolbtn').addEventListener('click',function(){grant('tool');});
576
+ document.getElementById('grantdirbtn').addEventListener('click',function(){grant('dir');});
577
+ document.getElementById('granttool').addEventListener('keydown',function(e){if(e.key==='Enter')grant('tool');});
578
+ document.getElementById('grantdir').addEventListener('keydown',function(e){if(e.key==='Enter')grant('dir');});
568
579
  document.getElementById('mailon').addEventListener('change',setMailOn);
569
580
  document.getElementById('mailaddrs').addEventListener('change',saveMailAddrs);
570
581
  document.getElementById('addmail').addEventListener('click',addMailUI);