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.
- package/CHANGELOG.md +49 -0
- package/dist/cli.js +51 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/admin-allowlist.d.ts +11 -0
- package/dist/core/admin-allowlist.d.ts.map +1 -0
- package/dist/core/admin-allowlist.js +77 -0
- package/dist/core/admin-allowlist.js.map +1 -0
- package/dist/core/approval-router.d.ts.map +1 -1
- package/dist/core/approval-router.js +15 -0
- package/dist/core/approval-router.js.map +1 -1
- package/dist/core/commands/builtin.d.ts +1 -1
- package/dist/core/commands/builtin.d.ts.map +1 -1
- package/dist/core/commands/builtin.js +6 -4
- package/dist/core/commands/builtin.js.map +1 -1
- package/dist/core/commands/service.d.ts +14 -0
- package/dist/core/commands/service.d.ts.map +1 -0
- package/dist/core/commands/service.js +85 -0
- package/dist/core/commands/service.js.map +1 -0
- package/dist/core/restart-completion.d.ts +5 -0
- package/dist/core/restart-completion.d.ts.map +1 -0
- package/dist/core/restart-completion.js +156 -0
- package/dist/core/restart-completion.js.map +1 -0
- package/dist/core/restart-flow.d.ts +40 -0
- package/dist/core/restart-flow.d.ts.map +1 -0
- package/dist/core/restart-flow.js +177 -0
- package/dist/core/restart-flow.js.map +1 -0
- package/dist/core/restart-preflight.d.ts +7 -0
- package/dist/core/restart-preflight.d.ts.map +1 -0
- package/dist/core/restart-preflight.js +95 -0
- package/dist/core/restart-preflight.js.map +1 -0
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +16 -2
- package/dist/core/router.js.map +1 -1
- package/dist/core/self-protect.d.ts +8 -0
- package/dist/core/self-protect.d.ts.map +1 -0
- package/dist/core/self-protect.js +119 -0
- package/dist/core/self-protect.js.map +1 -0
- package/dist/core/service-intent.d.ts +17 -0
- package/dist/core/service-intent.d.ts.map +1 -0
- package/dist/core/service-intent.js +87 -0
- package/dist/core/service-intent.js.map +1 -0
- package/dist/core/types.d.ts +5 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/plugins/agents/claude-code/index.d.ts.map +1 -1
- package/dist/plugins/agents/claude-code/index.js +44 -14
- package/dist/plugins/agents/claude-code/index.js.map +1 -1
- package/dist/web/public/settings.html +117 -0
- package/dist/web/server.js +15 -39
- package/dist/web/server.js.map +1 -1
- 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.
|
package/dist/web/server.js
CHANGED
|
@@ -1039,7 +1039,7 @@ async function handleServiceStop(res) {
|
|
|
1039
1039
|
}, 200);
|
|
1040
1040
|
}
|
|
1041
1041
|
async function handleServiceRestart(res) {
|
|
1042
|
-
const { detectService
|
|
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':
|
|
1064
|
-
//
|
|
1065
|
-
// so the
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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) {
|