agim-cli 1.0.7 → 1.0.9

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 (50) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/cli.js +51 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/admin-allowlist.d.ts +11 -0
  5. package/dist/core/admin-allowlist.d.ts.map +1 -0
  6. package/dist/core/admin-allowlist.js +77 -0
  7. package/dist/core/admin-allowlist.js.map +1 -0
  8. package/dist/core/approval-router.d.ts.map +1 -1
  9. package/dist/core/approval-router.js +15 -0
  10. package/dist/core/approval-router.js.map +1 -1
  11. package/dist/core/commands/builtin.d.ts +1 -1
  12. package/dist/core/commands/builtin.d.ts.map +1 -1
  13. package/dist/core/commands/builtin.js +6 -4
  14. package/dist/core/commands/builtin.js.map +1 -1
  15. package/dist/core/commands/service.d.ts +14 -0
  16. package/dist/core/commands/service.d.ts.map +1 -0
  17. package/dist/core/commands/service.js +85 -0
  18. package/dist/core/commands/service.js.map +1 -0
  19. package/dist/core/restart-completion.d.ts +5 -0
  20. package/dist/core/restart-completion.d.ts.map +1 -0
  21. package/dist/core/restart-completion.js +156 -0
  22. package/dist/core/restart-completion.js.map +1 -0
  23. package/dist/core/restart-flow.d.ts +40 -0
  24. package/dist/core/restart-flow.d.ts.map +1 -0
  25. package/dist/core/restart-flow.js +177 -0
  26. package/dist/core/restart-flow.js.map +1 -0
  27. package/dist/core/restart-preflight.d.ts +7 -0
  28. package/dist/core/restart-preflight.d.ts.map +1 -0
  29. package/dist/core/restart-preflight.js +95 -0
  30. package/dist/core/restart-preflight.js.map +1 -0
  31. package/dist/core/router.d.ts.map +1 -1
  32. package/dist/core/router.js +16 -2
  33. package/dist/core/router.js.map +1 -1
  34. package/dist/core/self-protect.d.ts +8 -0
  35. package/dist/core/self-protect.d.ts.map +1 -0
  36. package/dist/core/self-protect.js +119 -0
  37. package/dist/core/self-protect.js.map +1 -0
  38. package/dist/core/service-intent.d.ts +17 -0
  39. package/dist/core/service-intent.d.ts.map +1 -0
  40. package/dist/core/service-intent.js +87 -0
  41. package/dist/core/service-intent.js.map +1 -0
  42. package/dist/core/types.d.ts +5 -1
  43. package/dist/core/types.d.ts.map +1 -1
  44. package/dist/plugins/agents/claude-code/index.d.ts.map +1 -1
  45. package/dist/plugins/agents/claude-code/index.js +44 -14
  46. package/dist/plugins/agents/claude-code/index.js.map +1 -1
  47. package/dist/web/public/settings.html +117 -0
  48. package/dist/web/server.js +15 -39
  49. package/dist/web/server.js.map +1 -1
  50. package/package.json +1 -1
@@ -128,6 +128,17 @@
128
128
  svcRestarted: '✓ Service restarted',
129
129
  svcStopped: '✓ Service stopped (this page is now disconnected)',
130
130
  svcFgWarning: 'Foreground service is running in another terminal — restart it there.',
131
+
132
+ // ── Safety / approval gating ─────────────────────────────
133
+ safetyTitle: 'Safety',
134
+ safetySkipLabel: 'Skip permission prompts for Claude Code (Dangerous)',
135
+ safetySkipHint: 'When ON, Claude Code launches with --dangerously-skip-permissions. Every tool call (Bash, Edit, Write, …) runs without approval and without honoring PreToolUse hooks. Only enable on private / sandboxed deployments you fully trust.',
136
+ safetySkipStatusOn: 'Currently: SKIPPING approvals — DANGEROUS',
137
+ safetySkipStatusOff: 'Currently: approval prompts enforced (default)',
138
+ safetySkipConfirmOn: 'Turn OFF approval prompts for Claude Code? Every tool call will run without confirmation. Click OK only if this deployment is private / sandboxed.',
139
+ safetyRestartHint: 'Click Save & Restart to apply.',
140
+ safetySaveRestart: 'Save & Restart',
141
+ safetySaved: '✓ Setting saved',
131
142
  },
