agim-cli 1.1.0 → 1.1.2

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 (92) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +7 -1
  3. package/README.zh-CN.md +7 -1
  4. package/dist/cli-ui/i18n.d.ts +4 -4
  5. package/dist/cli-ui/i18n.js +4 -4
  6. package/dist/cli-ui/i18n.js.map +1 -1
  7. package/dist/cli.js +265 -30
  8. package/dist/cli.js.map +1 -1
  9. package/dist/core/a2a.d.ts +36 -0
  10. package/dist/core/a2a.d.ts.map +1 -0
  11. package/dist/core/a2a.js +203 -0
  12. package/dist/core/a2a.js.map +1 -0
  13. package/dist/core/agent-base.d.ts +5 -2
  14. package/dist/core/agent-base.d.ts.map +1 -1
  15. package/dist/core/agent-base.js +68 -24
  16. package/dist/core/agent-base.js.map +1 -1
  17. package/dist/core/approval-bus.d.ts +81 -2
  18. package/dist/core/approval-bus.d.ts.map +1 -1
  19. package/dist/core/approval-bus.js +273 -36
  20. package/dist/core/approval-bus.js.map +1 -1
  21. package/dist/core/approval-router.d.ts.map +1 -1
  22. package/dist/core/approval-router.js +221 -89
  23. package/dist/core/approval-router.js.map +1 -1
  24. package/dist/core/commands/a2a.d.ts +3 -0
  25. package/dist/core/commands/a2a.d.ts.map +1 -0
  26. package/dist/core/commands/a2a.js +148 -0
  27. package/dist/core/commands/a2a.js.map +1 -0
  28. package/dist/core/commands/approval.d.ts.map +1 -1
  29. package/dist/core/commands/approval.js +7 -5
  30. package/dist/core/commands/approval.js.map +1 -1
  31. package/dist/core/commands/builtin.js +2 -2
  32. package/dist/core/commands/builtin.js.map +1 -1
  33. package/dist/core/commands/job.d.ts.map +1 -1
  34. package/dist/core/commands/job.js +11 -2
  35. package/dist/core/commands/job.js.map +1 -1
  36. package/dist/core/commands/outbox.d.ts +3 -0
  37. package/dist/core/commands/outbox.d.ts.map +1 -0
  38. package/dist/core/commands/outbox.js +92 -0
  39. package/dist/core/commands/outbox.js.map +1 -0
  40. package/dist/core/job-board.d.ts +122 -1
  41. package/dist/core/job-board.d.ts.map +1 -1
  42. package/dist/core/job-board.js +404 -21
  43. package/dist/core/job-board.js.map +1 -1
  44. package/dist/core/job-recovery.d.ts +48 -0
  45. package/dist/core/job-recovery.d.ts.map +1 -0
  46. package/dist/core/job-recovery.js +185 -0
  47. package/dist/core/job-recovery.js.map +1 -0
  48. package/dist/core/message-sink.d.ts +63 -0
  49. package/dist/core/message-sink.d.ts.map +1 -0
  50. package/dist/core/message-sink.js +296 -0
  51. package/dist/core/message-sink.js.map +1 -0
  52. package/dist/core/outbox.d.ts +71 -0
  53. package/dist/core/outbox.d.ts.map +1 -0
  54. package/dist/core/outbox.js +301 -0
  55. package/dist/core/outbox.js.map +1 -0
  56. package/dist/core/reminders.d.ts.map +1 -1
  57. package/dist/core/reminders.js +12 -1
  58. package/dist/core/reminders.js.map +1 -1
  59. package/dist/core/restart-completion.d.ts.map +1 -1
  60. package/dist/core/restart-completion.js +18 -1
  61. package/dist/core/restart-completion.js.map +1 -1
  62. package/dist/core/router.d.ts +8 -0
  63. package/dist/core/router.d.ts.map +1 -1
  64. package/dist/core/router.js +16 -0
  65. package/dist/core/router.js.map +1 -1
  66. package/dist/core/self-protect.d.ts +3 -2
  67. package/dist/core/self-protect.d.ts.map +1 -1
  68. package/dist/core/self-protect.js +75 -41
  69. package/dist/core/self-protect.js.map +1 -1
  70. package/dist/core/types.d.ts +22 -0
  71. package/dist/core/types.d.ts.map +1 -1
  72. package/dist/plugins/agents/claude-code/index.d.ts.map +1 -1
  73. package/dist/plugins/agents/claude-code/index.js +5 -0
  74. package/dist/plugins/agents/claude-code/index.js.map +1 -1
  75. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +21 -0
  76. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -1
  77. package/dist/plugins/agents/claude-code/mcp-approval-server.js +106 -0
  78. package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -1
  79. package/dist/plugins/agents/codex/index.d.ts.map +1 -1
  80. package/dist/plugins/agents/codex/index.js +5 -0
  81. package/dist/plugins/agents/codex/index.js.map +1 -1
  82. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts.map +1 -1
  83. package/dist/plugins/agents/opencode/opencode-http-adapter.js +46 -15
  84. package/dist/plugins/agents/opencode/opencode-http-adapter.js.map +1 -1
  85. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -1
  86. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +5 -0
  87. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -1
  88. package/dist/web/public/settings.html +89 -0
  89. package/dist/web/server.d.ts.map +1 -1
  90. package/dist/web/server.js +44 -16
  91. package/dist/web/server.js.map +1 -1
  92. package/package.json +1 -1
