duclaw-cli 1.9.4 → 1.9.5

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/dist/bundle.js CHANGED
@@ -30242,7 +30242,7 @@ function printHelp() {
30242
30242
  `);
30243
30243
  }
30244
30244
  function printVersion() {
30245
- console.log(`duclaw-cli v${true ? "1.9.4" : "unknown"}`);
30245
+ console.log(`duclaw-cli v${true ? "1.9.5" : "unknown"}`);
30246
30246
  }
30247
30247
  function getDuclawTemplate() {
30248
30248
  return {
@@ -33273,7 +33273,7 @@ var chokidar_default = { watch, FSWatcher };
33273
33273
  var import_node_cron = __toESM(require_node_cron());
33274
33274
 
33275
33275
  // src/agent/createAgent.ts
33276
- var import_node_crypto14 = require("node:crypto");
33276
+ var import_node_crypto15 = require("node:crypto");
33277
33277
  var import_node_fs7 = require("node:fs");
33278
33278
 
33279
33279
  // src/background/BackgroundManager.ts
@@ -38895,35 +38895,69 @@ var clearMessages = async (storage, userId, date, cronTitle) => {
38895
38895
  await storage.del(key);
38896
38896
  };
38897
38897
 
38898
- // src/runtime/activity.ts
38899
- async function reportRuntimeActivity(input) {
38900
- const baseUrl = process.env.DUCLAW_CONTROL_PLANE_BASE_URL?.replace(/\/$/, "");
38901
- const token = process.env.DUCLAW_CONTROL_PLANE_METERING_TOKEN;
38902
- const tenantId = process.env.DUCLAW_TENANT_ID;
38903
- if (!baseUrl || !token || !tenantId) return;
38904
- try {
38905
- const response = await fetch(`${baseUrl}/internal/runtime/activity`, {
38906
- method: "POST",
38907
- signal: AbortSignal.timeout(2500),
38908
- headers: {
38909
- authorization: `Bearer ${token}`,
38910
- "content-type": "application/json"
38911
- },
38912
- body: JSON.stringify({
38913
- tenantId,
38914
- kind: input.kind,
38915
- status: input.status,
38916
- toolName: input.toolName,
38917
- toolCallId: input.toolCallId ?? null,
38918
- occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
38919
- metadata: input.metadata ?? {}
38920
- })
38921
- });
38922
- if (!response.ok) {
38923
- console.warn(`[runtime-activity] \u4E0A\u62A5\u5931\u8D25: tool=${input.toolName} status=${input.status} http=${response.status}`);
38898
+ // src/tools/ToolExecutor.ts
38899
+ var import_node_crypto2 = require("node:crypto");
38900
+
38901
+ // src/runtime/events.ts
38902
+ var createRuntimeEventBus = () => {
38903
+ const handlers = /* @__PURE__ */ new Map();
38904
+ return {
38905
+ async emit(event) {
38906
+ const subscribers = Array.from(handlers.get(event.type) ?? []);
38907
+ for (const handler of subscribers) {
38908
+ try {
38909
+ await handler(event);
38910
+ } catch (error) {
38911
+ console.warn(`[runtime-event-bus] handler failed: type=${event.type} error=${error.message}`);
38912
+ }
38913
+ }
38914
+ },
38915
+ subscribe(type, handler) {
38916
+ const current = handlers.get(type) ?? /* @__PURE__ */ new Set();
38917
+ current.add(handler);
38918
+ handlers.set(type, current);
38919
+ return () => {
38920
+ current.delete(handler);
38921
+ if (current.size === 0) handlers.delete(type);
38922
+ };
38924
38923
  }
38925
- } catch (err) {
38926
- console.warn(`[runtime-activity] \u4E0A\u62A5\u5F02\u5E38: tool=${input.toolName} status=${input.status} error=${err.message}`);
38924
+ };
38925
+ };
38926
+
38927
+ // src/runtime/toolHooks.ts
38928
+ function sortToolHookPlugins(plugins = []) {
38929
+ return [...plugins].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
38930
+ }
38931
+ async function runPreToolUseHooks(plugins, ctx) {
38932
+ for (const plugin of plugins) {
38933
+ if (!plugin.preToolUse) continue;
38934
+ const decision = await plugin.preToolUse(ctx);
38935
+ if (decision?.metadataPatch) {
38936
+ ctx.metadata = {
38937
+ ...ctx.metadata,
38938
+ ...decision.metadataPatch
38939
+ };
38940
+ }
38941
+ if (decision?.deny || decision?.allow === false) {
38942
+ return {
38943
+ allow: false,
38944
+ deny: true,
38945
+ reason: decision.reason ?? `blocked_by_${plugin.id}`,
38946
+ userMessage: decision.userMessage,
38947
+ metadataPatch: decision.metadataPatch
38948
+ };
38949
+ }
38950
+ }
38951
+ return { allow: true };
38952
+ }
38953
+ async function runAfterToolUseHooks(plugins, ctx) {
38954
+ for (const plugin of plugins) {
38955
+ if (plugin.afterToolUse) await plugin.afterToolUse(ctx);
38956
+ }
38957
+ }
38958
+ async function runToolUseErrorHooks(plugins, ctx) {
38959
+ for (const plugin of plugins) {
38960
+ if (plugin.onToolUseError) await plugin.onToolUseError(ctx);
38927
38961
  }
38928
38962
  }
38929
38963
 
@@ -38941,7 +38975,9 @@ var UserRecoverableToolError = class extends Error {
38941
38975
  var isUserRecoverableToolError = (error) => error instanceof UserRecoverableToolError || typeof error === "object" && error !== null && error.name === "UserRecoverableToolError" && typeof error.userMessage === "string";
38942
38976
 
38943
38977
  // src/tools/ToolExecutor.ts
38944
- var createToolExecutor = (registry2) => {
38978
+ var createToolExecutor = (registry2, options = {}) => {
38979
+ const hookPlugins = sortToolHookPlugins(options.hookPlugins ?? []);
38980
+ const eventBus = options.eventBus ?? createRuntimeEventBus();
38945
38981
  return {
38946
38982
  async execute(name, input, userRequest) {
38947
38983
  const tool = registry2.get(name);
@@ -38953,42 +38989,78 @@ var createToolExecutor = (registry2) => {
38953
38989
  }
38954
38990
  }
38955
38991
  const toolCallId = typeof userRequest?.metadata?.toolCallId === "string" ? userRequest.metadata.toolCallId : null;
38956
- await reportRuntimeActivity({
38957
- kind: "tool_use",
38992
+ const startedAt = /* @__PURE__ */ new Date();
38993
+ const ctx = {
38958
38994
  toolName: name,
38959
- status: "started",
38960
38995
  toolCallId,
38961
- metadata: {
38962
- platform: userRequest?.platform,
38963
- requestId: userRequest?.requestId,
38964
- departmentAgentId: userRequest?.departmentAgentId
38965
- }
38996
+ input: input ?? {},
38997
+ userRequest,
38998
+ startedAt,
38999
+ metadata: requestContext(userRequest)
39000
+ };
39001
+ await eventBus.emit({
39002
+ eventId: runtimeEventId("toolUse.started", name, toolCallId, startedAt),
39003
+ type: "toolUse.started",
39004
+ occurredAt: startedAt.toISOString(),
39005
+ toolName: name,
39006
+ toolCallId,
39007
+ inputSummary: summarizeJson(ctx.input),
39008
+ requestContext: ctx.metadata
38966
39009
  });
39010
+ const preDecision = await runPreToolUseHooks(hookPlugins, ctx);
39011
+ if (preDecision.deny || preDecision.allow === false) {
39012
+ ctx.durationMs = Date.now() - startedAt.getTime();
39013
+ const reason = preDecision.reason ?? "tool_use_blocked";
39014
+ await eventBus.emit({
39015
+ eventId: runtimeEventId("toolUse.blocked", name, toolCallId, startedAt),
39016
+ type: "toolUse.blocked",
39017
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
39018
+ toolName: name,
39019
+ toolCallId,
39020
+ inputSummary: summarizeJson(ctx.input),
39021
+ durationMs: ctx.durationMs,
39022
+ errorSummary: reason,
39023
+ requestContext: ctx.metadata
39024
+ });
39025
+ throw new UserRecoverableToolError(
39026
+ "tool_use_blocked",
39027
+ preDecision.userMessage ?? "\u5F53\u524D\u5DE5\u5177\u8C03\u7528\u5DF2\u88AB\u7CFB\u7EDF\u7B56\u7565\u963B\u6B62\u3002",
39028
+ reason
39029
+ );
39030
+ }
38967
39031
  try {
38968
39032
  const result = await tool.execute(input ?? {}, userRequest);
38969
- await reportRuntimeActivity({
38970
- kind: "tool_use",
39033
+ ctx.resultText = result;
39034
+ ctx.durationMs = Date.now() - startedAt.getTime();
39035
+ await runAfterToolUseHooks(hookPlugins, ctx);
39036
+ await eventBus.emit({
39037
+ eventId: runtimeEventId("toolUse.completed", name, toolCallId, startedAt),
39038
+ type: "toolUse.completed",
39039
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
38971
39040
  toolName: name,
38972
- status: "completed",
38973
39041
  toolCallId,
38974
- metadata: {
38975
- platform: userRequest?.platform,
38976
- requestId: userRequest?.requestId,
38977
- departmentAgentId: userRequest?.departmentAgentId
38978
- }
39042
+ inputSummary: summarizeJson(ctx.input),
39043
+ durationMs: ctx.durationMs,
39044
+ resultSummary: summarizeText(result),
39045
+ requestContext: ctx.metadata
38979
39046
  });
38980
39047
  return result;
38981
39048
  } catch (error) {
38982
39049
  const err = error;
38983
- await reportRuntimeActivity({
38984
- kind: "tool_use",
39050
+ ctx.error = err;
39051
+ ctx.durationMs = Date.now() - startedAt.getTime();
39052
+ await runToolUseErrorHooks(hookPlugins, ctx);
39053
+ await eventBus.emit({
39054
+ eventId: runtimeEventId("toolUse.failed", name, toolCallId, startedAt),
39055
+ type: "toolUse.failed",
39056
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
38985
39057
  toolName: name,
38986
- status: "failed",
38987
39058
  toolCallId,
38988
- metadata: {
38989
- platform: userRequest?.platform,
38990
- requestId: userRequest?.requestId,
38991
- departmentAgentId: userRequest?.departmentAgentId,
39059
+ inputSummary: summarizeJson(ctx.input),
39060
+ durationMs: ctx.durationMs,
39061
+ errorSummary: summarizeText(err.message),
39062
+ requestContext: {
39063
+ ...ctx.metadata,
38992
39064
  error: err.message
38993
39065
  }
38994
39066
  });
@@ -39005,6 +39077,23 @@ var createToolExecutor = (registry2) => {
39005
39077
  }
39006
39078
  };
39007
39079
  };
39080
+ function requestContext(userRequest) {
39081
+ return {
39082
+ platform: userRequest?.platform,
39083
+ requestId: userRequest?.requestId,
39084
+ departmentAgentId: userRequest?.departmentAgentId
39085
+ };
39086
+ }
39087
+ function summarizeJson(value, maxChars = 1e3) {
39088
+ return summarizeText(JSON.stringify(value ?? null), maxChars);
39089
+ }
39090
+ function summarizeText(value, maxChars = 1e3) {
39091
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...`;
39092
+ }
39093
+ function runtimeEventId(type, toolName, toolCallId, startedAt) {
39094
+ if (typeof import_node_crypto2.randomUUID === "function") return (0, import_node_crypto2.randomUUID)();
39095
+ return (0, import_node_crypto2.createHash)("sha256").update(type).update("\0").update(toolName).update("\0").update(toolCallId ?? "").update("\0").update(startedAt.toISOString()).digest("hex");
39096
+ }
39008
39097
 