132
143
  zh: {
133
144
  title: 'Agim — 设置',
@@ -242,6 +253,17 @@
242
253
  svcRestarted: '✓ 服务已重启',
243
254
  svcStopped: '✓ 服务已停止(页面已断开连接)',
244
255
  svcFgWarning: '前台服务在另一个终端运行——回那个终端重启。',
256
+
257
+ // ── Safety / approval gating ─────────────────────────────
258
+ safetyTitle: '安全',
259
+ safetySkipLabel: '免审批:Claude Code(危险)',
260
+ safetySkipHint: '打开后 Claude Code 以 --dangerously-skip-permissions 启动,所有工具调用(Bash / Edit / Write …)不再申请审批,也不走 PreToolUse hooks。仅推荐在你完全信任的私人 / 沙箱环境使用。',
261
+ safetySkipStatusOn: '当前:免审批 — 危险',
262
+ safetySkipStatusOff: '当前:审批正常生效(默认)',
263
+ safetySkipConfirmOn: '关闭 Claude Code 审批?所有工具调用都会不经确认直接执行。仅当此部署是私人 / 沙箱环境时点确定。',
264
+ safetyRestartHint: '点「保存并重启」让改动生效。',
265
+ safetySaveRestart: '保存并重启',
266
+ safetySaved: '✓ 设置已保存',
245
267
  },
246
268
  };
247
269
  function t(key) { return T[window.__lang][key] || T.en[key] || key; }