@@ -141,6 +141,16 @@
141
141
  adminConfirmRemove: 'Remove admin {p}:{u}?',
142
142
  adminPublicBindWarn: '⚠️ Web is bound publicly — admin editor disabled to prevent random visitors from granting themselves access. Bind to 127.0.0.1 to enable.',
143
143
  adminBootstrapHint: 'Bootstrap token exists at {path}. Run `cat` on the server, then send `/setup admin <token>` in IM to self-onboard.',
144
+
145
+ // ── Approval policy (timeout default) ─────────────────────
146
+ approvalPolicyTitle: 'Approval Policy',
147
+ approvalPolicyHint: 'What does Agim do when a tool-use approval times out (no IM reply within the budget)? Default deny is strict; switch to allow when you\'ll be away from IM and want the agent to keep moving.',
148
+ approvalTimeoutDefaultLabel: 'On approval timeout',
149
+ approvalTimeoutDeny: 'Deny (default — strict, human in the loop)',
150
+ approvalTimeoutAllow: 'Allow (user-away mode — agent proceeds silently)',
151
+ approvalPolicySave: 'Save',
152
+ approvalPolicySaved: 'Saved — applies to the next pending approval, no restart needed.',
153
+ approvalPolicyLoadFailed: 'Failed to load approval policy',
144
154
  },
145
155
  zh: {
146
156
  title: 'Agim — 设置',
@@ -268,6 +278,16 @@
268
278
  adminConfirmRemove: '移除管理员 {p}:{u}?',
269
279
  adminPublicBindWarn: '⚠️ web 控制台目前监听公开地址 — 为防止陌生访问者擅自给自己加 admin,此编辑器已禁用。绑回 127.0.0.1 后启用。',
270
280
  adminBootstrapHint: '检测到一次性 token 文件在 {path}。在服务器上 `cat` 该文件取 token,然后在 IM 里发 /setup admin <token> 即可把自己设为 admin。',
281
+
282
+ // ── 审批策略 (超时默认决策) ───────────────────────────────
283
+ approvalPolicyTitle: '审批策略',
284
+ approvalPolicyHint: '当工具调用审批超时(IM 端没在限定时间内回复)时 Agim 该怎么办?默认 deny 严格、人在回路;切到 allow 是"出门模式",让 agent 继续跑、不卡你。',
285
+ approvalTimeoutDefaultLabel: '审批超时时',
286
+ approvalTimeoutDeny: '拒绝(默认 — 严格,人工把关)',
287
+ approvalTimeoutAllow: '放行(出门模式 — agent 静默继续)',
288
+ approvalPolicySave: '保存',
289
+ approvalPolicySaved: '已保存 — 下一个挂起的审批起就生效,无需重启。',
290
+ approvalPolicyLoadFailed: '加载审批策略失败',
271
291
  },