39009
39098
  // src/tools/ToolRegistry.ts
39010
39099
  var createToolRegistry = () => {
@@ -41409,7 +41498,7 @@ var isToolUseBlock = (block) => block.type === "tool_use";
41409
41498
  var extractText = (blocks) => blocks.filter(isTextBlock).map((b) => b.text).join("\n");
41410
41499
 
41411
41500
  // src/tools/tools/ImageUnderstandMetering.ts
41412
- var import_node_crypto2 = require("node:crypto");
41501
+ var import_node_crypto3 = require("node:crypto");
41413
41502
  var ImageUnderstandMeteringError = class extends Error {
41414
41503
  constructor(message, statusCode, meteringStatus) {
41415
41504
  super(message);
@@ -41499,7 +41588,7 @@ function inferImageProvider(baseUrl) {
41499
41588
  }
41500
41589
  }
41501
41590
  function fingerprintImageSource(imageSource) {
41502
- return (0, import_node_crypto2.createHash)("sha256").update(imageSource).digest("hex");
41591
+ return (0, import_node_crypto3.createHash)("sha256").update(imageSource).digest("hex");
41503
41592
  }
41504
41593
  function imageSourceKind(imageSource) {
41505
41594
  if (imageSource.startsWith("http://") || imageSource.startsWith("https://")) return "url";
@@ -42109,10 +42198,10 @@ var goalDelete = {
42109
42198
  };
42110
42199
 
42111
42200
  // src/tools/tools/department/DepartmentCreate.ts
42112
- var import_node_crypto7 = require("node:crypto");
42201
+ var import_node_crypto8 = require("node:crypto");
42113
42202
 
42114
42203
  // src/department/mailbox/mailbox.ts
42115
- var import_node_crypto6 = require("node:crypto");
42204
+ var import_node_crypto7 = require("node:crypto");
42116
42205
 
42117
42206
  // src/agent/interruptRegistry.ts
42118
42207
  var registry = /* @__PURE__ */ new Map();
@@ -42163,7 +42252,7 @@ var drainInterrupts = (userId) => {
42163
42252
  };
42164
42253
 
42165
42254
  // src/agent/events.ts
42166
- var import_node_crypto3 = require("node:crypto");
42255
+ var import_node_crypto4 = require("node:crypto");
42167
42256
  var rowToEvent = (row) => ({
42168
42257
  id: row.id,
42169
42258
  userId: row.userId,
@@ -42180,7 +42269,7 @@ var rowToEvent = (row) => ({
42180
42269
  var recordAgentEvent = (input) => {
42181
42270
  const db3 = createSqliteDB();
42182
42271
  const now = Date.now();
42183
- const id = `evt_${(0, import_node_crypto3.randomUUID)().slice(0, 12)}`;
42272
+ const id = `evt_${(0, import_node_crypto4.randomUUID)().slice(0, 12)}`;
42184
42273
  const payloadJson = JSON.stringify(input.payload);
42185
42274
  db3.prepare(`
42186
42275
  INSERT INTO agent_events (
@@ -42311,7 +42400,7 @@ ${ceoFollowupInstruction}
42311
42400
  };
42312
42401
 
42313
42402
  // src/department/mailbox/events.ts
42314
- var import_node_crypto4 = require("node:crypto");
42403
+ var import_node_crypto5 = require("node:crypto");
42315
42404
  var parseDetail = (detailJson) => {
42316
42405
  if (!detailJson) return void 0;
42317
42406
  try {
@@ -42352,7 +42441,7 @@ var mapMailboxEventRow = (row) => {
42352
42441
  var recordMailboxEvent = (input) => {
42353
42442
  const db3 = createSqliteDB();
42354
42443
  const event = {
42355
- id: (0, import_node_crypto4.randomUUID)().slice(0, 12),
42444
+ id: (0, import_node_crypto5.randomUUID)().slice(0, 12),
42356
42445
  messageId: input.messageId,
42357
42446
  mailboxId: input.mailboxId,
42358
42447
  actorMailboxId: input.actorMailboxId,
@@ -42636,7 +42725,7 @@ var deleteDepartmentMemberById = (departmentName, memberId) => {
42636
42725
  };
42637
42726
 
42638
42727
  // src/department/mailbox/ceoFollowup.ts
42639
- var import_node_crypto5 = require("node:crypto");
42728
+ var import_node_crypto6 = require("node:crypto");
42640
42729
  var rowToFollowup = (row) => ({
42641
42730
  id: row.id,
42642
42731
  sourceMessageId: row.sourceMessageId,
@@ -42683,7 +42772,7 @@ var enqueueCeoFollowupFromMailbox = (message) => {
42683
42772
  if (!message.originUserId || !message.originPlatform) return null;
42684
42773
  const db3 = createSqliteDB();
42685
42774
  const now = Date.now();
42686
- const id = `cfu_${(0, import_node_crypto5.randomUUID)().slice(0, 12)}`;
42775
+ const id = `cfu_${(0, import_node_crypto6.randomUUID)().slice(0, 12)}`;
42687
42776
  db3.prepare(`
42688
42777
  INSERT INTO ceo_followups (
42689
42778
  id,
@@ -43013,7 +43102,7 @@ var recordMailboxReceivedAgentEvent = (msg) => {
43013
43102
  };
43014
43103
  var sendMessage2 = (fromMailboxId, toMailboxId, content, options) => {
43015
43104
  const db3 = createSqliteDB();
43016
- const id = (0, import_node_crypto6.randomUUID)().slice(0, 8);
43105
+ const id = (0, import_node_crypto7.randomUUID)().slice(0, 8);
43017
43106
  const threadId = options?.threadId || id;
43018
43107
  const workItemContext = resolveWorkItemContext(fromMailboxId, toMailboxId, id, options);
43019
43108
  const stmt = db3.prepare(`insert into mailbox (
@@ -43156,7 +43245,7 @@ var departmentCreate = {
43156
43245
  return `[departmentCreate] \u4E0D\u5B58\u5728 id=${sourceGoalId} \u7684\u76EE\u6807`;
43157
43246
  }
43158
43247
  let departmentDefinition = {
43159
- id: (0, import_node_crypto7.randomUUID)().slice(0, 8),
43248
+ id: (0, import_node_crypto8.randomUUID)().slice(0, 8),
43160
43249
  name,
43161
43250
  charter,
43162
43251
  sourceGoalId,
@@ -43423,7 +43512,7 @@ var departmentList = {
43423
43512
  };
43424
43513
 
43425
43514
  // src/tools/tools/department/DepartmentMemberCreate.ts
43426
- var import_node_crypto8 = require("node:crypto");
43515
+ var import_node_crypto9 = require("node:crypto");
43427
43516
  var DESCRIPTION24 = `
43428
43517
  \u521B\u5EFA\u90E8\u95E8\u6210\u5458\u3002
43429
43518
 
@@ -43489,7 +43578,7 @@ var departmentMemberCreate = {
43489
43578
  }
43490
43579
  }
43491
43580
  let departmentMember = {
43492
- id: (0, import_node_crypto8.randomUUID)().slice(0, 8),
43581
+ id: (0, import_node_crypto9.randomUUID)().slice(0, 8),
43493
43582
  name,
43494
43583
  departmentId: department.id,
43495
43584
  mailBoxId: getMailBoxId(department.name, name),
@@ -43680,7 +43769,7 @@ ${replies}`;
43680
43769
  // src/department/learning.ts
43681
43770
  var import_node_fs4 = require("node:fs");
43682
43771
  var import_node_path13 = __toESM(require("node:path"));
43683
- var import_node_crypto9 = require("node:crypto");
43772
+ var import_node_crypto10 = require("node:crypto");
43684
43773
 
43685
43774
  // src/skill/SkillValidator.ts
43686
43775
  var import_node_fs3 = require("node:fs");
@@ -43918,7 +44007,7 @@ var listDepartmentMemories = (departmentName) => {
43918
44007
  var createDepartmentMemory = (departmentName, input) => {
43919
44008
  const now = Date.now();
43920
44009
  const memory = {
43921
- id: (0, import_node_crypto9.randomUUID)().slice(0, 8),
44010
+ id: (0, import_node_crypto10.randomUUID)().slice(0, 8),
43922
44011
  departmentName,
43923
44012
  title: input.title,
43924
44013
  content: input.content,
@@ -43969,7 +44058,7 @@ ${formatSkillValidationIssues(validation)}`);
43969
44058
  }
43970
44059
  const now = Date.now();
43971
44060
  const skill = {
43972
- id: (0, import_node_crypto9.randomUUID)().slice(0, 8),
44061
+ id: (0, import_node_crypto10.randomUUID)().slice(0, 8),
43973
44062
  departmentName,
43974
44063
  skillName: input.skillName,
43975
44064
  description: input.description,
@@ -44021,7 +44110,7 @@ var createDepartmentProposal = (input) => {
44021
44110
  const records = readJsonArray(proposalsPath());
44022
44111
  const proposal = {
44023
44112
  ...input,
44024
- id: (0, import_node_crypto9.randomUUID)().slice(0, 8),
44113
+ id: (0, import_node_crypto10.randomUUID)().slice(0, 8),
44025
44114
  status: "pending",
44026
44115
  createdAt: Date.now()
44027
44116
  };
@@ -44356,7 +44445,7 @@ var mailboxFollowup = {
44356
44445
 
44357
44446
  // src/tools/tools/Bash.ts
44358
44447
  var import_node_child_process = require("node:child_process");
44359
- var import_node_crypto10 = require("node:crypto");
44448
+ var import_node_crypto11 = require("node:crypto");
44360
44449
  var import_node_fs5 = require("node:fs");
44361
44450
  var DESCRIPTION29 = `\u5728\u7CFB\u7EDF shell \u4E2D\u6267\u884C\u547D\u4EE4\u3002
44362
44451
 
@@ -44560,7 +44649,7 @@ var bashTool = {
44560
44649
  ...options,
44561
44650
  stdio: ["pipe", "pipe", "pipe"]
44562
44651
  });
44563
- const id = (0, import_node_crypto10.randomUUID)().slice(0, 8);
44652
+ const id = (0, import_node_crypto11.randomUUID)().slice(0, 8);
44564
44653
  const session = {
44565
44654
  id,
44566
44655
  command,
@@ -44658,10 +44747,183 @@ var sendFile = {
44658
44747
  }
44659
44748
  };
44660
44749
 
44750
+ // src/runtime/activity.ts
44751
+ async function reportRuntimeActivity(input) {
44752
+ const baseUrl = process.env.DUCLAW_CONTROL_PLANE_BASE_URL?.replace(/\/$/, "");
44753
+ const token = process.env.DUCLAW_CONTROL_PLANE_METERING_TOKEN;
44754
+ const tenantId = process.env.DUCLAW_TENANT_ID;
44755
+ if (!baseUrl || !token || !tenantId) return;
44756
+ try {
44757
+ const response = await fetch(`${baseUrl}/internal/runtime/activity`, {
44758
+ method: "POST",
44759
+ signal: AbortSignal.timeout(2500),
44760
+ headers: {
44761
+ authorization: `Bearer ${token}`,
44762
+ "content-type": "application/json"
44763
+ },
44764
+ body: JSON.stringify({
44765
+ tenantId,
44766
+ kind: input.kind,
44767
+ status: input.status,
44768
+ toolName: input.toolName,
44769
+ toolCallId: input.toolCallId ?? null,
44770
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
44771
+ metadata: input.metadata ?? {}
44772
+ })
44773
+ });
44774
+ if (!response.ok) {
44775
+ console.warn(`[runtime-activity] \u4E0A\u62A5\u5931\u8D25: tool=${input.toolName} status=${input.status} http=${response.status}`);
44776
+ }
44777
+ } catch (err) {
44778
+ console.warn(`[runtime-activity] \u4E0A\u62A5\u5F02\u5E38: tool=${input.toolName} status=${input.status} error=${err.message}`);
44779
+ }
44780
+ }
44781
+
44782
+ // src/runtime/plugins/activityToolHook.ts
44783
+ var createRuntimeActivityToolHookPlugin = () => ({
44784
+ id: "runtime-activity",
44785
+ priority: 0,
44786
+ async preToolUse(ctx) {
44787
+ await reportRuntimeActivity({
44788
+ kind: "tool_use",
44789
+ toolName: ctx.toolName,
44790
+ status: "started",
44791
+ toolCallId: ctx.toolCallId,
44792
+ metadata: activityMetadata(ctx)
44793
+ });
44794
+ },
44795
+ async afterToolUse(ctx) {
44796
+ await reportRuntimeActivity({
44797
+ kind: "tool_use",
44798
+ toolName: ctx.toolName,
44799
+ status: "completed",
44800
+ toolCallId: ctx.toolCallId,
44801
+ metadata: activityMetadata(ctx)
44802
+ });
44803
+ },
44804
+ async onToolUseError(ctx) {
44805
+ await reportRuntimeActivity({
44806
+ kind: "tool_use",
44807
+ toolName: ctx.toolName,
44808
+ status: "failed",
44809
+ toolCallId: ctx.toolCallId,
44810
+ metadata: {
44811
+ ...activityMetadata(ctx),
44812
+ error: ctx.error?.message
44813
+ }
44814
+ });
44815
+ }
44816
+ });
44817
+ function activityMetadata(ctx) {
44818
+ return {
44819
+ platform: ctx.userRequest?.platform,
44820
+ requestId: ctx.userRequest?.requestId,
44821
+ departmentAgentId: ctx.userRequest?.departmentAgentId
44822
+ };
44823
+ }
44824
+
44825
+ // src/runtime/plugins/saasCreditToolHook.ts
44826
+ var DEFAULT_WEB_SEARCH_NUM_RESULTS = 8;
44827
+ var createSaasCreditToolHookPlugin = () => ({
44828
+ id: "saas-credit",
44829
+ priority: 100,
44830
+ async afterToolUse(ctx) {
44831
+ const usage = meteredToolUsage(ctx);
44832
+ if (!usage) return;
44833
+ const baseUrl = process.env.DUCLAW_CONTROL_PLANE_BASE_URL?.replace(/\/$/, "");
44834
+ const token = process.env.DUCLAW_CONTROL_PLANE_METERING_TOKEN;
44835
+ const tenantId = process.env.DUCLAW_TENANT_ID;
44836
+ if (!baseUrl || !token || !tenantId) {
44837
+ if (process.env.NODE_ENV === "production" || process.env.DUCLAW_RUNTIME_PROVIDER) {
44838
+ throw new Error("[saas-credit] runtime metering is not configured");
44839
+ }
44840
+ return;
44841
+ }
44842
+ const response = await fetch(`${baseUrl}/internal/metering/tool-usage`, {
44843
+ method: "POST",
44844
+ signal: AbortSignal.timeout(5e3),
44845
+ headers: {
44846
+ authorization: `Bearer ${token}`,
44847
+ "content-type": "application/json"
44848
+ },
44849
+ body: JSON.stringify({
44850
+ tenantId,
44851
+ toolName: ctx.toolName,
44852
+ toolCallId: ctx.toolCallId,
44853
+ requestId: ctx.userRequest?.requestId ?? null,
44854
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
44855
+ ...usage,
44856
+ metadata: {
44857
+ platform: ctx.userRequest?.platform ?? null,
44858
+ departmentAgentId: ctx.userRequest?.departmentAgentId ?? null,
44859
+ input: sanitizedWebSearchInput(ctx.input)
44860
+ }
44861
+ })
44862
+ });
44863
+ const body = await parseJson2(response);
44864
+ const status = body?.result?.status;
44865
+ if (response.status === 402 || status === "credit_exhausted") {
44866
+ throw new UserRecoverableToolError(
44867
+ "credit_exhausted",
44868
+ "Credit \u4F59\u989D\u4E0D\u8DB3\uFF0C\u65E0\u6CD5\u5B8C\u6210\u672C\u6B21\u7F51\u9875\u641C\u7D22\u3002\u8BF7\u5148\u5145\u503C\u6216\u4F7F\u7528\u6FC0\u6D3B\u7801\u589E\u52A0 Credit\u3002",
44869
+ `[saas-credit] ${ctx.toolName} credit exhausted`
44870
+ );
44871
+ }
44872
+ if (!response.ok || status === "failed") {
44873
+ throw new Error(`[saas-credit] tool metering failed: ${body?.error ?? body?.message ?? status ?? response.status}`);
44874
+ }
44875
+ }
44876
+ });
44877
+ function meteredToolUsage(ctx) {
44878
+ if (ctx.toolName !== "websearch") return null;
44879
+ if (isWebSearchErrorResult(ctx.resultText)) return null;
44880
+ return {
44881
+ meterType: "web.content_retrieval",
44882
+ provider: "exa",
44883
+ unit: "content",
44884
+ quantity: normalizeNumResults(ctx.input.numResults)
44885
+ };
44886
+ }
44887
+ function normalizeNumResults(value) {
44888
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_WEB_SEARCH_NUM_RESULTS;
44889
+ return Math.max(1, Math.floor(value));
44890
+ }
44891
+ function isWebSearchErrorResult(result) {
44892
+ if (!result) return true;
44893
+ return /Web search (请求失败|请求超时|执行失败)|未找到搜索结果/.test(result);
44894
+ }
44895
+ function sanitizedWebSearchInput(input) {
44896
+ return {
44897
+ query: typeof input.query === "string" ? input.query.slice(0, 500) : null,
44898
+ numResults: normalizeNumResults(input.numResults),
44899
+ livecrawl: typeof input.livecrawl === "string" ? input.livecrawl : null,
44900
+ type: typeof input.type === "string" ? input.type : null,
44901
+ contextMaxCharacters: typeof input.contextMaxCharacters === "number" ? input.contextMaxCharacters : null
44902
+ };
44903
+ }
44904
+ async function parseJson2(response) {
44905
+ try {
44906
+ return await response.json();
44907
+ } catch {
44908
+ return null;
44909
+ }
44910
+ }
44911
+
44912
+ // src/runtime/plugins/index.ts
44913
+ function createDefaultToolHookPlugins(extra = []) {
44914
+ return [
44915
+ createSaasCreditToolHookPlugin(),
44916
+ createRuntimeActivityToolHookPlugin(),
44917
+ ...extra
44918
+ ];
44919
+ }
44920
+
44661
44921
  // src/tools/index.ts
44662
- var createDefaultTools = (bg) => {
44922
+ var createDefaultTools = (bg, hookPlugins = []) => {
44663
44923
  const registry2 = createToolRegistry();
44664
- const executor = createToolExecutor(registry2);
44924
+ const executor = createToolExecutor(registry2, {
44925
+ hookPlugins: createDefaultToolHookPlugins(hookPlugins)
44926
+ });
44665
44927
  registerTool(registry2, dateTool);
44666
44928
  registerTool(registry2, bashTool);
44667
44929
  registerTool(registry2, globTool);
@@ -45141,7 +45403,7 @@ var readDreamHistoryLimit = () => {
45141
45403
  var import_node_fs6 = require("node:fs");
45142
45404
  var import_node_os2 = require("node:os");
45143
45405
  var import_node_path14 = require("node:path");
45144
- var import_node_crypto11 = require("node:crypto");
45406
+ var import_node_crypto12 = require("node:crypto");
45145
45407
  var SkillForgeEngine = class {
45146
45408
  proposalStorage;
45147
45409
  draftRoot;
@@ -45175,7 +45437,7 @@ ${formatSkillValidationIssues(validation)}`);
45175
45437
  if (pending.some((p) => p.skillName === skillName)) {
45176
45438
  return null;
45177
45439
  }
45178
- const id = (0, import_node_crypto11.randomBytes)(4).toString("hex");
45440
+ const id = (0, import_node_crypto12.randomBytes)(4).toString("hex");
45179
45441
  const draftDir = (0, import_node_path14.join)(this.draftRoot, userId, id);
45180
45442
  (0, import_node_fs6.mkdirSync)(draftDir, { recursive: true });
45181
45443
  (0, import_node_fs6.writeFileSync)((0, import_node_path14.join)(draftDir, "SKILL.md"), skillMd, "utf-8");
@@ -45457,7 +45719,7 @@ var skillForgeDrop = (engine) => ({
45457
45719
  });
45458
45720
 
45459
45721
  // src/memory/MemoryEngine.ts
45460
- var import_node_crypto12 = require("node:crypto");
45722
+ var import_node_crypto13 = require("node:crypto");
45461
45723
  var MemoryEngine = class {
45462
45724
  storage;
45463
45725
  recallIndexStorage;
@@ -45485,7 +45747,7 @@ var MemoryEngine = class {
45485
45747
  }
45486
45748
  const now = Date.now();
45487
45749
  const memory = {
45488
- id: (0, import_node_crypto12.randomBytes)(4).toString("hex"),
45750
+ id: (0, import_node_crypto13.randomBytes)(4).toString("hex"),
45489
45751
  userId,
45490
45752
  title,
45491
45753
  content,
@@ -46137,13 +46399,13 @@ var COMPANY_VALUES_PROMPT = `<\u516C\u53F8\u5171\u540C\u4FE1\u5FF5>
46137
46399
  </\u516C\u53F8\u5171\u540C\u4FE1\u5FF5>`;
46138
46400
 
46139
46401
  // src/agent/outboundDedup.ts
46140
- var import_node_crypto13 = require("node:crypto");
46402
+ var import_node_crypto14 = require("node:crypto");
46141
46403
  var DEFAULT_WINDOW_MS = 15e3;
46142
46404
  var recentSends = /* @__PURE__ */ new Map();
46143
46405
  var lastSweepAt = 0;
46144
46406
  var normalize3 = (text2) => text2.replace(/\s+/g, " ").trim();
46145
46407
  var keyFor = (userId, normalized) => {
46146
- const hash = (0, import_node_crypto13.createHash)("sha1").update(normalized).digest("hex");
46408
+ const hash = (0, import_node_crypto14.createHash)("sha1").update(normalized).digest("hex");
46147
46409
  return `${userId}::${hash}`;
46148
46410
  };
46149
46411
  var sweep = (now, windowMs) => {
@@ -46181,7 +46443,7 @@ var isAbortError2 = (error) => {
46181
46443
  return error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("AbortError");
46182
46444
  };
46183
46445
  var llmRequestIdForTurn = (request, messages, system, tools) => {
46184
- const hash = (0, import_node_crypto14.createHash)("sha256").update(request.requestId).update("\0").update(system).update("\0").update(JSON.stringify(messages)).update("\0").update(JSON.stringify(tools.map((tool) => tool.name).sort())).digest("hex").slice(0, 40);
46446
+ const hash = (0, import_node_crypto15.createHash)("sha256").update(request.requestId).update("\0").update(system).update("\0").update(JSON.stringify(messages)).update("\0").update(JSON.stringify(tools.map((tool) => tool.name).sort())).digest("hex").slice(0, 40);
46185
46447
  return `dreq_${hash}`;
46186
46448
  };
46187
46449
  var getDefaultAgentConfig = (tools, systemPrompt) => {
@@ -46376,7 +46638,9 @@ ${getSkillMeta()}
46376
46638
  } else {
46377
46639
  finalTools = tools;
46378
46640
  const toolRegistry = createToolRegistry();
46379
- const executor = createToolExecutor(toolRegistry);
46641
+ const executor = createToolExecutor(toolRegistry, {
46642
+ hookPlugins: createDefaultToolHookPlugins()
46643
+ });
46380
46644
  for (let tool of finalTools) {
46381
46645
  registerTool(toolRegistry, tool);
46382
46646
  }
@@ -47070,7 +47334,9 @@ var updateJobLastRunTime = (jobId) => {
47070
47334
  };
47071
47335
  var getCronTools = () => {
47072
47336
  const registry2 = createToolRegistry();
47073
- const executor = createToolExecutor(registry2);
47337
+ const executor = createToolExecutor(registry2, {
47338
+ hookPlugins: createDefaultToolHookPlugins()
47339
+ });
47074
47340
  registerTool(registry2, dateTool);
47075
47341
  registerTool(registry2, globTool);
47076
47342
  registerTool(registry2, grepTool);
@@ -48117,7 +48383,9 @@ var startMailboxPoller = (intervalMs = 3e3) => {
48117
48383
  };
48118
48384
  var createDepartmentAgentTools = () => {
48119
48385
  const registry2 = createToolRegistry();
48120
- const executor = createToolExecutor(registry2);
48386
+ const executor = createToolExecutor(registry2, {
48387
+ hookPlugins: createDefaultToolHookPlugins()
48388
+ });
48121
48389
  registerTool(registry2, dateTool);
48122
48390
  registerTool(registry2, bashTool);
48123
48391
  registerTool(registry2, globTool);
@@ -48255,7 +48523,9 @@ ${workspacePath ? `
48255
48523
  } else {
48256
48524
  finalTools = tools;
48257
48525
  const toolRegistry = createToolRegistry();
48258
- const executor = createToolExecutor(toolRegistry);
48526
+ const executor = createToolExecutor(toolRegistry, {
48527
+ hookPlugins: createDefaultToolHookPlugins()
48528
+ });
48259
48529
  for (let tool of finalTools) {
48260
48530
  registerTool(toolRegistry, tool);
48261
48531
  }
@@ -53173,7 +53443,7 @@ var systemRoutes = new Hono2();
53173
53443
  var startTime = Date.now();
53174
53444
  systemRoutes.get("/system/info", (c) => {
53175
53445
  return c.json({
53176
- version: true ? "1.9.4" : "unknown",
53446
+ version: true ? "1.9.5" : "unknown",
53177
53447
  uptime: Math.floor((Date.now() - startTime) / 1e3),
53178
53448
  env: process.env.NODE_ENV || "development",
53179
53449
  nodeVersion: process.version
@@ -53181,7 +53451,7 @@ systemRoutes.get("/system/info", (c) => {
53181
53451
  });
53182
53452
 
53183
53453
  // src/server/routes/mobile.ts
53184
- var import_node_crypto15 = require("node:crypto");
53454
+ var import_node_crypto16 = require("node:crypto");
53185
53455
  var import_promises12 = require("node:fs/promises");
53186
53456
  var import_node_path16 = __toESM(require("node:path"));
53187
53457
  var mobileRoutes = new Hono2();
@@ -53285,7 +53555,7 @@ mobileRoutes.post("/mobile/attachments", async (c) => {
53285
53555
  const fileName = sanitizeFileName(body.fileName || `attachment-${Date.now()}`);
53286
53556
  const mimeType = body.mimeType || "application/octet-stream";
53287
53557
  const type = inferAttachmentType2(mimeType, fileName);
53288
- const attachmentId = (0, import_node_crypto15.randomUUID)();
53558
+ const attachmentId = (0, import_node_crypto16.randomUUID)();
53289
53559
  const buffer = Buffer.from(dataBase64, "base64");
53290
53560
  const dir = import_node_path16.default.join(getDuclawWorkspaceDir(), mobileUserId, "mobile", type === "image" ? "images" : "files");
53291
53561
  await (0, import_promises12.mkdir)(dir, { recursive: true });
@@ -53317,7 +53587,7 @@ mobileRoutes.post("/mobile/messages", async (c) => {
53317
53587
  }
53318
53588
  const mobileUserId = resolveMobileUserId(body, c.req.header("x-user-id"));
53319
53589
  const threadId = resolveThreadId(body.goalId, mobileUserId);
53320
- const requestId = body.clientMessageId?.trim() || (0, import_node_crypto15.randomUUID)();
53590
+ const requestId = body.clientMessageId?.trim() || (0, import_node_crypto16.randomUUID)();
53321
53591
  const agentText = body.contextText?.trim() || text2;
53322
53592
  const attachmentContent = buildContentWithAttachments(agentText, attachments);
53323
53593
  const content = buildGoalPrompt(body.goalId, attachmentContent.content);