@@ -682,6 +704,7 @@
682
704
  app.innerHTML = `
683
705
  <h1>${t('h1')}</h1>
684
706
  ${renderServiceCard()}
707
+ ${renderSafetyCard()}
685
708
  ${renderAgentsCard()}
686
709
  ${renderMessengersCard()}
687
710
  ${renderAcpCard()}
@@ -692,6 +715,67 @@
692
715
  // Service status loads asynchronously; the card renders with a
693
716
  // placeholder, populated by the first /api/service/status response.
694
717
  void loadServiceStatus();
718
+ // Safety toggle reflects the current env value; load once on render.
719
+ void loadSafetyState();
720
+ }
721
+
722
+ // ==========================================
723
+ // Safety card — operator-level approval policy switches.
724
+ // Right now this has a single toggle (Claude --dangerously-skip-
725
+ // permissions). Future: codex / opencode equivalents, per-platform
726
+ // user allowlist, etc.
727
+ // ==========================================
728
+ function renderSafetyCard() {
729
+ return `
730
+ <div class="card">
731
+ <h2>${t('safetyTitle')} ⚠️</h2>
732
+ <div class="agent-row" style="border-bottom:none;padding:8px 0">
733
+ <div class="left" style="max-width:calc(100% - 60px)">
734
+ <div>
735
+ <div class="name">${t('safetySkipLabel')}</div>
736
+ <div class="hint" style="white-space:normal;line-height:1.5">${t('safetySkipHint')}</div>
737
+ <div class="hint" id="safety-skip-status" style="margin-top:6px">—</div>
738
+ </div>
739
+ </div>
740
+ <div class="toggle" id="safety-skip-toggle" data-active="0"></div>
741
+ </div>
742
+ <div class="hint" style="margin-top:10px">${t('safetyRestartHint')}</div>
743
+ <div class="actions">
744
+ <button type="button" class="btn btn-primary" id="safety-save-restart">${t('safetySaveRestart')}</button>
745
+ </div>
746
+ </div>
747
+ `;
748
+ }
749
+
750
+ // Pending state — what the toggle is set to RIGHT NOW (before save).
751
+ // Diffed against the on-disk env value to decide if Save-and-Restart
752
+ // should fire the restart.
753
+ let safetySkipCurrent = false;
754
+ let safetySkipPending = false;
755
+
756
+ async function loadSafetyState() {
757
+ try {
758
+ const data = await authFetch('/api/env').then(r => r.json());
759
+ // Treat env value '1' as on, anything else as off. /api/env masks
760
+ // sensitive values but plain '1'/'0' come through verbatim.
761
+ const v = data && data.env && data.env.IMHUB_DANGEROUSLY_SKIP_PERMISSIONS;
762
+ safetySkipCurrent = v === '1';
763
+ safetySkipPending = safetySkipCurrent;
764
+ renderSafetyToggleState();
765
+ } catch (err) {
766
+ const el = document.getElementById('safety-skip-status');
767
+ if (el) el.textContent = (t('error') + ': ' + (err && err.message ? err.message : err));
768
+ }
769
+ }
770
+
771
+ function renderSafetyToggleState() {
772
+ const toggle = document.getElementById('safety-skip-toggle');
773
+ const status = document.getElementById('safety-skip-status');
774
+ if (!toggle || !status) return;
775
+ toggle.classList.toggle('active', safetySkipPending);
776
+ toggle.setAttribute('data-active', safetySkipPending ? '1' : '0');
777
+ status.textContent = safetySkipPending ? t('safetySkipStatusOn') : t('safetySkipStatusOff');
778
+ status.style.color = safetySkipPending ? 'var(--red)' : 'var(--text-dim)';
695
779
  }
696
780
 
697
781
  // ==========================================
@@ -1219,6 +1303,39 @@
1219
1303
  document.getElementById('svc-stop')?.addEventListener('click', () => svcAction('stop'));
1220
1304
  document.getElementById('svc-start')?.addEventListener('click', () => svcAction('start'));
1221
1305
 
1306
+ // Safety toggle — flip pending state. Save & Restart commits to env.
1307
+ document.getElementById('safety-skip-toggle')?.addEventListener('click', () => {
1308
+ // Going from OFF → ON crosses a security boundary; double-check.
1309
+ if (!safetySkipPending && !confirm(t('safetySkipConfirmOn'))) return;
1310
+ safetySkipPending = !safetySkipPending;
1311
+ renderSafetyToggleState();
1312
+ });
1313
+ document.getElementById('safety-save-restart')?.addEventListener('click', async () => {
1314
+ const wantedOn = safetySkipPending;
1315
+ try {
1316
+ const updates = wantedOn
1317
+ ? { IMHUB_DANGEROUSLY_SKIP_PERMISSIONS: '1' }
1318
+ : { IMHUB_DANGEROUSLY_SKIP_PERMISSIONS: null };
1319
+ const res = await authFetch('/api/env', {
1320
+ method: 'PUT',
1321
+ headers: { 'Content-Type': 'application/json' },
1322
+ body: JSON.stringify({ updates }),
1323
+ });
1324
+ if (!res.ok) {
1325
+ const j = await res.json().catch(() => ({}));
1326
+ throw new Error(j.error || res.statusText);
1327
+ }
1328
+ toast(t('safetySaved'), 'success');
1329
+ safetySkipCurrent = wantedOn;
1330
+ // The env file write only takes effect on the next start. Reuse the
1331
+ // service-control restart path so the user gets the same overlay /
1332
+ // auto-reconnect they get clicking the Restart button manually.
1333
+ await svcAction('restart');
1334
+ } catch (err) {
1335
+ toast(`${t('error')}: ${err && err.message ? err.message : err}`, 'error');
1336
+ }
1337
+ });
1338
+
1222
1339
  // Agent toggles — flip config.agents in memory + auto-manage
1223
1340
  // defaultAgent (promote / demote as needed), then re-render so the
1224
1341
  // default-agent dropdown reflects the new eligible set.
@@ -1039,7 +1039,7 @@ async function handleServiceStop(res) {
1039
1039
  }, 200);
1040
1040
  }
1041
1041
  async function handleServiceRestart(res) {
1042
- const { detectService, PID_FILE, LOG_FILE } = await import('../cli-ui/service.js');
1042
+ const { detectService } = await import('../cli-ui/service.js');
1043
1043
  const st = detectService();
1044
1044
  if (st.mode === 'systemd') {
1045
1045
  try {
@@ -1060,44 +1060,17 @@ async function handleServiceRestart(res) {
1060
1060
  sendJson(res, 409, { error: 'foreground service is running in another terminal; restart it manually (Ctrl-C + re-run)' });
1061
1061
  return;
1062
1062
  }
1063
- // background or 'none': spawn a detached node -e helper that waits
1064
- // for our PID to die, then starts a fresh daemon. Then SIGTERM self
1065
- // so the helper's wait-loop unblocks and the new daemon takes over.
1066
- const __dn = dirname(fileURLToPath(import.meta.url));
1067
- const cliJs = join(__dn, '..', 'cli.js');
1068
- const parentPid = process.pid;
1069
- const helperCode = 'const{spawn}=require("child_process");' +
1070
- 'const fs=require("fs");' +
1071
- `const pid=${parentPid};` +
1072
- 'let n=0;' +
1073
- 'const t=setInterval(()=>{' +
1074
- ' try{process.kill(pid,0)}catch{' +
1075
- ' clearInterval(t);' +
1076
- ` const fd=fs.openSync(${JSON.stringify(LOG_FILE)},"a");` +
1077
- ` const c=spawn(${JSON.stringify(process.execPath)},[${JSON.stringify(cliJs)},"start"],{detached:true,stdio:["ignore",fd,fd],env:process.env});` +
1078
- ' c.unref();' +
1079
- ` fs.writeFileSync(${JSON.stringify(PID_FILE)},String(c.pid)+"\\n");` +
1080
- ' fs.closeSync(fd);' +
1081
- ' process.exit(0);' +
1082
- ' }' +
1083
- ' if(++n>150){clearInterval(t);process.exit(1)}' +
1084
- '},200);';
1085
- const { spawn: spawnChild } = await import('node:child_process');
1086
- const helper = spawnChild(process.execPath, ['-e', helperCode], {
1087
- detached: true,
1088
- stdio: 'ignore',
1089
- env: process.env,
1090
- });
1091
- helper.unref();
1092
- sendJson(res, 200, { ok: true, mode: st.mode, restarting: true });
1093
- setTimeout(() => {
1094
- try {
1095
- process.kill(process.pid, 'SIGTERM');
1096
- }
1097
- catch {
1098
- process.exit(0);
1099
- }
1100
- }, 300);
1063
+ // background or 'none': delegate to the shared restart-flow. The flow
1064
+ // runs pre-flight checks, writes restart-pending.json with source='web'
1065
+ // (so the new daemon doesn't try to push an IM completion message
1066
+ // browser polling handles that), and spawns the detached helper.
1067
+ const { initiateRestart } = await import('../core/restart-flow.js');
1068
+ const result = await initiateRestart({ source: 'web' });
1069
+ if (!result.ok) {
1070
+ sendJson(res, 409, { error: result.message, details: result.errors });
1071
+ return;
1072
+ }
1073
+ sendJson(res, 200, { ok: true, mode: st.mode, restarting: true, message: result.message });
1101
1074
  }
1102
1075
  /**
1103
1076
  * POST /api/notify → push a message to an IM thread.
@@ -1468,6 +1441,9 @@ const ENV_EDITABLE_KEYS = [
1468
1441
  'IMHUB_SMTP_FROM', 'IMHUB_SMTP_SECURE',
1469
1442
  'IMHUB_BAIDU_MAP_AK',
1470
1443
  'IMHUB_LOC_BASE_URL', 'IMHUB_TZ_OFFSET_HOURS',
1444
+ // Safety card toggle — drives the Claude --dangerously-skip-permissions
1445
+ // branch in plugins/agents/claude-code/index.ts. Not a secret, plain '1'/'0'.
1446
+ 'IMHUB_DANGEROUSLY_SKIP_PERMISSIONS',
1471
1447
  ];
1472
1448
  const SECRET_KEYS = new Set(['IMHUB_SMTP_PASS', 'IMHUB_BAIDU_MAP_AK']);
1473
1449
  function maskSecret(v) {