272
292
  };
273
293
  function t(key) { return T[window.__lang][key] || T.en[key] || key; }
@@ -709,6 +729,7 @@
709
729
  <h1>${t('h1')}</h1>
710
730
  ${renderServiceCard()}
711
731
  ${renderSafetyCard()}
732
+ ${renderApprovalPolicyCard()}
712
733
  ${renderAgentsCard()}
713
734
  ${renderMessengersCard()}
714
735
  ${renderAcpCard()}
@@ -721,6 +742,8 @@
721
742
  void loadServiceStatus();
722
743
  // Admin allowlist list — loads via /api/admin-allowlist.
723
744
  void loadAdminList();
745
+ // Approval policy (IMHUB_TIMEOUT_DEFAULT) — loaded via /api/env.
746
+ void loadApprovalPolicy();
724
747
  }
725
748
 
726
749
  // ==========================================
@@ -840,6 +863,69 @@
840
863
  `;
841
864
  }
842
865
 
866
+ // ==========================================
867
+ // Approval Policy card — IMHUB_TIMEOUT_DEFAULT (deny|allow).
868
+ // Hot-reloads via process.env mutation in handlePutEnv, no restart.
869
+ // ==========================================
870
+ function renderApprovalPolicyCard() {
871
+ return `
872
+ <div class="card">
873
+ <h2>${t('approvalPolicyTitle')}</h2>
874
+ <div class="hint" style="margin-bottom:10px">${t('approvalPolicyHint')}</div>
875
+ <div style="margin-bottom:8px">
876
+ <label for="approval-timeout-default">${t('approvalTimeoutDefaultLabel')}</label>
877
+ <select id="approval-timeout-default">
878
+ <option value="deny">${t('approvalTimeoutDeny')}</option>
879
+ <option value="allow">${t('approvalTimeoutAllow')}</option>
880
+ </select>
881
+ </div>
882
+ <div class="actions">
883
+ <button type="button" class="btn btn-primary" id="approval-policy-save">${t('approvalPolicySave')}</button>
884
+ <span id="approval-policy-status" class="hint" style="margin-left:10px"></span>
885
+ </div>
886
+ </div>
887
+ `;
888
+ }
889
+
890
+ async function loadApprovalPolicy() {
891
+ const sel = document.getElementById('approval-timeout-default');
892
+ const status = document.getElementById('approval-policy-status');
893
+ if (!sel) return;
894
+ try {
895
+ const res = await authFetch('/api/env?reveal=1');
896
+ if (!res.ok) throw new Error('HTTP ' + res.status);
897
+ const data = await res.json();
898
+ const v = (data.env && typeof data.env.IMHUB_TIMEOUT_DEFAULT === 'string')
899
+ ? data.env.IMHUB_TIMEOUT_DEFAULT.toLowerCase()
900
+ : 'deny';
901
+ sel.value = v === 'allow' ? 'allow' : 'deny';
902
+ } catch (err) {
903
+ if (status) status.textContent = t('approvalPolicyLoadFailed');
904
+ }
905
+ }
906
+
907
+ async function saveApprovalPolicy() {
908
+ const sel = document.getElementById('approval-timeout-default');
909
+ const status = document.getElementById('approval-policy-status');
910
+ if (!sel || !status) return;
911
+ const v = sel.value === 'allow' ? 'allow' : 'deny';
912
+ try {
913
+ // authFetch is a thin fetch wrapper that doesn't check res.ok — a
914
+ // 5xx from the server still resolves the promise. Without this guard
915
+ // the UI would lie ("已保存") on a failed write.
916
+ const res = await authFetch('/api/env', {
917
+ method: 'PUT',
918
+ headers: { 'Content-Type': 'application/json' },
919
+ body: JSON.stringify({ updates: { IMHUB_TIMEOUT_DEFAULT: v } }),
920
+ });
921
+ if (!res.ok) throw new Error('HTTP ' + res.status);
922
+ status.textContent = t('approvalPolicySaved');
923
+ setTimeout(() => { status.textContent = ''; }, 4000);
924
+ } catch (err) {
925
+ status.textContent = t('saveFailed');
926
+ }
927
+ }
928
+
843
929
  // ==========================================
844
930
  // Agents card
845
931
  // ==========================================
@@ -1346,6 +1432,9 @@
1346
1432
  document.getElementById('svc-stop')?.addEventListener('click', () => svcAction('stop'));
1347
1433
  document.getElementById('svc-start')?.addEventListener('click', () => svcAction('start'));
1348
1434
 
1435
+ // Approval Policy card — IMHUB_TIMEOUT_DEFAULT toggle.
1436
+ document.getElementById('approval-policy-save')?.addEventListener('click', saveApprovalPolicy);
1437
+
1349
1438
  // Add admin (Safety card → Admin Allowlist).
1350
1439
  document.getElementById('admin-add')?.addEventListener('click', async () => {
1351
1440
  const platform = (document.getElementById('admin-platform')?.value || '').trim();
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAqDA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAgoB/C"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAsDA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAmoB/C"}
@@ -9,6 +9,7 @@ import { WebSocketServer } from 'ws';
9
9
  import { parseMessage, routeMessage } from '../core/router.js';
10
10
  import { sessionManager } from '../core/session.js';
11
11
  import { registry } from '../core/registry.js';
12
+ import { sink, resolveMessenger } from '../core/message-sink.js';
12
13
  import { generateTraceId, createLogger, logger as rootLogger } from '../core/logger.js';
13
14
  import { validateConfig } from '../core/config-schema.js';
14
15
  import { consumeToken, peekToken } from '../core/location-token.js';
@@ -229,8 +230,6 @@ export async function startWebServer(options) {
229
230
  catch (err) {
230
231
  webLog.warn({ event: 'web.loc.memo_failed', err: String(err) }, 'failed to write/update memo');
231
232
  }
232
- const messengerName = ctx.platform === 'wechat' ? 'wechat-ilink' : ctx.platform;
233
- const messenger = registry.getMessenger(messengerName);
234
233
  const idTag = memoId ? `#${memoId}` : '';
235
234
  const accTxt = accuracy > 0 ? ` (±${Math.round(accuracy)}m)` : '';
236
235
  const headLabel = displayWhat ? `'${displayWhat}'` : '';
@@ -242,14 +241,18 @@ export async function startWebServer(options) {
242
241
  '',
243
242
  ` 🗺 [百度地图](${urls.baidu}) · [高德地图](${urls.amap}) · [Google](${urls.google})`,
244
243
  ].filter(Boolean).join('\n');
245
- if (messenger) {
246
- messenger.sendMessage(ctx.threadId, reply).catch((err) => {
247
- webLog.warn({ event: 'web.loc.dispatch_failed', threadId: ctx.threadId, err: String(err) });
248
- });
249
- }
250
- else {
251
- webLog.warn({ event: 'web.loc.no_messenger', platform: ctx.platform });
252
- }
244
+ // Route via sink — adapter resolution (e.g. 'wechat' → 'wechat-ilink')
245
+ // happens inside resolveMessenger(). priority='normal': location replies
246
+ // are non-urgent, queue is fine.
247
+ sink.deliver({
248
+ platform: ctx.platform,
249
+ channelId: ctx.channelId,
250
+ threadId: ctx.threadId,
251
+ payload: reply,
252
+ kind: 'text',
253
+ }).catch((err) => {
254
+ webLog.warn({ event: 'web.loc.dispatch_failed', threadId: ctx.threadId, err: String(err) });
255
+ });
253
256
  sendJson(res, 200, { ok: true, id: memoId, lat, lng });
254
257
  return;
255
258
  }
@@ -1187,9 +1190,10 @@ async function handleNotify(req, res) {
1187
1190
  sendJson(res, 400, { error: 'Missing platform / threadId / (text|card)' });
1188
1191
  return;
1189
1192
  }
1190
- // Map platform name to messenger plugin name.
1191
- const messengerName = platform === 'wechat' ? 'wechat-ilink' : platform;
1192
- const messenger = registry.getMessenger(messengerName);
1193
+ // Pre-flight: confirm a messenger exists so we can return a clean 404
1194
+ // synchronously. sink will resolve again at delivery time — but the
1195
+ // synchronous 404 is part of the API contract.
1196
+ const messenger = resolveMessenger(platform);
1193
1197
  if (!messenger) {
1194
1198
  sendJson(res, 404, { error: `Messenger "${platform}" not registered` });
1195
1199
  return;
@@ -1197,11 +1201,19 @@ async function handleNotify(req, res) {
1197
1201
  const traceId = generateTraceId();
1198
1202
  const log = createLogger({ traceId, platform, component: 'notify' });
1199
1203
  log.info({ threadId, hasCard: !!card, textLen: text?.length || 0 }, 'notify in');
1200
- if (card && typeof messenger.sendCard === 'function') {
1201
- await messenger.sendCard(threadId, card);
1204
+ // External /api/notify callers don't know the IM channelId, so we pass
1205
+ // 'rest' as a sentinel (consistent with other web-originated rows).
1206
+ if (card) {
1207
+ await sink.deliver({
1208
+ platform, channelId: 'rest', threadId,
1209
+ payload: card, kind: 'card',
1210
+ });
1202
1211
  }
1203
1212
  else if (text) {
1204
- await messenger.sendMessage(threadId, text);
1213
+ await sink.deliver({
1214
+ platform, channelId: 'rest', threadId,
1215
+ payload: text, kind: 'text',
1216
+ });
1205
1217
  }
1206
1218
  else {
1207
1219
  sendJson(res, 400, { error: 'card requires sendCard support, otherwise text is required' });
@@ -1544,6 +1556,11 @@ const ENV_EDITABLE_KEYS = [
1544
1556
  // Safety card toggle — drives the Claude --dangerously-skip-permissions
1545
1557
  // branch in plugins/agents/claude-code/index.ts. Not a secret, plain '1'/'0'.
1546
1558
  'IMHUB_DANGEROUSLY_SKIP_PERMISSIONS',
1559
+ // Approval Policy card — 'allow' | 'deny' (default deny). Decides what the
1560
+ // approval bus does when a tool-use prompt times out with no human reply.
1561
+ // Hot-reload: handlePutEnv mutates process.env so approval-bus picks up the
1562
+ // new value on the next timer fire, no restart needed.
1563
+ 'IMHUB_TIMEOUT_DEFAULT',
1547
1564
  ];
1548
1565
  const SECRET_KEYS = new Set(['IMHUB_SMTP_PASS', 'IMHUB_BAIDU_MAP_AK']);
1549
1566
  function maskSecret(v) {
@@ -1594,6 +1611,17 @@ async function handlePutEnv(req, res) {
1594
1611
  }
1595
1612
  const { updateEnvFile } = await import('../cli-ui/env-file.js');
1596
1613
  updateEnvFile(safe);
1614
+ // Hot-reload: mutate process.env so modules that re-read on use (e.g.
1615
+ // approval-bus's isTimeoutDefaultAllow) pick up the change without
1616
+ // waiting for a service restart. Modules that capture at module load
1617
+ // (most SMTP / Baidu callers) still need a restart to see the new value
1618
+ // — that's a per-module decision, not enforced here.
1619
+ for (const [k, v] of Object.entries(safe)) {
1620
+ if (v === null)
1621
+ delete process.env[k];
1622
+ else
1623
+ process.env[k] = v;
1624
+ }
1597
1625
  sendJson(res, 200, { ok: true, updated: Object.keys(safe) });
1598
1626
  }
1599
1627
  catch (err) {